Active Filters: UIKit

With additions like UIStackView, Auto Layout has matured into quite a powerful system for managing how our views are arranged on screen. When things go wrong though, it can sometimes be difficult to diagnose the specific cause of the issue. Today we'll look at a few techniques for making sense of the madness caused by... Auto Layout Bugs! 🐞πŸ”ͺπŸ”ͺπŸ”ͺ

Most issues arise from "Unsatisfiable Layouts". That's fancy-talk for "two or more constraints you gave Auto Layout conflict with each other." The solution will of course be different in every case, but here's some sensible things to ask ourselves when an error first occurs:

  • Is translatesAutoresizingMaskIntoConstraints set to false on the views we're adding constraints to?

  • Are the priorities of each of constraint, as well as content hugging and compression resistance priorities (Bite #69) what we expect them to be?

  • "Can this required constraint work at a 999 priority?" Remember, Auto Layout will try to get as close to our desired result as possible, while still satisfying all other constraints.

Identifiers

constraint.identifier = "image-fixed-width"

Identifiers help us more easily spot the important bits in those giant log outputs Auto Layout loves to show us. They can be added in code or in Interface Builder.

Log Specific Constraints

profileHeaderView.constraintsAffectingLayoutForAxis(.Vertical)

When debugging complex layouts, it can sometimes be helpful to look at only the constraints involving a specific problem view or area. We can use this function to grab an array of the constraints affecting a particular axis. Neat.

Brain Surgery

When all else fails, don't be afraid to go in and start temporarily commenting-out or disabling constraints then observe the results. This can often lead to unexpected insights into how constraints are behaving.

Practice!

A great way to fight Auto Layout issues is to try to catch them before they happen. That means becoming more familiar with Auto Layout, which means practice. For example: When a question or issue comes up, create a new 'dummy' Xcode project. Throw some views and constraints in there and try it out. Tweak some priorities, observe their effects in the isolated environment. When in doubt, try it out!

Topics

#116: Instructions πŸŽ“

Topics

Teaching users how to use our apps is incredibly important. We should strive to do this through familiar UI patterns, intuitive flows, good error handling, etc. Sometimes though, we just need to explain what's going on.

Today we'll look at a fantastic new project from FrΓ©dΓ©ric Maquin called Instructions that can help us do just that. It's a library for easily adding "coach marks" to our apps. Let's dive in.

We'll start by adding a new CoachMarksController property to our view controller, and setting it as the data source for it:

let coachMarksController = CoachMarksController()

override func viewDidLoad() {
  super.viewDidLoad()
  coachMarksController.datasource = self
}

Next, we'll extend our view controller so it conforms to the CoachMarksControllerDataSource protocol. Instructions is highly customizable. It has support for custom body and arrow views, positions, highlights, and more. Let's keep things simple here and add a regular coach mark for a button in our app.

func numberOfCoachMarksForCoachMarksController(coachMarksController: CoachMarksController) -> Int {
  return 1
}

func coachMarksController(coachMarksController: CoachMarksController, coachMarksForIndex index: Int) -> CoachMark {
  return coachMarksController.coachMarkForView(launchButton)
}

func coachMarksController(coachMarksController: CoachMarksController, coachMarkViewsForIndex index: Int, coachMark: CoachMark) -> (bodyView: CoachMarkBodyView, arrowView: CoachMarkArrowView?) {
  let coachViews = coachMarksController.defaultCoachViewsWithArrow(true, arrowOrientation: coachMark.arrowOrientation)

  coachViews.bodyView.hintLabel.text = "This button launches the spaceship, proceed with caution!"
  coachViews.bodyView.nextLabel.text = "Got it"

  return (bodyView: coachViews.bodyView, arrowView: coachViews.arrowView)
}

It's good practice to let users who don't need our coach marks skip them. Instructions has us covered. We can setup a "skip view" on our controller. We'll use the default one, which shows in the nav bar:

let skipView = CoachMarkSkipDefaultView()
skipView.setTitle("Skip", forState: .Normal)
coachMarksController.skipView = skipView

Finally we'll start the whole flow in viewDidAppear:

override func viewDidAppear(animated: Bool) {
  super.viewDidAppear(animated)
  coachMarksController.startOn(self)
}

Success! When we launch our app, the coach marks are now shown:

More info about Instructions can be found at git.io/instructions

Topics

#112: Layout Guides & Anchors βš“οΈ

Topics

Layout Guides & Anchors were added in iOS 9/OS X 10.11 as a more convenient way to add Auto Layout constraints. Let's dive in.

Layout Guides let us create sort of "invisible boxes" that exist solely for the purpose of layout. Instead of adding "spacer" views or using other such tricks, we can add some layout guides to a view, then add some constraints to them. It's a nice way to avoid needless performance wastes like rendering the dummy "spacer" views, and the code itself ends up being much more readable:

var textContainerGuide = UILayoutGuide()
view.addLayoutGuide(textContainerGuide)

Note: Layout Guides are defined in code using UILayoutGuide on iOS and NSLayoutGuide on OS X.

Anchors are best explained with an example. They allow us to turn verbose code like these manually configured constraints:

NSLayoutConstraint(item: someView,
  attribute: .Leading,
  relatedBy: .Equal,
  toItem: anotherView,
  attribute: .LeadingMargin,
  multiplier: 1.0,
  constant: 0.0
).active = true

NSLayoutConstraint(item: someView,
  attribute: .Trailing,
  relatedBy: .Equal,
  toItem: anotherView,
  attribute: .TrailingMargin,
  multiplier: 1.0,
  constant: 0.0
).active = true

...into a set of much more readable short lines of code (shown below). The new code reads much more easily from left to right, and is much easier to scan and reason about what's going on.

let margins = anotherView.layoutMarginsGuide

someView.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor).active = true
someView.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor).active = true

