Active Filters: Extensions

Topics

#296: Today Extension B-Sides πŸ“Ό

Topics

We first covered Today Extensions way back in Bite #36. They're a great way to offer quick, glanceable information or entry-points to our app. Today we'll take a look at some lesser known features of Today Extensions, and how we can use them in our code. Let's begin.

3D Touch Shortcut Widgets

First up, is one of the newest additions to the Today Extension world: 3D Touch Homescreen Widgets.

The coolest bit is, we don't really need to do anything to "get" this. If our app has a Today Extension, the widget will automatically appear when a user 3D Touches our app's icon.

The B-Side comes into play when we have multiple Today Extensions in our app. We'll need a way to tell the system which one to display above our icon. We can do this by adding a new key to our app's Info.plist:

UIApplicationShortcutWidget

(We can also choose "Home Screen Widget" from the keys dropdown).

Neat!

Conditional Display

Today Extensions Widgets don't have to always be visible in the Today view. We can actually tell the system whether or not our widget should be displayed with this one function:

NCWidgetController
  .widgetController()
  .setHasContent(true, 
    forWidgetWithBundleIdentifier: "com.littlebitesofcocoa.latest")

Calling this with false will hide the widget in the Today View until our app calls the function again with a true value.

Opening Our App

This one is a bit of a stretch to truly call a B-side, but it can easily be done incorrectly, so here we are.

It's quite common for a Today Extension Widget to need to open its containing app.

Apple has tightened the reigns in recent OS releases to "validate" when and how apps (and specifically Today Extensions) can open apps. Not to worry, we can use this special function on an NSExtensionContext to open a deep link into our app.

self.extensionContext?.openURL(NSURL("lboc://bites/296"), completionHandler: nil)

Pro Tip: Opening our own app (i.e. our app that contains the Today Extension) is just fine, but beware if we start trying to open other apps using this function, Apple may scrutinize our Today Extension further during App Review.

We're finishing up our extensive look into the new Notifications improvements in iOS 10 today with Notification Service Extensions. These fill out the notifications functionality by allowing us to intercept remote notifications as they are received on the device, take some action(s), and then modify the notification before it's shown to the user. Let's get started!

We'll begin by looking at the broad concept going on here. Until iOS 10, Remote Notifications were presented to the user as soon as they arrived, without any interaction from their app.

In iOS 10, Apple has provided a mechanism for us to actually "intercept" Remote Notifications when they arrive, do some work, modify the notification's content, then send it along to be displayed to the user.

The whole process now goes like this:

This new step opens up a ton of possibilites and gives us a chance to do things like download media, or perform some other short work to enrich the notification before the user sees it.

Let's try this out.

First, we'll add another new target to our app, and choose a Notification Service Extension:

In the new group that was created we'll find one file, a subclass of UNNotificationServiceExtension.

Notification Service Extensions run in the background, and never show any kind of interface themselves. Instead, they override one function that's called whenever a notification is received.

We're passed in a notification request and a completion closure.

The request is the same UNNotificationRequest type we first learned about in Bite #258, only with its trigger property set to a UNPushNotificationTrigger, neat!

We'll perform whatever work we need, grab the content from the the notification request's, modify its properties, and then send it along in the completion closure:

class NotificationService: UNNotificationServiceExtension {
  override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    guard let asteroidID = request.content.userInfo["asteroid-id"] as? String else { return }
    guard let mutableContent = request.content.mutableCopy() as? UNMutableNotificationContent else { return }

    AsteroidService.details(asteroidID: asteroidID) {
      mutableContent.body
        .append(" Estimated Mass: \($0.massInTons) tons")

      contentHandler(mutableContent)
    }
  }
}

Here we take advantage of the custom "userInfo" dictionary our server adds to the push notification payload. In this case, we're grabbing an asteroidID from it. We also grab a mutable instance of the notification's content so we can modify it later.

Then we load the details for the asteroid, and append a new piece of information to the notification's body, the estimated mass.

Finally, we send the modified content along in the contentHandler.

We don't need it here, but there's one more function we can override, and it's all about expiration.

Unlike some other more generous extension points, Notification Service Extensions are given an extremely short window to perform work and complete by the system.

We can override:

override func serviceExtensionTimeWillExpire() {

}

This will tell us when the system is about to forcibly stop our extension, if this happens, the system will display the notification using whatever push notification payload our server sent through, with no modifications.

In our example above, we lean on this fallback behavior, so we didn't need to override this function.

Finally, we'll push an example notification through using Knuff, (covered in Bite #177), and try this all out:

Success!

We've been looking at the various improvements in Notifications in iOS 10, and today we're trying out one of the coolest new features: Notification Content Extensions. These allow us to display completely custom content when the user drags down or 3D Touches on one of our app's notifications. Let's dive in.

We'll start by adding a new target to our app and choosing Notification Content Extension.

We'll give it a name, and check out the files we get.

Like many of the other extension types we've covered, the whole thing is basically a view controller, storyboard, and Info.plist.

We'll head to the Info.plist first and configure a few things.

Since we want our custom content to be all that's visible once the users opens up our notification, we've added the UNNotificationExtensionDefaultContentHidden key hide the title and body text labels that are visible before open

We've also set our notification category to match the one we'll send in our UNNotificationContent.

Before we continue, let's add some actions to our notification so the user can actually do something about it. We've covered Notification Actions in the past (Bite #122), but here's how things work in the User Notifications Framework world:

// Somewhere before we request our first notification:
let evade = UNNotificationAction(
  identifier: "evade",
  title: "Evade with Autopilot πŸ€–"
)
let destroy = UNNotificationAction(
  identifier: "destroy",
  title: "Destroy (Weapons to Maximum!) ",
  options: [.destructive]
)
let category = UNNotificationCategory(
  identifier: "asteroid-detected",
  actions: [evade, destroy],
  intentIdentifiers: []
)

UNUserNotificationCenter.current().setNotificationCategories([category])

Now, we can configure a notification just like we have before:

let notification = UNMutableNotificationContent()

notification.categoryIdentifier = "asteroid-detected"

notification.title = "Asteroid Detected! πŸš€"
notification.body = "Collision imminent. Immediate action required!"

Then we'll send off a request to the notification center to show the notification in 2 seconds. (Enough time for us to press the home button and head out to the home screen).

let request = UNNotificationRequest(
  identifier: "asteroid-id-123",
  content: notification,
  trigger: UNTimeIntervalNotificationTrigger(
    timeInterval: 2,
    repeats: false
  )
)

UNUserNotificationCenter.current().add(request)

Nice, so far so good. Now, let's see what happens when we open it up (either by 3D Touching on a supported device, or simply dragging down the banner):

Success! Our custom content is shown inside the notification, along with our custom actions.

We first looked at iMessage Apps in Bite #237, when we created a Sticker Pack app. Today we'll go one step further and create an iMessage app that provides it's own UI for displaying Stickers. Let's get started.

This time, we'll make an iMessage app to send Little Bites of Cocoa Bites to our friends!

We'll start by creating a new Messages Application in Xcode. Then, we'll make a new file called BiteBrowserViewController.swift. We'll make it a subclass of MSStickerBrowserViewController.

We'll need to conform to MSStickerBrowserViewDataSource so we'll add a property to hold the Bite images and implement a couple of simple functions:

class BiteBrowserViewController: MSStickerBrowserViewController {
  var stickers = [MSSticker]()

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    loadStickers()
    stickerBrowserView.reloadData()
  }

  override func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int {
    return stickers.count
  }

  override func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker {
    return stickers[index]
  }
}

The loadStickers function loads Bite images from disk, then adds them as MSSticker instances. The loading plumbing code isn't important as everyone will likely be implementing this specifically for their use case.

The important bit is that we need to create MSStickers and get them into our stickers array:

try sticker = MSSticker(contentsOfFileURL: stickerURL, localizedDescription: localizedDescription)
stickers.append(sticker)

We're almost done.

We'll need to add our BiteBrowserViewController as a child view controller of it. We'll do so like this:

When we created our Message App, Xcode created a MessagesViewController.swift file for us. This will get shown in the UI.

class MessagesViewController: MSMessagesAppViewController {
  var browserViewController: BiteBrowserViewController!

  override func viewDidLoad() {
    super.viewDidLoad()
    browserViewController = BiteBrowserViewController(stickerSize: .large)
    browserViewController.view.frame = self.view.frame

    self.addChildViewController(browserViewController)
    browserViewController.didMove(toParentViewController: self)
    self.view.addSubview(browserViewController.view)
  }
}

Finally, we'll update our Bundle Display Name in Info.plist to something like "Bite Sender". We can now Build & Run our app in Messages and start sending Bites to everyone we know!

WWDC brought us a whirlwind of changes all over Apple's platforms. One interesting announcement was Xcode Source Editor Extensions. These allow us to add commands to Xcode that can work with the contents of the code we're working on. Yes, Apple also announced that Xcode won't load plugins (like Alcatraz, etc.) anymore. Bummer.

Today we'll try to shake off our feels about plugins going away by making our first Source Editor Extension, let's do it!

We're going to make a super-simple Extension that lets us easily "sign" our code comments. Like this:

// - @jakemarsh

We'll start by creating a new macOS app. Then we'll add a new target to it, and choose Xcode Source Editor Extension.

Xcode gives us a nice template of files to start with. We can head into SourceEditorCommand.swift to implement our command.

class SourceEditorCommand: NSObject, XCSourceEditorCommand {
  func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (NSError?) -> Void ) -> Void {
    guard let selection = invocation.buffer.selections.firstObject as? XCSourceTextRange else { completionHandler(createError()); return }

    let commentText = "// - @\(NSUserName())"

    invocation.buffer.lines.insert(commentText, at: selection.end.line)

    completionHandler(nil)
  }
}

We start by looking at the invocation's selection and guard'ing to make sure there's a valid insertion point for us to append to, then create a new comment using the handy NSUserName() function in macOS.

Finally, we use the provided XCSourceEditorCommandInvocation one more time to insert the new comment text into the buffer. We call the completionHandler with a nil to let Xcode know we've completed our work without any errors, and we're done!

We can customize the name of our command in our Info.plist, like this:

Now, how are we going to test this thing out? Well, Xcode has some nice support built in for debugging these, but first we'll have to endure a little pain to get things working (at least during this first beta, later Xcode 8 releases will hopefully not need these steps).

We'll only need this while running OS X 10.11, macOS 10.12 users can skip this step:

We'll need to run sudo /usr/libexec/xpccachectl in Terminal, then reboot our Mac.

Once it's back up, we can open Xcode 8 again, and Build & Run our Extension. We'll be asked to choose which app we'd like to run it in. We'll choose Xcode 8 (be careful not to choose an older Xcode version as the icons are easy to miss).

Another instance of Xcode 8 will launch in our dock and… Boom! Xcode's dark heart is revealed!

Just kidding, Xcode is displaying itself this way to help us differentiate between the one that we're debugging, and the "real" one.

We can head to the Editor menu to test out our new command. That's not all though, we can go back to the other Xcode instance and set breakpoints, inspect variables, etc. Neat!

Plugins may be gone, but Source Editor Extensions still offer a ton of interesting possibilities. Looking for inspiration? Here's a few on Github already.

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!

Photo Editing Extensions are a powerful way for apps to integrate with the system Photos app. Users can begin editing a photo, jump into a third-party extension, and return seamlessly back to the system editing interface. Let's try making one.

We begin by adding a new Photo Editing Extension target to our project.

Our extension will use CoreImage (covered in Bite #32) to convert a photo to black and white. The basic workflow goes like this:

  1. User launches our extension while editing inside Photos.app
  2. a PHContentEditingInput object is handed to us via the startContentEditingWithInput... function
  3. User performs the edits they would like using our view controller
  4. User taps Done button (provided by the system)
  5. System asks us for a PHContentEditingOutput object containing our changes to the image via finishContentEditingWithCompletionHandler.


Let's move on to the code.

Along with the input object, we also receive a placeholderImage which we'll use as the initial image in our own image view.

func startContentEditingWithInput(contentEditingInput: PHContentEditingInput?, placeholderImage: UIImage) {
  input = contentEditingInput
  imageView.image = placeholderImage
}

After processing, we create an output using the intial input object we were handed, and write our modified image to it's content URL.

let output = PHContentEditingOutput(contentEditingInput: input)

processedImageData.writeToURL(
  output.renderedContentURL, 
  atomically: true
)

Lastly, we describe our adjustments, so we can be a good citizen in Photos.app's non-destructive editing world.

output.adjustmentData = PHAdjustmentData(
  formatIdentifier: NSBundle.mainBundle().bundleIdentifier!,
  formatVersion: "1.0",
  data: "Grayscale".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
)

Download the complete project here: j.mp/bite058

Topics

#36: Today View Widgets πŸ“Ÿ

Topics

Creating a Today View Widget for your app is a great way to give users a quick way to access timely information or controls. Let's make one!

Add the Widget Target

Implement Regular Table View Controller Stuff

class TodayViewController: UITableViewController, NCWidgetProviding {
  var bites = [Bite]()

  // not shown here because boring:
  // numberOfRowsInSection returns bites.count
  // cellForRowAtIndexPath just sets cell.textLabel.text = bite.title
}

Conform to NCWidgetProviding Protocol

func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)) {
  Bite.fetchLatest { (bites, error) in
    guard error == nil else { completionHandler(.Failed); return }
    self.bites = bites

      self.tableView?.reloadData()
      self.preferredContentSize = self.tableView!.contentSize

    completionHandler(.NewData)
  }
}

Success!

Topics

#10: Action Extensions πŸŽ₯

Topics

Action Extensions are incredibly powerful. Not only can they accept many different forms of input data, but they can return data back to the original application as well. Let's build an Action Extension to help us sound smart while writing. It will accept a word, let us choose a more intelligent sounding word, then return our selection to the original app.



First we'll add the extension:

File > New > Target... then use the Action extension template.

We grab the input word, then load replacement words:

let c = self.extensionContext!
let item = c.inputItems[0] as! NSExtensionItem
let provider = item.attachments![0] as! NSItemProvider
let textType = kUTTypeText as String

if provider.hasItemConformingToTypeIdentifier(textType) {
  provider.loadItemForTypeIdentifier(textType, options: nil) { (string, error) in
    if let word = string as? String {
      self.loadSmarterSoundingWords(word) {
        dispatch_async(dispatch_get_main_queue()) {
          self.tableView.reloadData()
        }
      }
    }
  }
}

Finally, we'll return our chosen replacement word back to the original app:

func completeWithWord(word: String) {
  var c = self.extensionContext!
  var typeID = kUTTypeText as String
  var provider = NSItemProvider(item: word, typeIdentifier: typeID)

  var item = NSExtensionItem()
  item.attachments = [provider]

  c.completeRequestReturningItems([item], completionHandler: nil)
}

// then inside didSelectRowAtIndexPath:
completeWithWord(words[indexPath.row])

You can download a complete working project here.

Run the MakeMeSmarter scheme. Select a word to replace, then press the action button.

Bonus: The project also shows how to retrieve the value back from an extension.