UIFieldBehavior
The decade mark for iOS has come and gone. Once a nascent craft, iOS development today has a well-worn, broken-in feel to it.
And yet, when I step outside my comfort zone of table views, labels, buttons, and the like, I often find myself stumbling upon pieces of Cocoa Touch that I’d either overlooked or completely forgotten about. When I do, it’s like picking an old book from a shelf; the anticipation of what might be tucked away in its pages invariably swells up within you.
Recently, UIField
has been my dust-covered tome
sitting idly inside UIKit.
An API built to model complex field physics for UI elements
isn’t a typical use case,
nor is it likely to be the hot topic among fellow engineers.
But, when you need it, you need it, and not much else will do.
And as purveyors of the oft-forgotten or seldom used,
it serves as an excellent topic for this week’s NSHipster article.
With the design refresh of iOS in its 7th release, skeuomorphic design was famously sunset. In its place, a new paradigm emerged, in which UI controls were made to feel like physical objects rather than simply look like them. New APIs would be needed to usher in this new era of UI design, and so we were introduced to UIKit Dynamics.
Examples of this reach out across the entire OS: the bouncy lock screen, the flickable photos, those oh-so-bubbly message bubbles — these and many other interactions leverage some flavor of UIKit Dynamics (of which there are several).
-
UIAttachment
: Creates a relationship between two items, or an item and a given anchor point.Behavior -
UICollision
: Causes one or more objects to bounce off of one another instead of overlapping without interaction.Behavior -
UIField
: Enables an area or item to participate in field-based physics.Behavior -
UIGravity
: Applies a gravitational force, or pull.Behavior -
UIPush
: Creates an instantaneous or continuous force.Behavior -
UISnap
: Produces a motion that dampens over time.Behavior
For this article,
let’s take a look at UIField
,
which our good friends in Cupertino used to build the
PiP functionality
seen in FaceTime calls.
Understanding Field Behaviors
Apple mentions that UIField
applies “field-based” physics,
but what does that mean, exactly?
Thankfully, it’s more relatable that one might think.
There are plenty of examples of field-based physics in the real world,
whether it’s
the pull of a magnet,
the *sproing* of a spring,
the force of gravity pulling you down to earth.
Using UIField
,
we can designate areas of our view to apply certain physics effects
whenever an item enters into them.
Its approachable API design allows us to complex physics without much more than a factory method:
let drag = UIField Behavior.drag Field()
UIField Behavior *drag = [UIField Behavior drag Field];
Once we have a field force at our disposal, it’s a matter of placing it on the screen and defining its area of influence.
drag.position = view.center
drag.region = UIRegion(size: bounds.size)
drag.position = self.view.center;
drag.region = [[UIRegion alloc] init With Size:self.view.bounds.size];
If you need more granular control over a field’s behavior,
you can configure its strength
and falloff
,
as well as any additional properties specific to that field type.
All UIKit Dynamics behaviors require some setup to take effect,
and UIField
is no exception.
The flow looks generally something like this:
- Create an instance of a
UIDynamic
to provide the context for any animations affecting its dynamic items.Animator - Initialize the desired behaviors to use.
- Add the views you wish to be involved with each behavior.
- Add those behaviors to the dynamic animator from step one.
lazy var animator:UIDynamic Animator = {
return UIDynamic Animator(reference View: view)
}()
let drag = UIField Behavior.drag Field()
// view Did Load:
drag.add Item(another View)
animator.add Behavior(drag)
@property (strong, nonatomic, nonnull) UIDynamic Animator *animator;
@property (strong, nonatomic, nonnull) UIField Behavior *drag;
// view Did Load:
self.animator = [[UIDynamic Animator alloc] init With Reference View:self.view];
self.drag = [UIField Behavior drag Field];
[self.drag add Item:self.another View];
[self.animator add Behavior:self.drag];
For a bona fide example of UIField
,
let’s take a look at how FaceTime leverages it
to make the small rectangular view of the front-facing camera
stick to each corner of the view’s bounds.
Face to Face with Spring Fields
During a FaceTime call, you can flick your picture-in-picture to one of the corners of the screen. How do we get it to move fluidly but still stick?
One approach might entail checking a gesture recognizer’s end state, calculating which corner to settle into, and animating as necessary. The problem here is that we likely would lose the “secret sauce” that Apple painstakingly applies to these little interactions, such as the interpolation and dampening that occurs as the avatar settles into a corner.
This is a textbook situation for UIField
’s spring field.
If we think about how a literal spring works,
it exerts a linear force equal to the amount of strain that’s put on it.
So, if we push down on a coiled spring
we expect it to snap back into place once we let go.
This is also why spring fields can help contain items within a particular part of your UI. You could think of a boxing ring and how its elastic rope keeps contestants within the ring. With springs, though, the rope would originate from the center of the ring and be pulled back to each edge.
A spring field works a lot like this. Imagine if our view’s bounds were divided into four rectangles, and we had these springs hanging out around the edges of each one. The springs would be “pushed” down from the center of the rectangle to the edge of its corner. When the avatar enters any of the corners, the spring is “let go” and gives us that nice little push that we’re after.
To take care of the avatar settling into each corner, we can do something clever like this:
let scale = CGAffine Transform(scale X: 0.5, y: 0.5)
for vertical in [\UIEdge Insets.left,
\UIEdge Insets.right]
{
for horizontal in [\UIEdge Insets.top,
\UIEdge Insets.bottom]
{
let spring Field = UIField Behavior.spring Field()
spring Field.position =
CGPoint(x: layout Margins[key Path: horizontal],
y: layout Margins[key Path: vertical])
spring Field.region =
UIRegion(size: view.bounds.size.applying(scale))
animator.add Behavior(spring Field)
spring Field.add Item(facetime Avatar)
}
}
UIField Behavior *top Left Corner Field = [UIField Behavior spring Field];
// Top left corner
top Left Corner Field.position = CGPoint Make(self.layout Margins.left, self.layout Margins.top);
top Left Corner Field.region = [[UIRegion alloc] init With Size:CGSize Make(self.bounds.size.width/2, self.bounds.size.height/2)];
[self.animator add Behavior:top Left Corner Field];
[self.top Left Corner Field add Item:self.facetime Avatar];
// Continue to create a spring field for each corner...
Debugging Physics
It’s not easy to conceptualize the interactions of invisible field forces. Thankfully, Apple anticipated as much and provides a somewhat out-of-the-box way to solve this problem.
Tucked away inside of UIDynamic
is a Boolean property, debug
.
Setting it to true
paints the interface with red lines
to visualize field-based effects and their influence.
This can go quite a long way to help you
make sense of how their dynamics are working.
This API isn’t exposed publicly, but you can unlock its potential through a category or using key-value coding:
@import UIKit;
#if DEBUG
@interface UIDynamic Animator (Debugging)
@property (nonatomic, getter=is Debug Enabled) BOOL debug Enabled;
@end
#endif
or
animator.set Value(true, for Key: "debug Enabled")
[self.animator set Value:@1 for Key:@"debug Enabled"];
Although creating a category involves a bit more legwork, it’s the safer option. The slippery slope of key-value coding can rear its exception-laden head with any iOS release in the future, as the price of convenience is typically anything but free.
With debugging enabled, it appears as though each corner has a spring effect attached to it. Running and using our fledgling app, however, reveals that it’s not enough to complete the effect we’re seeking.
Aggregating Behaviors
Let’s take stock of our current situation to deepen our understanding of field physics. Currently, we’ve got a few issues:
- The avatar could fly off the screen with nothing to keep it constrained aside from spring fields
- It has a knack for rotating in circles.
- Also, it’s a tad slow.
UIKit Dynamics simulates physics — perhaps too well.
Fortunately, we can mitigate all of these undesirable side effects. To wit, they are rather trivial fixes, but it’s the reason why they’re needed that’s key.
The first issue is solved in a rather trivial fashion with what is likely UIKit Dynamics most easily understood behavior: the collision. To better hone in on how the avatar view should react once it’s acted upon by a spring field, we need to describe its physical properties in a more intentional manner. Ideally, we’d want it to behave like it would in real life, with gravity and friction acting to slow down its momentum.
For such occasions,
UIDynamic
is ideal.
It lets us attach physical properties to what would otherwise be mere
abstract view instances interacting with a physics engine.
Though UIKit does provide default values for each of these properties
when interacting with the physics engine,
they are likely not tuned to your specific use case.
And UIKit Dynamics almost always falls into the “specific use case” bucket.
It’s not hard to foresee how the lack of such an API could quickly turn problematic. If we want to model things like a push, pull or velocity but have no way to specify the object’s mass or density, we’d be omitting a critical piece of the puzzle.
let avatar Physical Properties= UIDynamic Item Behavior(items: [facetime Avatar])
avatar Physical Properties.allows Rotation = false
avatar Physical Properties.resistance = 8
avatar Physical Properties.density = 0.02
UIDynamic Item Behavior *avatar Physical Properties = [[UIDynamic Item Behavior alloc] init With Items:@[self.facetime Avatar]];
avatar Physical Properties.allows Rotation = NO;
avatar Physical Properties.resistance = 8;
avatar Physical Properties.density = 0.02;
Now the avatar view more closely mirrors real-world physics
in that it slows down a tinge after pushed by a spring field.
The configurations available from UIDynamic
are impressive,
as support for elasticity, charge and anchoring are also available
to ensure you can continuing tweaking things until they feel right.
Further, it also includes out-of-the-box support
for attaching linear or angular velocity to an object.
This serves as the perfect bookend to our journey with UIDynamic
,
as we probably want to give our FaceTime avatar a friendly nudge
at the end of the gesture recognizer
to send it off to its nearest corner,
thus letting the relevant spring field take over:
// Inside a switch for a gesture recognizer...
case .canceled, .ended:
let velocity = pan Gesture.velocity(in: view)
facetime Avatar Behavior.add Linear Velocity(velocity, for: facetime Avatar)
// Inside a switch for a gesture recognizer...
case UIGesture Recognizer State Cancelled:
case UIGesture Recognizer State Ended:
{
CGPoint velocity = [pan Gesture velocity In View:self.view];
[facetime Avatar Behavior add Linear Velocity:velocity for Item:self.facetime Avatar];
break;
}
We’re almost finished creating our faux FaceTime UI.
To pull the entire experience together,
we need to account for what our FaceTime avatar should do
when it reaches the corners of the animator’s view.
We want it to stay contained within it,
and currently,
nothing is keeping it from flying off the screen.
UIKit Dynamics offers us such behavior
to account for these situations by way of UICollision
.
Creating a collision follows a similar pattern as with using any other UIKit Dynamics behavior, thanks to consistent API design:
let parent View Bounds Collision = UICollision Behavior(items: [facetime Avatar])
parent View Bounds Collision.translates Reference Bounds Into Boundary = true
UICollision Behavior *parent View Bounds Collision = [[UICollision Behavior alloc] init With Items:@[self.facetime Avatar]];
parent View Bounds Collision.translates Reference Bounds Into Boundary = YES;
Take note of translates
.
When true
,
it treats our animator view’s bounds as its collision boundaries.
Recall that this was our initial step in setting up our dynamics stack:
lazy var animator:UIDynamic Animator = {
return UIDynamic Animator(reference View: view)
}()
self.animator = [[UIDynamic Animator alloc] init With Reference View:self.view];
By aggregating several behaviors to work as one, we can now bask in our work:
If you want to stray from the FaceTime “sticky” corners,
you are in an ideal position to do so.
UIField
has many more field physics to offer other than just a spring.
You could experiment by replacing it with a magnetism effect,
or constantly have the avatar rotate around a given point.
iOS has largely parted ways with skeuomorphism, and user experience has come a long way as a result. We no longer necessarily require green felt to know that Game Center represents games and how we can manage them.
Instead, UIKit Dynamics introduces an entirely new way for users to interact and connect with iOS. Making UI components behave as they do in the real world instead of simply looking like them is a good illustration of how far user experience has evolved since 2007.
Stripping away this layer across the OS opened the door for UIKit Dynamics to connect our expectations of how visual elements should react to our actions. These little connections may seem inconsequential at first glance, but take them away, and you’ll likely start to realize that things would feel “off.”
UIKit Dynamics offers up many flavors of physical behaviors to leverage,
and its field behaviors are perhaps some of the most interesting and versatile.
The next time you see an opportunity to create a connection in your app,
UIField
might give you the start you need.