UIView, NSView, and UILayoutGuide all have lots of new anchor properties available to make adding constraints to them much simpler than before. We also get a little extra type safety as a bonus.

Animation is one of the greatest parts about building (and using) iOS apps. The APIs however, can feel a bit scattered. UIView's animation functions are wonderful, but some animations require using Core Animation directly.

When they do, things get progressively more complex depending on if we need to run multiple animations in succession, or just run code after an animation completes.

Today we'll look at a great library from Marin Todorov called EasyAnimation that improves on all of this. Let's dive in:

EasyAnimation makes animating CALayers that normally would require CABasicAnimation (or one of its siblings) work with the standard UIView.animateWithDuration functions:

UIView.animateWithDuration(0.3, animations: {
  self.view.layer.position.y = 64.0
})

Under the hood, EasyAnimation does all the heavy lifting of translating our animations back into CAAnimation code and handling all of the implementation details for us. Neat!

Normally, if we wanted to run code after one the animations on a CALayer finished, we'd need to wire up an animation delegate, implement the callback functions, make sure to clean up after ourselves, etc.

With EasyAnimation though, we're able to just use the normal completion closure.

UIView.animateWithDuration(
  0.3,
  delay: 0.1,
  options: [.BeginFromCurrentState],
  animations: {
  self.view.layer.borderWidth = 2.0
  self.view.layer.cornerRadius = 12.0
}, completion: { finished in
  self.tableView.reloadData()
})

Last but certainly not least, EasyAnimation makes "chaining" multiple animations together (running one after another) extremely convenient. It also supports cancelling the chain, repeating, delays and more:

let chain = UIView.animateAndChainWithDuration(0.3, animations: {
  self.avatarView.center = headerView.center
}).animateWithDuration(0.2, animations: {
  self.headerView.alpha = 1.0
})

More info about EasyAnimation can be found at git.io/easyanimation

In iOS 9, UICollectionView learned to interactively re-order cells. Let's take a look.

Before we dive in, one quick note: If we just need re-orderable cells, adopting this functionality can be incredibly simple:

override func collectionView(collectionView: UICollectionView, moveItemAtIndexPath source: NSIndexPath, toIndexPath destination: NSIndexPath) {
  let person = crew.removeAtIndex(source.item)
  crew.insert(person, atIndex: destination.item)
}

UICollectionViewController's new property called installsStandardGestureForInteractiveMovement defaults to true, so once we implement delegate function above, we're good to go.

To customize things further, we'll need to manually call UICollectionView's new interactive movement functions. Let's look at a very rough idea of how we might make the picked up cell "hover" and the others "wiggle" like iOS's home screen.

func longPressed(gesture: UILongPressGestureRecognizer) {
  let location = gesture.locationInView(collectionView)
  movingIndexPath = collectionView.indexPathForItemAtPoint(location)

  switch(gesture.state) {
  case .Began:
    guard let indexPath = movingIndexPath else { break }
    setEditing(true, animated: true)
    collectionView.beginInteractiveMovementForItemAtIndexPath(indexPath)
    pickedUpCell()?.stopWiggling()
    animatePickingUpCell(pickedUpCell())
  case .Changed:
    collectionView.updateInteractiveMovementTargetPosition(location)
  case .Ended:
    collectionView.endInteractiveMovement()
    animatePuttingDownCell(pickedUpCell())
    movingIndexPath = nil
  default:
    collectionView.cancelInteractiveMovement()
    animatePuttingDownCell(pickedUpCell())
    movingIndexPath = nil
  }
}

override func setEditing(editing: Bool, animated: Bool) {
  super.setEditing(editing, animated: true)
  startWigglingAllVisibleCells()
}

We'll also start/stop wiggling when we dequeue cells. Lastly, we'll apply the same changes that are in animatePickingUpCell to the cell's layout attributes. To do this we can subclass UICollectionViewFlowLayout and override layoutAttributesForInteractivelyMovingItemAtIndexPath.

After all that's done, this is the outcome:

Here's a direct link to this example video, just in case.

Download the project at j.mp/bite104 to see a complete working example.

Update on March 22nd, 2019: Thanks to reader Le Zeng, the project download now supports Swift 4.2. Thanks so much!

Topics

#103: UIStackView in Code πŸš₯πŸ“

Topics

We covered UIStackView when it was first announced, way back in Bite #16. Today we'll look at how to use it in code to build a section header view:

We'll be stacking 3 regular views: a UIImageView, then a UILabel, and finally a UIButton

Nothing special about them besides fonts/colors, so we've created them off camera.

Here's our first attempt:

stackView = UIStackView(arrangedSubviews: [imageView, label, button])

stackView.axis = .Horizontal
stackView.translatesAutoresizingMaskIntoConstraints = false

addSubview(stackView)

addConstraintsWithVFL("|[stackView]|")
addConstraintsWithVFL("V:|[stackView]|")

Like in Bite #99, we'll use Auto Layout Visual Format Language to pin the stack view's edges to its superview. Look at that, not bad for our first try!

Those views are close talkers, let's add some spacing:

stackView.spacing = 10.0

Ack! What the heck is going on here?!

Let's break it down: In the default β€˜Fill' distribution mode, if views don't naturally fill the axis of the stack view, the stack view will resize one (or more) according to their hugging priority (covered in Bite #69).

We'll solve our issue by setting a low hugging priority on our label, signaling to the stack view that it be the one to stretch, not our image view.

titleLabel.setContentHuggingPriority(1, forAxis: .Horizontal)

Nice, much room-ier! Finally, let's use one more trick to make the stack view add some padding around its edges:

stackView.layoutMarginsRelativeArrangement = true
stackView.layoutMargins = UIEdgeInsetsMake(7.0, 7.0, 7.0, 7.0)

Success, matches our design perfectly! Download the project at j.mp/bite103

Topics

#95: 3D Touch πŸ‘Š

Topics

We've covered a few of the new 3D Touch APIs in iOS 9 like Static and Dynamic Shortcut Items (Bite #79, #88) as well as View Controller Previews (Bite #80), but today we'll look at how to access and utilize raw force values from a user's touches. Let's get started.

