Tryswiftnyc ellen shapiro facebook

Pushing the Envelope with iOS 10 Notifications

Apple made major changes to the Notification APIs in iOS 10, affecting both Push and Local notifications. In this session, you’ll get a high-level overview on what’s changed, what you need to do to make sure your existing apps keep working, a few pitfalls to avoid during the transition, and some examples of the cool stuff you can do with the new toys.


Introduction (0:00)

Those of you who have seen me speak before know that I do a lot of talks about testing. I mentioned when I was at UIKonf in Berlin, back in the spring, that I was really excited to see what Apple was going to do with XCUI for Xcode 8. I was so excited that that was originally what I was planning to talk about. I watched WWDC and I was really excited. Then I checked out the API Diffs and there were so few changes to the XC Test framework that that went right out the window. And I panicked a little bit. I’m not going to lie.

I did find one other big set of changes that I was really looking forward to. That brings us back to the actual talk that I’m doing here. iOS 10 notifications. I’m pretty excited about these changes to iOS 10 notifications and what they mean for us as developers. I’m also very excited for how they’re going to change a bunch of what we do in our app at SpotHero.

In addition to having way too much fun creating animated gifs like this one, what we do at SpotHero, is we sell parking spots. I’m sorry. We disrupt parking. Users book a parking spot at a specific place and a specific time. They pay through our app. We take the money. We take a cut. Then we give it to the people who own the parking space. We do this many times a day, all over the United States.

Notification (2:12)

There are a whole bunch of notifications that we wind up using when people do this, to help them find and use the parking spots that they’ve paid for. We use push notifications to alert the user about any critical changes to their reservation. We use silent push notifications to update data that’s been updated either by the customer themselves on our website or potentially by one of our customer heroes when they’ve called into customer service. We use geofence notifications if the user has granted us location permissions, so that we can give people information about where they are parking as they approach the place they’re going to park. We also use schedule notifications around the times that the user’s going to be parking to help them find their spot and when their reservation is going to be ending. We can tell them, “Hey, you might want to move your car or extend your reservation”.

As of iOS 9, there was the old and busted way of dealing with notifications. There were UI local and UI remote notifications, and never the twain shall meet. You really do the same handling on local and remote notifications. Push notifications and silent notifications are remote notifications. Things that are sent from the server to the application. Geofenced and time and date schedule notifications are local notifications. Things that are scheduled right on the phone. You had no idea what was coming as a remote notification until the user opens your app from that application. You couldn’t update those notifications if anything changed. If you send things which change remotely, you have to send a kajillion separate notifications. Though, bear in mind, that one kajillion is a rough estimate.

Get more development news like this

This is especially annoying for something like a sports app, which will send you 14 different notifications for score changes when you just want to know how much the score has changed since the last time you looked at your phone. You don’t want to deal with 20 different notifications like, a run was scored, a run was scored, a run was scored, three runs were scored. Apple realized that’s a bit silly. We should probably make that better. WWDC came along this year and a whole bunch of stuff changed. The biggest change was that Apple moved a bunch of stuff from the application delegate into two new frameworks.


UserNotifications.framework
UserNotificationsUI.framework

UserNotifications (UN) and UserNotificationsUI. This makes it now possible to have a unified API across iOS, watchOS, and tvOS. I’m going to give you an overview of what’s going on on the iOS side.

Ye awesome new way of dealing with notifications starts with the fact that a notification is a notification is a notification. There are no longer remote and local notifications. They’re just UN notification objects.

Notification extensions have also been introduced that allow you to grab additional content like images and videos from remote notifications. Remember that remote notifications have a fairly limited payload, so you really can’t sent that much data with it. Now that the notification extensions exist, you can grab them as they come in, get more data from your server, and show them. There’s a limited amount of time to do that, but it’s pretty sweet.

Remote notifications can have unique identifiers and be updated, rather than having to send separate notifications. That’s something where you do have to set up those identifiers properly with your server, but it makes it way easier to either cancel the things before they’re displayed, if they’ve changed to the extent that they’re no longer relevant; or you can take something that’s already been displayed and update it, so that you now have, instead of like I said, the 14 different notifications about baseball scores, just one notification that says, “Hey, since the last time you’ve looked at your phone, this is how the score has changed.”

