Sharing and Notifications - Swift Development with Cocoa (2015)

Swift Development with Cocoa (2015)

Chapter 18. Sharing and Notifications

Just about every app these days deals with some kind of content—whether it’s business documents written in an office suite, images created in an image editor, or even high scores earned in a game. Users frequently want to be able to show this content to other people, and the OS provides built-in sharing APIs that let your application send various kinds of content to services that can handle them. For example, online services like YouTube and Vimeo can receive video files and share them over the Internet, the Messages app can send text, photos, and videos, and email can send just about any file.

In addition to sending content to other locations, the OS is also capable of receiving notifications. These are short messages sent from a server to an iOS device, which are received regardless of whether the app is running or the phone is awake.

In this chapter, you’ll learn how to share data from your application using the built-in sharing APIs, and how to send and receive both push and local notifications.

Sharing

From the user’s perspective, the problem of data sharing can be rephrased as, “How can I send this to someone else?” From your application’s perspective, however, the problem of data sharing is really the question, “Where can I send this data?”

Different systems are capable of accepting different kinds of data. A video, for example, cannot be sent to a printer, and plain text cannot be sent to a photo-hosting site like Flickr. Fortunately, the sharing systems on both iOS and OS X already know what different data types are supported by the sharing destinations that the OS knows about.

As of OS X 10.10, the available sharing destinations for OS X are as follows:

Email

Text, images, videos, and anything that can be copied and pasted

Messages

Same content as email

AirDrop

Files

Aperture

Photos

iPhoto

Photos

Flickr

Photos

YouTube, Vimeo, Todou, Youku

Videos

Safari Reading List

URLs

Setting the Desktop background

Images

Setting a Twitter, LinkedIn, or Facebook profile picture

Images

Twitter, Facebook, LinkedIn, Tencent Weibo, and Sina Weibo

Text, images, videos, and URLs

As of iOS 8, the available sharing destinations for iOS are as follows:

Email

Text, images, videos, and URLs (including URLs pointing to local files)

Messages

Text and images

AirDrop

Files

Flickr

Photos

YouTube, Vimeo, Todou, Youku

Videos

Twitter, Facebook, Tencent Weibo, and Sina Weibo

Text, images, videos, and URLs

Copying to the pasteboard

Text, images, URLs, colors, and NSDictionary objects

Saving to the camera roll

Images and videos

Printing

Text, images, and any of the UIPrintRenderer or related printing objects (see Printing Documents)

Assigning to a contact

Images

NOTE

Sina Weibo and Tencent Weibo are social media services, similar to Twitter and Facebook, based in the People’s Republic of China. Youku and Tudou are video hosting services, similar to YouTube and Vimeo, and are also based in the People’s Republic of China.

As you can see, there are a number of different kinds of content that can be given to the various sharing destinations. Fortunately, the method for actually sharing is rather straightforward:

1. Make an array containing all of the things you want to share. This array should contain everything that you want to share—text, images, videos, and so on.

2. Give this array to the sharing system. The OS will figure out which sharing destinations can be used based on the content that was provided. The greater the number of different kinds of content you provide, the more sharing destinations will be offered to the user.

For example, if you provide both text and an image on iOS, “Save to camera roll” appears, even though it doesn’t support both. Only the supported content will be shared by the selected sharing destination.

3. Let the sharing system actually handle the sharing. Depending on which sharing destination was selected, the user might be prompted to provide a little more information. For example, if the user is posting an image to Twitter, he’ll be presented with a Twitter share sheet, which allows him to add some text before sending the tweet.

It’s a simple and elegant system, and can be a very positive thing for your apps.

To get our hands dirty, we’re going to take a look at the different sharing APIs that are available on both iOS and OS X.

Sharing on iOS

Sharing content on iOS is handled by the UIActivityViewController. When you have some content that you want to share—some text stored in a string, say—all you need to do is create a new UIActivityViewController and provide it with an array containing that object:

let text = "Hello, world!"

let activity = UIActivityViewController(activityItems: [text],

applicationActivities: nil)

The second parameter, left nil in the previous example, can also take an array of UIActivity subclasses. These can be used if you want your app to provide custom sharing destinations. Note, however, that any custom sharing destinations you include will only show up inside your own app.