We'll start with the "Single View" template. We'll open up our Main.storyboard and drag a UIProgressView out to the top of the screen. Then we'll give it an @IBOutlet in our ViewController and wire it up.

Next, the fun part. We'll create a new function called updateForTouches, that will take in an optional Set of UITouch objects. We'll guard to make sure 3D Touch is available, and assume our force is 0.0 unless a touch is present. Then, we update the progress property of our progress view and for good measure, we'll set a red background color on our view, mapping it's opacity to the touch's force as well.

func updateForTouches(touches: Set<UITouch>?) {
  guard traitCollection.forceTouchCapability == .Available else { return }

  var force: Float = 0.0

  if let touches = touches, let touch = touches.first {
    force = Float(touch.force / touch.maximumPossibleForce)
  }

  forceProgressView.progress = force
  view.backgroundColor = UIColor.redColor().colorWithAlphaComponent(CGFloat(force))
}

Lastly we'll need to implement all the touchesBegan, touchesMoves, touchesCancelled, and touchesEnded functions, calling our function and passing in the touches on each:

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
  super.touchesBegan(touches, withEvent: event)
  updateForTouches(touches)
}

override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
  super.touchesMoved(touches, withEvent: event)
  updateForTouches(touches)
}

override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
  super.touchesCancelled(touches, withEvent: event)
  updateForTouches(touches)
}

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
  super.touchesEnded(touches, withEvent: event)
  updateForTouches(touches)
}

Success! If we build and run on an iPhone 6S or iPhone 6S Plus, we can start pressing the screen lightly and watch as the progress view and background color change as we begin pressing more firmly.

It might not be immediately obvious when playing around with Shortcut Items or View Controller Previews, but the hardware does in fact report every single tiny change in the force in real time. Neat!

You can download the project we built here.

Topics

#91: Universal Links πŸŒŽπŸ”—

Topics

Universal Links arrived with iOS 9. Conceptually they're a way for us to logically tie the content in our app to the content on our website. Once this is done, iOS will be able to launch our app when a user taps a link somewhere, rather than opening our site in Safari. Let's take a further look by adding Universal Links to a fictional Little Bites of Cocoa app. We'll start by registering and setting up SSL for our domain: littlebitesofcocoa.com.

Then, we'll head into Xcode and to the Capabilities tab of our project. We'll flip on the switch for Associated Domains, then click the + button and add our domain. Note that we prefix it with the phrase applinks:.

Now, our app will silently make an HTTP GET request to
https://littlebitesofcocoa.com/apple-app-site-association.

It will expect us to return some JSON which describes our app's Bundle ID, and which paths should open it.

We'll open all paths on our domain using a wildcard character here, but we could easily, for example, limit to just Bite URLs.

{
  "applinks": {
    "apps": [],
    "details": {
      "TEAMIDHERE.com.magnus.lboc": {
        "paths": [ "*" ]
      }
    }
  }
}

