Chapter 13. Modal Dialogs

A modal dialog demands attention; while it is present, the user can do nothing other than work within it or dismiss it. This chapter discusses various forms of modal dialog:

  • Within your app, you might want to interrupt things to give the user some information or to ask the user how to proceed. For this purpose, iOS provides two types of rudimentary modal dialog — alerts and action sheets. An alert is basically a message, possibly with an opportunity for text entry, and some buttons. An action sheet is effectively a column of buttons.

  • You can provide a sort of action sheet even when your app is not frontmost (or even running) by allowing the user to summon quick actions — also known as shortcut items — by pressing with 3D touch on your app’s icon.

  • A local notification is an alert that the system presents at a predetermined time or place on your app’s behalf, even when your app isn’t frontmost.

  • A today extension lets your app present interface on the Today side of the device’s notification center. New in iOS 10, a today extension can also appear in the search interface and as a quick action.

  • An activity view is a modal dialog displaying icons representing possible courses of action, and intended in certain circumstances to replace the action sheet. For example, Mobile Safari’s Share button presents an activity view whose icons represent possible operations on a URL, such as handing it off to Mail, Message, or Twitter, or saving it internally as a bookmark or reading list item. You can present an activity view; you can also provide your own activities, either privately within your app or publicly as an action extension or share extension.

Alerts and Action Sheets

Alerts and action sheets are both forms of presented view controller (Chapter 6). They are managed through the UIAlertController class, a UIViewController subclass. To show an alert or an action sheet is a three-step process:

  1. Instantiate UIAlertController with init(title:message:preferredStyle:). The title: and message: are large and small descriptive text to appear at the top of the dialog. The preferredStyle: argument (UIAlertControllerStyle) will be either .alert or .actionSheet.

  2. Configure the dialog by calling addAction(_:) on the UIAlertController as many times as needed. An action is a UIAlertAction, which basically means it’s a button to appear in the dialog, along with a function to be executed when the button is tapped; to create one, call init(title:style:handler:). Possible style: values are (UIAlertActionStyle):

    • .default

    • .cancel

    • .destructive

    An alert may also have text fields (I’ll talk about that in a moment).

  3. Call present(_:animated:completion:) to present the UIAlertController.

The dialog is automatically dismissed when the user taps any button.

Alerts

An alert (UIAlertController style .alert) pops up unexpectedly in the middle of the screen, with an elaborate animation, and may be thought of as an attention-getting interruption. It contains a title, a message, and some number of buttons, one of which may be the cancel button, meaning that it does nothing but dismiss the alert. In addition, an alert may contain one or two text fields.

Alerts are minimal, but intentionally so; they are meant for simple, quick interactions or display of information. Often there is only a cancel button, the primary purpose of the alert being to show the user the message (“You won the game”); additional buttons may be used to give the user a choice of how to proceed (“You won the game; would you like to play another?” “Yes,” “No,” “Replay”).

Figure 13-1 shows a basic alert, illustrating the title, the message, and the three button styles: .destructive, .default, and .cancel respectively. Here’s the code that generated it:

let alert = UIAlertController(title: "Not So Fast!",
    message: "Do you really want to do this " +
    "tremendously destructive thing?",
    preferredStyle: .alert)
func handler(_ act:UIAlertAction!) {
    print("User tapped (act.title)")
}
alert.addAction(UIAlertAction(title: "No",
    style: .cancel, handler: handler))
alert.addAction(UIAlertAction(title: "Yes",
    style: .destructive, handler: handler))
alert.addAction(UIAlertAction(title: "Maybe",
    style: .default, handler: handler))
self.present(alert, animated: true)
pios 2601
Figure 13-1. An alert

In Figure 13-1, observe that the .destructive button appears first and the .cancel button appears last, without regard to the order in which they are defined. The .default button order of definition, on the other hand, will be the order of the buttons themselves. If no .cancel button is defined, the last .default button will be displayed as a .cancel button.

Starting in iOS 9, you can designate an action as the alert’s preferredAction. This appears to boldify the title of that button. For example, suppose I append this to the preceding code:

alert.preferredAction = alert.actions[2]

The order of the actions array is the order in which we added actions; thus, the preferred action is now the Maybe button. The order isn’t changed — the Maybe button still appears second — but the bold styling is removed from the No button and placed on the Maybe button instead.

As I’ve already mentioned, the dialog is dismissed automatically when the user taps a button. If you don’t want to respond to the tap of a particular button, you can supply nil as the handler: argument (or omit the handler: argument altogether). In the preceding code, I’ve provided a minimal handler: function for each button, just to show what one looks like. As the example demonstrates, the function receives the original UIAlertAction as a parameter, and can examine it as desired. The function can also access the alert controller itself, provided the alert controller is in scope at the point where the handler: function is defined (which will usually be the case). My example code assigns the same function to all three buttons, but more often you’ll give each button its own individual handler: function.

Now let’s talk about adding text fields to an alert. Because space is limited on the smaller iPhone screen, especially when the keyboard is present, an alert that is to contain a text field should probably should have at most two buttons, with short titles such as “OK” and “Cancel,” and at most two text fields. To add a text field to an alert, call addTextField(configurationHandler:). The handler: function will receive the text field as a parameter; it is called before the alert appears, and can be used to configure the text field. Other handler: functions can access the text field through the alert’s textFields property, which is an array. In this example, the user is invited to enter a number in the text field; if the alert is dismissed with the OK button, its handler: function reads the text from the text field:

let alert = UIAlertController(title: "Enter a number:",
    message: nil, preferredStyle: .alert)
alert.addTextField { tf in
    tf.keyboardType = .numberPad
}
func handler(_ act:UIAlertAction) {
    let tf = alert.textFields![0]
    // ... can read tf.text here ...
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "OK",
    style: .default, handler: handler))
self.present(alert, animated: true)

A puzzle arises as to how to prevent the user from dismissing the alert if the text fields are not acceptably filled in. The alert will be dismissed if the user taps a button, and no button handler: function can prevent this. The solution is to disable the relevant buttons until the text fields are satisfactory. A UIAlertAction has an isEnabled property for this very purpose. I’ll modify the preceding example so that the OK button is disabled initially:

alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "OK",
    style: .default, handler: handler))
alert.actions[1].isEnabled = false
self.present(alert, animated: true)

But this raises a new puzzle: how will the OK button ever be enabled? The text field can have a delegate or a control event target–action pair (Chapter 10), and so we can hear about the user typing in it. I’ll modify the example again so that I’m notified as the user edits in the text field:

alert.addTextField { tf in
    tf.keyboardType = .numberPad
    tf.addTarget(self,
        action: #selector(self.textChanged), for: .editingChanged)
}

Our textChanged method will now be called when the user edits, but this raises one final puzzle: how will this method, which receives a reference to the text field, get a reference to the OK button in the alert in order to enable it? My approach is to work my way up the responder chain from the text field to the alert controller. Here, I enable the OK button if and only if the text field contains some text:

func textChanged(_ sender: Any) {
    let tf = sender as! UITextField
    var resp : UIResponder! = tf
    while !(resp is UIAlertController) { resp = resp.next }
    let alert = resp as! UIAlertController
    alert.actions[1].isEnabled = (tf.text != "")
}

Action Sheets

An action sheet (UIAlertController style .actionSheet) may be considered the iOS equivalent of a menu; it consists primarily of buttons. On the iPhone, it slides up from the bottom of the screen; on the iPad, it appears as a popover.

Where an alert is an interruption, an action sheet is a logical branching of what the user is already doing: it typically divides a single piece of interface into multiple possible courses of action. For example, in Apple’s Mail app, a single Action button summons an action sheet that lets the user reply to the current message, forward it, or print it (or cancel and do nothing).

Figure 13-2 shows a basic action sheet on the iPhone. Here’s the code that constructed it:

let action = UIAlertController(
    title: "Choose New Layout", message: nil, preferredStyle: .actionSheet)
action.addAction(UIAlertAction(title: "Cancel", style: .cancel))
func handler(_ act:UIAlertAction) {
    // ... do something here with act.title ...
}
for s in ["3 by 3", "4 by 3", "4 by 4", "5 by 4", "5 by 5"] {
    action.addAction(UIAlertAction(title: s,
        style: .default, handler: handler))
}
self.present(action, animated: true)
pios 2602
Figure 13-2. An action sheet on the iPhone

On the iPad, the action sheet wants to be a popover. This means that a UIPopoverPresentationController will take charge of it. It will thus be incumbent upon you to provide something for the popover’s arrow to point to; otherwise, you’ll crash at runtime. You already know (from Chapter 9) how to do that!

The cancel button for a popover action sheet on the iPad is suppressed, because the user can dismiss the popover by tapping outside it. On the iPhone, too, where the cancel button is displayed, the user can still dismiss the action sheet by tapping outside it. When the user does that, the cancel button’s handler: function will be called, just as if the user had tapped the cancel button — even if the cancel button is not displayed.

An action sheet can also be presented inside a popover. In that case, the containing popover is treated as an iPhone: the action sheet slides up from the bottom of the popover, and the cancel button is not suppressed. The action sheet’s modal presentation style defaults to .overCurrentContext, which is exactly what we want, so there is no need to set it. You are then presenting a view controller inside a popover; see “Popover Presenting a View Controller” for the considerations that apply.

Dialog Alternatives

Alerts and action sheets are limited, inflexible, and inappropriate to any but the simplest cases. Their interface can contain title text, buttons, and (for an alert) one or two text fields, and that’s all. What if you wanted more interface than that?

Some developers have hacked into their alerts or action sheets in an attempt to force them to be more customizable. This is wrong, and in any case there is no need for such extremes. These are just presented view controllers, and if you don’t like what they contain, you can make your own presented view controller with its own customized view. If you also want that view to look and behave like an alert or an action sheet, then simply make it so! As I have already shown (“Custom Presented View Controller Transition”), it is easy to create a small presented view that looks and behaves quite like an alert or action sheet, floating in front of the main interface and darkening everything behind it — the difference being that this is an ordinary view controller’s view, belonging entirely to you, and capable of being populated with any interface you like (Figure 13-3). You can even add a UIMotionEffect to your presented view, giving it the same parallax as a real alert.

pios 2604b
Figure 13-3. A presented view behaving like an alert

Often, however, there will no need for such elaborate measures. A popover, after all, is virtually a secondary window, and can be truly modal. The popovers in Figure 9-1, for example, are effectively modal dialogs. A popover can internally display a secondary presented view or even an action sheet, as we’ve already seen. Also, a presented view can use the .formSheet presentation style, which is effectively a dialog window smaller than the screen. On the iPhone, furthermore, any presented view is essentially a modal dialog. The color picker in my Zotz! app (Figure 13-4) has the same lightweight, temporary quality that an alert offers, and on the desktop would probably be presented as part of a secondary Preferences window; it happens that, on the iPhone, it occupies the entire screen, but it is still effectively a modal dialog.

pios 2605
Figure 13-4. A presented view functioning as a modal dialog

Quick Actions

Quick actions, similar to an action sheet, are essentially a column of buttons. They are summoned outside of your app, when the user employs 3D touch on your app’s icon in the springboard. If the user’s device lacks 3D touch, quick actions won’t be available. They should represent convenient ways of accessing functionality that the user could equally have performed from within your app.

Quick actions are of two kinds:

Static quick actions

Static quick actions are described in your app’s Info.plist. The system can present them even if your app isn’t running — indeed, even if your app has never run — because it can read your app’s Info.plist.

Dynamic quick actions

Dynamic quick actions are configured in code. This means that they are not available until your app’s code has actually run. Your code can alter and remove dynamic quick actions, but it cannot affect your app’s static quick actions.

When the user taps a quick action, your app is brought to the front (launching it if necessary) and your app delegate’s application(_:performActionFor:completionHandler:) is called. The second parameter is a UIApplicationShortcutItem describing the button the user tapped. You can now respond as appropriate. You are then supposed to call the completionHandler, passing a Bool to indicate success or failure (though in fact I see no difference in behavior regardless of whether you pass true or false, or even if you omit to call the completionHandler entirely).

A UIApplicationShortcutItem is just a value class, embodying five properties describing the button that will appear in the interface. In order for static quick actions to work, those five properties all have analogs in the Info.plist. The Info.plist entry that generates your static quick actions is an array called UIApplicationShortcutItems. This array’s items are dictionaries, one for each quick action, containing the properties and values you wish to set. The UIApplicationShortcutItem properties and corresponding Info.plist keys are:

type
UIApplicationShortcutItemType

An arbitrary string. You’ll use this string in your implementation of application(_:performActionFor:completionHandler:) to identify the button that was tapped. Required.

localizedTitle
UIApplicationShortcutItemTitle

The button title; a string. Required.

localizedSubtitle
UIApplicationShortcutItemSubtitle

The button subtitle; a string. Optional.

icon

UIApplicationShortcutItemIconType

UIApplicationShortcutItemIconFile

An icon, shown at the right end of the button. Optional, but it’s good to supply some icon, because if you don’t, you’ll get an ugly filled circle by default.

When forming a UIApplicationShortcutItem in code, you’ll supply a UIApplicationShortcutIcon object. It has three initializers:

init(type:)

A UIApplicationShortcutIconType. This is an enum of about 30 cases, each representing a built-in standard image, such as .time (a clock icon).

init(templateImageName:)

Works like UIImage’s init(named:). The image will be treated as a template image. Apple says that the image should be 35x35, though a larger image will be scaled down appropriately.

init(contact:)

A CNContact (see Chapter 18). The icon will be based on the contact’s picture or initials.

In the Info.plist, you may use either of the two keys. The value for the type key is the string Objective-C name of a UIApplicationShortcutIconType case — for example, UIApplicationShortcutIconTypeTime. The value for the file key is the string name of an image file in your app.

userInfo
UIApplicationShortcutItemUserInfo

A dictionary of additional information, whose usage in your application(_:performActionFor:completionHandler:) is completely up to you. Optional.

pios 2605a
Figure 13-5. Quick actions

Imagine, for example, that our app’s purpose is to remind the user periodically to go get a cup of coffee. Figure 13-5 shows a quick actions menu of three items generated when the user uses 3D touch to press our app’s icon. The first two items are static items, generated by our settings in the Info.plist, which is shown in Figure 13-6.

pios 2605b
Figure 13-6. Static quick actions in the Info.plist

The third quick action item in Figure 13-5 is a dynamic item. The idea is that the user, employing our app to configure a reminder, also gets to set a time interval as a favorite default interval. We cannot know what this favorite interval will be until the app runs and the user sets it; that’s why this is a dynamic item. Here’s the code that generates it; as you can see, all we have to do is set our shared UIApplication object’s shortcutItems property:

// ... assume we have worked out the subtitle and time ...
let item = UIApplicationShortcutItem(type: "coffee.schedule",
    localizedTitle: "Coffee Reminder", localizedSubtitle: subtitle,
    icon: UIApplicationShortcutIcon(templateImageName: "cup"),
    userInfo: ["time":time])
UIApplication.shared.shortcutItems = [item]

When the user taps a quick action button, our app delegate’s application(_:performActionFor:completionHandler:) is called. Here you can see my purpose in setting the userInfo (and UIApplicationShortcutItemUserInfo) of these shortcut items; to learn what time interval the user wants to use for this reminder, we just look at the "time" key:

func application(_ application: UIApplication,
    performActionFor shortcutItem: UIApplicationShortcutItem,
    completionHandler: @escaping (Bool) -> Void) {
        if shortcutItem.type == "coffee.schedule" {
            if let d = shortcutItem.userInfo {
                if let time = d["time"] as? Int {
                    // ... do something with time ...
                    completionHandler(true)
                }
            }
        }
        completionHandler(false)
}

Local Notifications

A local notification is an alert to the user that can appear even if your app is not running. Where it may appear depends upon the user’s preferences in the Settings app, either under Notification Center or under your app’s own listing:

  • A momentary banner at the top of the screen, which vanishes automatically if the user does nothing

  • An alert, which in iOS 10 is identical to the banner, except that it doesn’t vanish until the user dismisses it (mutually exclusive with the banner)

  • On the lock screen

  • In the notification center

New in iOS 10, the appearance of a local notification alert is identical, regardless of the location in which it appears. Figure 13-7 shows a local notification alert appearing in the user’s notification center.

pios 2606a
Figure 13-7. A local notification alert

Your app does not present a local notification; the system does. You hand the system instructions for when the local notification is to fire, and then you just stand back and let the system deal with it. That’s why the local notification can appear even if your app isn’t frontmost or isn’t even running. New in iOS 10, the local notification’s alert (or banner) can appear even when your app is frontmost; but even then it is the system that is presenting it on your behalf.

The user, in the Settings app, can veto any of your local notification’s interface options, or turn off your app’s local notifications entirely. Thus, your local notification can be effectively suppressed; you can still create a local notification, but when it fires, only your app will hear about it, and only if it is frontmost. Moreover, the system will suppress your local notifications entirely unless the user approves first; thus, the user must deliberately opt in if your notification is ever to appear in any form. Figure 13-8 shows the alert that the system will show your user, once, offering the initial opportunity to opt in to your local notifications.

pios 2606b
Figure 13-8. The user will see this only once

A local notification alert is also a way for the user to summon your app, bringing it to the front if it is backgrounded, and launching it if it isn’t running. This response to the alert is the default action when the user taps the alert.

You can add further custom actions, in the form of buttons. The user must manipulate the alert in order to reveal the buttons. For example, on a device with 3D touch, the user presses with force touch to summon the buttons; on a device without 3D touch, the user drags downward on the alert, or slides the alert sideways to reveal the View button and taps the View button. The custom action buttons then appear, and the user can tap one (but the user still remains free to tap the alert to perform the default action, or to dismiss the alert). Let’s call this the alert’s secondary interface. An action button can communicate with your app without bringing it to the front — though it can alternatively be told to bring your app to the front as well.

New in iOS 10, a local notification can carry an attachment, which may be an image, a sound file, or a video. If it’s an image, the image is previewed with a small thumbnail in the alert itself. But the real way the user gets to see the attachment is the same way the user summons the custom actions. In the alert’s secondary interface, if the attachment is an image, the image is shown; if the attachment is audio or video, interface is provided for playing it.

In Figure 13-7, the little image at the right of the alert is the thumbnail of an image attachment. In Figure 13-9, the user has summoned the alert’s secondary interface, revealing the image as well as two custom action buttons.

pios 2606c
Figure 13-9. Local notification secondary interface with custom actions

New in iOS 10, you can modify the secondary interface by writing a notification content extension. Figure 13-10 shows an example; I’ve removed the default title and subtitle display and replaced them with my own title in my own font, and the attachment image is shown in a smaller size.

pios 2606cc
Figure 13-10. Local notification with custom secondary interface

Use of a local notification involves several steps:

  1. Your app must request authorization for notifications. This ensures that the user has seen the opt-in dialog (Figure 13-8).

  2. If your notification has custom actions or a custom interface, you must register a notification category, including the custom actions.

  3. Your app creates and schedules the local notification itself.

  4. Your app is prepared to hear about the user responding to the notification alert.

I’ll describe this sequence one step at a time, and then I’ll return to the business of writing a notification content extension. The API for dealing with local notifications is new in iOS 10, replacing the previous API completely. You’ll need to import the User Notifications framework (import UserNotifications). Most of your activity will ultimately involve the user notification center, a singleton UNUserNotificationCenter instance available by calling UNUserNotificationCenter.current().

Authorizing for Local Notifications

The first step in requesting authorization for local notifications is to find out whether we are already authorized. To do so, call getNotificationSettings on the user notification center, which returns a UNNotificationSettings object asynchronously. You’ll examine this property of the settings object:

authorizationStatus

A UNAuthorizationStatus: .authorized, .denied, or .notDetermined.

If the status is .notDetermined, the user has never seen the authorization request dialog, and you’ll present it by sending requestAuthorization(options:) to the user notification center. A Bool is returned asynchronously, telling you whether authorization was granted. The options: (UNAuthorizationOptions) are modes in which you’d like to affect the user interface:

.badge

You want to be allowed to badge your app’s icon with a number. Apple’s Phone and Mail apps are familiar examples.

.sound

You want to play a sound when your notification fires.

.alert

You want to present a notification alert when your notification fires.

If the status is .authorized, there’s no point requesting authorization; you already have it. If the status is .denied, there’s no point requesting authorization, and there’s probably no point scheduling any local notifications, as the user will likely never receive them.

The thing to watch out for is that both getNotificationSettings and requestAuthorization(options:) return their results asynchronously. This means that you cannot simply follow a call to the first with a call to the second; you must chain the calls by means of their completion functions, like this:

let center = UNUserNotificationCenter.current()
center.getNotificationSettings { settings in
    switch settings.authorizationStatus {
    case .notDetermined:
        center.requestAuthorization(options:[.alert, .sound]) { ok, err in
            if let err = err {
                print(err); return
            }
            if ok {
                // authorized; could proceed to schedule a notification
            }
        }
    case .denied: break
    case .authorized: break // or proceed to schedule a notification
    }
}
// *

The point of the starred final line is that any code placed here will run before the results from getNotificationSettings are returned, and thus before the switch construct is executed; such code thus cannot depend on those results. This is a standard pattern with asynchronous code; failing to understand it is a common beginner mistake (see Appendix C).

You might also wish to call getNotificationSettings again later, perhaps just before configuring and scheduling a local notification. Unlike earlier iOS versions, iOS 10 provides full information on how the user has configured the relevant settings through the properties of the UNNotificationSettings object:

soundSetting
badgeSetting
alertSetting
notificationCenterSetting
lockScreenSetting

A UNNotificationSetting: .enabled, .disabled, or .notSupported.

alertStyle

A UNAlertStyle: .banner, .alert, or .none.

Notification Category

If your local notification alert is to have a secondary interface that displays custom action buttons or a custom interface, you’ll need to register a notification category. You might have expected to associate the custom action buttons directly with the local notification itself, but this would be wasteful and inefficient. If, over the course of your app’s lifetime, you’re going to be scheduling multiple notifications with the same custom action buttons, it makes sense to create those buttons once and then, when you create the notification itself, tell the notification center that this notification should be accompanied by this set of action buttons. The notification itself is matched with a previously registered category by an arbitrary string identifier.

An action button is a UNNotificationAction, a value class whose initializer is:

  • init(identifier:title:options:)

The identifier, as I’ve already said, is arbitrary. The title is what will appear in the button. The options: are a UNNotificationActionOptions bitmask:

.foreground

Tapping this button summons your app to the foreground. If not present, this button will call your app in the background; your app, if suspended, will be awakened just long enough to respond.

.destructive

This button will be marked in the interface as dangerous (by being displayed in red).

.authenticationRequired

If this option is present, and if this is not a .foreground button, then if the user’s device requires a passcode to go beyond the lock screen, tapping this button in the lock screen will also require a passcode. The idea is to prevent performance of this action without authentication directly from the lock screen.

An action, instead of being a button, can be a text field where the user can type and then tap a button to send the text to your app. This is a UNTextInputNotificationAction, and its initializer is:

  • init(identifier:title:options:textInputButtonTitle:textInputPlaceholder:)

To configure a category, create your UNNotificationActions and call the UNNotificationCategory initializer:

  • init(identifier:actions:intentIdentifiers:options:)

The key options: value (UNNotificationCategoryOptions) is .customDismissAction; if you don’t set this, your code won’t hear about it when the user dismisses your notification alert without tapping it to summon your app — the default action — and without tapping a custom action button.

Having created all your categories, you then call setNotificationCategories on the user notification center. So, for example:

let action1 = UNNotificationAction(identifier: "snooze", title: "Snooze")
let action2 = UNNotificationAction(identifier: "reconfigure",
    title: "Reconfigure", options: [.foreground])
let cat = UNNotificationCategory(identifier: self.categoryIdentifier,
    actions: [action1, action2], intentIdentifiers: [],
    options: [.customDismissAction])
let center = UNUserNotificationCenter.current()
center.setNotificationCategories([cat])

Scheduling a Local Notification

A local notification is scheduled to fire in accordance with a trigger — a way for the system to know independently that it’s time to fire. This will be expressed as a subclass of UNNotificationTrigger:

UNTimeIntervalNotificationTrigger

Fires starting a certain number of seconds from now, possibly repeating every time that number of seconds elapses. The initializer is:

  • init(timeInterval:repeats:)

UNCalendarNotificationTrigger

Fires at a certain date-time, expressed using DateComponents, possibly repeating when the same DateComponents occurs again. (For example, you might use the DateComponents to express nine o’clock in the morning, without regard to date; the trigger, if repeating, would then be nine o’clock every morning.) The initializer is:

  • init(dateMatching:repeats:)

UNLocationNotificationTrigger

Fires when the user enters or leaves a certain geographical region. I’ll discuss this further in Chapter 21.

Unlike previous iOS versions, iOS 10 lets you retrieve scheduled notifications and query them. If this notification has an interval trigger or a calendar trigger, you can ask for its nextTriggerDate to learn when the notification will fire.

The payload of the notification is expressed as a UNMutableNotificationContent object. Its properties are:

title, subtitle, body

Text visible in the notification alert.

attachments

UNNotificationAttachment objects. In the simplest case, attachments work best if there is just one, as the secondary interface may not give the user a way to access them all; however, if you are supplying a custom secondary interface, you might be able to retrieve and display multiple attachments. Attachment objects must be fairly small, because the system, in order to present them on your behalf whenever this notification fires, is going to copy them off to a private secure area of its own.

sound

A sound (UNNotificationSound) to be played when the notification fires. You can specify a sound file in your app bundle by name, or call default to specify the default sound.

badge

A number to appear on your app’s icon after this notification fires. Specify 0 to remove an existing badge. (You can also set or remove your app’s icon badge at any time by means of the shared application’s applicationIconBadgeNumber.)

categoryIdentifier

The identifier string of a previously registered category. This is how your local notification will be associated at presentation time with custom action buttons or a custom secondary interface.

userInfo

An arbitrary dictionary, to carry extra information you’ll retrieve later.

threadIdentifier

A string; notification alerts with the same thread identifier are clumped together physically.

launchImageName

Your app might be launched from scratch by the user tapping this notification’s alert. Now suppose that you know that when this happens, you’re going to configure your app so that it appears differently from how it normally launches. You might want the momentary screen, shown while your app is launching, to correspond to that different interface. This is how you specify the alternative launch image to be used in that situation.

Having constructed your notification’s trigger and content, you package them up with an arbitrary identifier into a UNNotificationRequest by calling its initializer:

  • init(identifier:content:trigger:)

You then tell the user notification center to add this notification to its internal list of scheduled notifications. As an example, here’s the code that generated Figure 13-7:

let interval = // ... whatever ...
let trigger = UNTimeIntervalNotificationTrigger(
    timeInterval: interval, repeats: false)
let content = UNMutableNotificationContent()
content.title = "Caffeine!"
content.body = "Time for another cup of coffee!"
content.sound = UNNotificationSound.default()
content.categoryIdentifier = self.categoryIdentifier
let url = Bundle.main.url(forResource: "cup2", withExtension: "jpg")!
if let att = try? UNNotificationAttachment(
    identifier: "cup", url: url, options:nil) {
        content.attachments = [att]
}
let req = UNNotificationRequest(
    identifier: "coffeeNotification", content: content, trigger: trigger)
let center = UNUserNotificationCenter.current()
center.add(req)

Hearing About a Local Notification

In order to hear about it when your scheduled local notification fires, you need to configure some object to be the user notification center’s delegate, adopting the UNUserNotificationCenterDelegate protocol. You need to do this very early your app’s lifetime, because you might need to be sent a delegate message immediately upon launching. Thus, application(_:didFinishLaunchingWithOptions:) is a good place:

func application(_ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions:
    [UIApplicationLaunchOptionsKey : Any]?) -> Bool {
        let center = UNUserNotificationCenter.current()
        center.delegate = self // or whatever
        return true
}

The UNUserNotificationCenterDelegate protocol consists of just two optional methods:

userNotificationCenter(_:willPresent:withCompletionHandler:)

New in iOS 10, you can ask that the system present the local notification alert and play the sound and badge the icon even if the notification fires when your app is frontmost, and this is how you do so. (If your app is not frontmost when the notification fires, this method won’t be called.)

The second parameter is a UNNotification object, itself containing the fire date and your original request (UNNotificationRequest). Thus, you can identify the local notification if desired, and can respond accordingly; you can also extract information from it, such as an attachment or the userInfo dictionary.

You are handed a completion function; you must call it with some combination of (UNNotificationPresentationOptions) .alert, .sound, and .badge — or nothing, if you want to suppress the alert completely.

userNotificationCenter(_:didReceive:withCompletionHandler:)

Called when the user interacts with your local notification alert. The second parameter is a UNNotificationResponse, consisting of two properties. One, the notification, is the same UNNotification object that I described for the previous method; again, you can use it to identify and extract information from this local notification. The other, the actionIdentifier, is a string telling you what the user did. There are three possibilities:

UNNotificationDefaultActionIdentifier

The user performed the default action, tapping the alert to summon your app.

A custom action identifier string

The user tapped a custom action button, and this is its identifier.

UNNotificationDismissActionIdentifier

The user dismissed the local notification alert. You won’t hear about this (and this method won’t even be called) unless you specified the .customDismissAction option for this notification’s category.

You are handed a completion function, which you must call when you’re done. Remember, you must be quick, because it may be that you are being awakened momentarily in the background.

Here’s an example of implementing the first delegate method, telling the runtime to present the local notification alert within our app:

func userNotificationCenter(_ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler:
    @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.sound, .alert])
}

Here’s an example of implementing the second delegate method, responding to the user tapping a custom action button; I use my delay utility (Appendix B) so as to return immediately before proceeding to obey the button:

func userNotificationCenter(_ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void) {
        let id = response.actionIdentifier
        if id == "snooze" {
            delay(0.1) {
                self.rescheduleNotification(response.notification)
            }
        }
        // ... other tests might go here ...
        completionHandler()
}

If the custom action was a text input action, then this UNNotificationResponse will be a subclass, UNTextInputNotificationResponse, which has an additional userText property. Thus, to learn whether this was a text input action, you simply test its class with is or as?, and then retrieve its userText:

if let textresponse = response as? UNTextInputNotificationResponse {
    let text = textresponse.userText
    // ...
}

Managing Scheduled Notifications

New in iOS 10, the user notification center is completely introspectable. This means, among other things, that you can examine the list of scheduled notifications. This makes notification management much easier than in the past. You can learn when each notification is scheduled to fire. You can remove a notification from the list, thus canceling it. By removing, copying, and adding a notification, you can effectively reschedule a notification. UNUserNotificationCenter methods for managing scheduled notifications are:

  • getPendingNotificationRequests(completionHandler:)

  • removePendingNotificationRequests(withIdentifiers:)

  • removeAllPendingNotificationRequests

You can also examine the list of notifications that have already fired but have not yet been removed from the notification center. By judicious removal of notifications, you can keep the user’s notification center trimmed; for example, you might like only your most recently delivered notification to appear in the notification center. You can even modify the text of a delivered notification, so that the notification will be up-to-date when the user gets around to dealing with it; to do so, you add a notification with the same identifier as an existing notification. UNUserNotificationCenter methods for managing delivered notifications are:

  • getDeliveredNotifications(completionHandler:)

  • removeDeliveredNotifications(withIdentifiers:)

  • removeAllDeliveredNotifications

Warning

Canceling a repeating local notification is up to your code; if you don’t provide the user with a way of doing that, then if the user wants to prevent the notification from recurring, the user’s only recourse will be to delete your app.

Notification Content Extensions

If your local notification has a category, you can customize what appears in its secondary interface, in the central region between the app’s icon and display name, at the top, and the custom actions (if any), at the bottom. To do so, you write a notification content extension. This is a target, separate from your app target, because the system needs to access it outside your app, and even if your app isn’t running.

To add a notification content extension to your app, create a new target and specify iOS → Application Extension → Notification Content. The template gives you a good start on your extension. You have a storyboard with a single scene, and the code for a corresponding view controller that imports both the User Notifications framework and the User Notifications UI framework, as well as adopting the UNNotificationContentExtension protocol.

