Adding Delight to your iOS App

Description: iOS contains powerful technologies you can use to make your app truly delightful. Learn how to take your app to the next level with easy-to-implement features such as Handoff and External Display support. Preserve that feeling of magic in your app with pro-tips that combine animations, gestures and layout, while keeping your scrolling smooth, and your code scalable. Dive into the anatomy of a launch to get your app responsive quickly, and learn some great debugging tricks from the pros!

This topic is split in six topics:

  • External Display Support
  • Layout-Driven UI
  • Laser-Fast Launches
  • Smooth Scrolling
  • Continuing with Continuity
  • Debugging Like a Pro

External Display Support

  • As we know we screen mirroring on the apple tv we replicate our screen to the tv display.
  • It would be much nicer to have custom screens for example this in game:
  • When adding external display support, you should keep in mind that what is displayed on the phone is personal/private, while what is in the screen is public.
  • Also the external display is not interactive, only the phone one is.
  • When designing for external display, there are three topics to focus on:
    • Connectivity
      • How do you know that there's a screen connected?
        • UIScreen.screens is an array of screens: if > 1 you have at least one external
        • We also get notifications when a new screen get connected/disconnected via:
UIScreen.didConnectNotification
UIScreen.didDisconnectNotification
        • What to do on connected and disconnected?
        • Connected:
if let externalScreen = UIScreen.screens.last {
  externalWindow = UIWindow()
  externalWindow.screen = externalScreen
  configureExternalWindow(externalWindow)
  externalWindow.isHidden = false
} 
        • Disconnected:
externalWindow.isHidden = true
externalWindow = nil
    • Behavior
      • Update your app behavior (lots of if-else whether we have an external display or not)
    • Transitions
      • Make sure that the transitions between one screen to two and viceversa is graceful

Layout-Driven UI

  • Find and track state that affects UI
  • Dirty layout when state changes with setNeedsLayout()
  • Update UI with state in layoutSubviews()
class MyView: UIView { 
  // ... 
  let coolView = CoolView()
  var feelingCool = true { 
    didSet { 
      setNeedsLayout() 
    }
  }

  override func layoutSubviews() { 
    super.layoutSubviews()
    coolView.isHidden = !feelingCool 
  }
}

Laser-Fast Launches

What can we do to improve the app launches?

  • Process Forking
  • This is taken care by iOS, nothing we can do
  • Dynamic Linking
    • What we do here is:
      • Allocating memory for execution
      • Linking libraries and frameworks
      • Initialization of Swift, Objective-C, Foundation, etc.
      • Static object initialization
      • 40–50% of typical app launch time
    • What we can do is:
      • Avoid code duplication
      • Limit use of third-party libraries
      • Avoid static initializers
  • UI Construction
    • What we do here is:
      • Prepare UI
      • State restoration
      • Load preferences
      • Load model data
    • What we can do here is:
      • Return quickly from:
        • application(_:willFinishLaunchingWithOptions:)
        • application(_:didFinishLaunchingWithOptions:)
        • applicationDidBecomeActive(_:)
      • Avoid writing to disk
      • Avoid loading very large data sets
      • Check database hygiene
  • First Frame
    • What we do here is:
      • Core Animation renders your first frame
      • Text drawing
      • Image loading and decompression
    • What we can do here is:
      • Only prepare the UI you need
      • Avoid hiding views and layers
    • Extended Launch Actions

Smooth Scrolling

Why the app stutters?

  • Too much computation
    • Instrument's Time Profile will help you with that (suggested session: Using Time Profiler in Instruments)
    • Solutions:
      • use UICollectionView and UITableView pre-fetching (suggested session: What's New in UICollectionView in iOS 10)
      • Push some work off the main queue:
        • network and file system access
        • Image drawing
        • Text sizing:
          • UIGraphicsImageRenderer, and its distributed string, both have functions available that are safe to use on a background thread, that might just help you move some of that complex computation off of your main queue.
  • Complex Graphics
    • Instrument's Core Animation will help you with that (session: Advanced Graphics and Animations for iOS Apps)
    • What do we mean by complex graphics? Probably Visual Effects (blur & vibrancy, avoid especially having blur over blur over blur) and Masking (and clipping)
    • Solutions:
      • Use opaque instead of blur/mask

Continuing with Continuity

  • Everything is built on top of NSUserActivity, basically every device broadcast the activity it is doing to all other devices (of the same user) in the vicinity. This makes an icon appear (on the dock on a mac, on the app switcher on the iPhone) and, if you tap it, iOS/macOS will launch that app and call. func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool in your app delegate, passing the activity that was publicized by the other user (note that this takes a few seconds in iOS 10/11, should be much faster in iOS 12).
  • This is an example of NSUserActivity:
// NSUserActivity 
// Originating Device 

let activity = NSUserActivity(activityType:"com.apple.developer.video")
activity.title = "Adding, Delight to your iOS App"
activity.isEligibleForHandoff = true
activity.userInfo = ["session-id": "2018-223", "currentTime": 2340]
userActivity = activity 

In the userInfo dictionary you must put all the necessary information that will enable the user/app to continue the activity in another device. the last line assign the activity to the ViewController userActivity, making it really the current activity

  • On the receiving device, this is what we need to do: note how the first function is called right away when the user touches the icon (on the app switcher or the dock), while the second, as I said above, is called after a few seconds with the whole NSUserActivity payload (it doesn't matter how little info there is inside, it still will take a while 😑, in my apps I just put two or four strings of less than 20 chars each, and it still takes forever...)
  • If we need more info that it can be fit in a dictionary, you can use NSUserActivity Continuation Streams this stream allow a continuous talk between devices, but we need to do this fast, as probably the user is moving the two devices apart (like continuing the activity from a mac to an iPad because the user has to leave home)
  • Document based apps get this NSUserActivity for free
  • If you don't have a mac app, you can hand off to a website via safari! You'll need to set the webpageURL property to the user activity.
  • You can also do the opposite, from website to app (session Adopting Handoff on iOS and OS X)

Debugging Like a Pro

  • ⚠️These tools are meant to be used only during debugging, if you submit an app to the app store with these you'll get rejected.
  • For Views debugging:
    • -[UIView recursiveDescription]; will print the subviews hierarchy (with frame info, gesture recognizers, properties like isHidden)
    • -[UIView parentDescription]; like above, but for parents view instead of subviews
    • +[UIViewController _printHierarchy]; shows all of our presenting viewControllers, our presented viewControllers, our parentViewControllers and childViewControllers, and even our presentationControllers.

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Federico Zanetello

Federico Zanetello

Software engineer with a strong passion for well-written code, thought-out composable architectures, automation, tests, and more.