Jekyll2021-05-17T20:49:13+00:00http://devsign.co/feed.xmldevsigniOS front-end development, animation design, visual design, and prototyping, among other tidbits.
An iOS 13 Nitpick Wishlist2019-05-24T00:00:00+00:002019-05-24T00:00:00+00:00http://devsign.co/notes/ios-13-nitfix-wishlist<p>It’s that time of year again, where tech peeps write up wishlists and predictions for the big, shiny tentpole features of the upcoming iOS release.</p>
<p><em>But!</em> I think it’d be an interesting exercise to look at the other end of the spectrum, at some smaller-sized improvements — the little issues that feel like a pebble-in-your-shoe when you run into them, or small extensions of existing features that’d make them shine.</p>
<p>Often, <a href="https://devsign.co/notes/everybody-is-an-edge-case">these little-fixes are de-prioritized in any product organization</a> — I’d bet that many of these are filed as a P3 or P4 in Apple’s bug-tracker. It’s <em>totally reasonable</em> that these have been hanging around — Apple’s engineers have lots of bigger fish to fry — but I’d love to see these issues resolved someday!</p>
<h3 id="unify-create-new-contact--add-to-existing-contact">Unify “Create New Contact” & “Add to Existing Contact”</h3>
<p>Picture this: You get a text message from an unknown number. Turns out, it’s your new friend Jesse! “Ah nice, let’s save that to my contacts info!”, you think. Ok, tap the contact, then… you face a choice: “Create New Contact” or “Add to Existing Contact” — which one should you tap? Did you already have a contact for Jesse, or do you need to make one?</p>
<p>If you’re like me, you try “Add to Existing Contact” first, search for the name “Jesse”, only to realize that no, you need to create a contact, so you tap back out, and summon the menu again: “Create New Contact”. Now you have a form to fill out!</p>
<p><em>Let’s do better!</em> I’d love to see a unified “Add to Contacts” flow, where you type in “Jesse”, and it shows search results, along with a “Create New Contact” button. If you wind up creating a new contact, it’d be great to have autofilled suggestions. This way, you don’t have to duck in-and-out of fullscreen modal forms, and can instead just save the dang phone number without a lot of hassle.</p>
<h3 id="hide-wifi-networks-in-settings">Hide WiFi networks in Settings</h3>
<p>If you’ve got neighbors nearby, you’ve got lots of WiFi networks in your Settings app. If you’re in an apartment, there might be dozens of ‘em! Sometimes the names are clever, and sometimes, they’re <em>deeply obnoxious</em>. I’d love a swipe-to-delete option in <code class="language-plaintext highlighter-rouge">Settings > WiFi</code> that would hide these networks across all of my devices — it’d reduce clutter, since I <em>never</em> want to use these networks!</p>
<h3 id="siri-should-unlock-faceid-sooner">Siri should unlock FaceID sooner</h3>
<p>Raise your phone, and ask Siri to open an app. Siri will say “You’ll need to unlock your iPhone first”, even though you’ve been looking at your phone for a while. Why doesn’t Siri start scanning my face as soon as it’s summoned?</p>
<h3 id="let-me-dial-back-siris-personality">Let me dial-back Siri’s personality</h3>
<p>Most of the time, when Siri makes a joke, it’s because it misunderstood what I said. On HomePod, these misunderstandings are kinda awful for Siri, because the whole room is groaning at the lame, out-of-place joke. I’d rather it just… not! Can we get a setting to <a href="https://www.youtube.com/watch?v=p3PfKf0ndik">dial-back the humor setting to 75</a>?</p>
<h3 id="move-the-volume-ui-to-the-status-bar">Move the volume UI to the status bar</h3>
<p>Many times a day, I pull up a video (or long-press a Live Photo), and adjust the volume — prompting a screen-gobbling, non-dismissible volume-adjustment interface. Instead, I’d love it if Apple put a volume-adjustment slider in the top-right corner of the status bar (where Control Center lives). <a href="https://devsign.co/notes/custom-volume-adjustment-on-ios">Third-party developers</a> have been implementing this for a while - it’s time it becomes a system-standard. (<a href="https://www.macrumors.com/roundup/ios-13/#updated_volume_hud">I’ve seen rumors</a> this change might be happening!)</p>
<h3 id="whitelist-apps-and-contacts-for-do-not-disturb">Whitelist apps and contacts for Do Not Disturb</h3>
<p>We have a few apps (for example, <a href="https://owletcare.com">Owlet</a> and <a href="https://nest.com/alarm-system/overview/">Nest</a>) that haven’t bothered to implement <a href="https://www.imore.com/how-turn-critical-alerts-ios-12">iOS 12’s Critical Alerts feature</a>. I’d love it if these apps could bypass my device’s Do Not Disturb settings. Likewise, I ought to be able to whitelist all iMessage notifications for certain people, just like I can allow Favorites to call during Do Not Disturb.</p>
<h3 id="help-me-stay-focused-while-using-the-device">Help me stay focused while using the device.</h3>
<p>A major reason that I prefer reading on a Kindle is that the device doesn’t feel “hot” — it has no notifications. If I could easily enable a Focus Mode on my iOS devices, that’d do a lot to address this issue.</p>
<p>I’ve really enjoyed using <a href="https://www.forestapp.cc">Forest</a> to <em>kinda</em> lock myself out of my phone for stretches of time — maybe I’m trying to read for a while, or focus on something for a bit. I’d love to see Screen Time adopt a “Focus Mode”, which would <a href="https://humanetech.com/resources/take-control/">set the phone’s screen to grayscale</a> and only allow my whitelisted apps.</p>
<h3 id="freshen-up-the-sounds-in-alarm-and-timer">Freshen up the sounds in Alarm and Timer</h3>
<p>Take a spin through Clock.app and look at the sounds available for your timers and alarms. These sounds haven’t changed in years, and I don’t think they’ve aged very well. I’d love a refreshed set of choices for these! Apple’s got <a href="https://developer.apple.com/videos/play/wwdc2017/803/">fantastic sound designers</a>, and the timers and alarms on HomePod sound great. I think this area of iOS just needs a little love.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>
<h3 id="ignore-non-responsive-homekit-devices">Ignore non-responsive HomeKit devices</h3>
<p>A neat part about HomeKit is that it lets you turn otherwise non-dimmable lights into dimmable ones, by replacing their bulbs with a Hue bulb. Yay!</p>
<p>However, if the lamp’s switch is off, and you say “Hey Siri, turn off all of my lights”, you’ll hear “I tried, but some devices are not responding.”</p>
<p>Non-responsive devices are treated like errors, and I’d rather I be able to ignore them instead - perhaps on a per-device basis. Just turn off the lights, thanks!</p>
<h3 id="hey-siri-restart-my-homepod">“Hey Siri, restart my HomePod”</h3>
<p>We’ve got a stereo-pair of these in our main space, and they’re terrific. We use them <em>constantly</em> (mostly to play music and set timers) — but every once in a while, they get tangled up, and the solution is to unplug them from the wall for a bit.</p>
<p>HomePods have no power button — so as far as I can tell, the only way to restart them is to cut the power, which feels… not great. I’d love it if I could use a voice command instead.</p>
<h3 id="reduced-white-point-at-night">Reduced white point at night</h3>
<p>Night shift is great, but I’d like to go one step further: connect the Reduce White Point accessibility setting to it. This setting makes your phone much darker, which is excellent for night-time use.</p>
<h3 id="send-reminders-in-imessage">Send reminders in iMessage</h3>
<p>“Remind my brother tomorrow that it’s dad’s birthday”. “Remind my mom to pick up the cake when she leaves her house”. It’d be wonderful if I could send somebody a reminder over iMessage! I’m sure there would have to be some kind of permissions thing around it, but imagine it: you could compose and send a reminder to someone, with attachments, triggers, and everything, and they could import it into their Reminders app.</p>
<h3 id="improve-apple-musics-infrastructure">Improve Apple Music’s infrastructure</h3>
<p>Why can’t I add something to Up Next while I’m playing a radio station? Perhaps the queue system and the radio stations aren’t compatible data types?</p>
<p>Also — can we take a look at the <a href="https://en.wikipedia.org/wiki/Data_degradation">bit rot</a> in Apple Music? So often, I’ll find that one of my favorite albums has the “Add” button a the top — why can’t Apple Music remember which albums are in my collection?</p>
<p>I have a feeling that there’s a common cause for these concerns — that the infrastructure underneath Apple Music is long-overdue for some love.</p>
<hr />
<p>So… that’s it for my nitfixes! I know these aren’t headliner features, but they’d improve my day-to-day experience a great deal. Fingers-crossed we see some of these improvements in the famous “word-cloud” summary slide next month!</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>While we’re at it, how about support for timers and alarms on macOS? <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>It’s that time of year again, where tech peeps write up wishlists and predictions for the big, shiny tentpole features of the upcoming iOS release.Custom Navigation Transitions, Part IV: An Interactive Pop Transition2019-05-23T00:00:00+00:002019-05-23T00:00:00+00:00http://devsign.co/notes/navigation-transitions-iv<p><em>This is the fourth and final post in a series on navigation transitions, one of the trickier parts of building a great iOS app. In the <a href="https://devsign.co/notes/navigation-transitions-1">first post</a>, we simplified the cast-of-characters in the API. In the <a href="https://devsign.co/notes/navigation-transitions-2">second</a>, we built a simple, self-contained modal transition. In the <a href="https://devsign.co/notes/navigation-transitions-iii">third</a>, we built a more-complex push/pop animation. Today, we’ll tackle the most-challenging one of all: an interactive navigation transition, by implementing the drag-to-dismiss gesture from the Photos app.</em></p>
<p><img src="/assets/images/nav-transitions/interactive-dismiss-demo.gif" alt="" /></p>
<p>Every year at WWDC, there’s a talk on building excellent, interactive animations. Maybe it’s <a href="https://developer.apple.com/videos/wwdc/2014/">“Building Interruptible and Responsive Interactions”</a> (2014), <a href="https://developer.apple.com/videos/play/wwdc2017/230/">“Advanced Animations with UIKit”</a> (2017), or <a href="https://developer.apple.com/videos/play/wwdc2018/803/">“Designing Fluid Interfaces”</a> (2018). These sessions are all <em>excellent</em> — and they emphasize the importance of direct manipulation. Rather than tapping a back button, you ought to be able to flick, drag, pan, and pinch to control what’s onscreen.</p>
<p>Here’s the Nielsen Norman Group’s <a href="https://www.nngroup.com/articles/direct-manipulation/">definition</a> of direct manipulation (emphasis mine):</p>
<blockquote>
<p>“Direct manipulation is an interaction style in which the objects of interest in the UI are visible and can be acted upon via <strong>physical, reversible, incremental actions that receive immediate feedback</strong>.”</p>
</blockquote>
<p><strong>Today, we’re going to build exactly that</strong>: the drag-to-dismiss transition from <a href="https://locket.photos">Locket Photos</a>, which mimics the same interaction from Apple’s Photos app.</p>
<p>You should also <a href="https://github.com/bryanjclark/devsign-nav-transitions">download the source code for this series</a>.</p>
<p>This post is a culmination of the others in this series. To build it, we’ll not only need to support the many custom navigation APIs that we’ve previously discussed — we also need to drive the interaction ourselves, by calculating progress from a gesture recognizer, reporting it to the system, and cleaning up when we’re done.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>
<p>Despite the difficulty, these gesture-driven navigation transitions are <em>completely worth it</em>. They take your app from lifeless paperwork to feeling like a playful, crafted product, and that can make all the difference in the perceived quality of your app.</p>
<p>So: let’s dig in!</p>
<h2 id="how-an-interactive-animation-works">How an interactive animation works</h2>
<p>Let’s start by talking about the order-of-operations for getting an interactive navigation transition to occur:<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup></p>
<ol>
<li><strong>The pan-gesture on our photo-detail screen starts</strong>, so we call <code class="language-plaintext highlighter-rouge">self.navigationController?.popViewController(animated: true)</code>.</li>
<li><strong>In our navigation controller</strong>, the system asks if we’ve got an animated transition for the current navigation operation. We’ll create and return our interactive animation here.</li>
<li><strong>The system then tells our interactive animation to begin</strong> (via <code class="language-plaintext highlighter-rouge">startInteractiveTransition</code>) — and now autopilot is off, and we get to drive the animation ourselves. We’ll use the pan gesture to drive the percentage-complete for the animation, and decide whether to update, cancel, or finish the transition.</li>
<li><strong>When the transition is over</strong>, it’s either complete, or it’s been cancelled. We’ll animate to the end-state, and notify the system that we’re all done.</li>
</ol>
<h2 id="composing-the-animation">Composing the animation</h2>
<p>In our last post, we built out non-interactive push and pop animations — so today, we’re going to do two things: add a <code class="language-plaintext highlighter-rouge">UIPanGestureRecognizer</code> to our photo-detail screen, and build a new <code class="language-plaintext highlighter-rouge">PhotoDetailInteractiveDismissTransition</code> to drive the animation:</p>
<p><img src="/assets/images/nav-transitions/Interactive Animation Diagram.png" alt="" /></p>
<p>We’ve already built a few <code class="language-plaintext highlighter-rouge">UIViewControllerAnimatedTransitioning</code> animations with <code class="language-plaintext highlighter-rouge">UIViewPropertyAnimator</code>, so we’re going to find that we’re able to reuse those skills today. <em>Yay!</em></p>
<h2 id="designing-the-animation">Designing the animation</h2>
<p>For Locket Photos, I wanted to closely-mimic the drag-to-dismiss gesture from the Photos app. Instead of having to tap the back button, you can drag down on the image to pop back from the photo-detail screen.</p>
<p>Let’s <a href="https://devsign.co/notes/look-at-your-fish">look at our fish</a> and see some of the details in this interactive transition.</p>
<ul>
<li><strong>Once interactive</strong>, you can drag the photo around anywhere on the screen: it’s not on a vertical rail, it follows your finger. This is direct manipulation, and it feels <em>great</em>.</li>
<li><strong>During the gesture</strong>, we need some affordances to imply what will happen if the user lifts their finger. We’ll scale the image down, and decrease the opacity of the photo-detail screen.</li>
<li><strong>If the user drags above the start point</strong>, that cancels the gesture — so we’d want the image at full-scale and the background to be opaque.</li>
<li><strong>We don’t want to over-shrink the image</strong>, so after a certain drag-distance, we’ll want to just have the image shrunk and the background transparent, until the user completes the gesture.</li>
<li><strong>When the user lets go</strong>, we need to figure out whether to cancel or complete the transition, and animate to the end state.</li>
<li><strong>The tab bar shows and hides during the transition</strong>, but we already wrote the code for this last time!</li>
<li><strong>We need it to feel great in-hand</strong>, so I recommend using SwiftTweaks to dial-in the animation timings and other thresholds.</li>
</ul>
<h2 id="building-it-out-in-code">Building it out in code</h2>
<p>It’s all going to start with a pan-gesture in our photo-detail view controller:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">// PhotoDetailViewController.swift</span>
<span class="c1">// MARK: Drag-to-dismiss</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">dismissPanGesture</span> <span class="o">=</span> <span class="kt">UIPanGestureRecognizer</span><span class="p">()</span>
<span class="kd">public</span> <span class="k">var</span> <span class="nv">isInteractivelyDismissing</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">false</span>
<span class="c1">// By holding this as a property, we can then notify it about the current</span>
<span class="c1">// state of the pan-gesture as the user moves their finger around.</span>
<span class="kd">public</span> <span class="k">weak</span> <span class="k">var</span> <span class="nv">transitionController</span><span class="p">:</span> <span class="kt">PhotoDetailInteractiveDismissTransition</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="c1">// We'll call this in viewDidLoad to set up the gesture</span>
<span class="kd">private</span> <span class="kd">func</span> <span class="nf">configureDismissGesture</span><span class="p">()</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">view</span><span class="o">.</span><span class="nf">addGestureRecognizer</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">dismissPanGesture</span><span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">dismissPanGesture</span><span class="o">.</span><span class="nf">addTarget</span><span class="p">(</span><span class="k">self</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kd">#selector(</span><span class="nf">dismissPanGestureDidChange(_:)</span><span class="kd">)</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">@objc</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">dismissPanGestureDidChange</span><span class="p">(</span><span class="n">_</span> <span class="nv">gesture</span><span class="p">:</span> <span class="kt">UIPanGestureRecognizer</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// Decide whether we're interactively-dismissing, and notify our navigation controller.</span>
<span class="k">switch</span> <span class="n">gesture</span><span class="o">.</span><span class="n">state</span> <span class="p">{</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">began</span><span class="p">:</span>
<span class="k">self</span><span class="o">.</span><span class="n">isInteractivelyDismissing</span> <span class="o">=</span> <span class="kc">true</span>
<span class="k">self</span><span class="o">.</span><span class="n">navigationController</span><span class="p">?</span><span class="o">.</span><span class="nf">popViewController</span><span class="p">(</span><span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
<span class="k">case</span> <span class="o">.</span><span class="n">cancelled</span><span class="p">,</span> <span class="o">.</span><span class="n">failed</span><span class="p">,</span> <span class="o">.</span><span class="nv">ended</span><span class="p">:</span>
<span class="k">self</span><span class="o">.</span><span class="n">isInteractivelyDismissing</span> <span class="o">=</span> <span class="kc">false</span>
<span class="k">case</span> <span class="o">.</span><span class="n">changed</span><span class="p">,</span> <span class="o">.</span><span class="nv">possible</span><span class="p">:</span>
<span class="k">break</span>
<span class="kd">@unknown</span> <span class="k">default</span><span class="p">:</span>
<span class="k">break</span>
<span class="p">}</span>
<span class="c1">// ...and here's where we pass up the current-state of our gesture</span>
<span class="c1">// to our `PhotoDetailInteractiveDismissTransition`:</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitionController</span><span class="p">?</span><span class="o">.</span><span class="nf">didPanWith</span><span class="p">(</span><span class="nv">gestureRecognizer</span><span class="p">:</span> <span class="n">gesture</span><span class="p">)</span>
<span class="p">}</span></code></pre></figure>
<p>When the pan gesture fires, it calls <code class="language-plaintext highlighter-rouge">popViewController(animated:)</code>, so the system will ask our navigation controller if it has an animated transition. We do! We just need to decide if it’s interactive or not — so we’ll check if the pan-gesture is active on our photo-detail screen to decide whether to return the interactive or non-interactive pop animation:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">// LocketNavigationController.swift</span>
<span class="k">if</span> <span class="n">photoDetailVC</span><span class="o">.</span><span class="n">isInteractivelyDismissing</span> <span class="p">{</span>
<span class="n">result</span> <span class="o">=</span> <span class="kt">PhotoDetailInteractiveDismissTransition</span><span class="p">(</span><span class="nv">fromDelegate</span><span class="p">:</span> <span class="n">photoDetailVC</span><span class="p">,</span> <span class="nv">toDelegate</span><span class="p">:</span> <span class="n">toVC</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">result</span> <span class="o">=</span> <span class="kt">PhotoDetailPopTransition</span><span class="p">(</span><span class="nv">toDelegate</span><span class="p">:</span> <span class="n">toVC</span><span class="p">,</span> <span class="nv">fromPhotoDetailVC</span><span class="p">:</span> <span class="n">photoDetailVC</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">self</span><span class="o">.</span><span class="n">currentAnimationTransition</span> <span class="o">=</span> <span class="n">result</span>
<span class="k">return</span> <span class="n">result</span></code></pre></figure>
<p>Next, the system is going to ask our navigation controller if it has an <em>interactive</em> animated transition. (I know, it feels a little repetitive, right?) We held on to our <code class="language-plaintext highlighter-rouge">currentAnimationTransition</code> for a reason: to return it here:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">// LocketNavigationController.swift</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">navigationController</span><span class="p">(</span>
<span class="n">_</span> <span class="nv">navigationController</span><span class="p">:</span> <span class="kt">UINavigationController</span><span class="p">,</span>
<span class="n">interactionControllerFor</span> <span class="nv">animationController</span><span class="p">:</span> <span class="kt">UIViewControllerAnimatedTransitioning</span>
<span class="p">)</span> <span class="o">-></span> <span class="kt">UIViewControllerInteractiveTransitioning</span><span class="p">?</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">self</span><span class="o">.</span><span class="n">currentAnimationTransition</span> <span class="k">as?</span> <span class="kt">UIViewControllerInteractiveTransitioning</span>
<span class="p">}</span></code></pre></figure>
<p>Now we hop over to our <code class="language-plaintext highlighter-rouge">PhotoDetailInteractiveDismissTransition</code>. Like the animated transitions we built last time, it uses the <code class="language-plaintext highlighter-rouge">PhotoDetailTransitionAnimatorDelegate</code> to coordinate with the photo-detail and photo-grid screens. It implements the functions for <code class="language-plaintext highlighter-rouge">UIViewControllerAnimatedTransitioning</code> and <code class="language-plaintext highlighter-rouge">UIViewControllerInteractiveTransitioning</code>. Also, we have a “update progress” function that we’ll use with our pan-gesture to drive the percentage-complete for the transition.</p>
<p><strong>Let’s start with some pseudocode</strong>, so you can see a high-altitude view of what’s going on:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">public</span> <span class="kd">class</span> <span class="kt">PhotoDetailInteractiveDismissTransition</span><span class="p">:</span> <span class="kt">NSObject</span> <span class="p">{</span>
<span class="c1">// Like the other transitions we've made, </span>
<span class="c1">// this has an imageview that we animate onscreen.</span>
<span class="kd">fileprivate</span> <span class="k">let</span> <span class="nv">transitionImageView</span><span class="p">:</span> <span class="kt">UIImageView</span> <span class="o">=</span> <span class="c1">// ...</span>
<span class="c1">/// The from- and to- viewControllers can conform to the protocol in order to get updates and vend snapshotViews</span>
<span class="kd">fileprivate</span> <span class="k">let</span> <span class="nv">fromDelegate</span><span class="p">:</span> <span class="kt">PhotoDetailTransitionAnimatorDelegate</span>
<span class="kd">fileprivate</span> <span class="k">weak</span> <span class="k">var</span> <span class="nv">toDelegate</span><span class="p">:</span> <span class="kt">PhotoDetailTransitionAnimatorDelegate</span><span class="p">?</span>
<span class="c1">/// The background animation is the "photo-detail background opacity goes to zero"</span>
<span class="kd">fileprivate</span> <span class="k">var</span> <span class="nv">backgroundAnimation</span><span class="p">:</span> <span class="kt">UIViewPropertyAnimator</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="c1">/// Called by the photo-detail screen, this function updates the state of</span>
<span class="c1">/// the interactive transition, based on the state of the gesture.</span>
<span class="kd">func</span> <span class="nf">didPanWith</span><span class="p">(</span><span class="nv">gestureRecognizer</span><span class="p">:</span> <span class="kt">UIPanGestureRecognizer</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// TODO update the transition-image, etc based on the gesture's state.</span>
<span class="p">}</span>
<span class="c1">/// If the gesture recognizer is completed/cancelled/failed,</span>
<span class="c1">/// we call this method to animate to our end-state and wrap things up.</span>
<span class="kd">private</span> <span class="kd">func</span> <span class="nf">completeTransition</span><span class="p">(</span><span class="nv">didCancel</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// TODO animate to the cancelled or completed state.</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">extension</span> <span class="kt">PhotoDetailInteractiveDismissTransition</span><span class="p">:</span> <span class="kt">UIViewControllerAnimatedTransitioning</span> <span class="p">{</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">transitionDuration</span><span class="p">(</span><span class="n">using</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">?)</span> <span class="o">-></span> <span class="kt">TimeInterval</span> <span class="p">{</span>
<span class="c1">// You can return most-anything you want here;</span>
<span class="c1">// even though this function is called by the system,</span>
<span class="c1">// it doesn't seem to affect anything, because we're going to drive</span>
<span class="c1">// the animation ourselves.</span>
<span class="k">return</span> <span class="mf">0.3</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">animateTransition</span><span class="p">(</span><span class="n">using</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// Never called; this is always an interactive transition.</span>
<span class="nf">fatalError</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">extension</span> <span class="kt">PhotoDetailInteractiveDismissTransition</span><span class="p">:</span> <span class="kt">UIViewControllerInteractiveTransitioning</span> <span class="p">{</span>
<span class="c1">// The system will call this function once at the very start;</span>
<span class="c1">// it's our chance to take over and start driving the transition.</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">startInteractiveTransition</span><span class="p">(</span><span class="n">_</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// TODO implement</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>…and that’s it! Let’s go fill in those TODOs.</p>
<h2 id="filling-in-the-animations-details">Filling in the animation’s details</h2>
<p>Let’s follow the lifecycle of the transition — so we’ll begin in <code class="language-plaintext highlighter-rouge">startInteractiveTransition(_)</code>. We’re going to pick apart some of the properties from the <code class="language-plaintext highlighter-rouge">transitionContext</code>, notify our delegates that the animation has started (so they can hide their imageviews), and set up the view hierarchy for the transition.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">extension</span> <span class="kt">PhotoDetailInteractiveDismissTransition</span><span class="p">:</span> <span class="kt">UIViewControllerInteractiveTransitioning</span> <span class="p">{</span>
<span class="c1">// The system will call this function once at the very start;</span>
<span class="c1">// it's our chance to take over and start driving the transition.</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">startInteractiveTransition</span><span class="p">(</span><span class="n">_</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitionContext</span> <span class="o">=</span> <span class="n">transitionContext</span>
<span class="k">let</span> <span class="nv">containerView</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="n">containerView</span>
<span class="k">guard</span>
<span class="k">let</span> <span class="nv">fromView</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="nf">view</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">from</span><span class="p">),</span>
<span class="k">let</span> <span class="nv">toView</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="nf">view</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">to</span><span class="p">),</span>
<span class="k">let</span> <span class="nv">fromImageFrame</span> <span class="o">=</span> <span class="n">fromDelegate</span><span class="o">.</span><span class="nf">imageFrame</span><span class="p">(),</span>
<span class="k">let</span> <span class="nv">fromImage</span> <span class="o">=</span> <span class="n">fromDelegate</span><span class="o">.</span><span class="nf">referenceImage</span><span class="p">(),</span>
<span class="k">let</span> <span class="nv">fromVC</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="nf">viewController</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">from</span><span class="p">)</span> <span class="k">as?</span> <span class="kt">PhotoDetailViewController</span><span class="p">,</span>
<span class="k">let</span> <span class="nv">toVC</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="nf">viewController</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">to</span><span class="p">)</span>
<span class="k">else</span> <span class="p">{</span>
<span class="nf">fatalError</span><span class="p">()</span>
<span class="p">}</span>
<span class="k">self</span><span class="o">.</span><span class="n">fromVC</span> <span class="o">=</span> <span class="n">fromVC</span>
<span class="k">self</span><span class="o">.</span><span class="n">toVC</span> <span class="o">=</span> <span class="n">toVC</span>
<span class="n">fromVC</span><span class="o">.</span><span class="n">transitionController</span> <span class="o">=</span> <span class="k">self</span>
<span class="c1">// Notify our delegates that the transition has begun:</span>
<span class="n">fromDelegate</span><span class="o">.</span><span class="nf">transitionWillStart</span><span class="p">()</span>
<span class="n">toDelegate</span><span class="p">?</span><span class="o">.</span><span class="nf">transitionWillStart</span><span class="p">()</span>
<span class="k">self</span><span class="o">.</span><span class="n">fromReferenceImageViewFrame</span> <span class="o">=</span> <span class="n">fromImageFrame</span>
<span class="c1">// Decide where the image should move during the transition.</span>
<span class="c1">// NOTE: We'll replace this with a better one during the transition,</span>
<span class="c1">// because the collectionview on the parent screen needs a chance to re-layout.</span>
<span class="k">self</span><span class="o">.</span><span class="n">toReferenceImageViewFrame</span> <span class="o">=</span> <span class="kt">PhotoDetailPopTransition</span><span class="o">.</span><span class="nf">defaultOffscreenFrameForDismissal</span><span class="p">(</span>
<span class="nv">transitionImageSize</span><span class="p">:</span> <span class="n">fromImageFrame</span><span class="o">.</span><span class="n">size</span><span class="p">,</span>
<span class="nv">screenHeight</span><span class="p">:</span> <span class="n">fromView</span><span class="o">.</span><span class="n">bounds</span><span class="o">.</span><span class="n">height</span>
<span class="p">)</span>
<span class="c1">// Build the view-hierarchy for the animation</span>
<span class="n">containerView</span><span class="o">.</span><span class="nf">addSubview</span><span class="p">(</span><span class="n">toView</span><span class="p">)</span>
<span class="n">containerView</span><span class="o">.</span><span class="nf">addSubview</span><span class="p">(</span><span class="n">fromView</span><span class="p">)</span>
<span class="n">containerView</span><span class="o">.</span><span class="nf">addSubview</span><span class="p">(</span><span class="n">transitionImageView</span><span class="p">)</span>
<span class="n">transitionImageView</span><span class="o">.</span><span class="n">image</span> <span class="o">=</span> <span class="n">fromImage</span>
<span class="n">transitionImageView</span><span class="o">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">fromImageFrame</span>
<span class="c1">// Create the "photo-detail background fades away" animation</span>
<span class="c1">// NOTE: The duration and damping ratio here don't matter!</span>
<span class="c1">// This animation is only programmatically adjusted in the drag state,</span>
<span class="c1">// and then the duration is altered in the completion state.</span>
<span class="k">self</span><span class="o">.</span><span class="n">backgroundAnimation</span> <span class="o">=</span> <span class="kt">UIViewPropertyAnimator</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nv">dampingRatio</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nv">animations</span><span class="p">:</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">self</span><span class="o">.</span><span class="n">toDelegate</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
<span class="c1">// This occurs in Locket if you tap a single image on the map.</span>
<span class="n">fromView</span><span class="o">.</span><span class="n">frame</span><span class="o">.</span><span class="n">origin</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">containerView</span><span class="o">.</span><span class="n">frame</span><span class="o">.</span><span class="n">maxX</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitionImageView</span><span class="o">.</span><span class="n">alpha</span> <span class="o">=</span> <span class="mf">0.4</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">fromView</span><span class="o">.</span><span class="n">alpha</span> <span class="o">=</span> <span class="mi">0</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="c1">// Look! Our friend, the custom-tab-bar-hiding animation, working</span>
<span class="c1">// perfectly with our UIViewPropertyAnimator. Isn't that neat?</span>
<span class="n">toVC</span><span class="o">.</span><span class="n">locketTabBarController</span><span class="p">?</span><span class="o">.</span><span class="nf">setTabBar</span><span class="p">(</span><span class="nv">hidden</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nv">alongside</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">backgroundAnimation</span><span class="o">!</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p><em>Phew!</em> Ok, after the system calls <code class="language-plaintext highlighter-rouge">startInteractiveTransition</code>, we’re in control of the transition. Our pan-gesture will continue to send updates to <code class="language-plaintext highlighter-rouge">didPanWith(gestureRecognizer:)</code>, so let’s hop up there: We’ll inspect the gesture, and decide whether to update progress, cancel the transition, or complete it.</p>
<p>First, we’re going to need to decide how the pan gesture’s translation maps to a percentage-complete:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">// PhotoDetailInteractiveDismissTransition.swift</span>
<span class="c1">/// For a given vertical offset, what's the percentage complete for the transition?</span>
<span class="c1">/// e.g. -100pts -> 0%, 0pts -> 0%, 20pts -> 10%, 200pts -> 100%, 400pts -> 100%</span>
<span class="kd">func</span> <span class="nf">percentageComplete</span><span class="p">(</span><span class="n">forVerticalDrag</span> <span class="nv">verticalDrag</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">)</span> <span class="o">-></span> <span class="kt">CGFloat</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">maximumDelta</span> <span class="o">=</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="mi">200</span><span class="p">)</span>
<span class="k">return</span> <span class="kt">CGFloat</span><span class="o">.</span><span class="nf">scaleAndShift</span><span class="p">(</span>
<span class="nv">value</span><span class="p">:</span> <span class="n">verticalDrag</span><span class="p">,</span>
<span class="nv">inRange</span><span class="p">:</span> <span class="p">(</span><span class="nv">min</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="nv">max</span><span class="p">:</span> <span class="n">maximumDelta</span><span class="p">)</span>
<span class="p">)</span>
<span class="p">}</span>
<span class="c1">/// The transition image scales down from 100% to a minimum of 68%,</span>
<span class="c1">/// based on the percentage-complete of the gesture.</span>
<span class="kd">func</span> <span class="nf">transitionImageScaleFor</span><span class="p">(</span><span class="nv">percentageComplete</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">)</span> <span class="o">-></span> <span class="kt">CGFloat</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">minScale</span> <span class="o">=</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="mf">0.68</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">result</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">minScale</span><span class="p">)</span> <span class="o">*</span> <span class="n">percentageComplete</span>
<span class="k">return</span> <span class="n">result</span>
<span class="p">}</span></code></pre></figure>
<p>That CGFloat function is a nifty way to convert the drag-distance into a value between 0 and 1. (This is one of my <a href="http://khanlou.com/talks/">“You Deserve Nice Things”</a> bits that I use often in projects.)</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">public</span> <span class="kd">extension</span> <span class="kt">CGFloat</span> <span class="p">{</span>
<span class="c1">/// Returns the value, scaled-and-shifted to the targetRange.</span>
<span class="c1">/// If no target range is provided, we assume the unit range (0, 1)</span>
<span class="kd">static</span> <span class="kd">func</span> <span class="nf">scaleAndShift</span><span class="p">(</span>
<span class="nv">value</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">,</span>
<span class="nv">inRange</span><span class="p">:</span> <span class="p">(</span><span class="nv">min</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">,</span> <span class="nv">max</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">),</span>
<span class="nv">toRange</span><span class="p">:</span> <span class="p">(</span><span class="nv">min</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">,</span> <span class="nv">max</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="nv">min</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span> <span class="nv">max</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">)</span>
<span class="p">)</span> <span class="o">-></span> <span class="kt">CGFloat</span> <span class="p">{</span>
<span class="nf">assert</span><span class="p">(</span><span class="n">inRange</span><span class="o">.</span><span class="n">max</span> <span class="o">></span> <span class="n">inRange</span><span class="o">.</span><span class="n">min</span><span class="p">)</span>
<span class="nf">assert</span><span class="p">(</span><span class="n">toRange</span><span class="o">.</span><span class="n">max</span> <span class="o">></span> <span class="n">toRange</span><span class="o">.</span><span class="n">min</span><span class="p">)</span>
<span class="k">if</span> <span class="n">value</span> <span class="o"><</span> <span class="n">inRange</span><span class="o">.</span><span class="n">min</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">toRange</span><span class="o">.</span><span class="n">min</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">value</span> <span class="o">></span> <span class="n">inRange</span><span class="o">.</span><span class="n">max</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">toRange</span><span class="o">.</span><span class="n">max</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">ratio</span> <span class="o">=</span> <span class="p">(</span><span class="n">value</span> <span class="o">-</span> <span class="n">inRange</span><span class="o">.</span><span class="n">min</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">inRange</span><span class="o">.</span><span class="n">max</span> <span class="o">-</span> <span class="n">inRange</span><span class="o">.</span><span class="n">min</span><span class="p">)</span>
<span class="k">return</span> <span class="n">toRange</span><span class="o">.</span><span class="n">min</span> <span class="o">+</span> <span class="n">ratio</span> <span class="o">*</span> <span class="p">(</span><span class="n">toRange</span><span class="o">.</span><span class="n">max</span> <span class="o">-</span> <span class="n">toRange</span><span class="o">.</span><span class="n">min</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>Cool. Let’s hop in and implement the guts of our <code class="language-plaintext highlighter-rouge">didPanWith(gestureRecognizer:)</code> function from earlier:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">/// Called by the photo-detail screen, this function updates the state of</span>
<span class="c1">/// the interactive transition, based on the state of the gesture.</span>
<span class="kd">func</span> <span class="nf">didPanWith</span><span class="p">(</span><span class="nv">gestureRecognizer</span><span class="p">:</span> <span class="kt">UIPanGestureRecognizer</span><span class="p">)</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">transitionContext</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">transitionContext</span><span class="o">!</span>
<span class="k">let</span> <span class="nv">transitionImageView</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">transitionImageView</span>
<span class="c1">// For a given vertical-drag, we calculate our percentage complete</span>
<span class="c1">// and how shrunk-down the transition-image should be.</span>
<span class="k">let</span> <span class="nv">translation</span> <span class="o">=</span> <span class="n">gestureRecognizer</span><span class="o">.</span><span class="nf">translation</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">translationVertical</span> <span class="o">=</span> <span class="n">translation</span><span class="o">.</span><span class="n">y</span>
<span class="k">let</span> <span class="nv">percentageComplete</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="nf">percentageComplete</span><span class="p">(</span><span class="nv">forVerticalDrag</span><span class="p">:</span> <span class="n">translationVertical</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">transitionImageScale</span> <span class="o">=</span> <span class="nf">transitionImageScaleFor</span><span class="p">(</span><span class="nv">percentageComplete</span><span class="p">:</span> <span class="n">percentageComplete</span><span class="p">)</span>
<span class="c1">// Now, we inspect the gesture's state, and decide whether to update/cancel/complete.</span>
<span class="k">switch</span> <span class="n">gestureRecognizer</span><span class="o">.</span><span class="n">state</span> <span class="p">{</span>
<span class="k">case</span> <span class="o">.</span><span class="n">possible</span><span class="p">,</span> <span class="o">.</span><span class="nv">began</span><span class="p">:</span>
<span class="k">break</span>
<span class="k">case</span> <span class="o">.</span><span class="n">cancelled</span><span class="p">,</span> <span class="o">.</span><span class="nv">failed</span><span class="p">:</span>
<span class="k">self</span><span class="o">.</span><span class="nf">completeTransition</span><span class="p">(</span><span class="nv">didCancel</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">changed</span><span class="p">:</span>
<span class="c1">// Apply a transform to our imageview, to scale/translate it into place.</span>
<span class="n">transitionImageView</span><span class="o">.</span><span class="n">transform</span> <span class="o">=</span> <span class="kt">CGAffineTransform</span><span class="o">.</span><span class="n">identity</span>
<span class="o">.</span><span class="nf">scaledBy</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">transitionImageScale</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">transitionImageScale</span><span class="p">)</span>
<span class="o">.</span><span class="nf">translatedBy</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">translation</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">translation</span><span class="o">.</span><span class="n">y</span><span class="p">)</span>
<span class="c1">// Notify the system about the percentage-complete.</span>
<span class="n">transitionContext</span><span class="o">.</span><span class="nf">updateInteractiveTransition</span><span class="p">(</span><span class="n">percentageComplete</span><span class="p">)</span>
<span class="c1">// Update the background animation (yay property animators!)</span>
<span class="k">self</span><span class="o">.</span><span class="n">backgroundAnimation</span><span class="p">?</span><span class="o">.</span><span class="n">fractionComplete</span> <span class="o">=</span> <span class="n">percentageComplete</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">ended</span><span class="p">:</span>
<span class="c1">// Here, we decide whether to complete or cancel the transition.</span>
<span class="k">let</span> <span class="nv">fingerIsMovingDownwards</span> <span class="o">=</span> <span class="n">gestureRecognizer</span><span class="o">.</span><span class="nf">velocity</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span><span class="o">.</span><span class="n">y</span> <span class="o">></span> <span class="mi">0</span>
<span class="k">let</span> <span class="nv">transitionMadeSignificantProgress</span> <span class="o">=</span> <span class="n">percentageComplete</span> <span class="o">></span> <span class="mf">0.1</span>
<span class="k">let</span> <span class="nv">shouldComplete</span> <span class="o">=</span> <span class="n">fingerIsMovingDownwards</span> <span class="o">&&</span> <span class="n">transitionMadeSignificantProgress</span>
<span class="k">self</span><span class="o">.</span><span class="nf">completeTransition</span><span class="p">(</span><span class="nv">didCancel</span><span class="p">:</span> <span class="o">!</span><span class="n">shouldComplete</span><span class="p">)</span>
<span class="kd">@unknown</span> <span class="k">default</span><span class="p">:</span>
<span class="k">break</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>Now all we need to do is fill-in our <code class="language-plaintext highlighter-rouge">completeTransition(didCancel:)</code> function! This function will set up a property animator to complete or cancel the transition - and it’ll update the property animators on our tab bar and <code class="language-plaintext highlighter-rouge">backgroundAnimation</code>, too!</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">private</span> <span class="kd">func</span> <span class="nf">completeTransition</span><span class="p">(</span><span class="nv">didCancel</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// If the gesture was cancelled, we reverse the "fade out the photo-detail background" animation.</span>
<span class="k">self</span><span class="o">.</span><span class="n">backgroundAnimation</span><span class="p">?</span><span class="o">.</span><span class="n">isReversed</span> <span class="o">=</span> <span class="n">didCancel</span>
<span class="k">let</span> <span class="nv">transitionContext</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">transitionContext</span><span class="o">!</span>
<span class="k">let</span> <span class="nv">backgroundAnimation</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">backgroundAnimation</span><span class="o">!</span>
<span class="c1">// The cancel and complete animations have different timing values.</span>
<span class="c1">// I dialed these in on-device using SwiftTweaks.</span>
<span class="k">let</span> <span class="nv">completionDuration</span><span class="p">:</span> <span class="kt">Double</span>
<span class="k">let</span> <span class="nv">completionDamping</span><span class="p">:</span> <span class="kt">CGFloat</span>
<span class="k">if</span> <span class="n">didCancel</span> <span class="p">{</span>
<span class="n">completionDuration</span> <span class="o">=</span> <span class="mf">0.45</span>
<span class="n">completionDamping</span> <span class="o">=</span> <span class="mf">0.75</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">completionDuration</span> <span class="o">=</span> <span class="mf">0.37</span>
<span class="n">completionDamping</span> <span class="o">=</span> <span class="mf">0.90</span>
<span class="p">}</span>
<span class="c1">// The transition-image needs to animate into its final place.</span>
<span class="c1">// That's either:</span>
<span class="c1">// - its original spot on the photo-detail screen (if the transition was cancelled),</span>
<span class="c1">// - or its place in the photo-grid (if the transition completed).</span>
<span class="k">let</span> <span class="nv">foregroundAnimation</span> <span class="o">=</span> <span class="kt">UIViewPropertyAnimator</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="n">completionDuration</span><span class="p">,</span> <span class="nv">dampingRatio</span><span class="p">:</span> <span class="n">completionDamping</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// Reset our scale-transform on the imageview</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitionImageView</span><span class="o">.</span><span class="n">transform</span> <span class="o">=</span> <span class="kt">CGAffineTransform</span><span class="o">.</span><span class="n">identity</span>
<span class="c1">// NOTE: It's important that we ask the toDelegate *here*,</span>
<span class="c1">// because if the device has rotated,</span>
<span class="c1">// the toDelegate needs a chance to update its layout</span>
<span class="c1">// before asking for the frame.</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitionImageView</span><span class="o">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">didCancel</span>
<span class="p">?</span> <span class="k">self</span><span class="o">.</span><span class="n">fromReferenceImageViewFrame</span><span class="o">!</span>
<span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">toDelegate</span><span class="p">?</span><span class="o">.</span><span class="nf">imageFrame</span><span class="p">()</span> <span class="p">??</span> <span class="k">self</span><span class="o">.</span><span class="n">toReferenceImageViewFrame</span><span class="o">!</span>
<span class="p">}</span>
<span class="c1">// When the transition-image has moved into place, the animation completes,</span>
<span class="c1">// and we close out the transition itself.</span>
<span class="n">foregroundAnimation</span><span class="o">.</span><span class="n">addCompletion</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="p">(</span><span class="n">position</span><span class="p">)</span> <span class="k">in</span>
<span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="n">transitionImageView</span><span class="o">.</span><span class="nf">removeFromSuperview</span><span class="p">()</span>
<span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="n">transitionImageView</span><span class="o">.</span><span class="n">image</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="n">toDelegate</span><span class="p">?</span><span class="o">.</span><span class="nf">transitionDidEnd</span><span class="p">()</span>
<span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="n">fromDelegate</span><span class="o">.</span><span class="nf">transitionDidEnd</span><span class="p">()</span>
<span class="k">if</span> <span class="n">didCancel</span> <span class="p">{</span>
<span class="n">transitionContext</span><span class="o">.</span><span class="nf">cancelInteractiveTransition</span><span class="p">()</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">transitionContext</span><span class="o">.</span><span class="nf">finishInteractiveTransition</span><span class="p">()</span>
<span class="p">}</span>
<span class="n">transitionContext</span><span class="o">.</span><span class="nf">completeTransition</span><span class="p">(</span><span class="o">!</span><span class="n">didCancel</span><span class="p">)</span>
<span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="n">transitionContext</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="c1">// Update the backgroundAnimation's duration to match.</span>
<span class="c1">// PS: How *cool* are property-animators? I say: very. This "continue animation" bit is magic!</span>
<span class="k">let</span> <span class="nv">durationFactor</span> <span class="o">=</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="n">foregroundAnimation</span><span class="o">.</span><span class="n">duration</span> <span class="o">/</span> <span class="n">backgroundAnimation</span><span class="o">.</span><span class="n">duration</span><span class="p">)</span>
<span class="n">backgroundAnimation</span><span class="o">.</span><span class="nf">continueAnimation</span><span class="p">(</span><span class="nv">withTimingParameters</span><span class="p">:</span> <span class="kc">nil</span><span class="p">,</span> <span class="nv">durationFactor</span><span class="p">:</span> <span class="n">durationFactor</span><span class="p">)</span>
<span class="n">foregroundAnimation</span><span class="o">.</span><span class="nf">startAnimation</span><span class="p">()</span>
<span class="p">}</span></code></pre></figure>
<h2 id="and-youre-done">…and you’re done!!</h2>
<p>omg, <em>you did it!</em> We’ve built out a non-trivial, production-quality interactive navigation transition. Congratulations!! This was a lot of work; thanks for sticking with it.</p>
<p>I hope that the <a href="https://github.com/bryanjclark/devsign-nav-transitions">source code</a> will provide a great reference for you when you’re building out these transitions yourself!</p>
<p><img src="/assets/images/nav-transitions/interactive-dismiss-demo.gif" alt="" /></p>
<p>It’s been <em>really nice</em> to get back into writing regularly here.<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup> I’m going to keep this going. Follow along <a href="https://twitter.com/devsignblog/">@devsignblog</a> on Twitter, or subscribe to this site via RSS. Thanks for reading!</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>At first, I found it a challenge to figure out where I was meant to put that gesture recognizer — does it live in my navigation controller, or the photo-detail screen, or the transition animation? I’m happy with where I wound up, but I imagine that your choice might vary depending on your app’s architecture. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:2" role="doc-endnote">
<p>This dance was much harder to coordinate before <code class="language-plaintext highlighter-rouge">UIViewPropertyAnimator</code> came to town. We’re going to find it much easier to update, cancel, and complete our animation by using property animators! <a href="#fnref:2" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:3" role="doc-endnote">
<p>Thanks for the nudge, <a href="https://twitter.com/_HarshilShah">Harshil</a>! <a href="#fnref:3" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>This is the fourth and final post in a series on navigation transitions, one of the trickier parts of building a great iOS app. In the first post, we simplified the cast-of-characters in the API. In the second, we built a simple, self-contained modal transition. In the third, we built a more-complex push/pop animation. Today, we’ll tackle the most-challenging one of all: an interactive navigation transition, by implementing the drag-to-dismiss gesture from the Photos app.Custom Navigation Transitions, Part III: A Complex Push/Pop Animation2019-05-22T00:00:00+00:002019-05-22T00:00:00+00:00http://devsign.co/notes/navigation-transitions-iii<p><em>This is the third in a series on building navigation transitions for iOS apps.
In the <a href="http://devsign.co/notes/navigation-transitions-1">first post</a>, we
simplified the API and learned about its key protocols. In the <a href="https://devsign.co/notes/navigation-transitions-2">second
post</a>, we built a simple
modal animation, and learned about UIViewPropertyAnimator.</em></p>
<p><img src="/assets/images/nav-transitions/Locket Photos Complex Push.gif" alt="" /></p>
<p>Today, we’ll be building a complex push/pop navigation transition from my app,
<a href="https://locket.photos">Locket Photos</a>. This animation mimics the transition in
the native Photos app: you select an image from a grid, and it grows to
fill the screen, coming to rest in the photo-detail screen. (We’ll get to the swipe-to-dismiss interactive-pop animation in the next post!) We’re going to do a detailed walkthrough of building the push transition and then (<em>spoiler!</em>) we’ll find that building the pop transition is <em>extremely</em> similar.</p>
<p>I recommend <a href="https://github.com/bryanjclark/devsign-nav-transitions">downloading the sample
project</a> for this series.</p>
<p>Here’s the push/pop transition animation we’ll be building today:</p>
<p><img src="/assets/images/nav-transitions/demo complex push pop.gif" alt="" /></p>
<blockquote>
<p><strong>Note: This is a long post.</strong> I’m trying to walk through a non-trivial
implementation of a navigation-transition animation, and that means addressing
a dozen-or-so details along the way. <a href="https://twitter.com/skeuo">Bryan Hansen</a>
once did this in an <a href="http://web.archive.org/web/20160308041455/http://skeuo.com/uicollectionview-custom-layout-tutorial">amazing UICollectionView
tutorial</a>,
and I found it immensely helpful — so I’m trying to do something similar here:
demonstrate a production-quality, non-trivial implementation.</p>
</blockquote>
<h2 id="planning-the-push-animation">Planning the push animation</h2>
<p>The overall idea of this animation is that a little photo-thumbnail should grow
up to fill the screen when you tap it. Simple, right? <em>Not so fast</em>: there are a
lot of details we’ll need to handle to avoid a glitchy animation — and it’ll
often take several implementation iterations until we can get things running
smoothly.</p>
<p>Here’s what we need in our push animation:</p>
<ul>
<li><strong>the selected image scales up and moves</strong> to its position on the
photo-detail screen,</li>
<li><strong>the tab bar slides away</strong>.</li>
</ul>
<p>Great! <em>But wait! Not so fast!</em> We don’t want to <em>actually</em> move the
<code class="language-plaintext highlighter-rouge">UIImageView</code> from the grid to the photo detail screen — that would mean lots of
code to pull out that image view, and insert it deep into the photo-detail
screen’s view hierarchy. Instead, we’ll create a temporary third <code class="language-plaintext highlighter-rouge">UIImageView</code>,
and hide the grid and photo-detail images during the animation.</p>
<p>Excellent! <em>Oh wait, ugh, one more thing…</em> If we just use the little image from
our photo-grid and scale it up, then we’ll have this weird, scaled-up pixelated
thing that flickers into the final spot. Instead, we’ll want to try and fetch
the image at a larger size, if we have that cached via the Photos API.</p>
<p>Magnificent! <em>Ahh wait, one more snag.</em> The animation we want (where the tab
bar slides down offscreen) isn’t something available via UIKit — so we’re gonna
have to figure out something else there, that doesn’t mess up our
<code class="language-plaintext highlighter-rouge">safeAreaInsets</code> on the collection grid or the photo-detail screen.</p>
<blockquote>
<p>😅 See how these details add up? This was my experience in building this
animation transition — and each one of those paragraphs involved building the
animation, realizing it was glitchy, updating my design plans for the
animation, exploring options, and rebuilding the animation. <em>So:</em> if you’re
doing this in your app, do not be hard on yourself if this pattern happens to
you — this custom-animation dance is one of the trickier parts of UIKit, but
you can do it - be patient!</p>
</blockquote>
<p>Here’s our final list of what we’ll implement in the animation:</p>
<ul>
<li>we create a temporary <code class="language-plaintext highlighter-rouge">UIImageView</code>, and set its image to be the larger-size
one we’ll want in the photo-detail screen,</li>
<li>we’ll place this temporary image in the spot of the cell that the user has
just tapped,</li>
<li>we’ll <strong>hide the other images</strong> (the thumbnail that the user
tapped, and the one in the photo-detail screen) so there’s only one image
during the transition,</li>
<li>the <strong>transition image scales up and moves</strong> to its position on the
photo-detail screen,</li>
<li>the <strong>tab bar slides away</strong>,</li>
<li>then once the animation completes, we <strong>un-hide the photo-detail image</strong>, and
<strong>remove the transition image</strong>,</li>
<li>and <strong>un-hide the collection cell</strong> back on the grid screen.</li>
</ul>
<p>Alright, that’s a healthy list! <strong>Let’s build it!</strong></p>
<h2 id="composing-the-push-animation">Composing the push animation</h2>
<p>In the first post in this series, I drew some diagrams that showed different
ways of composing these animations. Here are the parts of that diagram that we’ll build in this post:</p>
<p><img src="/assets/images/nav-transitions/Push Animation Diagram.png" alt="" /></p>
<p>We’ll have a <code class="language-plaintext highlighter-rouge">UINavigationController</code> that conforms to
<code class="language-plaintext highlighter-rouge">UIViewControllerTransitioningDelegate</code> — this is the “vending machine” that
creates the <code class="language-plaintext highlighter-rouge">PhotoDetailPushTransition</code>.</p>
<p>We’re going to create a separate class, <code class="language-plaintext highlighter-rouge">PhotoDetailPushTransition</code>, that will
implement the <code class="language-plaintext highlighter-rouge">UIViewControllerAnimatedTransitioning</code> protocol.</p>
<p>We’ll also build a protocol, <code class="language-plaintext highlighter-rouge">PhotoDetailTransitionAnimatorDelegate</code>, (not
pictured) that will allow our <code class="language-plaintext highlighter-rouge">PhotoDetailPushTransition</code> to show/hide the image
views on the photo-grid and photo-detail screens.</p>
<h2 id="outlining-the-push-animation-in-pseudocode">Outlining the push animation in pseudocode</h2>
<p>Let’s rough out the implementation, and we’ll fill in the details as we go.</p>
<p><strong>First</strong>, we need to implement <code class="language-plaintext highlighter-rouge">UIViewControllerTransitioningDelegate</code>, where
we’ll vend an animation every time a photo-detail screen is shown. For Locket, I
chose to create <code class="language-plaintext highlighter-rouge">LocketNavigationController</code>, a subclass of
<code class="language-plaintext highlighter-rouge">UINavigationController</code> that just adds the ability to create transition animations.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">public</span> <span class="kd">class</span> <span class="kt">LocketNavigationController</span><span class="p">:</span> <span class="kt">UINavigationController</span> <span class="p">{</span>
<span class="kd">public</span> <span class="k">override</span> <span class="kd">func</span> <span class="nf">viewDidLoad</span><span class="p">()</span> <span class="p">{</span>
<span class="k">super</span><span class="o">.</span><span class="nf">viewDidLoad</span><span class="p">()</span>
<span class="k">self</span><span class="o">.</span><span class="n">delegate</span> <span class="o">=</span> <span class="k">self</span> <span class="c1">// UINavigationControllerDelegate</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">extension</span> <span class="kt">LocketNavigationController</span><span class="p">:</span> <span class="kt">UINavigationControllerDelegate</span> <span class="p">{</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">navigationController</span><span class="p">(</span>
<span class="n">_</span> <span class="nv">navigationController</span><span class="p">:</span> <span class="kt">UINavigationController</span><span class="p">,</span>
<span class="n">animationControllerFor</span> <span class="nv">operation</span><span class="p">:</span> <span class="kt">UINavigationController</span><span class="o">.</span><span class="kt">Operation</span><span class="p">,</span>
<span class="n">from</span> <span class="nv">fromVC</span><span class="p">:</span> <span class="kt">UIViewController</span><span class="p">,</span>
<span class="n">to</span> <span class="nv">toVC</span><span class="p">:</span> <span class="kt">UIViewController</span>
<span class="p">)</span> <span class="o">-></span> <span class="kt">UIViewControllerAnimatedTransitioning</span><span class="p">?</span> <span class="p">{</span>
<span class="c1">// Whenever we push on a photo-detail screen,</span>
<span class="c1">// we’ll return an animation.</span>
<span class="k">if</span>
<span class="k">let</span> <span class="nv">photoDetailVC</span> <span class="o">=</span> <span class="n">toVC</span> <span class="k">as?</span> <span class="kt">PhotoDetailViewController</span><span class="p">,</span>
<span class="n">operation</span> <span class="o">==</span> <span class="o">.</span><span class="n">push</span>
<span class="p">{</span>
<span class="c1">// TODO create and return a custom push animation.</span>
<span class="c1">// return PhotoDetailPushTransition(...)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p><strong>Second</strong>, we need to build out the animation itself - let’s call it
<code class="language-plaintext highlighter-rouge">PhotoDetailPushTransition</code>:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">/// Controls the "non-interactive push animation" used for the PhotoDetailViewController</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="kt">PhotoDetailPushTransition</span><span class="p">:</span> <span class="kt">NSObject</span><span class="p">,</span> <span class="kt">UIViewControllerAnimatedTransitioning</span> <span class="p">{</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">transitionDuration</span><span class="p">(</span><span class="n">using</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">?)</span> <span class="o">-></span> <span class="kt">TimeInterval</span> <span class="p">{</span>
<span class="k">return</span> <span class="mf">0.38</span> <span class="c1">// NOTE: This duration felt right in-hand when using SwiftTweaks.</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">animateTransition</span><span class="p">(</span><span class="n">using</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// TODO build out the animation.</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>Nice! We’ll have to build out the animation, but let’s keep sketching out our
pseudocode, and we’ll come back to fill in the details.</p>
<p><strong>Third</strong>, we need to coordinate the animation’s effects — we need to create
that “transition image” that appears to move between screens, manipulate the tab
bar, and do it in a way that doesn’t tightly-couple our screens’ code.</p>
<p>To do this, I chose to create another delegate protocol:
<code class="language-plaintext highlighter-rouge">PhotoDetailTransitionAnimatorDelegate</code>, which we’ll implement in our
photo-detail and photo-grid screens. It gathers snapshots of image views (and
their location onscreen), and notifies the view controllers when the transition
will start, and when it’s finished.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">/// Allows view controllers to participate in the photo-detail transition animation.</span>
<span class="kd">public</span> <span class="kd">protocol</span> <span class="kt">PhotoDetailTransitionAnimatorDelegate</span><span class="p">:</span> <span class="kd">class</span> <span class="p">{</span>
<span class="c1">/// Called just-before the transition animation begins.</span>
<span class="c1">/// Use this to prepare for the transition.</span>
<span class="kd">func</span> <span class="nf">transitionWillStart</span><span class="p">()</span>
<span class="c1">/// Called right-after the transition animation ends.</span>
<span class="c1">/// Use this to clean up after the transition.</span>
<span class="kd">func</span> <span class="nf">transitionDidEnd</span><span class="p">()</span>
<span class="c1">/// The animator needs a UIImageView for the transition;</span>
<span class="c1">/// eg the Photo Detail screen should provide a snapshotView of its image,</span>
<span class="c1">/// and a collectionView should do the same for its image views.</span>
<span class="kd">func</span> <span class="nf">referenceImage</span><span class="p">()</span> <span class="o">-></span> <span class="kt">UIImage</span><span class="p">?</span>
<span class="c1">/// The location onscreen for the imageView provided in `referenceImageView(for:)`</span>
<span class="kd">func</span> <span class="nf">imageFrame</span><span class="p">()</span> <span class="o">-></span> <span class="kt">CGRect</span><span class="p">?</span>
<span class="p">}</span></code></pre></figure>
<p><strong>Fourth</strong>, we’re going to need a way to animate the tab bar up and down.
There’s not (at least, as far as I’m aware) a way to get the system to perform
this animation for free, so I’ve got <code class="language-plaintext highlighter-rouge">LocketTabBarController</code>, a subclass of
<code class="language-plaintext highlighter-rouge">UITabBarController</code>, that performs this animation.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="cm">/** A custom tab-bar-controller that:
- requires that its viewControllers be LocketNavigationControllers,
- keeps its tab bar hidden appropriately
- animates its tab bar in/out nicely
**/</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="kt">LocketTabBarController</span><span class="p">:</span> <span class="kt">UITabBarController</span> <span class="p">{</span>
<span class="kd">func</span> <span class="nf">setTabBar</span><span class="p">(</span>
<span class="nv">hidden</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">,</span>
<span class="nv">animated</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
<span class="n">alongside</span> <span class="nv">animator</span><span class="p">:</span> <span class="kt">UIViewPropertyAnimator</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="p">)</span> <span class="p">{</span>
<span class="c1">// TODO implement tab-bar animation.</span>
<span class="p">}</span>
</code></pre></figure>
<blockquote>
<p>Note: remember last time, how I said we’ll be using <code class="language-plaintext highlighter-rouge">UIViewPropertyAnimator</code>,
because it makes it easy to coordinate animations between view controllers?
This is a perfect example: later in this post, we’ll be coordinating the
tab-bar’s show/hide animation with the rest of the animations, by passing in a
property animator and attaching more animations to it!</p>
</blockquote>
<p>So that’s our high-level pseudocode: from <code class="language-plaintext highlighter-rouge">LocketNavigationController</code>, we’ll
vend a <code class="language-plaintext highlighter-rouge">PhotoDetailPushTransition</code> animation to UIKit. We’ll make our photo-grid
and photo-detail screens conform to <code class="language-plaintext highlighter-rouge">PhotoDetailTransitionAnimatorDelegate</code>. We
also have <code class="language-plaintext highlighter-rouge">LocketTabBarController</code> to show and hide the tab bar appropriately.</p>
<h2 id="filling-in-the-push-animations-details">Filling in the push animation’s details</h2>
<p>Now that we’ve got the broad outline, let’s go back in and finish up those
<code class="language-plaintext highlighter-rouge">TODO</code> bits.</p>
<p><strong>First</strong>, we’re going to fill in a little detail in our
<code class="language-plaintext highlighter-rouge">LocketNavigationController</code>. We need to provide the photo-grid and photo-detail
screen to <code class="language-plaintext highlighter-rouge">PhotoDetailPushTransition</code>, so we can create and animate the
transition-image properly.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">public</span> <span class="kd">func</span> <span class="nf">navigationController</span><span class="p">(</span>
<span class="n">_</span> <span class="nv">navigationController</span><span class="p">:</span> <span class="kt">UINavigationController</span><span class="p">,</span>
<span class="n">animationControllerFor</span> <span class="nv">operation</span><span class="p">:</span> <span class="kt">UINavigationController</span><span class="o">.</span><span class="kt">Operation</span><span class="p">,</span>
<span class="n">from</span> <span class="nv">fromVC</span><span class="p">:</span> <span class="kt">UIViewController</span><span class="p">,</span>
<span class="n">to</span> <span class="nv">toVC</span><span class="p">:</span> <span class="kt">UIViewController</span>
<span class="p">)</span> <span class="o">-></span> <span class="kt">UIViewControllerAnimatedTransitioning</span><span class="p">?</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">result</span><span class="p">:</span> <span class="kt">UIViewControllerAnimatedTransitioning</span><span class="p">?</span>
<span class="k">if</span>
<span class="k">let</span> <span class="nv">photoDetailVC</span> <span class="o">=</span> <span class="n">toVC</span> <span class="k">as?</span> <span class="kt">PhotoDetailViewController</span><span class="p">,</span>
<span class="n">operation</span> <span class="o">==</span> <span class="o">.</span><span class="n">push</span>
<span class="p">{</span>
<span class="n">result</span> <span class="o">=</span> <span class="kt">PhotoDetailPushTransition</span><span class="p">(</span><span class="nv">fromDelegate</span><span class="p">:</span> <span class="n">fromVC</span><span class="p">,</span> <span class="nv">toPhotoDetailVC</span><span class="p">:</span> <span class="n">photoDetailVC</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">result</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="k">self</span><span class="o">.</span><span class="n">currentAnimationTransition</span> <span class="o">=</span> <span class="n">result</span>
<span class="k">return</span> <span class="n">result</span>
<span class="p">}</span></code></pre></figure>
<p><strong>Second</strong>, we’re going to fill in our <code class="language-plaintext highlighter-rouge">PhotoDetailPushTransition</code>. It’s now
going to keep a reference to the photo-grid and photo-detail screens, and we’ll
be able to implement the <code class="language-plaintext highlighter-rouge">animateTransition</code>!</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">public</span> <span class="kd">class</span> <span class="kt">PhotoDetailPushTransition</span><span class="p">:</span> <span class="kt">NSObject</span><span class="p">,</span> <span class="kt">UIViewControllerAnimatedTransitioning</span> <span class="p">{</span>
<span class="kd">fileprivate</span> <span class="k">let</span> <span class="nv">fromDelegate</span><span class="p">:</span> <span class="kt">PhotoDetailTransitionAnimatorDelegate</span>
<span class="kd">fileprivate</span> <span class="k">let</span> <span class="nv">photoDetailVC</span><span class="p">:</span> <span class="kt">PhotoDetailViewController</span>
<span class="c1">/// The snapshotView that is animating between the two view controllers.</span>
<span class="kd">fileprivate</span> <span class="k">let</span> <span class="nv">transitionImageView</span><span class="p">:</span> <span class="kt">UIImageView</span> <span class="o">=</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">imageView</span> <span class="o">=</span> <span class="kt">UIImageView</span><span class="p">()</span>
<span class="n">imageView</span><span class="o">.</span><span class="n">contentMode</span> <span class="o">=</span> <span class="o">.</span><span class="n">scaleAspectFill</span>
<span class="n">imageView</span><span class="o">.</span><span class="n">clipsToBounds</span> <span class="o">=</span> <span class="kc">true</span>
<span class="n">imageView</span><span class="o">.</span><span class="n">accessibilityIgnoresInvertColors</span> <span class="o">=</span> <span class="kc">true</span>
<span class="k">return</span> <span class="n">imageView</span>
<span class="p">}()</span>
<span class="c1">/// If fromDelegate isn't PhotoDetailTransitionAnimatorDelegate, returns nil.</span>
<span class="nf">init</span><span class="p">?(</span>
<span class="nv">fromDelegate</span><span class="p">:</span> <span class="kt">Any</span><span class="p">,</span>
<span class="n">toPhotoDetailVC</span> <span class="nv">photoDetailVC</span><span class="p">:</span> <span class="kt">PhotoDetailViewController</span>
<span class="p">)</span> <span class="p">{</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">fromDelegate</span> <span class="o">=</span> <span class="n">fromDelegate</span> <span class="k">as?</span> <span class="kt">PhotoDetailTransitionAnimatorDelegate</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="k">self</span><span class="o">.</span><span class="n">fromDelegate</span> <span class="o">=</span> <span class="n">fromDelegate</span>
<span class="k">self</span><span class="o">.</span><span class="n">photoDetailVC</span> <span class="o">=</span> <span class="n">photoDetailVC</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">animateTransition</span><span class="p">(</span><span class="n">using</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// As of 2014, you're meant to use .view(forKey:) instead of .viewController(forKey:).view to get the views.</span>
<span class="c1">// (It's not in the original 2013 WWDC talk, but it's in the 2014 one!)</span>
<span class="k">let</span> <span class="nv">toView</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="nf">view</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">to</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">fromView</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="nf">view</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">from</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">containerView</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="n">containerView</span>
<span class="n">toView</span><span class="p">?</span><span class="o">.</span><span class="n">alpha</span> <span class="o">=</span> <span class="mi">0</span>
<span class="c1">// Next, let's add our fromView and toView to the containerView</span>
<span class="p">[</span><span class="n">fromView</span><span class="p">,</span> <span class="n">toView</span><span class="p">]</span>
<span class="o">.</span><span class="n">compactMap</span> <span class="p">{</span> <span class="nv">$0</span> <span class="p">}</span> <span class="c1">// (because these are Optional<UIView>)</span>
<span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">containerView</span><span class="o">.</span><span class="nf">addSubview</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span> <span class="p">}</span>
<span class="c1">// Set up our transition image</span>
<span class="k">let</span> <span class="nv">transitionImage</span> <span class="o">=</span> <span class="n">fromDelegate</span><span class="o">.</span><span class="nf">referenceImage</span><span class="p">()</span><span class="o">!</span>
<span class="n">transitionImageView</span><span class="o">.</span><span class="n">image</span> <span class="o">=</span> <span class="n">transitionImage</span>
<span class="n">containerView</span><span class="o">.</span><span class="nf">addSubview</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">transitionImageView</span><span class="p">)</span>
<span class="c1">// If the from-view doesn't return a frame for the image, we'll figure out one ourselves.</span>
<span class="c1">// In practice, this almost-never happens!</span>
<span class="n">transitionImageView</span><span class="o">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">fromDelegate</span><span class="o">.</span><span class="nf">imageFrame</span><span class="p">()</span>
<span class="p">??</span> <span class="kt">PhotoDetailPushTransition</span><span class="o">.</span><span class="nf">defaultOffscreenFrameForPresentation</span><span class="p">(</span><span class="nv">image</span><span class="p">:</span> <span class="n">transitionImage</span><span class="p">,</span> <span class="nv">forView</span><span class="p">:</span> <span class="n">toView</span><span class="o">!</span><span class="p">)</span>
<span class="c1">// For the photo-detail view controller, it hasn't got a frame yet (because it's not onscreen),</span>
<span class="c1">// so we'll calculate it ourselves - it's just centered in the view!</span>
<span class="k">let</span> <span class="nv">toReferenceFrame</span> <span class="o">=</span> <span class="kt">PhotoDetailPushTransition</span><span class="o">.</span><span class="nf">calculateZoomInImageFrame</span><span class="p">(</span><span class="nv">image</span><span class="p">:</span> <span class="n">transitionImage</span><span class="p">,</span> <span class="nv">forView</span><span class="p">:</span> <span class="n">toView</span><span class="o">!</span><span class="p">)</span>
<span class="c1">// Notify the view controllers that the transition will begin.</span>
<span class="c1">// They'll hide their image views here.</span>
<span class="k">self</span><span class="o">.</span><span class="n">fromDelegate</span><span class="o">.</span><span class="nf">transitionWillStart</span><span class="p">()</span>
<span class="k">self</span><span class="o">.</span><span class="n">photoDetailVC</span><span class="o">.</span><span class="nf">transitionWillStart</span><span class="p">()</span>
<span class="c1">// Now let's animate, using our old friend UIViewPropertyAnimator!</span>
<span class="k">let</span> <span class="nv">duration</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="nf">transitionDuration</span><span class="p">(</span><span class="nv">using</span><span class="p">:</span> <span class="n">transitionContext</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">spring</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mf">0.95</span>
<span class="k">let</span> <span class="nv">animator</span> <span class="o">=</span> <span class="kt">UIViewPropertyAnimator</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="n">duration</span><span class="p">,</span> <span class="nv">dampingRatio</span><span class="p">:</span> <span class="n">spring</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitionImageView</span><span class="o">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">toReferenceFrame</span>
<span class="n">toView</span><span class="p">?</span><span class="o">.</span><span class="n">alpha</span> <span class="o">=</span> <span class="mi">1</span>
<span class="p">}</span>
<span class="c1">// Once the animation is complete, we'll need to clean up.</span>
<span class="n">animator</span><span class="o">.</span><span class="n">addCompletion</span> <span class="p">{</span> <span class="p">(</span><span class="n">position</span><span class="p">)</span> <span class="k">in</span>
<span class="c1">// Remove the transition image</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitionImageView</span><span class="o">.</span><span class="nf">removeFromSuperview</span><span class="p">()</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitionImageView</span><span class="o">.</span><span class="n">image</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="c1">// Tell UIKit we're done with the transition</span>
<span class="n">transitionContext</span><span class="o">.</span><span class="nf">completeTransition</span><span class="p">(</span><span class="o">!</span><span class="n">transitionContext</span><span class="o">.</span><span class="n">transitionWasCancelled</span><span class="p">)</span>
<span class="c1">// Tell our view controllers that we're done, too.</span>
<span class="k">self</span><span class="o">.</span><span class="n">photoDetailVC</span><span class="o">.</span><span class="nf">transitionDidEnd</span><span class="p">()</span>
<span class="k">self</span><span class="o">.</span><span class="n">fromDelegate</span><span class="o">.</span><span class="nf">transitionDidEnd</span><span class="p">()</span>
<span class="p">}</span>
<span class="c1">// ...and here's where we kick off the animation.</span>
<span class="n">animator</span><span class="o">.</span><span class="nf">startAnimation</span><span class="p">()</span>
<span class="p">}</span></code></pre></figure>
<p><strong>Third</strong>, we’re going to need to implement our
<code class="language-plaintext highlighter-rouge">PhotoDetailTransitionAnimatorDelegate</code> protocol in the photo-grid and
photo-detail screen.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">// PhotoDetailViewController.swift</span>
<span class="kd">extension</span> <span class="kt">PhotoDetailViewController</span><span class="p">:</span> <span class="kt">PhotoDetailTransitionAnimatorDelegate</span> <span class="p">{</span>
<span class="kd">func</span> <span class="nf">transitionWillStart</span><span class="p">()</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">imageView</span><span class="o">.</span><span class="n">isHidden</span> <span class="o">=</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">transitionDidEnd</span><span class="p">()</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">imageView</span><span class="o">.</span><span class="n">isHidden</span> <span class="o">=</span> <span class="kc">false</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">referenceImage</span><span class="p">()</span> <span class="o">-></span> <span class="kt">UIImage</span><span class="p">?</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">self</span><span class="o">.</span><span class="n">imageView</span><span class="o">.</span><span class="n">image</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">imageFrame</span><span class="p">()</span> <span class="o">-></span> <span class="kt">CGRect</span><span class="p">?</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">rect</span> <span class="o">=</span> <span class="kt">CGRect</span><span class="o">.</span><span class="nf">makeRect</span><span class="p">(</span><span class="nv">aspectRatio</span><span class="p">:</span> <span class="n">imageView</span><span class="o">.</span><span class="n">image</span><span class="o">!.</span><span class="n">size</span><span class="p">,</span> <span class="nv">insideRect</span><span class="p">:</span> <span class="n">imageView</span><span class="o">.</span><span class="n">bounds</span><span class="p">)</span>
<span class="k">return</span> <span class="n">rect</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">// PhotoGridViewController.swift</span>
<span class="kd">extension</span> <span class="kt">PhotoGridViewController</span><span class="p">:</span> <span class="kt">PhotoDetailTransitionAnimatorDelegate</span> <span class="p">{</span>
<span class="kd">func</span> <span class="nf">transitionWillStart</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// We keep track of the last-selected cell, so we can show/hide it here.</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">lastSelected</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">lastSelectedIndexPath</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
<span class="k">self</span><span class="o">.</span><span class="n">collectionView</span><span class="o">.</span><span class="nf">cellForItem</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">lastSelected</span><span class="p">)?</span><span class="o">.</span><span class="n">isHidden</span> <span class="o">=</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">transitionDidEnd</span><span class="p">()</span> <span class="p">{</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">lastSelected</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">lastSelectedIndexPath</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
<span class="k">self</span><span class="o">.</span><span class="n">collectionView</span><span class="o">.</span><span class="nf">cellForItem</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">lastSelected</span><span class="p">)?</span><span class="o">.</span><span class="n">isHidden</span> <span class="o">=</span> <span class="kc">false</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">referenceImage</span><span class="p">()</span> <span class="o">-></span> <span class="kt">UIImage</span><span class="p">?</span> <span class="p">{</span>
<span class="k">guard</span>
<span class="k">let</span> <span class="nv">lastSelected</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">lastSelectedIndexPath</span><span class="p">,</span>
<span class="k">let</span> <span class="nv">cell</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">collectionView</span><span class="o">.</span><span class="nf">cellForItem</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">lastSelected</span><span class="p">)</span> <span class="k">as?</span> <span class="kt">PhotoGridCell</span>
<span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">cell</span><span class="o">.</span><span class="n">image</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">imageFrame</span><span class="p">()</span> <span class="o">-></span> <span class="kt">CGRect</span><span class="p">?</span> <span class="p">{</span>
<span class="k">guard</span>
<span class="k">let</span> <span class="nv">lastSelected</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">lastSelectedIndexPath</span><span class="p">,</span>
<span class="k">let</span> <span class="nv">cell</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">collectionView</span><span class="o">.</span><span class="nf">cellForItem</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">lastSelected</span><span class="p">)</span>
<span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="k">return</span> <span class="k">self</span><span class="o">.</span><span class="n">collectionView</span><span class="o">.</span><span class="nf">convert</span><span class="p">(</span><span class="n">cell</span><span class="o">.</span><span class="n">frame</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">view</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p><strong>Fourth</strong>, we’ve got to build out the <code class="language-plaintext highlighter-rouge">LocketTabBarController</code>, which animates
the tab bar up-and-down in the animation.</p>
<p>The implementation for the actual hiding-and-showing of the tab bar is from <a href="https://twitter.com/simmelj">Simon Ljungberg</a>, you can read <a href="https://www.iamsim.me/hiding-the-uitabbar-of-a-uitabbarcontroller/">his excellent blog post about it here</a>.</p>
<p>We then integrate it into our animation with the following:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">// back inPhotoDetailPushTransition.animateTransition</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">animateTransition</span><span class="p">(</span><span class="n">using</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// This feels a bit hacky, but: I've got an extension on UIViewController, </span>
<span class="c1">// that returns (self.tabBarController as? LocketTabBarController).</span>
<span class="k">let</span> <span class="nv">fromVCTabBarController</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="nf">viewController</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">from</span><span class="p">)?</span><span class="o">.</span><span class="n">locketTabBarController</span>
<span class="cm">/* <all the setting-up-the-animation-work from above... */</span>
<span class="c1">// ...then right before we animate,</span>
<span class="n">fromVCTabBarController</span><span class="p">?</span><span class="o">.</span><span class="nf">setTabBar</span><span class="p">(</span><span class="nv">hidden</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nv">alongside</span><span class="p">:</span> <span class="n">animator</span><span class="p">)</span>
<span class="c1">// And now, when we kick off our animation, the tab bar animation will occur, too!</span>
<span class="n">animator</span><span class="o">.</span><span class="nf">startAnimation</span><span class="p">()</span></code></pre></figure>
<h2 id="the-pop-animation">The pop animation</h2>
<p>That was quite a bit, right? <strong>Good news! The hard stuff is over</strong> - now we can implement the pop animation; it’s extremely similar to the push one! <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>
<p>The only meaningful differences in the pop transition are:</p>
<ul>
<li>the animation goes the other direction,</li>
<li>the animation has a different timing curve,</li>
<li>there’s some hackery around getting the grid-view to layout before determining the destination frame for the <code class="language-plaintext highlighter-rouge">transitionImageView</code>,</li>
<li>if the destination frame isn’t available, we slide the image off the bottom of the screen (see comments in the code for discussion).</li>
</ul>
<p>Meet <code class="language-plaintext highlighter-rouge">PhotoDetailPopTransition</code>:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">public</span> <span class="kd">class</span> <span class="kt">PhotoDetailPopTransition</span><span class="p">:</span> <span class="kt">NSObject</span><span class="p">,</span> <span class="kt">UIViewControllerAnimatedTransitioning</span> <span class="p">{</span>
<span class="kd">fileprivate</span> <span class="k">let</span> <span class="nv">toDelegate</span><span class="p">:</span> <span class="kt">PhotoDetailTransitionAnimatorDelegate</span>
<span class="kd">fileprivate</span> <span class="k">let</span> <span class="nv">photoDetailVC</span><span class="p">:</span> <span class="kt">PhotoDetailViewController</span>
<span class="c1">/// The snapshotView that is animating between the two view controllers.</span>
<span class="kd">fileprivate</span> <span class="k">let</span> <span class="nv">transitionImageView</span><span class="p">:</span> <span class="kt">UIImageView</span> <span class="o">=</span> <span class="p">{</span><span class="cm">/* etc */</span><span class="p">}</span>
<span class="c1">/// If toDelegate isn't PhotoDetailTransitionAnimatorDelegate, returns nil.</span>
<span class="nf">init</span><span class="p">?(</span>
<span class="nv">toDelegate</span><span class="p">:</span> <span class="kt">Any</span><span class="p">,</span>
<span class="n">fromPhotoDetailVC</span> <span class="nv">photoDetailVC</span><span class="p">:</span> <span class="kt">PhotoDetailViewController</span>
<span class="p">)</span> <span class="p">{</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">toDelegate</span> <span class="o">=</span> <span class="n">toDelegate</span> <span class="k">as?</span> <span class="kt">PhotoDetailTransitionAnimatorDelegate</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="k">self</span><span class="o">.</span><span class="n">toDelegate</span> <span class="o">=</span> <span class="n">toDelegate</span>
<span class="k">self</span><span class="o">.</span><span class="n">photoDetailVC</span> <span class="o">=</span> <span class="n">photoDetailVC</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">transitionDuration</span><span class="p">(</span><span class="n">using</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">?)</span> <span class="o">-></span> <span class="kt">TimeInterval</span> <span class="p">{</span>
<span class="k">return</span> <span class="mf">0.38</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">animateTransition</span><span class="p">(</span><span class="n">using</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">)</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">fromView</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="nf">view</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">from</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">toView</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="nf">view</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">to</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">toVCTabBar</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="nf">viewController</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">to</span><span class="p">)?</span><span class="o">.</span><span class="n">locketTabBarController</span>
<span class="k">let</span> <span class="nv">containerView</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="n">containerView</span>
<span class="k">let</span> <span class="nv">fromReferenceFrame</span> <span class="o">=</span> <span class="n">photoDetailVC</span><span class="o">.</span><span class="nf">imageFrame</span><span class="p">()</span><span class="o">!</span>
<span class="k">let</span> <span class="nv">transitionImage</span> <span class="o">=</span> <span class="n">photoDetailVC</span><span class="o">.</span><span class="nf">referenceImage</span><span class="p">()</span>
<span class="n">transitionImageView</span><span class="o">.</span><span class="n">image</span> <span class="o">=</span> <span class="n">transitionImage</span>
<span class="n">transitionImageView</span><span class="o">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">photoDetailVC</span><span class="o">.</span><span class="nf">imageFrame</span><span class="p">()</span><span class="o">!</span>
<span class="p">[</span><span class="n">toView</span><span class="p">,</span> <span class="n">fromView</span><span class="p">]</span>
<span class="o">.</span><span class="n">compactMap</span> <span class="p">{</span> <span class="nv">$0</span> <span class="p">}</span>
<span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">containerView</span><span class="o">.</span><span class="nf">addSubview</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span> <span class="p">}</span>
<span class="n">containerView</span><span class="o">.</span><span class="nf">addSubview</span><span class="p">(</span><span class="n">transitionImageView</span><span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">photoDetailVC</span><span class="o">.</span><span class="nf">transitionWillStart</span><span class="p">()</span>
<span class="k">self</span><span class="o">.</span><span class="n">toDelegate</span><span class="o">.</span><span class="nf">transitionWillStart</span><span class="p">()</span>
<span class="k">let</span> <span class="nv">duration</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="nf">transitionDuration</span><span class="p">(</span><span class="nv">using</span><span class="p">:</span> <span class="n">transitionContext</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">spring</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mf">0.9</span>
<span class="k">let</span> <span class="nv">animator</span> <span class="o">=</span> <span class="kt">UIViewPropertyAnimator</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="n">duration</span><span class="p">,</span> <span class="nv">dampingRatio</span><span class="p">:</span> <span class="n">spring</span><span class="p">)</span> <span class="p">{</span>
<span class="n">fromView</span><span class="p">?</span><span class="o">.</span><span class="n">alpha</span> <span class="o">=</span> <span class="mi">0</span>
<span class="p">}</span>
<span class="n">animator</span><span class="o">.</span><span class="n">addCompletion</span> <span class="p">{</span> <span class="p">(</span><span class="n">position</span><span class="p">)</span> <span class="k">in</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitionImageView</span><span class="o">.</span><span class="nf">removeFromSuperview</span><span class="p">()</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitionImageView</span><span class="o">.</span><span class="n">image</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="n">transitionContext</span><span class="o">.</span><span class="nf">completeTransition</span><span class="p">(</span><span class="o">!</span><span class="n">transitionContext</span><span class="o">.</span><span class="n">transitionWasCancelled</span><span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">toDelegate</span><span class="o">.</span><span class="nf">transitionDidEnd</span><span class="p">()</span>
<span class="k">self</span><span class="o">.</span><span class="n">photoDetailVC</span><span class="o">.</span><span class="nf">transitionDidEnd</span><span class="p">()</span>
<span class="p">}</span>
<span class="n">toVCTabBar</span><span class="p">?</span><span class="o">.</span><span class="nf">setTabBar</span><span class="p">(</span><span class="nv">hidden</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nv">alongside</span><span class="p">:</span> <span class="n">animator</span><span class="p">)</span>
<span class="n">animator</span><span class="o">.</span><span class="nf">startAnimation</span><span class="p">()</span>
<span class="c1">// HACK: By delaying 0.005s, I get a layout-refresh on the toViewController,</span>
<span class="c1">// which means its collectionview has updated its layout,</span>
<span class="c1">// and our toDelegate?.imageFrame() is accurate, even if</span>
<span class="c1">// the device has rotated. :scream_cat:</span>
<span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="nf">asyncAfter</span><span class="p">(</span><span class="nv">deadline</span><span class="p">:</span> <span class="o">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="mf">0.005</span><span class="p">)</span> <span class="p">{</span>
<span class="n">animator</span><span class="o">.</span><span class="n">addAnimations</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">toReferenceFrame</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">toDelegate</span><span class="o">.</span><span class="nf">imageFrame</span><span class="p">()</span> <span class="p">??</span>
<span class="kt">PhotoDetailPopTransition</span><span class="o">.</span><span class="nf">defaultOffscreenFrameForDismissal</span><span class="p">(</span>
<span class="nv">transitionImageSize</span><span class="p">:</span> <span class="n">fromReferenceFrame</span><span class="o">.</span><span class="n">size</span><span class="p">,</span>
<span class="nv">screenHeight</span><span class="p">:</span> <span class="n">containerView</span><span class="o">.</span><span class="n">bounds</span><span class="o">.</span><span class="n">height</span>
<span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitionImageView</span><span class="o">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">toReferenceFrame</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">/// If we need a "dummy reference frame", let's throw the image off the bottom of the screen.</span>
<span class="c1">/// Photos.app transitions to CGRect.zero, but I don't like that as much.</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">func</span> <span class="nf">defaultOffscreenFrameForDismissal</span><span class="p">(</span>
<span class="nv">transitionImageSize</span><span class="p">:</span> <span class="kt">CGSize</span><span class="p">,</span>
<span class="nv">screenHeight</span><span class="p">:</span> <span class="kt">CGFloat</span>
<span class="p">)</span> <span class="o">-></span> <span class="kt">CGRect</span> <span class="p">{</span>
<span class="k">return</span> <span class="kt">CGRect</span><span class="p">(</span>
<span class="nv">x</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="nv">y</span><span class="p">:</span> <span class="n">screenHeight</span><span class="p">,</span>
<span class="nv">width</span><span class="p">:</span> <span class="n">transitionImageSize</span><span class="o">.</span><span class="n">width</span><span class="p">,</span>
<span class="nv">height</span><span class="p">:</span> <span class="n">transitionImageSize</span><span class="o">.</span><span class="n">height</span>
<span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<h2 id="the-finished-product">The finished product</h2>
<p><em><strong>Nice work!</strong> You’ve now gone through an example of a fairly-complex custom
navigation transition. Thanks for sticking with it!</em> 🎉🎉🎉</p>
<p>At this point, I’d really recommend that you <a href="https://github.com/bryanjclark/devsign-nav-transitions">download the source code</a> and
explore it yourself.</p>
<p><img src="/assets/images/nav-transitions/demo complex push pop.gif" alt="" /></p>
<h2 id="whats-next">What’s next?</h2>
<p>So far, we’ve <a href="https://devsign.co/notes/navigation-transitions-1">learned about the navigation-transition API</a>, built a
<a href="https://devsign.co/notes/navigation-transitions-1">simple modal transition</a>, and now have a more-complex, non-interactive
push transition. In the <a href="https://devsign.co/notes/navigation-transitions-iv">final post</a>, we’ll wrap up this series by building the most-complex one of all: an interactive drag-to-dismiss transition!</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>They’re <em>so similar</em>, in fact, that you might even consider refactoring <code class="language-plaintext highlighter-rouge">PhotoDetailPushTransition</code> and <code class="language-plaintext highlighter-rouge">PhotoDetailPopTransition</code> to be the same class — but I’ll leave that as an exercise to you! <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>This is the third in a series on building navigation transitions for iOS apps. In the first post, we simplified the API and learned about its key protocols. In the second post, we built a simple modal animation, and learned about UIViewPropertyAnimator.Custom Navigation Transitions, Part II: A Simple Modal2019-05-08T00:00:00+00:002019-05-08T00:00:00+00:00http://devsign.co/notes/navigation-transitions-2<p><em>This is the second in a series on navigation transitions. In the <a href="http://devsign.co/notes/navigation-transitions-1">first post</a>, we simplified the API by ignoring most of it, and just composing a few parts.</em></p>
<p><img src="/assets/images/nav-transitions/Slowed Modal Loop.gif" alt="" /></p>
<p>Today, we’ll review the implementation in <a href="http://locket.photos">Locket Photos</a> of a custom navigation transition: a simple, self-contained modal transition. (We’ll get into a much-more-complex transition in a future post.)</p>
<p><a href="https://github.com/bryanjclark/devsign-nav-transitions">You can download the source code here.</a>.</p>
<p>There are two screens in this transition: the photo grid, and the presented modal card. Before Locket’s v1.3 update, I was using a simple crossfade animation, provided by UIKit. It looked like this:</p>
<p><img src="/assets/images/nav-transitions/Modal-Crossfade.gif" alt="" /></p>
<p>This was… <em>okay</em>, but not something I felt super-proud to ship! What I really wanted was the background to fade in, and the card to rise up from the bottom, with a springy feel. On dismissal, I’d want the reverse to occur, with a different animation timing. Here’s what shipped in v1.3:</p>
<p><img src="/assets/images/nav-transitions/Demo-App.gif" alt="" /></p>
<h2 id="composing-the-animation">Composing the Animation</h2>
<p>Remember our diagram from <a href="https://devsign.co/notes/navigation-transitions-1">last time</a>? Let’s build it!</p>
<p><img src="/assets/images/nav-transitions/Modal%20Presentation.png" alt="" /></p>
<p>We’re presenting a modal screen, and we want to customize the animation. The presenting screen doesn’t need to know <em>anything</em> about this custom animation - the modal will handle all of that internally! <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>
<p>In our <em>presenting</em> screen, we call this:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="k">let</span> <span class="nv">modalCard</span> <span class="o">=</span> <span class="kt">ModalCardViewController</span><span class="p">()</span>
<span class="k">self</span><span class="o">.</span><span class="nf">present</span><span class="p">(</span><span class="n">modalCard</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nv">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span></code></pre></figure>
<p>…and the rest is all handled by our ModalCardViewController!</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">public</span> <span class="kd">class</span> <span class="kt">ModalCardViewController</span><span class="p">:</span> <span class="kt">UIViewController</span> <span class="p">{</span>
<span class="nf">init</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">//...</span>
<span class="k">self</span><span class="o">.</span><span class="n">transitioningDelegate</span> <span class="o">=</span> <span class="k">self</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>Next, let’s extend ModalCardViewController so it conforms to our animation protocols: <code class="language-plaintext highlighter-rouge">UIViewControllerTransitioningDelegate</code> and <code class="language-plaintext highlighter-rouge">UIViewControllerAnimatedTransitioning</code>.</p>
<blockquote>
<p><strong>Note:</strong> Today, we’ll be building these protocol conformances <em>directly into the view controller</em>. Yes, the architecture would be cleaner to break them out into a separate class, perhaps called “ModalAnimationController”. However: I’m intentionally keeping everything in one place, to simplify this introductory example. In the more-complex implementation later in this series, we’ll break these animation protocol conformances out into separate classes.) <sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup></p>
</blockquote>
<h2 id="uiviewcontrollertransitioningdelegate">UIViewControllerTransitioningDelegate</h2>
<p>First, we need to implement <code class="language-plaintext highlighter-rouge">UIViewControllerTransitioningDelegate</code> <em>(i.e. “the vending machine that dispenses navigation animations”)</em>. We’ll need to vend an animation controller for the presentation and dismissal transitions.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">extension</span> <span class="kt">ModalCardViewController</span><span class="p">:</span> <span class="kt">UIViewControllerTransitioningDelegate</span> <span class="p">{</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">animationController</span><span class="p">(</span><span class="n">forPresented</span> <span class="nv">presented</span><span class="p">:</span> <span class="kt">UIViewController</span><span class="p">,</span> <span class="nv">presenting</span><span class="p">:</span> <span class="kt">UIViewController</span><span class="p">,</span> <span class="nv">source</span><span class="p">:</span> <span class="kt">UIViewController</span><span class="p">)</span> <span class="o">-></span> <span class="kt">UIViewControllerAnimatedTransitioning</span><span class="p">?</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">result</span> <span class="o">=</span> <span class="p">(</span><span class="n">presented</span> <span class="o">==</span> <span class="k">self</span><span class="p">)</span> <span class="p">?</span> <span class="nv">self</span> <span class="p">:</span> <span class="kc">nil</span>
<span class="n">result</span><span class="p">?</span><span class="o">.</span><span class="n">currentModalTransitionType</span> <span class="o">=</span> <span class="o">.</span><span class="n">presentation</span>
<span class="k">return</span> <span class="n">result</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">animationController</span><span class="p">(</span><span class="n">forDismissed</span> <span class="nv">dismissed</span><span class="p">:</span> <span class="kt">UIViewController</span><span class="p">)</span> <span class="o">-></span> <span class="kt">UIViewControllerAnimatedTransitioning</span><span class="p">?</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">result</span> <span class="o">=</span> <span class="p">(</span><span class="n">dismissed</span> <span class="o">==</span> <span class="k">self</span><span class="p">)</span> <span class="p">?</span> <span class="nv">self</span> <span class="p">:</span> <span class="kc">nil</span>
<span class="n">result</span><span class="p">?</span><span class="o">.</span><span class="n">currentModalTransitionType</span> <span class="o">=</span> <span class="o">.</span><span class="n">dismissal</span>
<span class="k">return</span> <span class="n">result</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>(You’ll note that in this code, I’m setting a <code class="language-plaintext highlighter-rouge">currentModalTransitionType</code> on my presented view controller — this is a little enum I’ve defined, to aid in simplifying the process of figuring out which animation to run for a given transition.<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>)</p>
<p>…and now, all that’s left is to write out the animation code itself!</p>
<h2 id="a-recommendation-for-uiviewpropertyanimator">A recommendation for UIViewPropertyAnimator</h2>
<p>Here’s another <em>big</em> thing that helps with animation transitions: <code class="language-plaintext highlighter-rouge">UIViewPropertyAnimator</code>, introduced in iOS 10. <a href="https://twitter.com/kharrison">Keith Harrison</a> has a <a href="https://useyourloaf.com/blog/quick-guide-to-property-animators/">great write-up on property animators</a>.</p>
<p>Here’s why I recommend this API: I believe that <code class="language-plaintext highlighter-rouge">UIViewPropertyAnimator</code> makes it <em>far easier</em> to build non-trivial custom navigation transitions:</p>
<ul>
<li>it <strong>simplifies coordination</strong> of animations in disparate areas of your code, via <code class="language-plaintext highlighter-rouge">UIViewPropertyAnimator.addAnimations(_)</code>,</li>
<li>you can <strong>modify, cancel, and reverse the animation</strong>, which is essential for interactive, gesture-driven transitions,</li>
<li><strong>timing parameters</strong> are easier to define and use.</li>
</ul>
<p>One of the hardest parts (for me at least!) in building out complex navigation transitions is keeping my code properly scoped: I prefer to keep my subviews <code class="language-plaintext highlighter-rouge">private</code> or <code class="language-plaintext highlighter-rouge">fileprivate</code>, and <code class="language-plaintext highlighter-rouge">UIViewPropertyAnimator</code> makes it much easier to keep ‘em that way. When I’m able to build animations while keeping the guts of my views <code class="language-plaintext highlighter-rouge">private</code>, I’m much happier with the process and the finished product.</p>
<p>For today’s implementation, we could just-as-easily use <code class="language-plaintext highlighter-rouge">UIView.animateWithDuration</code>, but instead, let’s practice using <code class="language-plaintext highlighter-rouge">UIViewPropertyAnimator</code>, so it’s not scary when we see it next time!</p>
<h2 id="writing-the-animations">Writing the animations</h2>
<p>Let’s implement the second protocol, <code class="language-plaintext highlighter-rouge">UIViewControllerAnimatedTransitioning</code> <em>(i.e. “the dang animation itself”)</em>. We’ll write out these animations with <code class="language-plaintext highlighter-rouge">UIViewPropertyAnimator</code>, as recommended above!</p>
<p>We want the background to fade in, and the card to slide up from the bottom — and the reverse when the modal is dismissed. <em>The only thing that differs between presentation and dismissal are the animations’ timing curves</em> — the rest of the code can be shared between the two! Here’s the animation code:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">extension</span> <span class="kt">ModalCardViewController</span><span class="p">:</span> <span class="kt">UIViewControllerAnimatedTransitioning</span> <span class="p">{</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">transitionDuration</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="p">{</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">transitionType</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">currentModalTransitionType</span> <span class="k">else</span> <span class="p">{</span> <span class="nf">fatalError</span><span class="p">()</span> <span class="p">}</span>
<span class="k">switch</span> <span class="n">transitionType</span> <span class="p">{</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">presentation</span><span class="p">:</span>
<span class="k">return</span> <span class="mf">0.44</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">dismissal</span><span class="p">:</span>
<span class="k">return</span> <span class="mf">0.32</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">transitionDuration</span><span class="p">(</span>
<span class="n">using</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">?</span>
<span class="p">)</span> <span class="o">-></span> <span class="kt">TimeInterval</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">transitionDuration</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">animateTransition</span><span class="p">(</span><span class="n">using</span> <span class="nv">transitionContext</span><span class="p">:</span> <span class="kt">UIViewControllerContextTransitioning</span><span class="p">)</span> <span class="p">{</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">transitionType</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">currentModalTransitionType</span> <span class="k">else</span> <span class="p">{</span> <span class="nf">fatalError</span><span class="p">()</span> <span class="p">}</span>
<span class="c1">// Here's the state we'd be in when the card is offscreen</span>
<span class="k">let</span> <span class="nv">cardOffscreenState</span> <span class="o">=</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">offscreenY</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">view</span><span class="o">.</span><span class="n">bounds</span><span class="o">.</span><span class="n">height</span> <span class="o">-</span> <span class="k">self</span><span class="o">.</span><span class="n">cardView</span><span class="o">.</span><span class="n">frame</span><span class="o">.</span><span class="n">minY</span> <span class="o">+</span> <span class="mi">20</span>
<span class="k">self</span><span class="o">.</span><span class="n">cardView</span><span class="o">.</span><span class="n">transform</span> <span class="o">=</span> <span class="kt">CGAffineTransform</span><span class="o">.</span><span class="n">identity</span><span class="o">.</span><span class="nf">translatedBy</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">offscreenY</span><span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">view</span><span class="o">.</span><span class="n">backgroundColor</span> <span class="o">=</span> <span class="o">.</span><span class="n">clear</span>
<span class="p">}</span>
<span class="c1">// ...and here's the state of things when the card is onscreen.</span>
<span class="k">let</span> <span class="nv">presentedState</span> <span class="o">=</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">cardView</span><span class="o">.</span><span class="n">transform</span> <span class="o">=</span> <span class="kt">CGAffineTransform</span><span class="o">.</span><span class="n">identity</span>
<span class="k">self</span><span class="o">.</span><span class="n">view</span><span class="o">.</span><span class="n">backgroundColor</span> <span class="o">=</span> <span class="kt">ModalCardViewController</span><span class="o">.</span><span class="n">overlayBackgroundColor</span>
<span class="p">}</span>
<span class="c1">// We want different animation timing, based on whether we're presenting or dismissing.</span>
<span class="k">let</span> <span class="nv">animator</span><span class="p">:</span> <span class="kt">UIViewPropertyAnimator</span>
<span class="k">switch</span> <span class="n">transitionType</span> <span class="p">{</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">presentation</span><span class="p">:</span>
<span class="n">animator</span> <span class="o">=</span> <span class="kt">UIViewPropertyAnimator</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="n">transitionDuration</span><span class="p">,</span> <span class="nv">dampingRatio</span><span class="p">:</span> <span class="mf">0.82</span><span class="p">)</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">dismissal</span><span class="p">:</span>
<span class="n">animator</span> <span class="o">=</span> <span class="kt">UIViewPropertyAnimator</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="n">transitionDuration</span><span class="p">,</span> <span class="nv">curve</span><span class="p">:</span> <span class="kt">UIView</span><span class="o">.</span><span class="kt">AnimationCurve</span><span class="o">.</span><span class="n">easeIn</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">switch</span> <span class="n">transitionType</span> <span class="p">{</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">presentation</span><span class="p">:</span>
<span class="c1">// We need to add the modal to the view hierarchy,</span>
<span class="c1">// and perform the animation.</span>
<span class="k">let</span> <span class="nv">toView</span> <span class="o">=</span> <span class="n">transitionContext</span><span class="o">.</span><span class="nf">view</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">to</span><span class="p">)</span><span class="o">!</span>
<span class="kt">UIView</span><span class="o">.</span><span class="nf">performWithoutAnimation</span><span class="p">(</span><span class="n">cardOffscreenState</span><span class="p">)</span>
<span class="n">transitionContext</span><span class="o">.</span><span class="n">containerView</span><span class="o">.</span><span class="nf">addSubview</span><span class="p">(</span><span class="n">toView</span><span class="p">)</span>
<span class="n">animator</span><span class="o">.</span><span class="nf">addAnimations</span><span class="p">(</span><span class="n">presentedState</span><span class="p">)</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">dismissal</span><span class="p">:</span>
<span class="c1">// The modal is already in the view hierarchy,</span>
<span class="c1">// so we just perform the animation.</span>
<span class="n">animator</span><span class="o">.</span><span class="nf">addAnimations</span><span class="p">(</span><span class="n">cardOffscreenState</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// When the animation finishes,</span>
<span class="c1">// we tell the system that the animation has completed,</span>
<span class="c1">// and clear out our transition type.</span>
<span class="n">animator</span><span class="o">.</span><span class="n">addCompletion</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span>
<span class="n">transitionContext</span><span class="o">.</span><span class="nf">completeTransition</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">currentModalTransitionType</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="c1">// ... and here's where we kick off the animation:</span>
<span class="n">animator</span><span class="o">.</span><span class="nf">startAnimation</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<h2 id="hand-tuning-the-animations">Hand-tuning the animations</h2>
<p>To make animations truly wonderful, you’ve got to hand-tune ‘em on-device. Animations that look great on your laptop <a href="https://engineering.khanacademy.org/posts/introducing-swifttweaks.htm" title="Introducing SwiftTweaks | Khan Academy Engineering">often feel too slow when in-hand.</a></p>
<p>For this reason, I highly recommend using a tool like <a href="https://github.com/Khan/SwiftTweaks">SwiftTweaks</a> (or for my Obj-C peeps out there, <a href="https://github.com/facebook/Tweaks">Facebook’s original Tweaks tool</a>).</p>
<h2 id="the-finished-product">The finished product</h2>
<p>Today, we’ve composed our very first custom navigation animation - nice work! 🥳</p>
<p>We built a self-contained modal presentation animation (and the dismissal animation), hand-tuned ‘em on-device using SwiftTweaks, and everything’s composed in a way that keeps our modal’s subviews <code class="language-plaintext highlighter-rouge">private</code> and/or <code class="language-plaintext highlighter-rouge">fileprivate </code>. We also learned why <code class="language-plaintext highlighter-rouge">UIViewPropertyAnimator</code> is a really great tool for custom navigation transitions.</p>
<p>You can download the <a href="https://github.com/bryanjclark/devsign-nav-transitions">full source code here</a>, or just read the <a href="https://github.com/bryanjclark/devsign-nav-transitions/blob/master/DevsignNavigationTransitions/Simple%20Transition/ModalCardViewController.swift">ModalCardViewController’s implementation</a>!</p>
<h2 id="whats-next">What’s next?</h2>
<p>There are two more posts in this series. Next, we build out a <a href="https://devsign.co/notes/navigation-transitions-iii">complex, non-interactive push/pop animation</a>, like the one in the Photos app, with all the trimmings and little details you’d want in a polished implementation. Then, in the last post, <a href="https://devsign.co/notes/navigation-transitions-iv">we’ll build Photos’ interactive drag-to-dismiss transition</a>.</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>This is kinda like how <code class="language-plaintext highlighter-rouge">UIAlertController</code> handles its own animated transitions — you just present it from your code, and it handles the animations! <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:2" role="doc-endnote">
<p>Think of it like building a <code class="language-plaintext highlighter-rouge">UITableView</code>: sure, you can (and often should!) break out <code class="language-plaintext highlighter-rouge">UITableViewDelegate</code> and <code class="language-plaintext highlighter-rouge">UITableViewDataSource</code> from the table’s view controller — but you and I both know that it’s often completely fine to start by building them in the view controller’s code, unless-and-until we feel it’s in need of a refactor! 😘 <a href="#fnref:2" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:3" role="doc-endnote">
<p>This is like a modal-animation-transition version of <a href="https://developer.apple.com/documentation/uikit/uinavigationcontroller/operation"><code class="language-plaintext highlighter-rouge">UINavigationController.Operation</code></a>. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>This is the second in a series on navigation transitions. In the first post, we simplified the API by ignoring most of it, and just composing a few parts.Custom Navigation Transitions, Part I: An Introduction2019-04-26T00:00:00+00:002019-04-26T00:00:00+00:00http://devsign.co/notes/navigation-transitions-1<p><em>This is the first in a series on navigation transitions. To kick things off,
let’s get a simple preview of the big-moving-pieces in a navigation
transition. In future posts, I’ll write about how I’ve come to think about
these APIs and compose them in non-trivial iOS implementations, like the ones in
my app, <a href="http://locket.photos">Locket Photos</a>.</em></p>
<p><img src="/assets/images/nav-transitions/custom-transitions-full.gif" alt="preview of different animation transitions" /></p>
<p>Every project has one: a feature that will make a big contribution to the
quality of the product, but the first steps of that implementation aren’t
immediately obvious, so you kick that task down the road, because the costs seem
too high.</p>
<p>For me (and many other iOS peeps), custom navigation transitions are <em>totally</em>
one of those daunting features. They were introduced with iOS 7, are important
for <a href="https://medium.com/elepath-exports/spatial-interfaces-886bccc5d1e9">spatially explaining your app’s
layout</a>.
They’re an affordance for the fluid, interruptible gestures that make it a
breeze to float around iOS. If you want your app to feel excellent
in-hand, then you’re going to want to implement these APIs — but it’s hard to
know where to start, and even harder to know how to avoid a glitchy, jittery
result.</p>
<p>Implementing a non-trivial, non-glitchy transition is a challenge. The <a href="https://developer.apple.com/videos/play/wwdc2013/218/">WWDC
videos are confusing</a>, Apple didn’t
provide source code with them, and while there are many “Hello World” tutorials
out there, I find that these implementations get
tangled in a non-trivial implementation. When that happens, I’m often
inclined to revert my changes and say “well, I tried it, but
the effort required is just too much for me right now” — and then the feature
never gets built.</p>
<p>I know for a <em>fact</em> that I’m extremely-not-alone in finding these APIs to be so
intimidating. And yet! <em>I believe there’s a way to describe these APIs in a way that might
help.</em></p>
<p>The APIs for this are powerful and flexible, and for good reason: you can
compose them in a variety of ways to suit your app’s architecture and design
needs. However, this flexibility also includes a lot of upfront complexity,
with a large cast of characters — so before we write code, let’s simplify the
shape of the API.</p>
<h2 id="simplifying-the-api-documentation">Simplifying the API Documentation</h2>
<p>At first glance, <a href="https://developer.apple.com/documentation/uikit/animation_and_haptics/view_controller_transitions">the
documentation</a>
for view controller transitions is scary. There are many protocols, and it’s not
immediately obvious how they relate to each other — or how they fit into your
app’s architecture:</p>
<figure class="highlight"><pre><code class="language-text" data-lang="text">protocol UIViewControllerTransitioningDelegate
protocol UIViewControllerAnimatedTransitioning
protocol UIViewControllerInteractiveTransitioning
protocol UIViewImplicitlyAnimating
protocol UIViewControllerTransitionCoordinator
protocol UIViewControllerTransitionCoordinatorContext </code></pre></figure>
<p>That’s a <em>lot</em> of protocols! It’s not clear where to start, the names are
tricky, there are “ing” suffixes everywhere, and to make things harder, these
protocols bounce around in a complicated dance — <a href="https://developer.apple.com/videos/play/wwdc2013/218/">here are a few screenshots
from the WWDC video that introduces these
APIs</a>:</p>
<p><img src="/assets/images/nav-transitions/WWDC Videos.png" alt="" /></p>
<p>Those are some scary-looking diagrams! I still find them to be head-scratchers.</p>
<p>Let’s simplify this list of protocols: we’ll remove some, drop the goofy “ing”
suffixes<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>, and add some informal descriptions:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">UIViewControllerTransitionDelegate</code>: tells the system that you want a custom
transition.</li>
<li><code class="language-plaintext highlighter-rouge">UIViewControllerAnimatedTransition</code>: the transition animation.</li>
<li><code class="language-plaintext highlighter-rouge">UIViewControllerInteractiveTransition</code>: an <em>interactive</em> transition.</li>
</ul>
<p>That’s a little easier to understand, right? Now that we’ve got these three principal protocols, we’ll want to talk about
which classes in your app will conform to them. Where does the animation live -
is it in your <code class="language-plaintext highlighter-rouge">UIViewController</code>, or do you need to build a new class to control
the transition?</p>
<p>The answer is: <em>it depends</em> — so how can we make ourselves more comfortable in
taking our first step here? I suggest we compare two implementations in Locket
— and in future posts, we’ll walk through implementing them, including the
little details.</p>
<h2 id="lockets-transitions">Locket’s Transitions</h2>
<p>Currently, <a href="http://locket.photos">Locket</a> has two custom transition animations: a simple, self-contained
modal presentation, and a complex one that mimics Apple’s Photos app. I’ve
included half-speed GIFs of the transitions, so you can see the details
in-action.</p>
<h3 id="simple-modal-transition">Simple Modal Transition</h3>
<p>First, let’s talk about the simple modal transition for its Select Date screen.
In this transition, the card slides-up with a springy overshoot. When dismissed,
it falls offscreen. Here’s a half-speed look at the animation:</p>
<p><img src="/assets/images/nav-transitions/Slowed Modal Loop.gif" alt="" /></p>
<p>Here’s a rough diagram of how the view controller manages its own modal
presentation animation:</p>
<p><img src="/assets/images/nav-transitions/Modal%20Presentation.png" alt="" /></p>
<p>Do you see how <em>all of the guts of the presentation are self-contained</em>? You
could present this view controller from anywhere, and it’d still have the custom
animation — because it’s its own <code class="language-plaintext highlighter-rouge">transitioningDelegate</code>, and conforms to the
appropriate protocols internally. (I imagine that <code class="language-plaintext highlighter-rouge">UIAlertController</code> has a
similarly self-contained architecture, but I can’t say for sure!)</p>
<h3 id="complex-interactive-transition">Complex, Interactive Transition</h3>
<p>Here’s a more complicated composition, for Locket’s custom push, pop, and
interactive-pop transitions. This half-speed GIF cycles through nearly-all of the
features of this transition; I’ll outline them all below.</p>
<p><img src="/assets/images/nav-transitions/Interactive transition Final.gif" alt="" /></p>
<p>In the above GIF, you see:</p>
<ul>
<li><strong>the selected image scales up</strong> from its square thumbnail to its position on the
photo-detail screen,</li>
<li>there’s an <strong>interactive, cancellable gesture</strong> to exit the screen,</li>
<li><strong>when drag-dismissing</strong>, the image scales down during the interactive portion of
the transition,</li>
<li>if you page around in the photo-detail screen, and pop out, the
newly-selected <strong>image goes back to its proper spot</strong>,</li>
<li>and the <strong>tab bar</strong> slides down (and then back up again when you pop out)</li>
</ul>
<p>There are a few more details that aren’t included in this GIF:</p>
<ul>
<li>if the photo-detail screen is “focused” (that is, just the photo on a black
background) the transition works properly,</li>
<li>if you’ve scrolled in the photo-detail carousel to an image that’s
offscreen in the grid-view, the grid-view scrolls that thumbnail into frame
before the transition begins,</li>
<li>when you dismiss-drag a photo, its z-index is behind the tab bar,</li>
<li>there’s a non-interactive pop gesture (that you get by tapping the
back-button),</li>
<li>these animations work on a non-“notch” iPhone,</li>
<li>and the animation timings were hand-tuned on-device using
<a href="https://github.com/khan/swifttweaks">SwiftTweaks</a>.</li>
</ul>
<p><em>Phew!</em> That was a long list… but for good reason! These details are essential
to a proper, non-trivial implementation of navigation transitions. When any of
them were missing, the animations felt glitchy. Paying attention to these
details is what makes them feel invisible and intuitive.</p>
<p>So: how are these transitions built? Here’s that same
high-level diagram:</p>
<p><img src="/assets/images/nav-transitions/Interactive%20Navigation%20Transition.png" alt="" /></p>
<p>Let’s break this diagram down:</p>
<ul>
<li>The <code class="language-plaintext highlighter-rouge">LocketNavigationController</code> vends different transition controllers — and
the transitions don’t know much about the other view controllers in the app.</li>
<li>The gesture that drives the interactive-pop transition belongs to the Photo
Detail screen.</li>
<li>You can see (at a high-level) where the protocols are implemented — there
are three types of transition animation (push, pop, and interactive pop).</li>
</ul>
<p><em>(And yes! I know these diagrams are way-too-abstract to be a meaningful
first-step in making your implementation — I’m removing a bunch of detail here
so you can see the bigger picture. We’ll get to the details in future posts!)</em></p>
<h2 id="wrapping-it-up">Wrapping it up</h2>
<p>In this post, we’ve talked about how scary it can feel to approach these APIs -
you’re not alone! We’ve also discussed some initial footholds for you to climb
this API, with some diagrams to (hopefully) make it feel less-scary. At this
point, I still haven’t provided any code, so you’re probably not feeling ready
to dive in and implement this in your own app — so please consider this a
preamble to a larger discussion of how to implement these transition animations.</p>
<h2 id="whats-next">What’s Next?</h2>
<p>In this four-part series, we’ll learn how to build some non-trivial navigation transitions from <a href="https://locket.photos">Locket Photos</a>. I hope they’ll be a useful reference for you, and make building custom navigation transitions as familiar as making a <code class="language-plaintext highlighter-rouge">UICollectionView</code>.</p>
<p>So: let’s keep going! In the <a href="https://devsign.co/notes/navigation-transitions-2">next post</a>, we build the simple, self-contained modal transition.</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>Thanks to <a href="https://twitter.com/davedelong">Dave DeLong</a> for this suggestion! <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>This is the first in a series on navigation transitions. To kick things off, let’s get a simple preview of the big-moving-pieces in a navigation transition. In future posts, I’ll write about how I’ve come to think about these APIs and compose them in non-trivial iOS implementations, like the ones in my app, Locket Photos.Throttle & Debounce2017-07-18T00:00:00+00:002017-07-18T00:00:00+00:00http://devsign.co/notes/throttle-and-debounce<p>In the past couple years, I’ve been working with <a href="https://reactivecocoa.io">Reactive Cocoa (RAC)</a> in my daily work, which makes you think about values changing over time.</p>
<p>(<em>Wait, wait, don’t leave!</em> This isn’t going to be a highly-technical post - I’m gonna try to explore a bit of this thinking-about-time stuff here in a way that should be palatable! I think that designers and developers would benefit from thinking about it.)</p>
<p>One neat trick that RAC simplifies is throttling information when a value is changing super-often. In these cases, it’s often helpful to throttle these changes, so that the app doesn’t look frantic or waste data.</p>
<p>For example, you might throttle a user’s search term in a text field, so that a query is only performed every half-second, to avoid having the search results refresh frantically while making soon-to-be-discarded API calls.</p>
<p>You might have noticed that iOS throttles how often you can take screenshots: if you take one, you can’t take another for a few seconds. I suspect this is to prevent accidental duplicates.</p>
<h2 id="comparing-throttle-and-debounce">Comparing throttle and debounce</h2>
<p>Recently, I learned about Debounce, which is kinda like Throttle’s cousin. They are similar in some respects, but are suitable for very different use cases:</p>
<ul>
<li>
<p><strong>Throttle</strong> says “this value is changing rapidly, I want to at most accept a new value every 0.5 seconds”.</p>
</li>
<li>
<p><strong>Debounce</strong> says “this value is changing rapidly, I want to wait until things haven’t changed for 0.5 seconds and then perform an action.”</p>
</li>
</ul>
<p>See the differences?</p>
<ul>
<li><em>Throttle</em> gives you the first value immediately.</li>
<li>
<p><em>Debounce</em> always has a delay.</p>
</li>
<li><em>Throttle</em> isn’t always the most recent value.</li>
<li>
<p><em>Debounce</em> is always the most recent value when it fires.</p>
</li>
<li><em>Throttle</em> feels faster and more responsive.</li>
<li><em>Debounce</em> feels more accurate.</li>
</ul>
<p>Here’s a diagram to compare ‘em:
<img src="/assets/images/throttle-and-debounce.png" alt="comparing throttle and debounce" />
<em>See how throttle “suppresses events for an amount of time”, and debounce “waits until things calm down”?</em></p>
<p>Both keep your app from being too frantic; they accomplish this in different ways. Let’s talk about when each would be appropriate!</p>
<h2 id="different-tools-for-different-use-cases">Different tools for different use cases</h2>
<p>In the above examples (search fields and screenshots) we talked about throttle. What other situations use throttle/debounce, and which one is right for the job?</p>
<p>If you’ve got a syncing app, it might be helpful to debounce syncs until local changes have settled, to help reduce the number of sync API calls. For example, it probably doesn’t make sense to sync your last-page-read in iBooks every time you turn the page; it likely makes more sense to debounce a minute or two. If you’re editing a text file, you might want to debounce syncing until editing has slowed down, to reduce the number of sync-merges that your API has to handle.</p>
<p>Debounce also appears when taking screenshots, but in a different spot: iOS debounces the hardware button presses to make the “I meant to press these buttons simultaneously” gesture work. If the home button is pressed, there’s a very-brief moment where the system waits to see if the sleep-wake button is pressed too, indicating a screenshot.</p>
<p>Debounce makes another appearance in home button clicking: if you’ve got triple-click for accessibility enabled, the system must debounce when you double-click for multitasking. (Try it yourself! Toggling triple-click changes how quickly iOS enters multitasking mode on a double click.)</p>
<p>I’d bet that the QuickType keyboard suggestions in iOS are also debounced. When you type quickly, they seem to not update until there’s a pause.</p>
<h2 id="real-world-use-cases">Real-world use cases</h2>
<p>A potential case for throttle and debounce is in notifications. For example, look at this video of a popular Instagram user’s experience:</p>
<iframe width="400" height="225" src="https://www.youtube.com/embed/zGl796352RI" frameborder="0" allowfullscreen=""></iframe>
<p>Those notifications are flying in! Wouldn’t it be better if Instagram used throttle and debounce to consolidate them? (Perhaps they could use a default debounce of 1 second, with a throttle of 5 seconds - and then as the firehose continues, these values could increase, so a popular user might get a single summary notification at the end of each hour with the number of interactions that had occurred.)</p>
<p>Or, let’s go back to our search results example. Search-as-you-type is a great feature, and using a small throttle helps keep the UI from getting too frantic. However, on a poor or expensive network connection, you might want to increase the throttle delay, or switch to debouncing — these would reduce the data costs without degrading the experience.</p>
<p>If you were making a music-streaming app, you might slightly debounce the skip song button - so if somebody clicks it several times to skip ahead, you don’t bother fetching/playing the intermediate songs.</p>
<hr />
<p>So, designers, why did I drag you through all of this, too?</p>
<p><strong>If you have these concepts in your vocabulary, you’ll be able to think of places to use them in your designs, and have the ability to express them to your team so they can build ‘em into the real product.</strong></p>
<p>I only very-recently learned about debounce, and I kinda wish I had known about it earlier! Depending on your design tools, throttle and debounce might be tricky to describe, and developers and PMs don’t always have the insight or budget to build them unless they’re requested, prioritized, and designed-for up front.</p>
<p><strong>So: can you think of ways that throttle and debounce might be useful in your design?</strong> (Some ideas: Where are values changing quickly? Where do you need to wait-and-check before deciding between two gestures the user is making? Where are things annoyingly repetitive? When does your UI get frantic?)</p>In the past couple years, I’ve been working with Reactive Cocoa (RAC) in my daily work, which makes you think about values changing over time.Hiding the Volume UI on iOS2016-03-10T00:00:00+00:002016-03-10T00:00:00+00:00http://devsign.co/notes/custom-volume-adjustment-on-ios<p>The system-standard volume-adjustment UI on iOS is obvious and intuitive - but it’s also obnoxious: it blocks the center of the screen, and can’t be dismissed:</p>
<p><img src="/assets/images/little-vine-workout-dude.png" alt="the custom volume-adjustment UI on iOS" /></p>
<p><em>Me too, lil fella. Me too.</em></p>
<p>Snapchat has a custom volume-adjustment UI (up in the status bar area). It’s a critical feature: if you miss a video on there, you can’t replay it!</p>
<p>It’s the kind of little detail that’s especially nice in apps like Vine or Tweetbot, where videos tend to be short and sweet, so the volume overlay really torches the experience.</p>
<p>However, it’s not 100% clear how you could go about suppressing the system-standard volume-adjustment UI. Fortunately, <a href="https://twitter.com/theandreamazz">Andrea Mazzini</a>’s already done the hard work for us by creating <a href="https://github.com/andreamazz/SubtleVolume">SubtleVolume</a>. Here’s how it looks in-action:
<img src="/assets/images/SubtleVolume.png" alt="what subtle volume looks like" /></p>The system-standard volume-adjustment UI on iOS is obvious and intuitive - but it’s also obnoxious: it blocks the center of the screen, and can’t be dismissed:Creating Focus Effects in tvOS2016-03-01T00:00:00+00:002016-03-01T00:00:00+00:00http://devsign.co/notes/custom-focus-effects-in-tvos<p><strong>UPDATE:</strong> <a href="https://developer.apple.com/videos/play/wwdc2017/209/?time=1080">tvOS 11 now supports non-roundrect buttons</a>! You don’t need to do this any more to get circular buttons — yay! However: if you’re still interested in building your own Focus Effect by using <code class="language-plaintext highlighter-rouge">UIInterpolatingMotionEffect</code>, read on!</p>
<hr />
<p>In my current side-project, I’m making a tvOS app. One of the screens features a handful of circular buttons. Unfortunately, when the tvOS Focus Engine renders these buttons, they get really awful shadows and focus states, that look a bit like this:</p>
<p><img src="/assets/images/tvos-bad-focus-effect.png" alt="tvOS doesn't properly handle non-roundrect shapes" /></p>
<h5 id="the-system-standard-uibutton-looks-awful-with-circular-images">The system-standard UIButton looks awful with circular images.</h5>
<p>I had the good fortune of attending Apple’s tvOS Tech Talk in Seattle this winter - which meant I got to ask a few folks in the Q&A lab! They told me that, unfortunately, tvOS doesn’t allow non-roundrect focus effects: the system-standard focused state are pretty “simple” and aren’t clever enough (yet) to alter their shape depending on the alpha-channel of the image in the <code class="language-plaintext highlighter-rouge">UIButton</code>.</p>
<p>We needed to roll our own custom focus effect - but fortunately, it turned out to be pretty straightforward!</p>
<p>Here’s what the finished product looks like:</p>
<p><img src="/assets/images/round+tvos+buttons.gif" alt="custom tvos buttons" /></p>
<h5 id="ah-much-better-apologies-for-the-gif-compression-its-rasterizing-the-shadows">Ah, much better! (Apologies for the GIF compression; it’s rasterizing the shadows.)</h5>
<p>You can find the <a href="https://github.com/bryanjclark/devsign-CustomFocusEffect">sample code for this project on GitHub</a> - but read on to learn what’s under the hood!</p>
<h2 id="whats-in-a-focus-effect">What’s in a Focus Effect?</h2>
<p>There’s a ton going on in the system-standard Focus Effect:</p>
<ul>
<li>a scale transform that makes the button appear larger,</li>
<li>a shadow that makes the control appear to lift off the screen,</li>
<li>a white glare that reflects a “light source” across the surface, probably a masked blurry white circle,</li>
<li>a parallax tilt and shift that rotates the button in 3D, to provide continuous gestural feedback as you nudge your finger slightly on the trackpad,</li>
<li>and if the view contains a layered image, there’s a neat 3D parallax effect there, too.</li>
</ul>
<p>Put that all together, and you get this effect:</p>
<p><img src="/assets/images/apple-tvos-focus-effect.gif" alt="gif of apple's tvos effect" /></p>
<h5 id="source-apples-tvos-hig">Source: <a href="https://developer.apple.com/tvos/human-interface-guidelines/">Apple’s tvOS HIG</a></h5>
<h2 id="creating-the-focus-effect">Creating the Focus Effect</h2>
<p>From the above list, we know we’re going to need some transforms, a shadow, a parallax tilt, a parallax shift, a parallax “white glare”, and a parallax layered image. <em>(Note: for my project’s design, I don’t need the glare or layered image - but I’ll have a footnote below outlining what I might try if I were building those effects.)</em></p>
<p>Really, there are two subgroups here: - a “focused style” (“what does this view look like when it’s focused?”) - and a “parallax style” (“how does this view tilt and shift around when it’s focused?”)</p>
<h3 id="focusedstyle">FocusedStyle</h3>
<p>Let’s start by creating a FocusedStyle:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">public</span> <span class="kd">struct</span> <span class="kt">FocusedStyle</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">transform</span><span class="p">:</span> <span class="kt">CGAffineTransform</span>
<span class="k">let</span> <span class="nv">shadowColor</span><span class="p">:</span> <span class="kt">CGColor</span>
<span class="k">let</span> <span class="nv">shadowOffset</span><span class="p">:</span> <span class="kt">CGSize</span>
<span class="k">let</span> <span class="nv">shadowRadius</span><span class="p">:</span> <span class="kt">CGFloat</span>
<span class="p">}</span></code></pre></figure>
<h3 id="customfocusableviewtype">CustomFocusableViewType</h3>
<p>We can then create a protocol that allows any view or control to render with this style:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">public</span> <span class="kd">protocol</span> <span class="kt">CustomFocusableViewType</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">view</span><span class="p">:</span> <span class="kt">UIView</span> <span class="p">{</span> <span class="k">get</span> <span class="p">}</span>
<span class="k">var</span> <span class="nv">focusedStyle</span><span class="p">:</span> <span class="kt">FocusedStyle</span> <span class="p">{</span> <span class="k">get</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="kd">extension</span> <span class="kt">CustomFocusableViewType</span> <span class="p">{</span>
<span class="kd">func</span> <span class="nf">displayAsFocused</span><span class="p">(</span><span class="nv">focused</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">)</span> <span class="p">{</span>
<span class="n">view</span><span class="o">.</span><span class="n">layer</span><span class="o">.</span><span class="n">shadowOpacity</span> <span class="o">=</span> <span class="n">focused</span> <span class="p">?</span> <span class="mi">1</span> <span class="p">:</span> <span class="mi">0</span>
<span class="n">view</span><span class="o">.</span><span class="n">transform</span> <span class="o">=</span> <span class="n">focused</span> <span class="p">?</span> <span class="n">focusedStyle</span><span class="o">.</span><span class="nv">transform</span> <span class="p">:</span> <span class="kt">CGAffineTransformIdentity</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<h3 id="customfocusablebutton">CustomFocusableButton</h3>
<p>Now let’s create a <code class="language-plaintext highlighter-rouge">CustomFocusableButton</code> that conforms to our <code class="language-plaintext highlighter-rouge">CustomFocusableViewType</code> protocol. There’s a bit of code here to make the “select” animation work (when you click down on a button) - but outside of that, there’s not much here:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">/// An implementation of CustomParallaxView,</span>
<span class="c1">/// implements a pressDown state when Select is clicked.</span>
<span class="c1">/// Particularly useful for non-roundrect button shapes.</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="kt">CustomFocusableButton</span><span class="p">:</span> <span class="kt">UIButton</span> <span class="p">{</span>
<span class="kd">public</span> <span class="k">let</span> <span class="nv">focusedStyle</span><span class="p">:</span> <span class="kt">FocusedStyle</span>
<span class="kd">public</span> <span class="nf">init</span><span class="p">(</span><span class="nv">focusedStyle</span><span class="p">:</span> <span class="kt">FocusedStyle</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">focusedStyle</span> <span class="o">=</span> <span class="n">focusedStyle</span>
<span class="k">super</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">frame</span><span class="p">:</span> <span class="kt">CGRectZero</span><span class="p">)</span>
<span class="n">view</span><span class="o">.</span><span class="n">layer</span><span class="o">.</span><span class="n">shadowColor</span> <span class="o">=</span> <span class="n">focusedStyle</span><span class="o">.</span><span class="n">shadowColor</span>
<span class="n">view</span><span class="o">.</span><span class="n">layer</span><span class="o">.</span><span class="n">shadowOffset</span> <span class="o">=</span> <span class="n">focusedStyle</span><span class="o">.</span><span class="n">shadowOffset</span>
<span class="n">view</span><span class="o">.</span><span class="n">layer</span><span class="o">.</span><span class="n">shadowRadius</span> <span class="o">=</span> <span class="n">focusedStyle</span><span class="o">.</span><span class="n">shadowRadius</span>
<span class="p">}</span>
<span class="kd">required</span> <span class="kd">public</span> <span class="nf">init</span><span class="p">?(</span><span class="n">coder</span> <span class="nv">aDecoder</span><span class="p">:</span> <span class="kt">NSCoder</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">fatalError</span><span class="p">(</span><span class="s">"init(coder:) has not been implemented"</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// MARK: Animating selection - buttons should shrink when clicked.</span>
<span class="kd">public</span> <span class="k">override</span> <span class="kd">func</span> <span class="nf">pressesBegan</span><span class="p">(</span>
<span class="nv">presses</span><span class="p">:</span> <span class="kt">Set</span><span class="o"><</span><span class="kt">UIPress</span><span class="o">></span><span class="p">,</span>
<span class="n">withEvent</span> <span class="nv">event</span><span class="p">:</span> <span class="kt">UIPressesEvent</span><span class="p">?</span>
<span class="p">)</span> <span class="p">{</span>
<span class="k">super</span><span class="o">.</span><span class="nf">pressesBegan</span><span class="p">(</span><span class="n">presses</span><span class="p">,</span> <span class="nv">withEvent</span><span class="p">:</span> <span class="n">event</span><span class="p">)</span>
<span class="c1">// If you press multiple buttons at the same time,</span>
<span class="c1">// that shouldn't trigger a pressDown() animation.</span>
<span class="k">guard</span> <span class="n">presses</span><span class="o">.</span><span class="n">count</span> <span class="o">==</span> <span class="mi">1</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="k">for</span> <span class="n">press</span> <span class="k">in</span> <span class="n">presses</span> <span class="k">where</span> <span class="n">press</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="o">.</span><span class="kt">Select</span> <span class="p">{</span>
<span class="nf">pressDown</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="k">override</span> <span class="kd">func</span> <span class="nf">pressesEnded</span><span class="p">(</span>
<span class="nv">presses</span><span class="p">:</span> <span class="kt">Set</span><span class="o"><</span><span class="kt">UIPress</span><span class="o">></span><span class="p">,</span>
<span class="n">withEvent</span> <span class="nv">event</span><span class="p">:</span> <span class="kt">UIPressesEvent</span><span class="p">?</span>
<span class="p">)</span> <span class="p">{</span>
<span class="k">super</span><span class="o">.</span><span class="nf">pressesEnded</span><span class="p">(</span><span class="n">presses</span><span class="p">,</span> <span class="nv">withEvent</span><span class="p">:</span> <span class="n">event</span><span class="p">)</span>
<span class="k">for</span> <span class="n">press</span> <span class="k">in</span> <span class="n">presses</span> <span class="k">where</span> <span class="n">press</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="o">.</span><span class="kt">Select</span> <span class="p">{</span>
<span class="nf">pressUp</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="k">override</span> <span class="kd">func</span> <span class="nf">pressesCancelled</span><span class="p">(</span>
<span class="nv">presses</span><span class="p">:</span> <span class="kt">Set</span><span class="o"><</span><span class="kt">UIPress</span><span class="o">></span><span class="p">,</span>
<span class="n">withEvent</span> <span class="nv">event</span><span class="p">:</span> <span class="kt">UIPressesEvent</span><span class="p">?</span>
<span class="p">)</span> <span class="p">{</span>
<span class="k">super</span><span class="o">.</span><span class="nf">pressesCancelled</span><span class="p">(</span><span class="n">presses</span><span class="p">,</span> <span class="nv">withEvent</span><span class="p">:</span> <span class="n">event</span><span class="p">)</span>
<span class="k">for</span> <span class="n">press</span> <span class="k">in</span> <span class="n">presses</span> <span class="k">where</span> <span class="n">press</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="o">.</span><span class="kt">Select</span> <span class="p">{</span>
<span class="nf">pressUp</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">private</span> <span class="kd">func</span> <span class="nf">pressDown</span><span class="p">()</span> <span class="p">{</span>
<span class="kt">UIView</span><span class="o">.</span><span class="nf">animateWithDuration</span><span class="p">(</span><span class="mf">0.1</span><span class="p">,</span>
<span class="nv">delay</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span>
<span class="nv">usingSpringWithDamping</span><span class="p">:</span> <span class="mf">0.9</span><span class="p">,</span>
<span class="nv">initialSpringVelocity</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span>
<span class="nv">options</span><span class="p">:</span> <span class="o">.</span><span class="kt">BeginFromCurrentState</span><span class="p">,</span>
<span class="nv">animations</span><span class="p">:</span> <span class="p">{</span> <span class="p">()</span> <span class="o">-></span> <span class="kt">Void</span> <span class="k">in</span>
<span class="k">self</span><span class="o">.</span><span class="nf">displayAsFocused</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span>
<span class="p">},</span> <span class="nv">completion</span><span class="p">:</span> <span class="kc">nil</span>
<span class="p">)</span>
<span class="p">}</span>
<span class="kd">private</span> <span class="kd">func</span> <span class="nf">pressUp</span><span class="p">()</span> <span class="p">{</span>
<span class="kt">UIView</span><span class="o">.</span><span class="nf">animateWithDuration</span><span class="p">(</span><span class="mf">0.2</span><span class="p">,</span>
<span class="nv">delay</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="nv">usingSpringWithDamping</span><span class="p">:</span> <span class="mf">0.9</span><span class="p">,</span>
<span class="nv">initialSpringVelocity</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="nv">options</span><span class="p">:</span> <span class="o">.</span><span class="kt">BeginFromCurrentState</span><span class="p">,</span>
<span class="nv">animations</span><span class="p">:</span> <span class="p">{</span> <span class="p">()</span> <span class="o">-></span> <span class="kt">Void</span> <span class="k">in</span>
<span class="k">self</span><span class="o">.</span><span class="nf">displayAsFocused</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
<span class="p">},</span> <span class="nv">completion</span><span class="p">:</span> <span class="kc">nil</span>
<span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">extension</span> <span class="kt">CustomFocusableButton</span><span class="p">:</span> <span class="kt">CustomFocusableViewType</span> <span class="p">{</span>
<span class="kd">public</span> <span class="k">var</span> <span class="nv">view</span><span class="p">:</span> <span class="kt">UIView</span> <span class="p">{</span> <span class="k">return</span> <span class="k">self</span> <span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<h3 id="parallaxstyle">ParallaxStyle</h3>
<p>Next, let’s make a <code class="language-plaintext highlighter-rouge">ParallaxStyle</code>, which wraps our <code class="language-plaintext highlighter-rouge">FocusedStyle</code> along with the <code class="language-plaintext highlighter-rouge">UIInterpolatingMotionEffect</code>s that compose into a parallax effect when you slide your thumb around.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">/// Represents the tilting & shifting parallax effect</span>
<span class="c1">/// when you nudge your thumb slightly on a focused UIView</span>
<span class="kd">public</span> <span class="kd">struct</span> <span class="kt">ParallaxStyle</span> <span class="p">{</span>
<span class="c1">/// The focused appearance for a view</span>
<span class="k">let</span> <span class="nv">focusStyle</span><span class="p">:</span> <span class="kt">FocusedStyle</span>
<span class="c1">/// The max amount by which center.x will shift.</span>
<span class="c1">/// Use a negative number for a reverse effect.</span>
<span class="k">let</span> <span class="nv">shiftHorizontal</span><span class="p">:</span> <span class="kt">Double</span>
<span class="c1">/// The max amount by which center.y will shift.</span>
<span class="c1">/// Use a negative number for a reverse effect.</span>
<span class="k">let</span> <span class="nv">shiftVertical</span><span class="p">:</span> <span class="kt">Double</span>
<span class="c1">/// The max amount by which the view will rotate side-to-side, in radians.</span>
<span class="c1">/// Use a negative number for a reverse effect.</span>
<span class="k">let</span> <span class="nv">tiltHorizontal</span><span class="p">:</span> <span class="kt">Double</span>
<span class="c1">/// The max amount by which the view will rotate up-and-down, in radians.</span>
<span class="c1">/// Use a negative number for a reverse effect.</span>
<span class="k">let</span> <span class="nv">tiltVertical</span><span class="p">:</span> <span class="kt">Double</span>
<span class="k">var</span> <span class="nv">motionEffectGroup</span><span class="p">:</span> <span class="kt">UIMotionEffectGroup</span> <span class="p">{</span>
<span class="kd">func</span> <span class="nf">toRadians</span><span class="p">(</span><span class="nv">degrees</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Double</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">degrees</span> <span class="o">*</span> <span class="kt">M_PI_2</span> <span class="o">/</span> <span class="mi">180</span>
<span class="p">}</span>
<span class="k">let</span> <span class="nv">shiftX</span> <span class="o">=</span> <span class="kt">UIInterpolatingMotionEffect</span><span class="p">(</span><span class="nv">keyPath</span><span class="p">:</span> <span class="s">"center.x"</span><span class="p">,</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="kt">TiltAlongHorizontalAxis</span><span class="p">)</span>
<span class="n">shiftX</span><span class="o">.</span><span class="n">minimumRelativeValue</span> <span class="o">=</span> <span class="o">-</span><span class="n">shiftHorizontal</span>
<span class="n">shiftX</span><span class="o">.</span><span class="n">maximumRelativeValue</span> <span class="o">=</span> <span class="n">shiftHorizontal</span>
<span class="k">let</span> <span class="nv">shiftY</span> <span class="o">=</span> <span class="kt">UIInterpolatingMotionEffect</span><span class="p">(</span><span class="nv">keyPath</span><span class="p">:</span> <span class="s">"center.y"</span><span class="p">,</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="kt">TiltAlongVerticalAxis</span><span class="p">)</span>
<span class="n">shiftY</span><span class="o">.</span><span class="n">minimumRelativeValue</span> <span class="o">=</span> <span class="o">-</span><span class="n">shiftVertical</span>
<span class="n">shiftY</span><span class="o">.</span><span class="n">maximumRelativeValue</span> <span class="o">=</span> <span class="n">shiftVertical</span>
<span class="k">let</span> <span class="nv">rotateX</span> <span class="o">=</span> <span class="kt">UIInterpolatingMotionEffect</span><span class="p">(</span><span class="nv">keyPath</span><span class="p">:</span> <span class="s">"layer.transform.rotation.y"</span><span class="p">,</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="kt">TiltAlongHorizontalAxis</span><span class="p">)</span>
<span class="n">rotateX</span><span class="o">.</span><span class="n">minimumRelativeValue</span> <span class="o">=</span> <span class="nf">toRadians</span><span class="p">(</span><span class="o">-</span><span class="n">tiltHorizontal</span><span class="p">)</span>
<span class="n">rotateX</span><span class="o">.</span><span class="n">maximumRelativeValue</span> <span class="o">=</span> <span class="nf">toRadians</span><span class="p">(</span><span class="n">tiltHorizontal</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">rotateY</span> <span class="o">=</span> <span class="kt">UIInterpolatingMotionEffect</span><span class="p">(</span><span class="nv">keyPath</span><span class="p">:</span> <span class="s">"layer.transform.rotation.x"</span><span class="p">,</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="kt">TiltAlongVerticalAxis</span><span class="p">)</span>
<span class="n">rotateY</span><span class="o">.</span><span class="n">minimumRelativeValue</span> <span class="o">=</span> <span class="nf">toRadians</span><span class="p">(</span><span class="o">-</span><span class="n">tiltVertical</span><span class="p">)</span>
<span class="n">rotateY</span><span class="o">.</span><span class="n">maximumRelativeValue</span> <span class="o">=</span> <span class="nf">toRadians</span><span class="p">(</span><span class="n">tiltVertical</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">motionGroup</span> <span class="o">=</span> <span class="kt">UIMotionEffectGroup</span><span class="p">()</span>
<span class="n">motionGroup</span><span class="o">.</span><span class="n">motionEffects</span> <span class="o">=</span> <span class="p">[</span><span class="n">shiftX</span><span class="p">,</span> <span class="n">shiftY</span><span class="p">,</span> <span class="n">rotateX</span><span class="p">,</span> <span class="n">rotateY</span><span class="p">]</span>
<span class="k">return</span> <span class="n">motionGroup</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<h3 id="customfocuseffectcoordinator">CustomFocusEffectCoordinator</h3>
<p>So far, we’ve got <code class="language-plaintext highlighter-rouge">FocusedStyle</code>, <code class="language-plaintext highlighter-rouge">ParallaxStyle</code>, and a custom <code class="language-plaintext highlighter-rouge">UIButton</code> that implements <code class="language-plaintext highlighter-rouge">CustomFocusableViewType</code> and animates its <code class="language-plaintext highlighter-rouge">UIControlState.Selected</code> properly. Now we need a way to link up our <code class="language-plaintext highlighter-rouge">UIMotionEffectGroup</code> and <code class="language-plaintext highlighter-rouge">FocusedStyle</code> to the <code class="language-plaintext highlighter-rouge">didUpdateFocusInContext(_:withAnimationCoordinator:)</code> function in our UIView.</p>
<p>Enter the <code class="language-plaintext highlighter-rouge">CustomFocusEffectCoordinator</code>:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">/// Manages the intersection of UIMotionEffects</span>
<span class="c1">/// and the tvOS Focus Engine, to provide a nice</span>
<span class="c1">/// parallax/focus effect on custom controls.</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="kt">CustomFocusEffectCoordinator</span> <span class="p">{</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">views</span><span class="p">:</span> <span class="kt">Set</span><span class="o"><</span><span class="kt">UIView</span><span class="o">></span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">motionEffectGroup</span><span class="p">:</span> <span class="kt">UIMotionEffectGroup</span><span class="o"></</span><span class="n">uiview</span><span class="o">></span>
<span class="kd">public</span> <span class="nf">init</span><span class="p">(</span><span class="nv">views</span><span class="p">:</span> <span class="p">[</span><span class="kt">UIView</span><span class="p">],</span> <span class="nv">parallaxStyle</span><span class="p">:</span> <span class="kt">ParallaxStyle</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">views</span> <span class="o">=</span> <span class="kt">Set</span><span class="p">(</span><span class="n">views</span><span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">motionEffectGroup</span> <span class="o">=</span> <span class="n">parallaxStyle</span><span class="o">.</span><span class="n">motionEffectGroup</span>
<span class="p">}</span>
<span class="c1">/// Call this function within your `didUpdateFocusInContext` method</span>
<span class="c1">/// to create a parallax effect!</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">updateFromContext</span><span class="p">(</span>
<span class="nv">context</span><span class="p">:</span> <span class="kt">UIFocusUpdateContext</span><span class="p">,</span>
<span class="n">withAnimationCoordinator</span> <span class="nv">coordinator</span><span class="p">:</span> <span class="kt">UIFocusAnimationCoordinator</span>
<span class="p">)</span> <span class="p">{</span>
<span class="n">coordinator</span><span class="o">.</span><span class="nf">addCoordinatedAnimations</span><span class="p">({</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">previousView</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">previouslyFocusedView</span> <span class="k">as?</span> <span class="kt">CustomFocusableViewType</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">self</span><span class="o">.</span><span class="n">views</span><span class="o">.</span><span class="nf">contains</span><span class="p">(</span><span class="n">previousView</span><span class="o">.</span><span class="n">view</span><span class="p">)</span> <span class="p">{</span>
<span class="n">previousView</span><span class="o">.</span><span class="nf">displayAsFocused</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span>
<span class="n">previousView</span><span class="o">.</span><span class="n">view</span><span class="o">.</span><span class="nf">removeMotionEffect</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">motionEffectGroup</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">nextView</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">nextFocusedView</span> <span class="k">as?</span> <span class="kt">CustomFocusableViewType</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">self</span><span class="o">.</span><span class="n">views</span><span class="o">.</span><span class="nf">contains</span><span class="p">(</span><span class="n">nextView</span><span class="o">.</span><span class="n">view</span><span class="p">)</span> <span class="p">{</span>
<span class="n">nextView</span><span class="o">.</span><span class="nf">displayAsFocused</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
<span class="n">nextView</span><span class="o">.</span><span class="n">view</span><span class="o">.</span><span class="nf">addMotionEffect</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">motionEffectGroup</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">},</span> <span class="nv">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">/// When you're ready to tear down the effect, call this function.</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">removeMotionEffectsFromAllViews</span><span class="p">()</span> <span class="p">{</span>
<span class="n">views</span><span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="nv">$0</span><span class="o">.</span><span class="nf">removeMotionEffect</span><span class="p">(</span><span class="n">motionEffectGroup</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>This class has the following responsibilities: - Links a set of <code class="language-plaintext highlighter-rouge">CustomFocusableViewType</code>s to a <code class="language-plaintext highlighter-rouge">ParallaxStyle</code>, - Provides an easy way to update from a <code class="language-plaintext highlighter-rouge">UIFocusUpdateContext</code> and <code class="language-plaintext highlighter-rouge">UIFocusAnimationCoordinator</code> in the parent view. - Provides a way to tear down the effect.</p>
<h3 id="implementing-in-our-uiview--uiviewcontroller">Implementing in our UIView / UIViewController</h3>
<p>Now for the fun part: hooking it all together in the <code class="language-plaintext highlighter-rouge">UIViewController</code> or <code class="language-plaintext highlighter-rouge">UIView</code>!</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">class</span> <span class="kt">MyView</span><span class="p">:</span> <span class="kt">UIView</span> <span class="p">{</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">viewData</span><span class="p">:</span> <span class="kt">MyViewData</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">customButtonOne</span> <span class="o">=</span> <span class="kt">CustomFocusableButton</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">customButtonTwo</span> <span class="o">=</span> <span class="kt">CustomFocusableButton</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">viewData</span><span class="p">:</span> <span class="kt">MyViewData</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">viewData</span> <span class="o">=</span> <span class="n">viewData</span>
<span class="k">let</span> <span class="nv">focusedStyle</span> <span class="o">=</span> <span class="kt">FocusedStyle</span><span class="p">(</span>
<span class="nv">transform</span><span class="p">:</span> <span class="kt">CGAffineTransformMakeScale</span><span class="p">(</span><span class="mf">1.1</span><span class="p">,</span> <span class="mf">1.1</span><span class="p">),</span>
<span class="nv">shadowColor</span><span class="p">:</span> <span class="kt">UIColor</span><span class="o">.</span><span class="nf">blackColor</span><span class="p">()</span><span class="o">.</span><span class="nf">colorWithAlphaComponent</span><span class="p">(</span><span class="mf">0.3</span><span class="p">)</span><span class="o">.</span><span class="kt">CGColor</span><span class="p">,</span>
<span class="nv">shadowOffset</span><span class="p">:</span> <span class="kt">CGSize</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">16</span><span class="p">),</span>
<span class="nv">shadowRadius</span><span class="p">:</span> <span class="mi">25</span>
<span class="p">)</span>
<span class="k">let</span> <span class="nv">parallaxStyle</span> <span class="o">=</span> <span class="kt">ParallaxStyle</span><span class="p">(</span>
<span class="nv">shiftHorizontal</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
<span class="nv">shiftVertical</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
<span class="nv">tiltHorizontal</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
<span class="nv">tiltVertical</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
<span class="nv">focusStyle</span><span class="p">:</span> <span class="n">focusedStyle</span>
<span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">focusEffectCoordinator</span> <span class="o">=</span> <span class="kt">CustomFocusEffectCoordinator</span><span class="p">(</span>
<span class="nv">views</span><span class="p">:</span> <span class="p">[</span><span class="n">customButtonOne</span><span class="p">,</span> <span class="n">customButtonTwo</span><span class="p">],</span>
<span class="nv">parallaxStyle</span><span class="p">:</span> <span class="n">parallaxStyle</span>
<span class="p">)</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="k">override</span> <span class="kd">func</span> <span class="nf">didUpdateFocusInContext</span><span class="p">(</span>
<span class="nv">context</span><span class="p">:</span> <span class="kt">UIFocusUpdateContext</span><span class="p">,</span>
<span class="n">withAnimationCoordinator</span> <span class="nv">coordinator</span><span class="p">:</span> <span class="kt">UIFocusAnimationCoordinator</span>
<span class="p">)</span> <span class="p">{</span>
<span class="n">focusEffectCoordinator</span><span class="o">.</span><span class="nf">updateFromContext</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="nv">withAnimationCoordinator</span><span class="p">:</span> <span class="n">coordinator</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>Ta-da! Now you have a nice Focus behavior on your custom views - and since it’s all composed by little protocols and style-structs, you can easily tailor the focus and parallax behavior to suit your needs.</p>
<p><img src="/assets/images/round+tvos+buttons.gif" alt="custom tvos buttons" /></p>
<h2 id="one-last-thing">One Last Thing</h2>
<p>In my current project, we didn’t have a need for the white gloss or layered image aspects of the focused state. Using the above code, you could probably extend <code class="language-plaintext highlighter-rouge">ParallaxStyle</code> and <code class="language-plaintext highlighter-rouge">CustomFocusableViewType</code> to implement these behaviors.</p>
<p>But, a word of caution: if you’re going to implement these bits, spend extra time polishing ‘em, because it’s no good when custom UI attempts to mimic the native platform but gets stuck in the <a href="https://en.wikipedia.org/wiki/Uncanny_valley">Uncanny Valley</a>. If you don’t fully dial-in the gloss and layered image effects, you may find that your UI feels a bit out-of-place, like a non-native app running on iOS.</p>
<p>Enjoy!</p>UPDATE: tvOS 11 now supports non-roundrect buttons! You don’t need to do this any more to get circular buttons — yay! However: if you’re still interested in building your own Focus Effect by using UIInterpolatingMotionEffect, read on!Grading Your App with a Weighted GPA2016-02-23T00:00:00+00:002016-02-23T00:00:00+00:00http://devsign.co/notes/grading-an-app-with-a-weighted-gpa<p>In the <a href="/notes/everybody-is-an-edge-case">last post</a>, I wrote about how the standard “P1-P4” bug-management workflow has its perks, but often misses the forest for the trees. For that reason, it’s helpful to get a different perspective on our work. by measuring it against a different ruler.</p>
<p>A couple of years ago, my team was trying to figure out how to advocate for quality and polish in our app. The “design nitpick P4s” were piling up, and we felt that marred the legibility, usability, and branding of the product - but each individual bug was so minor, they tended to lose out when compared one-on-one with other issues.</p>
<p>We came up with a new metric: rather than debate the merits of each individual bug, what if we “graded” the components of the app?</p>
<hr />
<h2 id="scoring-your-app">Scoring Your App</h2>
<p>Imagine each part your app as a course in school: each part has its relative weight (or “credits”), and a grade: an “A” gets a 4.0, an “A-“ gets a 3.7, a “B+” gets a 3.3, a “B” gets a 3.0, and so on.</p>
<p>What’s important for your app? For example, if the first impression really matters, give extra weight to the empty and unauthenticated states.</p>
<p>To calculate the score for a given part, you multiply its credits times the grade earned. Then, sum up the weighted scores, and divide by the total number of credits. There’s your weighted GPA!</p>
<p>Here’s a little table to illustrate:</p>
<table>
<thead>
<tr>
<th>Component</th>
<th style="text-align: center">Grade</th>
<th style="text-align: center">Weight</th>
<th style="text-align: center">Weighted Grade</th>
</tr>
</thead>
<tbody>
<tr>
<td>Recipe List: Empty State</td>
<td style="text-align: center">3.0</td>
<td style="text-align: center">7</td>
<td style="text-align: center">21</td>
</tr>
<tr>
<td>Recipe List: Full State</td>
<td style="text-align: center">2.7</td>
<td style="text-align: center">6</td>
<td style="text-align: center">16.2</td>
</tr>
<tr>
<td>Recipe List: Edit Mode</td>
<td style="text-align: center">2.3</td>
<td style="text-align: center">4</td>
<td style="text-align: center">9.2</td>
</tr>
<tr>
<td>Recipe Details: Cooking Mode</td>
<td style="text-align: center">3.7</td>
<td style="text-align: center">6</td>
<td style="text-align: center">22.2</td>
</tr>
<tr>
<td>Sign In: “Happy Path”</td>
<td style="text-align: center">3.7</td>
<td style="text-align: center">8</td>
<td style="text-align: center">29.6</td>
</tr>
<tr>
<td>Sign In: Error State</td>
<td style="text-align: center">4.0</td>
<td style="text-align: center">7</td>
<td style="text-align: center">28</td>
</tr>
<tr>
<td>Sign In: Forgot Password</td>
<td style="text-align: center">3.3</td>
<td style="text-align: center">4</td>
<td style="text-align: center">13.2</td>
</tr>
<tr>
<td>Sign Up</td>
<td style="text-align: center">3.7</td>
<td style="text-align: center">7</td>
<td style="text-align: center">25.9</td>
</tr>
</tbody>
<tbody>
<tr>
<td><em>Totals:</em></td>
<td style="text-align: center"> </td>
<td style="text-align: center"><em>53</em></td>
<td style="text-align: center"><em>177.3</em></td>
</tr>
</tbody>
<tbody>
<tr>
<td><strong>Weighted GPA:</strong></td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center"><strong>3.35</strong></td>
</tr>
</tbody>
</table>
<p>Nice, right? You’ve now got an important-feeling score that quantifies how you <em>feel</em> about the overall experience of using the product. This score really helped my team explain how the small bugs were adding up.</p>
<h2 id="give-constructive-feedback">Give Constructive Feedback</h2>
<p><strong>Be really thoughtful with how you score and share this information.</strong> Grades are a touchy thing, and if you tell somebody their part got an “F” you need to consider how to deliver that feedback in a constructive way. So, uh, do that! This isn’t The Sanctioned Way Your Team Measures Things™, so don’t be pushy.</p>
<p><strong>Consider building this GPA with others</strong> - you could, for example, each assign credits and grades to a shared list of app components, and consolidate the collected scores - that way everyone can weigh in!</p>
<p><strong>Make specific, addressable notes</strong> for each component’s grade, that way you have a punchlist to work through! (Better yet, have links to the related bugs in whatever tracking system you use!)</p>
<h2 id="revisit-it-over-time">Revisit It Over Time</h2>
<p>Recalculate the grade every few months. We found it helpful (and motivating) to show the team how a little bit of polishing work went a long way in improving the overall look and feel of the product.</p>In the last post, I wrote about how the standard “P1-P4” bug-management workflow has its perks, but often misses the forest for the trees. For that reason, it’s helpful to get a different perspective on our work. by measuring it against a different ruler.Eventually, Everybody’s An Edge Case2016-02-18T00:00:00+00:002016-02-18T00:00:00+00:00http://devsign.co/notes/everybody-is-an-edge-case<p>Let’s say you’re making A Thing For Humans To Use. It has Many Features, and that Sometimes, Bugs Are Discovered.</p>
<p>In many organizations, bugs are organized by priority:</p>
<ul>
<li><strong>P1 (“Showstopper”)</strong> - a critical issue that must be fixed before any P2’s. Often a terrible crasher or something that prevents beta-testing.</li>
<li><strong>P2 (“Must-Have”)</strong> - an issue that must be fixed before the next release.</li>
<li><strong>P3 (“Below-The-Line”)</strong> - an issue that, while important, shouldn’t hold up a release. These are often edge-case issues.</li>
<li><strong>P4 (“Nice-To-Have”)</strong> - usually nit-fixes and pixel-nudges - aren’t prioritized over other bugs, and often aren’t prioritized over new features.</li>
</ul>
<p>This prioritization is important - but as with any system, what gets measured is what matters, and sometimes, those P3’s and P4’s really add up. While individual bugs matter, your users experience the product as a cohesive whole, and many will abandon it after one or two bugs mar their experience.</p>
<p>It’s important that bugs represent individual units of work for developers and testers - so bugs often get broken down into small, specific components, and teams fix those components.</p>
<p>However, this breaking-down of bugs often means that design bugs are filed as P4 issues - they aren’t crashing the app, and each individual design issue only affects one part of one screen.</p>
<hr />
<h2 id="how-do-small-bugs-add-up">How Do Small Bugs Add Up?</h2>
<p>Let’s say <code class="language-plaintext highlighter-rouge">bug1</code> appears, and it affects 10% of the users, so we decide it’s an “edge case”. This’d be a “P3” (or “Priority 3”) bug - not something worth delaying a release. Here’s what the odds of hitting <code class="language-plaintext highlighter-rouge">bug1</code> look like for a user:</p>
<p><img src="/assets/images/edge-case-1.png" alt="diagram of one 10-percent bug" /></p>
<p>What happens if <code class="language-plaintext highlighter-rouge">bug2</code> appears, and it <em>also</em> affects 10% of your users? Here’s our graph, now with two dimensions - your odds are now 19% that you’ll hit a bug:</p>
<p><img src="/assets/images/edge-case-2.png" alt="diagram of two 10-percent bugs" /></p>
<p>What if <code class="language-plaintext highlighter-rouge">bug3</code> shows up, and it <em>also</em> affects 10% of your users? At this point, we’d have to start drawing 3D diagrams, so Let’s Use Math Instead™:</p>
<p><code class="language-plaintext highlighter-rouge">oddsOfHittingABug = 1 - (bugProbability ^ bugCount)</code></p>
<p>What are the odds you’ll hit a bug?</p>
<table>
<thead>
<tr>
<th style="text-align: left">Bug count</th>
<th style="text-align: center">Odds you’ll see one</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">1</td>
<td style="text-align: center">10%</td>
</tr>
<tr>
<td style="text-align: left">2</td>
<td style="text-align: center">19%</td>
</tr>
<tr>
<td style="text-align: left">3</td>
<td style="text-align: center">27%</td>
</tr>
<tr>
<td style="text-align: left">4</td>
<td style="text-align: center">34%</td>
</tr>
<tr>
<td style="text-align: left">5</td>
<td style="text-align: center">41%</td>
</tr>
<tr>
<td style="text-align: left">6</td>
<td style="text-align: center">47%</td>
</tr>
<tr>
<td style="text-align: left">7</td>
<td style="text-align: center">52%</td>
</tr>
<tr>
<td style="text-align: left">8</td>
<td style="text-align: center">57%</td>
</tr>
<tr>
<td style="text-align: left">9</td>
<td style="text-align: center">61%</td>
</tr>
<tr>
<td style="text-align: left">10</td>
<td style="text-align: center">65%</td>
</tr>
</tbody>
</table>
<p>(Of course, this is an oversimplification - bugs are often not independently distributed. FWIW, I got the idea for this math from page 100 of <a href="http://www.amazon.com/The-Beginning-Infinity-Explanations-Transform/dp/0143121359">The Beginning of Infinity</a>.)</p>
<hr />
<h2 id="an-infestation-of-small-bugs">An Infestation of Small bugs</h2>
<p>When I was a designer, I’d often find many design bugs filed as P4 - things that were too small to bother fixing with urgency. This was <em>really</em> frustrating! We’d spend time making pixel-perfect designs, and then wind up shipping a product with glaring typography, layout, animation, and copywriting bugs.</p>
<p>Putting our Product Manager Hats on, these bugs are often goofy little giblets. “Look”, you say, “We built an app that talks to the server, gets a result back, and renders it - that is so much! <a href="https://www.youtube.com/watch?v=q8LaT5Iiwo4">Why complain about these little nits?</a>”</p>
<p>At some point, all these little tiny bugs form this Nasty Hive that really mucks up the experience:</p>
<ul>
<li>A mound of small bugs makes it harder for users to trust you with their data, their money, and their time.</li>
<li>Designer morale drops - why bother making pixel-perfect comps, style guides, animations, or filing bugs if they’re never going to get fixed?</li>
<li>Engineers, QA, and Product Managers want to build something beautiful, too! When your culture doesn’t value design bugs, it means that engineering slacks on implementing the design, and QA stops looking for them. After all, if design bugs aren’t important, why waste time on them?</li>
</ul>
<hr />
<h2 id="calling-in-an-exterminator">Calling In An Exterminator</h2>
<p>So, what can we do about this hive-of-a-thousand-P4-bugs?</p>
<ul>
<li><strong>Create a meta-bug</strong>, called “We Have Lots Of P4s”, give it a P2 or P3 priority, and let a few team members go off and tackle the bugs for a fixed amount of time. You’ll be surprised how much this can polish things up and boost morale.</li>
<li><strong>Have an occasional “nit-fix” week</strong> (say, once a quarter?), where you let team members fix whatever bugs are bothering them.</li>
<li><strong>Use tools that help avoid P4s in the first place</strong>, like a color / font palette in your code (more on this in a future post).</li>
<li><strong>Involve the team in the bug-prioritization</strong>, so they understand and accept the rationale.</li>
<li><strong>What’s your Design GPA?</strong> How important is each screen? What is each screen’s grade (A, B, C, etc)? Combining these two bits of information gives you a weighted “Design GPA” - a different measurement of your product that provides a different measurement of the design bugs in your app. Maybe those 5 P4 bugs on the checkout screen really are a collective problem!</li>
</ul>
<p>The P1-P4 prioritization system works great for many reasons, but it disincentivizes teams from polishing up the design of a product. <a href="https://duckduckgo.com/?q=what+gets+measured+gets+done">What Gets Measured Gets Done</a> - take time to measure things differently every once in awhile.</p>Let’s say you’re making A Thing For Humans To Use. It has Many Features, and that Sometimes, Bugs Are Discovered.