The view controller code contains a stub implementation of the didReceive(_:) method, which is the only required method. (Other methods are optional and have mostly to do with playback of media attached to your local notification; I’m not going to discuss them.) The parameter is a UNNotification containing your original UNNotificationRequest; you can examine this and extract information from it. The idea is that you might use this information to configure your interface. If you want to extract an attachment, you will have to wrap your access in calls to the URL methods startAccessingSecurityScopedResource and stopAccessingSecurityScopedResource.

The only other thing your view controller really needs to do is to set its own preferredContentSize to the desired dimensions of the custom interface. Alternatively, you can use autolayout to size the interface from the inside out, like a table view cell (Chapter 8).

For example, here’s how the custom interface in Figure 13-10 was attained. The interface consists of a label and an image view. The image view is to contain the image attachment from the local notification, so I extract the image from the attachment and set it as the image view’s image; I find that the interface doesn’t reliably appear unless we also call setNeedsLayout at the end:

override func viewDidLoad() {
    super.viewDidLoad()
    self.preferredContentSize = CGSize(320, 80)
}
func didReceive(_ notification: UNNotification) {
    let req = notification.request
    let content = req.content
    let atts = content.attachments
    if let att = atts.first, att.identifier == "cup" {
        if att.url.startAccessingSecurityScopedResource() {
            if let data = try? Data(contentsOf: att.url) {
                self.imageView.image = UIImage(data: data)
            }
            att.url.stopAccessingSecurityScopedResource()
        }
    }
    self.view.setNeedsLayout()
}

The template also includes an Info.plist for your extension. You will need to modify it by configuring these keys:

UNNotificationExtensionCategory

A string, corresponding to the categoryIdentifier of the local notification(s) to which this custom secondary interface is to be applied.

UNNotificationExtensionInitialContentSizeRatio

A number representing the width of your custom interface divided by its height. This doesn’t have to be perfect — and indeed it probably can’t be, since you don’t know the actual width of the screen on which this interface will be displayed — but the idea is to give the system a rough idea of the size as it prepares to display the custom interface.

UNNotificationExtensionDefaultContentHidden

Optional. A Boolean. Set to YES if you want to eliminate the default display of the local notification’s title, subtitle, and body from the custom interface.

Figure 13-11 shows the relevant part of the Info.plist for my content extension.

pios 2606ccc
Figure 13-11. A content extension’s Info.plist

Bear in mind that your custom interface is not interactive. There is no point putting a UIButton into the interface, for example, as the user cannot tap it. (This is why there are methods for asking the system to insert a tappable play/pause button; it’s because you cannot insert such a button directly.) What the user can tap are the custom action buttons. You can hear about such a tap in your content extension’s view controller by implementing this method:

  • didReceive(_:completionHandler:)

If you implement this method, you have effectively put yourself in front of your user notification center delegate’s userNotificationCenter(_:didReceive:withCompletionHandler:). All user actions will now be routed initially to the content extension’s view controller instead. You call the completion function with one of these responses (UNNotificationContentExtensionResponseOption):

.doNotDismiss

The local notification alert remains in place, still displaying the custom secondary interface — and you can modify that interface in response to the user action. Thus you can make the interface feel interactive, even though it isn’t.

.dismiss

The alert is dismissed. You can still modify the custom interface, delaying the call to the completion function so that the user has time to see the change.

.dismissAndForwardAction

The alert is dismissed and the action is passed along to your user notification center delegate’s userNotificationCenter(_:didReceive:withCompletionHandler:).

Today Extensions

The notification center that the user sees by pulling down from the top of the screen, as well as on the lock screen, has two pages. The list of local notifications is actually the second page. The first page is the today list. Here, apps can contribute widgets — informative bits of interface. For example, Apple’s Weather app posts the local temperature here, and the user can tap to open the Weather app itself. New in iOS 10, the today list appears also as part of the home screen search interface.

Your app, too, can provide a widget to appear here. To make that happen, you give your app a today extension. Your app vends the extension, and the user has the option of adding it to the today list (Figure 13-12).

pios 2606d
Figure 13-12. A today extension

To add a today extension to your app, create a new target and specify iOS → Application Extension → Today Extension. The template gives you a good start on your extension. You have a storyboard with a single scene, and the code for a corresponding view controller that adopts the NCWidgetProviding protocol. You might need to edit the extension’s Info.plist to set the “Bundle display name” entry — this is the title that will appear above your extension in the notification center.

Tip

The today extension target will be explicitly linked to the Notification Center framework (import NotificationCenter). Do not meddle with this linkage. This framework is crucial; without it, your today extension target may compile, but the today extension itself will crash.

Design your extension’s interface in the storyboard provided. To size your extension’s height, either provide sufficient constraints to determine the full height of your extension’s interface from the inside out (like a table view cell, Chapter 8), or set your view controller’s preferredContentSize.

Note

In previous versions of iOS, widgets were displayed using white-on-black with an optional vibrancy effect. In iOS 10, widgets are displayed in ordinary black-on-white. You should use the default view background for your interface’s main view, so as to get the standard light blur which is provided automatically.

Each time your today extension’s interface is about to appear, your code is given an opportunity to update its interface, through its implementation of the NCWidgetProviding method widgetPerformUpdate(completionHandler:). Be sure to finish up by calling the completionHandler, handing it an NCUpdateResult, which will be .newData, .noData, or .failed. Time-consuming work should be performed off the main thread (see Chapter 24):

func widgetPerformUpdate(completionHandler:
    @escaping (NCUpdateResult) -> Void) {
        // ... do stuff quickly ...
        completionHandler(.newData)
}

Communication back to your app can be a little tricky. In Figure 13-12, two buttons invite the user to set up a reminder notification; I’ve implemented these to open our CoffeeTime app by calling open(_:completionHandler:) — a method of the automatically provided extensionContext, not the shared application, which is not available from here:

@IBAction func doButton(_ sender: Any) {
    let v = sender as! UIView
    var comp = URLComponents()
    comp.scheme = "coffeetime"
    comp.host = String(v.tag) // button's tag is number of minutes
    if let url = comp.url {
        self.extensionContext?.open(url)
    }
}

The CoffeeTime app receives this message because I’ve given it two things:

A custom URL scheme

The coffeetime scheme is declared in the app’s Info.plist (Figure 13-13).

pios 2606e
Figure 13-13. A custom URL declaration
An implementation of application(_:open:options:)

In the app delegate, I’ve implemented application(_:open:options:) to analyze the URL when it arrives. I’ve coded the original URL so that the “host” is actually the number of minutes announced in the tapped button; thus, I can respond appropriately (presumably by scheduling a local notification for that number of minutes from now):

func application(_ app: UIApplication, open url: URL,
    options: [UIApplicationOpenURLOptionsKey : Any]) -> Bool {
        let scheme = url.scheme
        let host = url.host
        if scheme == "coffeetime" {
            if let host = host, let min = Int(host) {
                // ... do something here ...
                return true
            }
        }
        return false
}

New in iOS 10, a today extension’s widget interface can have two heights: compact and expanded. If you take advantage of this feature, your widget will have a Show More or Show Less button (similar to the Weather app’s widget). To do so, implement widgetActiveDisplayModeDidChange(_:withMaximumSize:). The first parameter is an NCWidgetDisplayMode, either .compact or .expanded. The idea is that you would then change your view controller’s preferredContentSize to the larger or smaller size.

New in iOS 10, if your app has a today extension, the today extension widget is displayed automatically when the user performs the 3D touch gesture that summons quick actions. Since our widget is interactive, we actually don’t need the static quick action buttons shown in Figure 13-5!

Activity Views

An activity view is the view belonging to a UIActivityViewController, typically appearing when the user taps a Share button. To display it, you start with one or more pieces of data, such as a string or an image, that you want the user to have the option of sharing or working with. The activity view, when it appears, will then contain an icon for every activity (UIActivity) that can work with this type of data. The user may tap an icon in the activity view, and is then perhaps shown additional interface, belonging to the provider of the chosen activity. Figure 13-14 shows an example, from Mobile Safari.

pios 2607
Figure 13-14. An activity view

In Figure 13-14, the top row of the activity view lists some applicable built-in system-wide activities; the bottom row shows some activities provided internally by Safari itself. When you present an activity view within your app, your app can add to the lower row additional activities that are available only within your app. Moreover, your app can provide system-wide activities that are available when any app presents an activity view; such system-wide activities come in two forms:

Share extensions

A share extension is shown in the upper row of an activity view. Share extensions are for apps that can accept information into themselves, either for storage, such as Notes and Reminders, or for sending out to a server, such as Twitter and Facebook.

Action extensions

An action extension is shown in the lower row of an activity view. Action extensions offer to perform some kind of manipulation on the data provided by the host app, and can hand back the resulting data in reply.

I’ll describe how to present an activity view and how to construct an activity that’s private to your app. Then I’ll give an example of writing an action extension, and finally an example of writing a share extension.

Presenting an Activity View

You will typically want to present an activity view in response to the user tapping a Share button in your app. To do so:

  1. Instantiate UIActivityViewController. The initializer you’ll be calling is init(activityItems:applicationActivities:), where the first argument is an array of objects to be shared or operated on, such as string or image objects. Presumably these are objects associated somehow with the interface the user is looking at right now.

  2. Set the activity view controller’s completionWithItemsHandler property to a function that will be called when the user’s interaction with the activity interface ends.

  3. Present the activity view controller, as a presented view controller; on the iPad, it will be a popover, so you’ll also configure the popover presentation controller. The presented view or popover will be dismissed automatically when the user cancels or chooses an activity.

So, for example:

let url = Bundle.main.url(forResource:"sunglasses", withExtension:"png")!
let things : [Any] = ["This is a cool picture", url]
let avc = UIActivityViewController(
    activityItems:things, applicationActivities:nil)
avc.completionWithItemsHandler = { type, ok, items, err in
    // ...
}
self.present(avc, animated:true)
if let pop = avc.popoverPresentationController {
    let v = sender as! UIView
    pop.sourceView = v
    pop.sourceRect = v.bounds
}

There is no cancel button in the popover presentation of the activity view; the user cancels by tapping outside the popover. Actually, the user can cancel by tapping outside the activity view even on the iPhone.

The activity view is populated automatically with known system-wide activities that can handle any of the types of data you provided as the activityItems: argument. These activities represent UIActivity types, and are designated by UIActivityType constants:

  • .postToFacebook

  • .postToTwitter

  • .postToWeibo

  • .message

  • .mail

  • .print

  • .copyToPasteboard

  • .assignToContact

  • .saveToCameraRoll

  • .addToReadingList

  • .postToFlickr

  • .postToVimeo

  • .postToTencentWeibo

  • .airDrop

  • .openInIBooks

Consult the UIActivity class documentation to learn what types of activity item each of these activities can handle. For example, the .mail activity will accept a string, an image, or a file on disk (such as an image file) designated by a URL; it will present a mail composition interface with the activity item(s) in the body.

Since the default is to include all the system-wide activities that can handle the provided data, if you don’t want a certain system-wide activity included in the activity view, you must exclude it explicitly. You do this by setting the UIActivityViewController’s excludedActivityTypes property to an array of activity type constants.

Tip

The Notes and Reminders activities have no corresponding UIActivity, because they are implemented as share extensions; it is up to the user to exclude them if desired.

In the UIActivityViewController initializer init(activityItems:applicationActivities:), if you would prefer that an element of the activityItems: array be an object that will supply the data, instead of the data itself, make that object adopt the UIActivityItemSource protocol. Typically, this object will be self (the view controller in charge of all this code). Here’s a minimal, artificial example:

extension ViewController : UIActivityItemSource {
    func activityViewControllerPlaceholderItem(
        _ activityViewController: UIActivityViewController) -> Any {
            return ""
    }
    func activityViewController(
        _ activityViewController: UIActivityViewController,
        itemForActivityType activityType: UIActivityType) -> Any? {
            return "Coolness"
    }
}

The first method provides a placeholder that exemplifies the type of data that will be returned; the second method returns the actual data. Observe that the second method can return different data depending on the activity type that the user chose; for example, you could provide one string to Notes and another string to Mail.

The UIActivitySource protocol also answers a commonly asked question about how to get the Mail activity to populate the mail composition form with a default subject:

extension ViewController : UIActivityItemSource {
    // ...
    func activityViewController(
        _ activityViewController: UIActivityViewController,
        subjectForActivityType activityType: UIActivityType?) -> String {
            return "This is cool"
    }
}

If your activityItems: data is time-consuming to provide, substitute an instance of a UIActivityItemProvider subclass:

let avc = UIActivityViewController(
    activityItems:[MyProvider(placeholderItem: "")],
    applicationActivities:nil)

The placeholderItem: in the initializer signals the type of data that this UIActivityItemProvider object will actually provide. Your UIActivityItemProvider subclass should override the item property to return the actual object. This property will be consulted on a background thread, and UIActivityItemProvider is itself an Operation subclass (see Chapter 24).

Custom Activities

The purpose of the applicationActivities: parameter of init(activityItems:applicationActivities:) is for you to list any additional activities implemented internally by your own app, so that their icons will appear as choices in the lower row when your app presents an activity view. Each activity will be an instance of one of your own UIActivity subclasses.

To illustrate, I’ll create a minimal (and nonsensical) activity called Be Cool that accepts string activity items. It is a UIActivity subclass called MyCoolActivity. So, to include Be Cool among the choices presented to the user by a UIActivityViewController, I’d say:

let things : [Any] = ["This is a cool picture", url]
let avc = UIActivityViewController(
    activityItems:things, applicationActivities:[MyCoolActivity()])

Now let’s implement MyCoolActivity. It has an array property called items, for reasons that will be apparent in a moment. We need to arm ourselves with an image to represent this activity in the activity view; this will be treated as a template image. It should be no larger than 60×60 (76×76 on iPad); it can be smaller, and looks better if it is, because the system will draw a rounded rectangle around it, and the image should be somewhat inset from this. It needn’t be square, as it will be centered in the rounded rectangle automatically.

Here’s the preparatory part of the implementation of MyCoolActivity:

override class var activityCategory : UIActivityCategory {
    return .action // the default
}
override var activityType : UIActivityType {
    return UIActivityType("com.neuburg.matt.coolActivity")
}
override var activityTitle : String? {
    return "Be Cool"
}
override var activityImage : UIImage? {
    return self.image
}
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
    for obj in activityItems {
        if obj is String {
            return true
        }
    }
    return false
}
override func prepare(withActivityItems activityItems: [Any]) {
    self.items = activityItems
}

If we return true from canPerform(withActivityItems:), then an icon for this activity, labeled Be Cool and displaying our activityImage, will appear in the activity view. If the user taps our icon, prepare(withActivityItems:) will be called. We retain the activityItems into our items property, because they won’t be arriving again when we are actually told to perform the activity.

The next step, if the user has tapped our icon in the activity view, is that we will be called upon to perform the activity. To do so, we implement one of these:

perform method

We simply perform the activity directly, using the activity items we’ve already retained. If the activity is time-consuming, the activity should be performed on a background thread (Chapter 24) so that we can return immediately; the activity view interface will be taken down and the user will be able to go on interacting with the app.

activityViewController property

We have further interface that we’d like to show the user as part of the activity, so we provide an instance of a UIViewController subclass. The activity view mechanism will present this view controller for us; it is not our job to present or dismiss it. (We may, however, present or dismiss dependent interface. For example, if our view controller is a navigation controller with a custom root view controller, we might push another view controller onto its stack while the user is interacting with the activity.)