There’s also a lot of other helpful things that are more about making life easier for developers rather than improving the UX and UI of notifications. While the app is in the foreground, notifications can now be shown using the system UI. If you’ve ever had to build a custom UI for showing notifications while the app is open, you are going to be rejoicing right now. Because doing that is such a pain in the ass, and totally unnecessary for most applications.

A nice side effect of Apple combining the UI local and UI remote notification is that there is now only one delegate method to handle a notification being tapped. The distinction between a local notification that was tapped and a remote notification that was tapped is meaningless anyway. They decided to get rid of it.

One other nice thing about this whole change is decoupling notifications from UI application delegate which will hopefully encourage developers to move all their notification handling into something that’s a little bit more encapsulated than a 5,000 line app delegate. There are a couple that are still there. But I’ll get back to that in a second.

There are few changes that are kind of annoying. The first is I really hope you like extensions. There’s a decent amount that you can do without adding extensions, but to get the most out of this, you have to add not one, but two different extensions: one to deal with the background fetching, and one to deal with updating your custom UI. I’m still a little bit “get off my lawn” about how the whole extension system is set up. But I am in the camp that has well lost that battle, so hopefully, you like extensions more than I do.

There are a couple of bits that are still in the app delegate. This is one thing that didn’t change. Stuff around the success or failure of registering for push notifications with the Apple push notification service, and handling background push notifications, still lives in the app delegate. This makes a little bit of sense, in terms of Apple wanting a unified API across all OSes for the user notifications framework. Those things that stayed in the app delegate are the things that are really only relevant to iOS. It is kind of annoying, though, to try and figure out. I’m trying to deal with notifications, so I have to go here, and here, and here, and here, and here, and deal with all this different stuff. It’s kind of obnoxious.

A decent number of these new methods don’t call back on the main thread by default. It’s understandable because there is a fair amount of stuff that you can be doing that doesn’t actually need to involve the main thread. It’s really kind of annoying when you’re trying to work with this stuff, that you’re constantly having to verify, do I need to wrap this into dispatch to the main queue? Like I said, annoying. Not a deal breaker. Simultaneous support of the old and busted and the new hotness is real obnoxious.

Stupid Swift Tricks (9:18)

Now, since this is a Swift conference, and we are right near the old Ed Sullivan Theater, home of David Letterman for many, many years, I decided I wanted to call this section “Stupid Swift Tricks.” However, these turned out reasonably well, so, I’m calling them “Sweet Swift Tricks” instead.

You are going to be shocked to hear that this involves a whole bunch of protocol oriented programming. It makes supporting multiple operating systems a little bit less hideous. How do we do this?


protocol VersionSpecificNotificationHandler {
	
	func handleActionWithIdentifier(identified: String?.
									for userInfo: [AnyHashable : Any]?,
									completionHandler: @escaping () -> Swift.Void)

	func hasUserBeenAskedAboutPushNotifications(hasBeenAsked: @escaping (Bool) -> ())

	func requestNotificationPermissionsWithCompletion(permissionsGranted: @escaping (Bool) -> ())

	func arePermissionsGranted(permissionsGranted: @escaping (Bool) -> ())

	func successfullyRegisteredForNotifications(deviceToken: Data)

	func failedToRegisterForNotifications(error: Error)

	func application (_ application: UIApplication,
					didReceiveRemoteNotification userInfo: [AnyHasable : Any],
					fetchCompletionHandler completionHandler: @scaping (UIBackgroundFetchResult) -> Swift. Void)

	func scheduleNotification(for parrot: PartyParrot, delay: TimeInterval)	

}

You start by declaring a protocol. Here are all the various things that I want my things that deal with notifications to handle. One of the things that you can do is you can create an extension that implements default methods, so that your protocol that’s more specific to one operating system or another only has to handle the things that are actually different.