When that’s done, you just need to present the view controller modally as you would any other modal view controller:

self.presentViewController(activity, animated: true, completion: nil)

From there, the OS takes over, allowing the user to select the sharing destination and completing the share.

To show this in action, we’ll build a simple iOS application that supports sharing both text and images:

1. Create a new single view iPhone application called iOSSharing.

2. Add an image to share. Find an image of some sort on your computer, or take one with a camera. Once you have it, drag it into the project’s asset catalog.

NOTE

Apps can also capture images from the built-in camera. We cover how to make apps that do this in Capturing Photos and Video from the Camera.

3. Create the interface. Open Main.storyboard, and drag a text field into the top of the window.

Drag in a UIButton just beneath the text field. Set its title to Share Text.

Drag in a UIImageView beneath this button, and set its image to the one that you added earlier.

Finally, drag in a second UIButton and place it beneath the image view. Set its title to Share Image.

4. Connect the interface. There are two actions to add: one for sharing text and one for sharing an image. We’re going to connect these two actions to the appropriate buttons; we’ll also use the text field and the image view as the sources for the content that will be shared:

Open ViewController.swift in the assistant, and Control-drag from the top and bottom buttons into the class’s interface. The two actions you want to create are shareImage and shareText.

Once that’s done, Control-drag from the text field into ViewController’s interface, and create a new outlet called textView. Then, Control-drag from the image view into ViewController’s interface and create a new outlet called imageView.

Finally, because we want the keyboard to go away when the user taps the Return button, Control-drag from the text field to the File’s Owner in the outline, and choose “delegate” from the menu that pops up.

The final result should look like Figure 18-1.

The code for this is extremely simple. The two methods that are run when the share buttons are tapped are only two lines each.

5. Add the image sharing method. Add the following code to shareImage:

6. @IBAction func shareImage(sender: AnyObject) {

7.

8. if let image = self.imageView?.image {

9.

10. let activity = UIActivityViewController(

11. activityItems: [image], applicationActivities: nil)

12.

13. self.presentViewController(activity,

14. animated: true, completion: nil)

15.

16. } else {

17. // No image to share!

18. }

19.

}

The connected interface

Figure 18-1. The connected interface

6. Add the text sharing method. Add the following code to shareText:

7. @IBAction func shareText(sender: AnyObject) {

8. let activity = UIActivityViewController(

9. activityItems: [self.textView.text], applicationActivities: nil)

10.

11. self.presentViewController(activity, animated: true, completion: nil)

}

12.Finally, add the code to dismiss the keyboard when the Return button is tapped. Add the following method to AppDelegate’s implementation:

13.func textFieldShouldReturn(textField: UITextField!) -> Bool {

14. textField.resignFirstResponder()

15. return false

}

16.Run the app. Try sharing both text and images, and see what sharing services can be used for each.

Sharing on OS X

Sharing content on OS X is very similar to sharing on iOS; the only real difference is in how the list of sharing destinations is presented.

On OS X, you create an NSSharingServicePicker, which presents a menu of available sharing destinations depending on what content you provide it. This pattern is very similar to iOS’s model.

Creating an NSSharingServicePicker looks like this:

var text = "Hello, world!"

var share = NSSharingServicePicker(items: [text])

After the picker has been created, it needs to be presented to the user. Because the picker shows a menu, it needs to know where the menu should appear on screen. This information is provided when you call the showRelativeToRect(_, ofView:, preferredEdge:) method. This method receives an NSRect and an NSView that the rectangle should be considered to be in the coordinate space of, as well as information indicating which edge of the rectangle the menu should appear on.

For example, if you have an NSView called myView, you can tell the menu to appear on the far right edge of its bounds rectangle with the following call:

share.showRelativeToRect(aView.bounds, ofView: aView, preferredEdge: 2)

The behavior of the picker after this point is identical to UIActivityViewController. The user is invited to choose which sharing service to use, and the OS will ask for additional information as necessary.

Notifications

Notifications are a way for your app to send messages to the user, regardless of whether the application is currently being used or even running. Originally introduced in iOS 3, they’ve become an indispensable tool for many apps. Starting with OS X 10.7, notifications are also available on the Mac. Figure 18-2 shows an example of a notification.

A notification

Figure 18-2. A notification

There are two kinds of notifications: push notifications (also known as remote notifications) and local notifications. Push notifications are sent from a server you control to the device, while local notifications are scheduled by your app to be displayed later.

Registering Notification Settings

Notifications on iOS can be intrusive. When you’re using an app, it can be annoying to have your experience interrupted by another app; it’s just as annoying to have your phone buzz in your pocket and disturb you when that notification isn’t particularly important. As a result, before you can show either type of notification, you need to first tell iOS what kinds of notifications your app intends to show.

NOTE

This section applies to iOS only. It doesn’t apply to OS X.

You do this by creating a UIUserNotificationSettings object, which specifies which kinds of notifications your app would like to show. You then call the registerUserNotificationSettings on the shared UIApplication object, which registers these settings:

// Indicating that we we want to deliver alerts

let notificationSettings = UIUserNotificationSettings(

forTypes: UIUserNotificationType.Alert, categories: nil)

UIApplication.sharedApplication().registerUserNotificationSettings(

notificationSettings)

When you call registerUserNotificationSettings for the first time, the system pops up a dialog box asking if the user wants to grant permission to display notifications, as shown in Figure 18-3.

In this example, the call to registerUserNotificationSettings was simply declaring that notifications were going to be sent. However, you can provide more information about what your notifications will contain, including whether your notifications should include actions.

Actions are simply small buttons that are attached to your notifications. When a notification arrives, you can attach actions that the user selects by swiping the notification to the left, as shown in Figure 18-4.

To set this up, you first need to create a notification action object, which is an instance of UIMutableUserInteractionAction. You then set up things like the text that should be shown to the user, an internal identifier string, and whether selecting the action should cause the app to launch or not:

When your app registers for notifications, iOS displays this alert to ask for the user’s permission

Figure 18-3. When your app registers for notifications, iOS displays this alert to ask for the user’s permission

let notificationAction = UIMutableUserNotificationAction()

// The title is shown to the user

notificationAction.title = "Save World"

// The identifier is used by your app

notificationAction.identifier = "saveWorldAction"

// When this action is selected, bring the app to the foreground

notificationAction.activationMode =

UIUserNotificationActivationMode.Foreground

// If true, the user must enter their passcode before the action runs.

// (Always set to true when the activation mode is Foreground.)

notificationAction.authenticationRequired = false

// Should the action be highlighted as destructive? (i.e., red and

// dangerous looking)

notificationAction.destructive = false

Actions can also be configured as destructive. Destructive actions are ones that can have some kind of irreversible, destructive effect, like deleting files. When you mark an action as destructive, the only thing that changes is its appearance:

A notification with an action attached

Figure 18-4. A notification with an action attached

// This action will be presented as destructive

let destructiveNotificationAction = UIMutableUserNotificationAction()

// Conquering the world is generally destructive

destructiveNotificationAction.title = "Conquer World"

// This action will launch the app in the background

destructiveNotificationAction.activationMode =

UIUserNotificationActivationMode.Background

// Highlight the action as destructive

destructiveNotificationAction.destructive = true

Once you’ve defined your actions, you create a category. Categories are just containers of actions combined with an identifier string; the idea is that, when you send a notification, you include which category should be used, which lets the system know which action buttons to display.

When you provide the actions to the category, you also specify which context these actions should be displayed in. There are two contexts: the default context, where there’s lots of space for showing buttons (in other words, the lock screen—see Figure 18-5), and the minimal context, where there’s less space (as seen in Figure 18-6, which shows a notification that you need to pull down on to reveal):

var notificationCategory = UIMutableUserNotificationCategory()

// The name of the category, used by the app to tell different

// notifications apart

notificationCategory.identifier = "com.oreilly.MyApplication.message"

// Set which actions are displayed when a large amount of space is

// visible; up to 4 can be provided

notificationCategory.setActions(

[notificationAction, destructiveNotificationAction],

forContext: UIUserNotificationActionContext.Default)

// Set which actions are displayed when not much space is visible;

// up to 2 can be provided

notificationCategory.setActions([notificationAction],

forContext: UIUserNotificationActionContext.Minimal)

NOTE

The default context has room for four actions, while the minimal context only has room for two.

The default notification context

Figure 18-5. The default notification context

Finally, when you’re done setting up your categories, you put those categories in an NSSet object, and use it to create your UIUserNotificationSettings:

// Create an NSSet containing a single object (the category

// we defined earlier)

var notificationCategories = NSSet(object: notificationCategory)

// Use that NSSet to create the notification settings

let notificationSettingsWithAction =

UIUserNotificationSettings(forTypes: UIUserNotificationType.Alert,

categories: notificationCategories)

// Register those notification settings

UIApplication.sharedApplication()

.registerUserNotificationSettings(notificationSettingsWithAction)

The minimal notification context

Figure 18-6. The minimal notification context

Push Notifications

Every iOS and OS X machine with notifications enabled maintains a permanent connection to Apple’s push notification service, which delivers short, infrequent messages to applications.

Push notifications work by having a server make an SSL-secured TCP connection to the Apple push notification service. When an application wants to receive push notifications, it calls the registerForRemoteNotifications method on the application’s global UIApplication orNSApplication object.

When registering for remote notifications, it’s important to know about the different kinds that can be delivered. Notifications can:

§ Set a badge (i.e., a number) on the app’s icon, either in the Dock on OS X or on the home screen in iOS

§ Play a sound file included in the app

§ Display an alert

All notifications on all platforms may also include additional application-specific information.

What Happens When a Notification Arrives

When a remote notification arrives, your application may or may not be running. If it’s running, your application delegate receives the application(_, didReceiveRemoteNotification:) message, which contains as its second parameter a dictionary containing any additional information in the notification. This is your app’s opportunity to do something useful with the notification.

If the application isn’t running when the notification arrives, what it’s able to do depends on the platform. On OS X, the only thing a notification can do if the app isn’t running is to modify the app icon’s badge. This is less of a restriction than it seems, because an application that’s currently running receives a message sent to its application delegate when the notification arrives, and your application can run any code you want when that happens.

On iOS, it’s another matter. Because only one app is allowed to be open at the same time (all other apps are allowed to run in the background for a bit, but are eventually suspended or terminated—see Multitasking on iOS), a notification is more likely to arrive when the app isn’t running.

An iOS notification can contain an alert, which is a string of text that’s shown to the user. When the notification arrives, if it contains an alert, that alert text appears on the screen. (The specific presentation depends on the user’s preferences, but it’s generally a banner at the top of the screen, or a pop-up box if the phone was locked when the notification arrived.) When the user interacts with this alert, the application is launched (or resumed if it was already launched). At this point, your application is informed that it received a notification, and can then respond appropriately.

This is why iOS notifications can contain more than OS X applications—because an iOS app is much less likely to be able to respond to a notification at the moment it arrives, the system steps in to provide some minimal functionality (showing some text, playing a sound, etc.). On OS X, that’s not necessary, because the application is much more likely to be running in the background.

Sending Push Notifications

A push notification is nothing more than a JSON-formatted dictionary. When a push notification is created, it can contain any valid JSON data that you want (i.e., strings, numbers, arrays, and dictionaries).

NOTE

For more information on JSON-formatted data, see Saving More Complex Data in Chapter 13.

When your application receives the notification, it receives an NSDictionary containing whatever was sent as the push notification.

In addition to the application-specific data that can be included in the JSON dictionary, push notifications also contain a special aps dictionary. This dictionary contains information about how the push notification should be presented: its alert text, the number to display in the application icon’s badge, and so on.

For example, here’s a sample push notification in JSON form:

{

"aps":{

"alert":"Hello, world!",

"badge":1,

"sound":"hello.wav"

},

"foo":"bar"

}

This push notification, when delivered to an iOS device, does the following things:

§ Displays the alert text “Hello, world!” on the screen

§ Sets the badge on the application’s icon to 1

§ Plays the sound hello.wav (which must be inside the application’s bundle—see Using NSBundle to Find Resources in Applications)

WARNING

The maximum size for a push notification is 256 bytes, including the aps dictionary. If you try to send a push notification larger than this, it won’t be accepted by the Apple Push Notification Service.