No matter which of these two methods we implement, we must eventually call this activity instance’s activityDidFinish(_:). This is the signal to the activity view mechanism that the activity is over. If the activity view mechanism is still presenting any interface, it will be taken down, and the argument we supply here, a Bool signifying whether the activity completed successfully, will be passed into the function we supplied earlier as the activity view controller’s completionWithItemsHandler. So, for example:

override func perform() {
    // ... do something with self.items here ...
    self.activityDidFinish(true)
}

If your UIActivity is providing a view controller as its activityViewController, it will want to hand that view controller a reference to self beforehand, so that the view controller can call its activityDidFinish(_:) when the time comes.

For example, suppose our activity involves letting the user draw a mustache on a photo of someone. Our view controller will provide interface for doing that, including some way of letting the user signal completion, such as a Cancel button and a Done button. When the user taps either of those, we’ll do whatever else is necessary (such as saving the altered photo somewhere if the user tapped Done) and then call activityDidFinish(_:). Thus, we could implement the activityViewController property like this:

override var activityViewController : UIViewController? {
    let mvc = MustacheViewController(activity: self, items: self.items!)
    return mvc
}

And then MustacheViewController would have code like this:

weak var activity : UIActivity?
var items: [Any]
init(activity:UIActivity, items:[Any]) {
    self.activity = activity
    self.items = items
    super.init(nibName: "MustacheViewController", bundle: nil)
}
// ... other stuff ...
@IBAction func doCancel(_ sender: Any) {
    self.activity?.activityDidFinish(false)
}
@IBAction func doDone(_ sender: Any) {
    self.activity?.activityDidFinish(true)
}

Note that MustacheViewController’s reference to the UIActivity (self.activity) is weak; otherwise, a retain cycle ensues.

Note

The purpose of the SFSafariViewController delegate method safariViewController(_:activityItemsFor:title:) (Chapter 11) is now clear. This view controller’s view appears inside your app, but it isn’t your view controller, its Share button is not your button, and the activity view that it presents is not your activity view. Therefore you need some other way to add custom UIActivity items to that activity view; to do so, implement this method.

Action Extensions

To provide a system-wide activity — one that appears when some other app puts up an activity view — you can write a share extension (to appear in the upper row) or an action extension (to appear in the lower row). Your app can provide just one share extension, but can provide multiple action extensions. I’ll describe first the basics of writing an action extension. (For full information, study the relevant documentation and WWDC 2014 videos.)

As with today extensions, start with the appropriate target template, iOS → Application Extension → Action Extension. There are two kinds of action extension, with or without an interface; you’ll make your choice in the second pane as you create the target.

In the Info.plist, in addition to setting the bundle name, which will appear below the activity’s icon in the activity view, you’ll need to specify what types of data this activity accepts as its operands. In the NSExtensionActivationRule dictionary, you’ll provide one or more keys, such as:

  • NSExtensionActivationSupportsFileWithMaxCount

  • NSExtensionActivationSupportsImageWithMaxCount

  • NSExtensionActivationSupportsMovieWithMaxCount

  • NSExtensionActivationSupportsText

  • NSExtensionActivationSupportsWebURLWithMaxCount

For the full list, see the “Action Extension Keys” section of Apple’s Information Property List Key Reference. It is also possible to declare in a more sophisticated way what types of data your activity accepts, by writing an NSPredicate string as the value of the NSExtensionActivationRule key. Figure 13-15 shows the relevant part of the Info.plist for an action extension that accepts one text object.

pios 2608
Figure 13-15. An action extension Info.plist

When your action extension appears in an activity view within some other app that provides the appropriate type(s) of data, it will be represented by an icon which you need to specify by editing your action extension target. This icon is the same size as an app icon, and can conveniently come from an asset catalog; it will be treated as a template image.

There is one big difference between an action extension and a custom UIActivity: an action extension can return data to the calling app. The transport mechanism for this data is rather elaborate, as I shall now explain.

Action extension without an interface

I’ll start by giving an example of an action extension that has no interface. Our code goes into the class provided by the template, ActionRequestHandler, an NSObject subclass.

Our example extension takes a string object and returns a string. In particular, it accepts a string that might be the two-letter abbreviation of one of the U.S. states, and if it is, it returns the name of the actual state. To prepare, we provide some properties:

var extensionContext: NSExtensionContext?
let desiredType = kUTTypePlainText as String
let list : [String:String] = { /* ... */ }()

self.extensionContext is a place to store the NSExtensionContext that will be provided to us. self.desiredType is just a convenient constant expressing the acceptable data type. In addition, we have a property self.list which, as in Chapter 10, is a dictionary whose keys are state name abbreviations and whose values are the corresponding state names.

There is just one entry point into our extension’s code — beginRequest(with:). Here we must store a reference to the NSExtensionContext provided as the parameter, retrieve the data, process the data, and return the result. You will probably want to factor the processing of the data out into a separate function; I’ve called mine process(item:). Here’s a sketch of my beginRequest(with:) implementation; as it shows, my plan is to make one of two possible calls to self.process(item:), either passing the string retrieved from items, or else passing nil to signify that there was no data:

func beginRequest(with context: NSExtensionContext) {
    self.extensionContext = context
    let items = self.extensionContext!.inputItems
    // ... if there is no data, call self.process(item:) with nil
    // ... if there is data, call self.process(item:) with the data
}

Now let’s implement the retrieval of the data. Think of this as a series of envelopes (or nested matryoshka dolls) that we must examine and open:

  • What arrives from the NSExtensionContext’s inputItems is an array of NSExtensionItem objects.

  • An NSExtensionItem has an attachments array of NSItemProvider objects.

  • An NSItemProvider vends items, each of which represents the data in a particular format. In particular:

    • We can ask whether an NSItemProvider has an item of a particular type, by calling hasItemConformingToTypeIdentifier(_:).

    • We can retrieve the item of a particular type, by calling loadItem(forTypeIdentifier:options:completionHandler:). The item may be vended lazily, and can thus take time to prepare and provide; so we proceed in the completionHandler: to receive the item and do something with it.

We are expecting only one item, so it will be provided by the first NSItemProvider inside the first NSExtensionItem. Here, then, is the code that I omitted from beginRequestWithExtensionContext:

guard let extensionItem = items[0] as? NSExtensionItem,
    let provider = extensionItem.attachments?[0] as? NSItemProvider,
    provider.hasItemConformingToTypeIdentifier(self.desiredType)
    else { self.process(item:nil); return }
provider.loadItem(forTypeIdentifier: self.desiredType) { item, err in
    DispatchQueue.main.async {
        self.process(item: item as? String)
    }
}

Now we have the data, and we’re ready to do something with it. In my code, that happens in the method that I’ve named process(item:). This method must do two things:

  1. Call the NSExtensionContext’s completeRequest(returningItems:completionHandler:) to hand back the data.

  2. Release the NSExtensionContext by setting our retaining property to nil.

I’ll start with the simplest case: we didn’t get any data. In that case, the returned value is nil:

func process(item:String?) {
    var result : [NSExtensionItem]? = nil
    // ... what goes here? ...
    self.extensionContext?.completeRequest(returningItems: result)
    self.extensionContext = nil
}

That was easy, because we cleverly omitted the only case where we have any work to do. Now let’s implement that case. We have received a string in the item parameter. The first question is: is it the abbreviation of a state? To answer that question, I’ve implemented a utility function:

func state(for abbrev:String) -> String? {
    return self.list[abbrev.uppercased()]
}

If we call that method with our item string and the answer comes back nil, we simply proceed just as before — we return nil:

func process(item:String?) {
    var result : [NSExtensionItem]? = nil
    if let item = item,
        let abbrev = self.state(for:item) {
            // ... what goes *here*? ...
    }
    self.extensionContext?.completeRequest(returningItems: result)
    self.extensionContext = nil
}

