5

Stupid Swift Tricks #6

 3 years ago
source link: http://www.wooji-juice.com/blog/stupid-swift-tricks-6-animations.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Stupid Swift Tricks #6

To Animate or Not to Animate

One of the things that has characterised iOS apps from the outset has been smooth animation. This is helped by the fact that there’s a shared animation engine running on the system that keeps everything ticking along while apps are doing other work, and also by the fact that animations are very easy to add:

Swift

Animation:
// Not animated: doStuff() // animated: UIView.animate(withDuration: 1) { doStuff() }

Just wrapping your code in a simple block causes it to be animated, with smooth “ease in” and “ease out” effects.

However, if you’ve spent some time using this system, you’ll have probably run into some problems with it. It’s great for simple situations, like fading something in or out, or changing its colour, but in more complex situations it starts to fall apart.

Take the example where you want to fade something out, then remove it. UIView does have support for this:

Swift

Animation and Completion:
UIView.animate(withDuration: 1, animations: { something.alpha = 0 }, completion: { something.removeFromSuperview() })

But this only works if you’re doing everything “inline”. In a big project, we need to break tasks down into smaller methods. But the problem is those methods, like doStuff() in our original example, have no way to add code to the completion block.

They also have no idea how long the animation is (or if there even is one!), so if they need to match something against “wall clock time” (for example, to advance the Play Head of an audio production tool in sync) they can’t.

Big picture: they have no context for the animation, they’re just doing their tasks and either getting animated or not, outside of their control.

It can get even more complicated when adding new items to a view, and with Auto Layout: you need to jump carefully through UIView.performWithoutAnimation { } hoops, otherwise you get the effect where newly-appearing views explode from (x: 0, y: 0, w: 0, h: 0) to their final frames.

View Property Animators

Over time, I’ve changed the way I write animation code. At first, I wrote my own AnimationContext class to assist, but later Apple released their own equivalent, UIViewPropertyAnimator, and now I use that wherever possible.

In general, the approach I’ve been finding most helpful is to write “animatable” methods that take an explicit animator parameter:

Swift

Animatable Method:
func doStuff(with animator: UIViewPropertyAnimator? = nil) { // ... }

Then I can call doStuff() to perform the task without animation, or doStuff(with: UIViewPropertyAnimator(duration: 1, curve: .easeInOut)) or whatever to perform it with animation.

(In reality, this is usually a method with a name like reflectCurrentState(), or something more domain-specific, that performs all necessary changes to bring the view into sync with the latest data. It’s not typically called by code outside of the view, but by the view itself, and in turn calls — and passes along the animator to — other internal methods as needed. But that’s not really relevant to this discussion.)

So doStuff() can perform the task, with or without animation, as before, but now it has some context: It knows if it’s animated or not. It can inspect the animator’s duration property, if there is one. It can call the animator’s addAnimation to specify exactly the code it wants animated, and perform the non-animated code directly. It can call addCompletion to handle things like removeFromSuperview().

So those are all improvements. But it’s also not without problems. Notably, it starts to get kinda verbose:

  • doStuff(with: ...) has that lengthy UIViewPropertyAnimator constructor. Not ideal, but it pales compared to:
  • Internally, doStuff() itself needs to check for the existence of the UIViewPropertyAnimator and adjust its code accordingly, every time it wants to do something.

It can’t simply rely on optional chaining (e.g. animator?.addCompletion { something.removeFromSuperview() }) because that wouldn’t do anything if animator is nil, and the view always needs removing from the superview, animated or not.

So you end up with a lot of this sort of thing:

Swift

The Wrong Way To Do It
func doStuff(with animator: UIViewPropertyAnimator? = nil) { if let animator = animator { animator.addCompletion { _ in something.removeFromSuperview() } } else { something.removeFromSuperview() } }

Objective-C fans looking down their noses at Optionals don’t get to laugh here either — it’s not like it’s any better:

Objective-C

Still Wrong
- (void) doStuffWithAnimator: (nullable UIViewPropertyAnimator*) animator { if (animator != nil) { [animator addCompletion: ^(UIViewAnimatingPosition position) { [something removeFromSuperview]; }]; } else { [something removeFromSuperview]; } }

Once you extrapolate this out to real-world code, you could end up with way more tangled code, difficult to read and maintain.

Luckily, we can do something about this.

Optionals Are Not Renamed Nils

The trick is in the fact that the UIViewPropertyAnimator is Optional, and what that means in Swift.

People sometimes complain about Swift Optionals being “annoying”, because in Objective-C (which uses nil pointers instead) you can just go right ahead and call a method on a pointer.

Obj-C won’t complain whether it’s nil or not: If it’s a non-nil pointer, the call goes ahead, but if it’s nil, it’ll be silently ignored, without the programmer having to do anything extra.

But I disagree. In Swift, in the case where this is the behaviour you want, you only need to add an extra “?” — not exactly a great burden. But, with Swift Optionals, you can also go further.

Because in Swift, Optionals are “a real thing”, not “the lack of a thing”. An Optional, even a nil one, is an enum value that you can call methods on, that will actually execute. Swift enums are super useful!

(A fun thing is that, in the case of Objective-C classes, the low-level representation Swift uses is just a nil pointer, so they are still efficient. But syntactically, they behave very differently, which we’re about to exploit to our advantage.)

And because in Swift, you can add extensions to almost any type, not just Objective-C Classes, you can do this:

Swift

Optional<UIViewPropertyAnimator> Extension:
extension Optional where Wrapped == UIViewPropertyAnimator { @discardableResult func addCompletion(_ block: @escaping (UIViewAnimatingPosition)->()) -> Optional<UIViewPropertyAnimator> { if let animator = self { animator.addCompletion(block) } else { block(.end) } return self } }

This moves the messiness into one piece of library code that’s built right into the Optional (but only for UIViewPropertyAnimator). Now, your views can do this:

Swift

The Right Way:
func doStuff(with animator: UIViewPropertyAnimator? = nil) { animator.addCompletion { _ in something.removeFromSuperview() } }

…and that completion handler will run, whether there’s an animator or not.

(Note how there’s no ? between animator and addCompletion)

If there is an animator, the block will be scheduled for when the animation completes. And if there isn’t, it will still happen, right away. Because a nil optional, is still an Optional, with all the Optional’s methods, including the one we just added — not a black hole that eats any method call that tumbles across its event horizon.

I have similar extension methods for tasks I want to always happen, either as part of the animation, or else immediately: for fading something in, I might set the item’s alpha to 0 before adding it to the view hierarchy, then call animator.perform { something.alpha = 1 } to ensure that it becomes visible, with or without animation.

Not related to Optionals, but while I’m at it, I also add static methods on UIViewPropertyAnimator to create common animation types, e.g. static func spring(...) or static func linear(...). Swift’s name resolution rules mean you can then write more concise code like doStuff(with: .spring(duration: 1))

Of course, these are all just small coding “comforts”, not dramatic re-imaginings of what coding or app architecture is all about ;-) But, as the complexity of a project increases, even these sorts of small improvements stack up, help us fight the complexity, and keep large projects manageable. Thanks, Swift. Thwift.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK