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, youneed 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 tofeel 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 toUIKit 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 atUIField
,which our good friends in Cupertino used to build thePiP functionalityseen in FaceTime calls.
Understanding Field Behaviors
Apple mentions thatUIField
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. UsingUIField
,we can designate areas of our view to apply certain physics effectswhenever an item enters into them.
Its approachable API design allows us to complex physics without much more than a factory method:
letdrag=UIFieldBehavior .dragField ()
UIFieldBehavior *drag=[UIFieldBehavior dragField ];
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.centerdrag.region=UIRegion(size:bounds.size)
drag.position=self.view.center;drag.region=[[UIRegionalloc]initWithSize :self.view.bounds.size];
If you need more granular control over a field’s behavior, you can configure itsstrength
andfalloff
, as well as any additional properties specific to that field type.
All UIKit Dynamics behaviors require some setup to take effect, andUIField
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.
lazyvaranimator:UIDynamicAnimator ={returnUIDynamicAnimator (referenceView :view)}()letdrag=UIFieldBehavior .dragField ()// viewDidLoad: drag.addItem (anotherView )animator.addBehavior (drag)
@property(strong,nonatomic,nonnull)UIDynamicAnimator *animator;@property(strong,nonatomic,nonnull)UIFieldBehavior *drag;// viewDidLoad: self.animator=[[UIDynamicAnimator alloc]initWithReferenceView :self.view];self.drag=[UIFieldBehavior dragField ];[self.dragaddItem :self.anotherView ];[self.animatoraddBehavior :self.drag];
Take care to keep a strong reference to youUIKit
object.You don’t typically need to do this for behaviorsbecause the animator takes ownership to a behavior once it’s added.
For abona fide example ofUIField
,let’s take a look at how FaceTime leverages itto make the small rectangular view of the front-facing camerastick 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 forUIField
’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 springwe 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.
The spring field is created by replicatingHooke’s Law to calculate how much force should be applied to the objects within the field.
To take care of the avatar settling into each corner, we can do something clever like this:
letscale=CGAffineTransform (scaleX :0.5,y:0.5)forverticalin[\UIEdgeInsets .left,\UIEdgeInsets .right]{forhorizontalin[\UIEdgeInsets .top,\UIEdgeInsets .bottom]{letspringField =UIFieldBehavior .springField ()springField .position=CGPoint(x:layoutMargins [keyPath :horizontal],y:layoutMargins [keyPath :vertical])springField .region=UIRegion(size:view.bounds.size.applying(scale))animator.addBehavior (springField )springField .addItem (facetimeAvatar )}}
UIFieldBehavior *topLeftCornerField =[UIFieldBehavior springField ];// Top left cornertopLeftCornerField .position=CGPointMake (self.layoutMargins .left,self.layoutMargins .top);topLeftCornerField .region=[[UIRegionalloc]initWithSize :CGSizeMake (self.bounds.size.width/2,self.bounds.size.height/2)];[self.animatoraddBehavior :topLeftCornerField ];[self.topLeftCornerField addItem :self.facetimeAvatar ];// 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 ofUIDynamic
is a Boolean property,debug
.Setting it totrue
paints the interface with red linesto visualize field-based effects and their influence.This can go quite a long way to help youmake 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:
@importUIKit;#if DEBUG@interfaceUIDynamicAnimator (Debugging)@property(nonatomic,getter=isDebugEnabled )BOOLdebugEnabled ;@end#endif
or
animator.setValue (true,forKey :"debugEnabled" )
[self.animatorsetValue :@1forKey :@"debugEnabled" ];
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 thereason 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 mereabstract view instances interacting with a physics engine.Though UIKit does provide default values for each of these propertieswhen 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.
letavatarPhysicalProperties =UIDynamicItemBehavior (items:[facetimeAvatar ])avatarPhysicalProperties .allowsRotation =falseavatarPhysicalProperties .resistance=8avatarPhysicalProperties .density=0.02
UIDynamicItemBehavior *avatarPhysicalProperties =[[UIDynamicItemBehavior alloc]initWithItems :@[self.facetimeAvatar ]];avatarPhysicalProperties .allowsRotation =NO;avatarPhysicalProperties .resistance=8;avatarPhysicalProperties .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 fromUIDynamic
are impressive,as support for elasticity, charge and anchoring are also availableto 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 withUIDynamic
,as we probably want to give our FaceTime avatar a friendly nudgeat the end of the gesture recognizerto 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:letvelocity=panGesture .velocity(in:view)facetimeAvatarBehavior .addLinearVelocity (velocity,for:facetimeAvatar )
// Inside a switch for a gesture recognizer...caseUIGestureRecognizerStateCancelled :caseUIGestureRecognizerStateEnded :{CGPointvelocity=[panGesture velocityInView :self.view];[facetimeAvatarBehavior addLinearVelocity :velocityforItem :self.facetimeAvatar ];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 ofUICollision
.
Creating a collision follows a similar pattern as with using any other UIKit Dynamics behavior, thanks to consistent API design:
letparentViewBoundsCollision =UICollisionBehavior (items:[facetimeAvatar ])parentViewBoundsCollision .translatesReferenceBoundsIntoBoundary =true
UICollisionBehavior *parentViewBoundsCollision =[[UICollisionBehavior alloc]initWithItems :@[self.facetimeAvatar ]];parentViewBoundsCollision .translatesReferenceBoundsIntoBoundary =YES;
Take note oftranslates
.Whentrue
,it treats our animator view’s bounds as its collision boundaries.Recall that this was our initial step in setting up our dynamics stack:
lazyvaranimator:UIDynamicAnimator ={returnUIDynamicAnimator (referenceView :view)}()
self.animator=[[UIDynamicAnimator alloc]initWithReferenceView :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 requiregreen 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.