After creating the file, we'll need to sign it so it is returned with a Content-Type of application/pkcs7-mime on our server. We'll use a command like the one shown here to sign the file. (This part stinks, but there's no way around it).

cat aasa.json | openssl smime -sign
                              -inkey littlebitesofcocoa.com.key
                              -signer littlebitesofcocoa.com.pem
                              -certfile intermediate.pem
                              -noattr
                              -nodetach
                              -outform DER > apple-app-site-association

Lastly, we'll wire up the other side of the equation. When a user opens a Universal Link that iOS recognizes, it will call the same delegate function that's used to implement features like Handoff (Bite #29) and Spotlight Search (Bite #23). We'll check if it's a Universal Links activity type, then use JLRoutes (Bite #62) to open a section of our app.

extension AppDelegate {
  func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {

    if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
      return JLRoutes.routeURL(userActivity.webpageURL!)
    }

    return true
  }
}

Topics

#88: Dynamic Shortcut Items πŸ“Œ

Topics

We covered Static Shortcut Items back in Bite #79, when 3D Touch was first introduced. Today we'll be taking a look at their close relative, Dynamic Shortcut Items. These offer the same behavior and functionality (allowing users to quickly jump to a particular section or feature of an app, straight from the home screen) but they can be configured and managed in code, and don't need to be defined in our Info.plist.

Dynamic Shortcut Items are particularly great for offering quick access to things such as user created content. Let's add a couple to our app. We'll use ours to let the user open recently viewed documents.

We'll create a function which we'll call whenever a document is opened in our app. It will update the current set of Dynamic Shortcut Items to match the 2 most recently opened documents.

We'll grab our recent documents, then define a new shortcut item for each one. We can handle tapping a Dynamic Shortcut Item just as we did in Bite #79, by passing a URL in the shortcut's userInfo property then using JLRoutes (Bite #62) to route it correctly inside application(application:performActionForShortcutItem shortcutItem:completionHandler:).

func updateDynamicShortcutItems() {
  let recentDocs = Document.recent(2)
  var shortcutItems = [UIMutableApplicationShortcutItem]()

  for doc in recentDocs {
    shortcutItems.append(
      UIMutableApplicationShortcutItem(
        type: "com.magnus.spaceships.document",
        localizedTitle: doc.name,
        localizedSubtitle: doc.excerpt,
        icon: UIApplicationShortcutIcon(type: .Compose),
        userInfo: [ "url": "spaceships://documents/\(doc.documentID)" ]
      )
    )
  }

  UIApplication.sharedApplication().shortcutItems = shortcutItems
}

Now both our static and Dynamic Shortcut Items are available by force pressing our app's icon on the home screen.

Note: At publish time of this Bite, Xcode did not offer a way to test shortcut items in the Simulator. To generate the screenshot above (and test shortcut items' functionality) this project by Conrad Kramer was used.

Topics

#83: UIImagePickerController Basics πŸ“·

Topics

UIImagePickerController has been part of iOS since it's first release and it's evolved quite a bit over the years. Let's take a look at what it can do:

  • Capture images and videos
  • Choose images and videos from the Photos library
  • Crop images after choosing/capturing
  • Trim videos after choosing/capturing

Whew! That's quite a bit of functionality packed into this one class.

We can't cover all of that in this Bite, instead let's look at a simple example use case. We'll be letting our users take a photo, crop it, and then show how to access it for use in our app.

The first step is to find out the device we're running on has a camera, can take photos. Then we'll configure the UIImagePickerController and present it.

let types = UIImagePickerController.availableMediaTypesForSourceType(.Camera)!
let canTakePhotos = types.contains(kUTTypeImage as String)

if UIImagePickerController.isSourceTypeAvailable(.Camera) && canTakePhotos {
  let ipc = UIImagePickerController()

  ipc.sourceType = .Camera
  ipc.mediaTypes = [kUTTypeImage as String]
  ipc.allowsEditing = true
  ipc.delegate = self

  presentViewController(ipc, animated: true, completion: nil)
}

Then we'll add a function from UIImagePickerControllerDelegate where we'll get a userInfo dictionary. We'll use the values inside to extract the captured image in either it's original or cropped form. We can also access a few other details, like the cropped rect as a CGRect or the image's metadata as a dictionary.

Note that we'll need to declare conformance to the UINavigationControllerDelegate protocol since UIImagePickerController is actually a subclass of UINavigationController under the hood.

class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
  func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String: AnyObject]) {
    let uncroppedImage = info[UIImagePickerControllerOriginalImage] as? UIImage
    let croppedImage = info[UIImagePickerControllerEditedImage] as? UIImage
    let cropRect = info[UIImagePickerControllerCropRect]!.CGRectValue

    picker.dismissViewControllerAnimated(true, completion: nil)
  }

  func imagePickerControllerDidCancel(picker: UIImagePickerController) {
    picker.dismissViewControllerAnimated(true, completion: nil)
  }
}
Page 7 of 9