We come at last to the dreaded moment that I have been postponing all this time: what if we get an abbreviation? In that case, we must reverse the earlier process of opening envelopes: we must put envelopes within envelopes and hand back an array of NSExtensionItems. We have only one result, so this will be an array of one NSExtensionItem, whose attachments is an array of one NSItemProvider, whose item is the string and whose typeIdentifier is the type of that string. Confused? Here, I’ve written a little utility function that should clarify:

func stuffThatEnvelope(_ item:String) -> [NSExtensionItem] {
    let extensionItem = NSExtensionItem()
    let itemProvider = NSItemProvider(
        item: item as NSString, typeIdentifier: desiredType)
    extensionItem.attachments = [itemProvider]
    return [extensionItem]
}

We can now write the full implementation of process(item:), and our action extension is finished:

func process(item:String?) {
    var result : [NSExtensionItem]? = nil
    if let item = item,
        let abbrev = self.state(for:item) {
            result = self.stuffThatEnvelope(abbrev)
    }
    self.extensionContext?.completeRequest(returningItems: result)
    self.extensionContext = nil
}

Action extension with an interface

If an action extension has an interface, then the template provides a storyboard with one scene, along with the code for a corresponding UIViewController class. The code is actually simpler, because:

  • A view controller already has an extensionContext property, and it is automatically set for us.

  • There are no special entry points to our code. This is a UIViewController, and everything happens just as you would expect.

So, in my implementation, I use viewDidLoad to open the data envelope from self.extensionContext, get the abbreviation if there is one, get the expansion if there is one (storing it in a property, self.expansion), and stop. I’ve equipped my interface with a Done button and a Cancel button. The action methods for those buttons are where I hand the result back to the extensionContext:

@IBAction func cancel(_ sender: Any) {
    self.extensionContext?.completeRequest(returningItems: nil)
}
@IBAction func done(_ sender: Any) {
    self.extensionContext?.completeRequest(
        returningItems: self.stuffThatEnvelope(self.expansion!))
}

The runtime responds by dismissing the interface in good order.

Receiving data from an action extension

Now switch roles and pretend that your app is presenting a UIActivityViewController. We now know that this activity view might contain action extension icons. If the user taps one, how will your code retrieve the result? In my earlier implementation, I avoided this issue by pretending that action extensions didn’t exist. Here’s a more complete sketch:

let avc = UIActivityViewController(
    activityItems:[things], applicationActivities:nil)
avc.completionWithItemsHandler = { type, ok, items, err in
    if ok {
        guard let items = items, items.count > 0 else { return }
        // ... open the envelopes! ...
    }
}
self.present(avc, animated:true)

If what the user interacted with in the activity view is one of the built-in UIActivity types, no data has been returned. But if the user interacted with an action extension, then there may be data inside the items envelopes, and it is up to us to retrieve it.

The structure here is exactly the same as the items of an NSExtensionContext: items is an array, each element of which is presumably an NSExtensionItem, whose attachments is presumably an array of NSItemProvider objects, each of which can be queried for its data. In the case where we are prepared to receive a string, therefore, the code is effectively just the same as the envelope-opening code we’ve already written:

guard let items = items, items.count > 0 else { return }
guard let extensionItem = items[0] as? NSExtensionItem,
    let provider = extensionItem.attachments?[0] as? NSItemProvider,
    provider.hasItemConformingToTypeIdentifier(self.desiredType)
    else { return }
provider.loadItem(forTypeIdentifier: self.desiredType) { item, err in
    DispatchQueue.main.async {
        if let s = item as? String {
            // ... do something with s ...
        }
    }
}

Share Extensions

Your app can appear in the top row of an activity view if it provides a share extension. A share extension is similar to an action extension, but simpler: it accepts some data and returns nothing. The idea is that it will then do something with that data, such as storing it or posting it to a server.

The user, after tapping an app’s icon in the activity view, is given an opportunity to interact further with the data, possibly modifying it or canceling the share operation. To make this possible, the Share Extension template, when you create the target (iOS → Application Extension → Share Extension), will give you a storyboard and a view controller. This view controller can be one of two types:

An SLComposeServiceViewController

The SLComposeServiceViewController provides a standard interface for displaying editable text in a UITextView along with a possible preview view, plus user-configurable option buttons, along with a Cancel button and a Post button.

A plain view controller subclass

If you opt for a plain view controller subclass, then designing its interface, including providing a way to dismiss it, will be up to you.

Whichever form of interface you elect to use, your way of dismissing it will be this familiar-looking incantation:

self.extensionContext?.completeRequestReturningItems([])

As you are most likely to use an SLComposeServiceViewController, I’ll briefly describe some of the basics of working with one. An SLComposeServiceViewController’s view is displayed with its text view already populated with the text passed along from the host app, so there’s very little more for you to do; you can add a preview view and option buttons, and that’s just about all. I’ll concentrate on option buttons.

An option button displays a title string and a value string. When tapped, it will typically summon interface where the user can change the value string. In Figure 13-16, I’ve created a single option button — a Size button, whose value can be Large, Medium, or Small. (I have no idea what this choice is supposed to signify for my app; it’s only an example!)

pios 2609
Figure 13-16. A share extension

To create the configuration option, I override the SLComposeServiceViewController configurationItems method to return an array of one SLComposeSheetConfigurationItem. Its title and value are displayed in the button. Its tapHandler will be called when the button is tapped. Typically, you’ll create a view controller and push it into the interface with pushConfigurationViewController:

weak var config : SLComposeSheetConfigurationItem?
var selectedText = "Large" {
    didSet {
        self.config?.value = self.selectedText
    }
}
override func configurationItems() -> [Any]! {
    let c = SLComposeSheetConfigurationItem()!
    c.title = "Size"
    c.value = self.selectedText
    c.tapHandler = { [unowned self] in
        let tvc = TableViewController(style: .grouped)
        tvc.selectedSize = self.selectedText
        tvc.delegate = self
        self.pushConfigurationViewController(tvc)
    }
    self.config = c
    return [c]
}

My TableViewController is a UITableViewController subclass. Its table view displays three rows whose cells are labeled Large, Medium, and Small, along with a checkmark (compare the table view described in “Cell Choice and Static Tables”). The tricky part is that I need a way to communicate with this table view controller: I need to tell it what the configuration item’s value is now, and I need to hear from it what the user chooses in the table view. So I’ve given the table view controller a property (selectedSize) where I can deposit the configuration item’s value, and I’ve declared a delegate protocol so that the table view controller can set a property of mine (selectedText). This is the relevant portion of my TableViewController class:

protocol SizeDelegate : class {
    var selectedText : String {get set}
}
class TableViewController: UITableViewController {
    var selectedSize : String?
    weak var delegate : SizeDelegate?
    override func tableView(_ tableView: UITableView,
        didSelectRowAt indexPath: IndexPath) {
            let cell = tableView.cellForRow(at:indexPath)!
            let s = cell.textLabel!.text!
            self.selectedSize = s
            self.delegate?.selectedText = s
            tableView.reloadData()
    }
    // ...
}

The navigation interface is provided for me, so I don’t have to do anything about popping the table view controller: the user will do that by tapping the Back button after choosing a size. In my configurationItems implementation, I cleverly kept a reference to my configuration item as self.config. When the user chooses from the table view, its tableView(_:didSelectRowAt:) sets my selectedText, and my selectedText setter observer promptly changes the value of the configuration item to whatever the user chose.

The user, when finished interacting with the share extension interface, will tap one of the provided buttons, either Cancel or Post. The Cancel button is handled automatically: the interface is dismissed. The Post button is hooked automatically to my didSelectPost implementation, where I fetch the text from my own contentText property, do something with it, and dismiss the interface:

override func didSelectPost() {
    let s = self.contentText
    // ... do something with it ...
    self.extensionContext?.completeRequest(returningItems:[])
}

If the material provided from the host app were more elaborate, I would pull it out of self.extensionContext in the same way as for an action extension. If there were networking to do at this point, I would initiate a background URLSession (as explained in Chapter 23).

Warning

There is no supported way, as far as I can tell, to change the title or appearance of the Cancel and Post buttons. Apps that show different buttons, such as Reminders and Notes, are either not using SLComposeServiceViewController or are using a technique available only to Apple.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.29.112