Active Filters: iOS 9

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

Shared Links Extensions arrived with iOS 9 and OS X El Capitan this year. They allow our app to insert content into the users' Shared Links section of Safari on iOS or OS X. Let's add one to our fictional Little Bites of Cocoa app so users can see all the latest Bites right inside Shared Links.

We'll start by going to File > New > Target... in Xcode and choosing a Shared Links Extension.

This will provide us with a new group in our project with one file inside, called RequestHandler.swift.

In this file there's one function called beginRequestWithExtensionContext(context:).

iOS will call this on our extension and that will be our cue to run whatever code we need to load the content we'd like to show in Shared Links, then return it in the form of an array of NSExtensionItems.

We'll return them via a completion function on the context we're passed in.

class RequestHandler: NSObject, NSExtensionRequestHandling {
  func beginRequestWithExtensionContext(context: NSExtensionContext) {
    Bite.loadNewest { bites in
      let extensionItems = bites.map { bite -> NSExtensionItem in
        let item = NSExtensionItem()

        item.userInfo = [
          "uniqueIdentifier": "lboc-bite-\(bite.number!)",
          "urlString": "https://littlebitesofcocoa.com/\(bite.number!)",
          "date": bite.publishedAt
        ]

        item.attributedTitle = NSAttributedString(string: bite.title)
        item.attributedContentText = bite.content
        item.attachments = [
          NSItemProvider(contentsOfURL: bite.imageURL())!
        ]

        return item
      }

      context.completeRequestReturningItems(extensionItems,
        completionHandler: nil)
    }
  }
}

Lastly, we'll install our app, then head to Shared Links in Safari. We'll enable our extension under Subscriptions.

Success!

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

#30: UI Testing ๐Ÿšธ

Topics

Testing is incredibly important in ensuring our app arrives to our users in as good a state as possible.

Xcode has had great unit testing support for a few releases now, but testing our UI has always been a challenge.

Instruments added support for it a while back, but itโ€™s never felt like a first class citizen... Until now!

Xcode 7 brings us an awesome, fully baked set of UI testing tools. We can even make Xcode write much of your test code for us by simply interacting with our app.

Here's an example test created using the default Master-Detail template in Xcode. It presses the add button 3 times, then verifies that 3 cells now exist in the table view:

func testExample() {
    let app = XCUIApplication()

    let masterNavigationBar = app.navigationBars["Master"]
    let addButton = masterNavigationBar.buttons["Add"]

    addButton.tap()
    addButton.tap()
    addButton.tap()

    XCTAssert(app.tables.childrenMatchingType(.Cell).count == 3)
}

Recording UI Tests couldn't be easier. We just write an new empty test function, put our cursor in the middle of it, and press record.

UI Testing uses Accessibility under the hood to identify and retrieve UI elements at runtime. Just one more reason to make our apps as accessible as possible!

Topics

#25: Picture in Picture ๐Ÿ“บ

Topics

One of the coolest new features in iOS 9 is the new Picture in Picture functionality on iPad. This lets users watch video content from an app even while it's in the background.

To support it in our app, we'll first make sure you set the Playback audio category in our application(application:didFinishLaunchingWithOptions:) function:

do {
  try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
} catch { }

Then we'll use AVPlayerViewController to play video content. Picture in Picture mode will automatically kick-in if our app enters background but only if: 1.) our player is full screen, 2.) video content is playing in it, and 3.) Picture in Picture is supported on the device.

Next we'll implement this wonderfully long delegate method to restore our player UI when the user returns from Picture in Picture mode:

func playerViewController(playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: (Bool) -> Void) {
  navigationController?.presentViewController(playerViewController, animated: true) {
    completionHandler(true)
  }
}

More About PIP

  • If we need to support a generic AVPlayerLayer, AVKit also includes a new AVPictureInPictureController.

  • We also get PIP for free in WKWebView assuming our app has the Playback audio session category set.

Topics

#24: Contacts and Contacts UI ๐Ÿ‘ฅ

Topics

Interacting with a user's Contacts database used to be, shall we say, "less than ideal". The AddressBook framework was great for itโ€™s time, but itโ€™s a bit past it's prime these days.

Contacts and Contacts UI are two new frameworks in iOS 9 (and OS X + watchOS) that make integrating Contact data into your app a breeze.

Here's how easy it is to search a userโ€™s contacts and present one for viewing:

func presentContactMatchingName(name: String) throws {
    let predicate = CNContact.predicateForContactsMatchingName(name)
    let keysToFetch = [CNContactGivenNameKey, CNContactFamilyNameKey]
    let store = CNContactStore()

    let contacts = try store.unifiedContactsMatchingPredicate(
        predicate, 
        keysToFetch: keysToFetch
    )

    if let firstContact = contacts.first {
        let viewController = CNContactViewController(forContact: firstContact)
        viewController.contactStore = self.store

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

Also, Apple has officially deprecated AddressBook and AddressBookUI so now's the time to make the switch!

Topics

#23: Core Spotlight ๐Ÿ”

Topics

Core Spotlight allows your content to appear in the results of system-level Spotlight searches.

let attrSet = CSSearchableItemAttributeSet(
  itemContentType: kUTTypeText as String
)

attrSet.title = "#23: CoreSpotlight"
attrSet.contentDescription = "CoreSpotlight allows your content to appear in the results of system-level Spotlight searches."

let item = CSSearchableItem(
  uniqueIdentifier: "023",
  domainIdentifier: "com.lilbitesofcocoa.bites",
  attributeSet: attrSet
)

CSSearchableIndex
  .defaultSearchableIndex()
  .indexSearchableItems([item]) { error in
    print("Success!")
  }

Of course Core Spotlight is just one of many ways to get your content into search results, be sure to also look into the NSUserActivity APIs as well as Apple's Web markup guides.

Topics

#20: ReplayKit ๐ŸŽฅ

Topics

ReplayKit in iOS 9 allows you to record a movie of what's happening on screen in your app or game. Here's a quick example of how to use it:

import ReplayKit

class GameViewController: UIViewController {
  func startRecording() {
    let recorder = RPScreenRecorder.sharedRecorder()
    recorder.startRecordingWithMicrophoneEnabled(true)
  }

  func stopRecording() {
    let recorder = RPScreenRecorder.sharedRecorder()

    recorder.stopRecordingWithHandler { (previewVC, error) in
      if let vc = previewVC {
        self.presentViewController(
          vc, 
          animated: true, 
          completion: nil
        )
      }
    }
  }
}

  • Records app audio, optionally also records microphone audio. The user is given the chance to preview and, edit , and trim the video before exporting.
  • You can't access the movie file itself. After recording, user is shown an activity view controller, which you can add custom actions to.
  • Recording is polite to battery and performance.
  • Only works A7 and A8 devices.
  • Permission from the user is required to begin recording.
  • Recording automatically excludes system UI like notifications or keyboard entry.

Topics

#18: SFSafariViewController ๐Ÿ‘’๐Ÿ„

Topics

SFSafariViewController is another new addition in iOS 9, and itโ€™s a great one.

Get ready to throw out all that custom in-app browser or third-party library code youโ€™ve been using.

Unlike other solutions, SFSafariViewController embeds all of the power of Safari on iOS (including Autofill, shared cookies, etc.) directly into your app.

Replacing your existing solutions should be trivial for most people, hereโ€™s how:

import SafariServices

class BookmarksViewController : UITableViewController {

  func didSelectBookmark(bookmark: Bookmark) {
    let vc = SFSafariViewController(
      URL: bookmark.URL,
      entersReaderIfAvailable: false
    )

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

}

SFSafariViewController Pro Tips

  • set a delegate to configure custom activity items in the share sheet
  • use the entersReaderIfAvailable property to start the user in reader view

Topics

#17: UIKeyCommand ๐Ÿ”‘โŒ˜

Topics

iOS has had support for hardware keyboard shortcuts for a while now. New in iOS 9, Apple has made some great improvements to how apps can take advantage of them. Your app can now register keyboard shortcuts per view controller, and all currently valid shortcuts will be neatly
summarized when the user brings up the new keyboard shortcuts view.

Wiring up a new keyboard shortcut in this system couldn't be easier. Just define a new function, create a new UIKeyCommand and then call addKeyCommand on your view controller:

// inside some UIViewController subclass:

override func viewDidLoad() {
  super.viewDidLoad()

  let shortcut = UIKeyCommand(
    input: "n",
    modifierFlags: UIKeyModifierFlags.Command,
    action: "createNewFile:"
  )

  addKeyCommand(shortcut)
}

func createNewFile(command: UIKeyCommand) {
  // do the thing
}

Page 1 of 2