There are things like successfullyRegisteredForNotifications and failedToRegisterForNotifications. Those things really haven’t changed across operating systems. You can use the default protocol implementation to figure out, here’s where I send the token to my server, or here’s where I tell the user, “Hey, you’re using a simulator, you can’t have push notifications.”

I’m going to focus on how I’ve made two of these work.


	func hasUserBeenAskedAboutPushNotifications(hasBeenAsked: @escaping (Bool) -> ())

	func requestNotificationPermissionsWithCompletion(permissionsGranted: @escaping (Bool) -> ())

These are around showing notifications, permissions or not, and how you figure out whether the user has already been asked whether they have notification permissions. Once you show that “Hey, would you like to let us send you push notifications?” prompt, if the user says no, you cannot show that to them again. It is one and done. You’re burnt.

There is a UX pattern where you prime the user to say yes, before you actually show that system prompt. If the user doesn’t say yes to the prime, then you never show the view that fires the system level thing, so that you can have more than one chance to ask the user about push notifications.


@available(iOS 10.0, *)
extension iOS10NotificationHandler: VersionSpecificNotificationHandler {
	
	private func getAuthStatus(status: @escaping (UNAuthorizationStatus) -> ()) {
	UNUserNotificationCenter
		.current()
		.getNotificationSettings {
			settings in
			status(settings.authorizationStatus)
		}
	}

	func hasUserBeenAskedAboutPushNotifications(hasBeenAsked: @escaping (Bool) -> ()) {
		self.getAuthStatus() {
			status in

			DispatchQueue.main.async {
				switch status {
				case .notDetermined: //Not asked yet
					hasBeenAsked(false)
				case .denied, //Asked and denied
				.authorized: //Asked and accepted
					hasBeenAsked(true)
				}
			}
		}
	}

	func requestNotificationPermissionsWithCompletion(permissionsGranted: @escaping (Bool) -> ()) {
		UNUserNotificationCenter.current()
			.requestAuthorization(options: [.alert, .sound]) {
				granted, error in

				DispatchQueue.main.async {
					if granted {
						permissionsGranted(true)
					} else {
						print("Error or nil: \(error?.localizedDescription ?? "nil")")
						permissionsGranted(false)
					}
				}
			}
	}
}

In iOS 10, this is reasonably easy. What you’re doing here is using the new notification API to get the authorization status. There are three authorization statuses: denied, authorized, and not determined. Not determined means nobody’s asked. That makes it really, really easy to do. You can pass in a completion handler and as soon as you know which one of those statuses you’re using, you can fire off that completion handler. In iOS 9 this is a little bit more of a pain in the ass.


//Silence warnings from the compiler about classes which are deprecated in iOS 10
@available(iOS, deprecated: 10.0)
extension iOS9AndBelowNotificationHandler: VersionSpecificNotificationHandler {
	
	func hasUserBeenAskedAboutPushNotifications(hasBeenAsked: @escaping (Bool) -> ()) {
		//Theoretically you could query UIApplication.shared.isRegisteredForRemoteNotifications
		//but that will return no if the user has been asked and declined notifications.
		//<10, the only reliable way to track this is to store whther this has been asked.
		hasBeenAsked(UserDefaults.standard.bool(forKey: self.notificationPermissionRequestedKey))
	}

	func requestNotificationPermissionWithCompletion(permissionsGranted: @escaping (Bool) -> ()) {
		self.permissionsCompletionWithGranted = permissionsGranted
		let settings = UIUserNotificationSettings(types: [
													.alert,
													.badge,
													.sound,
												]
												categories: nil)
		UIApplication.shared.registerUserNotificationSettings(settings)

		//Track that the user has been asked.
		UserDefaults.standard.set(true, forKey: self.notificationPermissionRequestedKey)
	}

	func arePermissionsGranted(permissionsGranted: @escaping (Bool) -> ()) {
		guard let settings = UIApplication.shared.currentUserNotificationSettings else {
			permissionsGranted(false)
			return
		}
	}

	permissionGranted(self.areAnyNotificationsEnabled(inL settings))
}

You setup a bunch of settings, and then you say, registerUserNotificationSettings. Then you have to track manually that it’s been asked, because there’s really not a good way to tell that without tracking it yourself. I have tried. It really doesn’t work.

What’s really obnoxious about that is you have to store your permissions block, and then kick off a process that calls back to the app delegate because it’s still iOS 9. Then, when that calls back to the old app delegate, the app delegate can then call back to your object, and say, “Okay, we’ve got permissions. Here are the settings.”


//Silence warnings from the compiler about classes which are deprecated in iOS 10
@available(iOS, deprecated: 10.0)
class iOS9AndBelowNotificationHandler {
	
	fileprivate let notificationPermissionRequestedKey = ".com.example.HasUserBeenAskedAboutNotifications"

	var permissionsCompletionWithGranted: (Bool) -> ()?

	func grantedPermissions(with settings: UIUserNotificationSettings) {
		guard let permissionsGranted = self.permissionsCompletionWithGranted else {
			//Nothing to do here.
			return
		}

		permissionsGranted(Self.areAnyNotificationsEngabled(in: settings))
	}

	fileprivate func areAnyNotificationsEnabled(in settings: UIUserNotificationSettings) -> Bool {
		return settings.types.contains(.alert)
			|| settings.types.contains(.sound)
			|| settings.types.contains(.badge)
	}
}

Then it can tell you if there were any actual settings that needed to happen there. You can go into your app delegate to have it determine at run time which of your things conform to the protocol that you’ve set up. In this case, it’s called VersionSpecificNotificationHandler.


@UIApplicationMain
calss AppDelegate: UIResponder, UIApplicationDelegate {
	
	var window: UIWindow?
	var notificationHandler: VersionSpecificNotificationHandler!

	func application(_ application: UIApplication,
					willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
		//Assign the appropriate notification handler as early in the app lifecycle as possible.
		if #available(iOS 10.0, *) {
			self.notificationHandler = iOS10NotificationHandler()
		} else {
			self.notificationHandler = iOS9AndBelowNotificationHandler()
		}

		guard let rootVC = self.window?.rootViewController as? ViewController else {
			assertionFailure("VC not found!")
			return false
		}

		rootVC.notificationHandler = self.notificationHandler

		return true
	}
}

You can tell which one you want to use based on the current operating system. Here you’ve got the #available, and then, depending on which operating system you’re using, it’ll have the notification handlers. In this case, I’m also passing through to the next view controller. That’s getting passed around, instead of hanging off of the app delegate, like a singleton. Please do not use the app delegate like a singleton. I beg you.

Conclusion (15:34)

What do you need to do? Using the UN notifications framework is required if you use notifications, whether local or remote. The app delegate methods from iOS 9 and below have been deprecated. They are going to be removed in a future version of iOS. For now, things are going to continue to work with the app delegate, but this gets you so much more power, and more information that you can get your users, that it’s really not worth putting off the transition.

Create a notification extension. Notification extension is going to allow you to download more data, and give your user way more context than you possibly could in old push notifications.

Creating a notification UI extension. Again, take that context that you now have, and use your own UI. For the marketing folk that are among you, you can reinforce your brand. For those of you who are less marketing oriented, you can make sure that you’re getting people all of the information that they need as quickly as possible.

Lastly, you’re really going to want to start irritating your product team to drop support for iOS 9, especially for apps that sell things. This is a really helpful way to get people the information that they need really quickly. There are other things that you can do that will encourage people to continue to come back. Continue to use your application. Continue to give your company money, so that your company continues to give you money.

Pushing the Envelope with iOS 10 Notifications Resources

Next Up: New Features in Realm Obj-C & Swift

General link arrow white

About the content

This talk was delivered live in September 2016 at try! Swift NYC. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Ellen Shapiro

Ellen is a Senior iOS Engineer at Vokal in Chicago, IL, who also dabbles in Android, running the Chicago AndroidListener meetup. She also works in her spare time to bring leading songwriting application Hum to life, and writes iOS tutorials for raywenderlich.com.