Additionally, delivery of push notifications is not guaranteed. Apple describes it as a “best effort” service, much like SMS. And the delivery mechanism for push notifications cannot be considered secure.

Therefore, your push notifications should never contain any sensitive information, and they shouldn’t be used as the primary way for your application to receive data. Instead, use push notifications to let your application know that new data is available and let the application itself do the work of actually retrieving that data.

Additionally, when the application receives the notification (either because the notification was opened by the user or the application was running when the notification was received), the NSDictionary that the application receives will contain a value @"bar" for the foo key.

NOTE

This is a book on Cocoa, not server programming, so we’re not going to go into a huge amount of detail on how to set up a server that sends push notifications to Apple. For information on how to do this, see “Apple Push Notification Service” in the Local and Remote Notification Guide, included in the Xcode developer documentation.

Setting Up to Receive Push Notifications

Applications don’t receive push notifications automatically. That’s because push notifications are considered a potential intrusion on the user’s device—if, as a user, you don’t want an app to interrupt you, it shouldn’t be able to.

In order to make your app indicate to the push notification system that it wants to receive notifications, it needs to make a method call to the global UIApplication or NSApplication object:

UIApplication.sharedApplication().registerForRemoteNotifications()

Before you call registerForRemoteNotifications, you must first indicate to the system which types of notifications you plan on receiving, just like you do for local notifications, using the registerUserNotificationSettings method.

When you register these notification types, the OS will present an alert box to the user, asking if he wants to receive notifications. If he chooses not to receive them, your application delegate receives the application(_, didFailToRegisterForRemoteNotificationsWithError:) message immediately after your call to registerForRemoteNotifications to let your code know about it:

func application(application: UIApplication!,

didFailToRegisterForRemoteNotificationsWithError error: NSError!) {

// Called when registering for remote notifications

// doesn't work for some reason

println("Failed to register for notifications! \(error)")

}

Registering for push notifications can also fail if there’s no Internet connection, if the push notification service is down, or if your code is running on a platform that doesn’t support push notification.

If the user does want notifications, the OS contacts the Apple Push Notification service (APNs), which registers the device and application as able to receive notifications. Once this is done, the APNs sends a device token back to your application, which is a unique ID that acts as a “telephone number” for push notifications. When a push notification is created, the device token is included and sent to the APNs, which uses it to figure out which of the millions of devices worldwide should receive the notification.

When the application successfully registers for push notifications, it receives the application(_, didRegisterForRemoteNotificationsWithDeviceToken:) message, which takes as a parameter an NSData object containing the device token:

func application(application: UIApplication!,

didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData!) {

// Called when we've successfully registered for remote notifications.

// Send the deviceToken to a server you control; it uses that token

// to send pushes to this specific device.

}

Once you have a device token, it needs to be sent to whatever server will actually be sending the push notifications. Without the device token, it’s not possible to indicate which device should receive a push.

NOTE

If you don’t want to deal with setting up your own push server, there are several existing services that can handle it for you. Most of them are based on usage—that is, the number of push notifications you send per month—and many include a free plan. We’ve used Urban Airship and Parse.

Receiving Push Notifications

Remember that a push notification may arrive when your application is open, or when it’s not.

When your application is open and a push notification is received, your application delegate receives the application(_, didReceiveRemoteNotification:) message:

func application(application: UIApplication!,

didReceiveRemoteNotification userInfo: NSDictionary!) {

// Called when a remote notification arrives, but no action was selected

// or the notification came in while using the app

// Do something with the information stored in userInfo

}

This method receives an NSDictionary that contains whatever information was contained inside the JSON bundle that was originally sent.

If the user selects an action (if one is available), the application delegate instead receives the application(_, handleActionWithIdentifier:, forRemoteNotification:, completionHandler) message:

func application(application: UIApplication!,

handleActionWithIdentifier identifier: String!,

forRemoteNotification userInfo: NSDictionary!,

completionHandler: (() -> Void)!) {

// Called when a remote notification arrives,

// and the user selected an action

}

If your application is not running and is opened from a push notification, then your application launches and your application delegate receives the application(_, didFinishLaunchingWithOptions:) message (on iOS). This is the same message that’s sent when an application normally launches, but when opening from a push, the launchOptions dictionary contains the contents of the JSON dictionary:

func application(application: UIApplication!,

didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {

var remoteNotification =

launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]

if remoteNotification? {

// do something with the notification info

}

return true;

}

On OS X, the message received by the application delegate is a little different. On launch, the application delegate receives the applicationDidFinishLaunching message, which takes an NSNotification object as a parameter. The equivalent to iOS’s launchOptions dictionary can be accessed thusly:

func applicationDidFinishLaunching(_ aNotification: NSNotification) {

if let remoteNotification = aNotification

.userInfo[NSApplicationLaunchRemoteNotificationKey] {

// do something with remoteNotification, which contains

// the notification info

}

}

Finally, if your application ever needs to stop receiving push notifications, it can unregister from the Apple Push Notification service by sending the unregisterForRemoteNotifications message to the global UIApplication or NSApplication object:

UIApplication.sharedApplication().unregisterForRemoteNotifications()

If you unregister for notifications, you can always register again later.

Local Notifications

While remote notifications require a complex setup involving a remote computer that communicates with the Apple Push Notification service, local notifications are created and presented entirely on the device.

NOTE

Local notifications are only available on iOS.

A local notification looks the same as a remote notification to the user, but its delivery is controlled by the application. Local notifications are represented by the UILocalNotification class.

Local notifications can either be created and presented immediately (if the application is currently running and is in the background), or scheduled to appear at a certain date and time.

To construct a local notification, you simply create, configure, and schedule a UILocalNotification:

var localNotification = UILocalNotification()

localNotification.fireDate = NSDate(timeIntervalSinceNow: 3);

localNotification.category = "com.oreilly.MyApplication.message"

localNotification.alertBody = "The world is in peril!"

UIApplication.sharedApplication()

.scheduleLocalNotification(localNotification)

This example code creates a local notification that displays the text “The world is in peril!” three seconds after it’s created.

When a notification fires and the user chooses to open it without selecting an action, the application delegate receives a message very similar to the one used when a remote notification arrives, application(didReceiveLocalNotification:):

func application(application: UIApplication!,

didReceiveLocalNotification notification: UILocalNotification!) {

// Called when the user taps on a local notification (without selecting

// an action), or if a local notification arrives while using the app

// (in which case the notification isn't shown to the user)

println("Received notification \(notification.category)!")

}

If an action was selected, your application delegate receives the application( handleActionWithIdentifier:forLocalNotification:completionHandler:) message, which receives both the UILocalNotification object, as well as the identifier string for the action that was selected. In addition to these, you also receive a completion block, which you must call after you finish dealing with the action. The reason you do this is that this method might be called when you’re in the background and because apps aren’t allowed to run in the background for long, you need to let the system know when it’s safe to suspend your process:

// This function may be called when the app is in the background, if the

// action's activation mode was Background

func application(application: UIApplication!,

handleActionWithIdentifier identifier: String!,

forLocalNotification notification: UILocalNotification!,

completionHandler: (() -> Void)!) {

// Called when the user selects an action from a local notification

println("Received \(notification.category)! Action: \(identifier)")

// You must call this block when done dealing with the

// action, or you'll be terminated

completionHandler()

}

WARNING

If you don’t call the completion block after you’re done responding to the action, your application will be terminated.

Likewise, if the application isn’t running when the local notification fires and the user opens the notification, the application is launched, and the launchOptions dictionary contains the notification:

func application(application: UIApplication!,

didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {

var localNotification =

launchOptions[UIApplicationLaunchOptionsLocalNotificationKey

if localNotification? {

// do something with the notification info

}

return true;

}

A local notification can also be presented immediately in the background. For example, if you have an application that is performing some work in the background and you want to let the user know that the work is complete, you can create a notification to that effect:

UIApplication.sharedApplication()

.presentLocalNotificationNow(localNotification)

Once a local notification has been scheduled, you can cancel it; you can also cancel all scheduled notifications:

UIApplication.sharedApplication().cancelLocalNotification(aNotification)

or

UIApplication.sharedApplication().cancelAllLocalNotifications()