Chapters

Hide chapters

Push Notifications by Tutorials

Second Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Push Notifications by Tutorials

Section 1: 14 chapters
Show chapters Hide chapters

12. Putting It All Together
Written by Scott Grosch

With 11 chapters behind you, you’ve become quite the master of everything related to Push Notifications!

This chapter is all about leveraging all that you’ve learned in this book into a single app, titled CoolCalendar.

When somebody sends you a calendar invite, it will be pushed to your device via a remote notification. You’ll have the ability to see how the new event relates to your existing calendars, be able to accept/reject right from the notification and have the option of sending a comment back.

Setting up the Xcode project

The starter project for this chapter uses a third-party library called CalendarKit, which comes pre-installed via CocoaPods, to simplify the presentation of a Calendar UI in your app. Be sure to open CoolCalendar.xcworkspace and not CoolCalendar.xcodeproj. If you accidentally open the latter, you’ll get multiple compiler errors.

First, it’s time to set up your workspace:

  1. Open CoolCalendar.xcworkspace from the starter project.

  2. Set the team signing for both the CoolCalendar target and the Custom UI extension, as discussed in Chapter 7, “Expanding the Application.”

  3. Enable the Push Notifications capability as discussed in Chapter 4, “Xcode Project Setup.”

  4. Enable Remote notifications as part of Background Modes as discussed in Chapter 8, “Handling Common Scenarios.”

  5. Add a Notification Service Extension as discussed in Chapter 10, “Modifying the Payload.”

A Notification Content Extension was already included in the starter project, as some sample code was provided for you. In your own projects, you’d have to create that target yourself.

AppDelegate code

Take a minute to set up your AppDelegate code the way you think it should be. Keep in mind all the items discussed in the preceding chapters and don’t be afraid to flip back to one or more for help!

Some features that you’ll want to be sure to handle:

  • You’ll need action identifiers to know which custom action buttons were selected. Plan to have buttons for Accept, Decline and Comment.
  • You’ll need to register all of your custom actions.
  • You’ll need to register for push notifications.
  • The payload’s custom category will be called CalendarInvite.

Try to do this yourself. You can use the registerForPushNotifications method found in ApnsUploads.swift — just make sure to uncomment it first!

When you’re ready, turn the page to see one potential solution. The code that was already provided in the starter project has been removed for clarity.

Here’s the solution:

Start by creating a new file called ActionIdentifier.swift where you’ll define your Action Identifiers. Add the following enum to the file:

enum ActionIdentifier: String {
  case accept
  case decline
  case comment
}

Select the newly created file and, in the File Inspector, make sure the Target Membership is checked for Custom UI.

Back in AppDelegate.swift, add the following code inside your AppDelegate:

private let categoryIdentifier = "CalendarInvite"

private func registerCustomActions() {
  let accept = UNNotificationAction(
    identifier: ActionIdentifier.accept.rawValue,
    title: "Accept")

  let decline = UNNotificationAction(
    identifier: ActionIdentifier.decline.rawValue,
    title: "Decline")

  let comment = UNTextInputNotificationAction(
    identifier: ActionIdentifier.comment.rawValue,
    title: "Comment", options: [])

  let category = UNNotificationCategory(
    identifier: categoryIdentifier,
    actions: [accept, decline, comment],
    intentIdentifiers: [])

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

Next, add this code to the UIApplicationDelegate extension inside the same file:

func application(_ application: UIApplication,
                didRegisterForRemoteNotificationsWithDeviceToken 
                deviceToken: Data) {
  registerCustomActions()
  sendPushNotificationDetails(
    to: "http://192.168.1.1:8080/api/token",
    using: deviceToken)
}

Also, add the following UNUserNotificationCenterDelegate extension at the bottom of the file:

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

ApnsUploads.swift already has the registerForPushNotifications method ready and waiting for you. Simply uncomment it.

Finally, all that’s left is calling the registerForPushNotifications method. Back in your AppDelegate, Look for the application(_:didFinishLaunchingWithOptions:) method, and add the following line right before the return clause.

registerForPushNotifications(application: application)

If you build your app, it should compile cleanly at this point. Be sure not to move on until you are left with neither warnings nor errors.

Requesting calendar permissions

Accessing the user’s calendar is a privacy concern, and so you’ll have to first request permission of your end users. Apple kindly ensured that the same authorization status is shared by all targets of your app.

This means that your extensions can simply look at the status and not have to ask for it, as the primary target already takes care of that. Like all good iOS apps, you’ll have to tell your end users why you want to get into their calendars, so go back to the CoolCalendar target’s Info panel and add a Privacy — Calendars Usage Description key.

You can use any text that explains why you need access to the Calendar, such as, “We need access to the Calendar to import your events and important dates”.

Now, edit ViewController.swift and request permission to the user’s calendar when the view appears. This is boilerplate code that you’ll use in any calendar app, but it’s important to get it right.

First, add an event store to the top of the class. The event store is the way your app will communicate with the calendar data:

private let eventStore = EKEventStore()

Next, add the following method underneath the event store property:

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

  let status = EKEventStore.authorizationStatus(for: .event)

  switch (status) {
  case .notDetermined:
    eventStore.requestAccess(to: .event) { 
      [weak self] granted, _ in
      guard !granted, let self = self else { return }

      self.askForAccess()
    }

  case .authorized:
    break

  default:
    askForAccess()
  }
}

When the screen appears, ask the EKEventStore if the user has authorized calendar access. If the status is not yet determined, request access from the event store, which will trigger a system alert asking the user for permission. If the user has denied permission, you will show a new alert with a handy button to open the settings screen.

As you might’ve noticed, you’re using askForAccess, which is still not implemented. Replace it with the following code:

private func askForAccess() {
  let alert = UIAlertController(
    title: nil,
    message: "This application requires calendar access",
    preferredStyle: .actionSheet)

  alert.addAction(UIAlertAction(
    title: "Settings", 
    style: .default) { _ in
      let url = URL(
        string: UIApplication.openSettingsURLString)!
      UIApplication.shared.open(url)
  })

  alert.addAction(UIAlertAction(
    title: "Cancel", 
    style: .default))

  present(alert, animated: true)
}

This will show a new alert in which one of the buttons opens up your application’s Settings.

Note: It’s a nice touch to give the user a simple way to get to your app settings if permissions aren’t currently granted.

However, you should be sure where you’re doing this from. In the case of this app, it’s done every time the view appears if permission has been denied or not yet requested.

If your app requires calendar access, this makes sense. However, if it’s optional, and not 100% necessary to your app’s functionality, then you’ll just annoy the end user if you ask every single time.

Build and run the app to verify that you’re asked to grant calendar permissions and to allow push notifications.

Be sure to tap OK on both! If the app crashes at this point, you probably added the privacy policy to one of the extension targets instead of the main app.

The payload

When it’s time to invite somebody to an event, you’ll send a remote notification with a payload that looks like this:

{
  "aps": {
    "alert": {
        "title": "New Calendar Invitation"
    },
    "badge": 1,
    "mutable-content": 1,
    "category": "CalendarInvite"
  },
  "title": "Family Reunion",
  "start": "2018-04-10T08:00:00-08:00",
  "end": "2018-04-10T12:00:00-08:00",
  "id": 12
}

Notice that you’re setting the mutable-content key to 1 so that your service extension runs, as well as a category so that your custom UI extension is triggered. The last four fields simply specify the details of the event.

This packet structure also assumes that your server is tracking the calendar invitations to know who accepted and rejected them, which is why there is an id key which uniquely identifies this invitation in your database. To make life easy, the dates use the ISO8601 date format.

Notification Service Extension

The goal of your remote push notification is to provide a custom user interface to accept/reject/comment on the calendar invitation that’s sent to the end user. What happens if the end user doesn’t allow calendar access? It would be pretty strange to pop up the UI asking them to take action.

Even though you can’t stop a notification from going through, you can change the notification. In this case, that means that you should check if calendar permissions are granted.

What action do you think you could take if permission is denied? The simplest solution is to remove the category from the payload, which would prevent the custom UI from appearing at all!

The pieces to be implemented in the service extension are threefold:

  1. Remove the category if calendar permissions aren’t granted.
  2. Update the app icon badge.
  3. Update the body of the notification.

Spend some time trying to implement those three items yourself and then come back to see the way the goals are accomplished, here.

Validating calendar permissions

Modify NotificationService.swift to include an import of EventKit:

import EventKit

Then update the didReceive method to blank out the category field of the payload if calendar permissions are denied by adding this check immediately before your call to the contentHandler closure:

if EKEventStore.authorizationStatus(for: .event) != .authorized {
  bestAttemptContent.categoryIdentifier = ""
}

Setting categoryIdentifier to an empty string will ensure the Content UI doesn’t display.

App badging

It’s time to add a badge to your app’s icon for when a new notification comes in. The first step is to create an App Group. If you don’t remember how to do this, follow the steps shown in Chapter 10, “Modifying the Payload.”

Badging the app is handled by using the App Group you created and the UserDefaults class. Create a new Swift file called UserDefaults.swift in the CoolCalendar target with code to set an integer in the proper App Group. Be sure you update the name of the suite to match what you called the App Group!

extension UserDefaults {
  static let appGroup = UserDefaults(
    suiteName: "group.com.raywenderlich.CoolCalendar")!

  private enum Keys {
    static let badge = "badge"
  }

  var badge: Int {
    get {
      return integer(forKey: Keys.badge)
    } set {
      set(newValue, forKey: Keys.badge)
    }
  }
}

Since you’re going to use this file in both the primary target and the service extension, click on the newly created file and, in the File Inspector, check Payload Modification inside Target membership.

With that done, you can now add a method to NotificationService.swift to update the badge:

private func updateBadge() {
  guard let bestAttemptContent = bestAttemptContent,
        let increment = bestAttemptContent.badge as? Int else { return }
  
  switch increment {
  case 0:
    UserDefaults.appGroup.badge = 0
    bestAttemptContent.badge = 0
  default:
    let current = UserDefaults.appGroup.badge
    let new = current + increment
    
    UserDefaults.appGroup.badge = new
    bestAttemptContent.badge = NSNumber(integerLiteral: new)
  }
}

When the service extension completes, iOS will set the badge on your app icon to the value specified in the payload. You’ve handled the update here by incrementing the existing badge count based on what was sent in the payload so that the count properly increments after each invitation is received.

When the user runs the app, you’ll of course want to blank the badge count out. To do so, return to AppDelegate.swift and blank out the badge count once the app starts by adding the following method to the UIApplicationDelegate extension:

func applicationDidBecomeActive(_ application: UIApplication) {
  UserDefaults.appGroup.badge = 0
  application.applicationIconBadgeNumber = 0
}

You’ll notice that the UserDefaults extension wasn’t technically necessary here as you’re not decrementing based on which invitations you’ve seen. In a normal production app, however, you’d only want to decrement the count when the user actually sees the specific invitations that were new.

Notification body

The final task is parsing your custom payload data and updating the body of the notification message to something more user friendly.

Add the following method inside the class in NotificationService.swift:

private func updateText(request: UNNotificationRequest) {
  let formatter = ISO8601DateFormatter()
  
  guard let bestAttemptContent = bestAttemptContent else { return }

  let authStatus = EKEventStore.authorizationStatus(for: .event)
  
  guard authStatus == .authorized,
        let userInfo = request.content.userInfo as? [String: Any],
        let title = userInfo["title"] as? String,
        !title.isEmpty,
        let start = userInfo["start"] as? String,
        let startDate = formatter.date(from: start),
        let end = userInfo["end"] as? String,
        let endDate = formatter.date(from: end),
        userInfo["id"] as? Int != nil else {
    bestAttemptContent.categoryIdentifier = ""
    return
  }
  
  let rangeFormatter = DateIntervalFormatter()
  rangeFormatter.dateStyle = .short
  rangeFormatter.timeStyle = .short
  
  let range = rangeFormatter.string(from: startDate,
                                    to: endDate)
  bestAttemptContent.body = "\(title)\n\(range)"
}

Notice how, if any piece of the required payload is missing, or in an incorrect format, the categoryIdentifier is blanked out. It doesn’t make sense to let the custom UI code get called when it would simply fail. Two separate guard clauses are required, as Swift will not allow access to bestAttemptContent in the failure condition of a guard clause where that variable is checked.

Using ISO8601 dates is very convenient, as Apple has provided a parser explicitly for that format! As long as the payload contains all the expected keys in the proper date formats, the body is updated to include the title and the date range. With dates, you’ll always want to utilize the provided classes, such as DateIntervalFormatter to ensure that the user’s locale is properly respected.

While nothing needs to be done with the invitation id, you still want to ensure that it exists in the payload so that you know there’s valid content to pass to the Content Service Extension.

All that’s left to do in this file is to call both of the methods that you just implemented from didReceive just before the completion handler is called. Your final didReceive method should now look like this:

override func didReceive(_ request: UNNotificationRequest, 
                         withContentHandler contentHandler: 
                         @escaping (UNNotificationContent) -> Void) {
  self.contentHandler = contentHandler
  bestAttemptContent = request.content.mutableCopy() 
    as? UNMutableNotificationContent
  guard 
    let bestAttemptContent = bestAttemptContent 
  else { return }

  if EKEventStore.authorizationStatus(for: .event) 
     != .authorized {
    bestAttemptContent.categoryIdentifier = ""
  }

  updateBadge()
  updateText(request: request)
  contentHandler(bestAttemptContent)
}

Phew! There’s almost more text explaining what to do than it actually takes to do it! You can see how modifying the payload might seem daunting at first, but you can take significant action with very little code. The net benefit to your end users is a much better experience, which always makes the little bit of extra effort worth it.

In a production app there are other considerations you might want to take, such as:

  • What about all-day events?
  • What about recurring events?
  • What if the title is an empty string?
  • What happens if you send a start date that comes after an end date?

This is a great time to build and run the app again and send yourself a push notification.

You can do this with the PushNotifications tester app as described in Chapter 5, “Apple Push Notifications Servers.” You can find the payload at the start of this chapter.

Has the body of the text message been updated properly? If there’s no change, make sure you remembered to set mutable-content to 1 in the aps part of the payload. If it’s still not working, refer back to Chapter 10, “Modifying the Payload,” for help.

Those goodies in your kitchen aren’t going to eat themselves. You’ve done some great work so grab yourself a snack, take a quick break and then it’ll be time to work on the user interface.

Content Service Extension

Instead of just asking for a response, wouldn’t it be nicer to show your users what their calendars look like for the time period related to the new event that they were invited to? To do this, you’ll use a library called CalendarKit that the starter project has included. As the goal here isn’t to teach you how to use CalendarKit, the starter project already includes the code related to that library for you.

Considering what the goals of the UI will be leads to the following five tasks:

  1. Set the Info.plist details related to the category that you’re using.
  2. Add the newly arrived invitation to CalendarKit.
  3. Display all events happening at the same time as the new event.
  4. If the user accepts the invite, add it to iOS’s calendar.
  5. If the user comments, update the server.

It probably seems a bit silly to list out such simple tasks, but thinking of the UI tasks ahead of time helps to break down what, at first, seems like a daunting challenge into manageable pieces that you can focus on.

Updating the Info.plist

Open up Info.plist inside of the Custom UI target folder and expand out the NSExtension key all the way, as you learned to do in Chapter 11, “Custom Interfaces.” You’ll need to update the UNNotificationExtensionCategory value to match what you set the categoryIdentifier to be in AppDelegate.swift. If you’ve used the same category name as the book example, that means you’ll need to put CalendarInvite as the value.

Since the calendar itself will contain the details of the notification, it’s definitely not desirable to have iOS display the body of the notification in the UI. I know, I know… right now you’re thinking to yourself, “What?! Then why did I just edit the body of the notification to be human readable?” Remember that you might have had to disable the custom UI portion. If it gets disabled, you’d still want a nice text message. If it’s not, then you want the visual UI.

Create a new Boolean key, under NSExtensionAttributes, named UNNotificationExtensionDefaultContentHidden and set the value to YES.

Adding information to CalendarKit

You’ll have to do the same extraction from the payload that you did in the Notification Service Extension, but, this time, there’s no need to check for calendar access because, if you get here, it’s guaranteed to be “on” as you just checked it in the Notification Service Extension.

In NotificationViewController.swift’s didReceive(_:), after you do the parsing, you’ll want to send the invitation details into CalendarKit. Add the below code just after the line that adds the timeline container as a subview:

let formatter = ISO8601DateFormatter()

guard let userInfo = notification.request.content.userInfo as? [String: Any],
      let title = userInfo["title"] as? String, !title.isEmpty,
      let start = userInfo["start"] as? String,
      let startDate = formatter.date(from: start),
      let end = userInfo["end"] as? String,
      let endDate = formatter.date(from: end),
      let id = userInfo["id"] as? Int else {
  return
}

var appointments = [addCalendarKitEvent(start: startDate, 
                                        end: endDate, 
                                        title: title)]

Getting nearby calendar items

Now that the new invitation is squared away, you’ll need to find the events in the users’ existing calendars that will occur around the same date/time. It’s probably a good idea to consider a couple of hours before and after the event so that your users can plan for drive times, doctors always being late to the start of an appointment or other buffers of time needed.

Start off by creating a property for the event store to the top of NotificationViewController:

private let eventStore = EKEventStore()

Determine what time is two hours before the invitation and two hours after.

In a production app, you’d need to do some extra checks to see how long the appointment is, for example, or whether it’s an all-day event; you’d then need to modify the times accordingly. Never just add seconds to a date thinking it’s the right thing to do. Always use the built-in calendrical calculations that Foundation provides so that you don’t get caught by leap years, leap seconds, missing midnight hours and a slew of other time-related issues.

Add the following code to the bottom of didRecieve, before the commented out lines:

let calendar = Calendar.current
let displayStart = calendar.date(byAdding: .hour, 
                                 value: -2,
                                 to: startDate)!
let displayEnd = calendar.date(byAdding: .hour, 
                               value: 2,
                               to: endDate)!

let predicate = eventStore.predicateForEvents(
  withStart: displayStart,
  end: displayEnd,
  calendars: nil)

appointments += eventStore
  .events(matching: predicate)
  .map {
    addCalendarKitEvent(
      start: $0.startDate, 
      end: $0.endDate,
      title: $0.title, 
      cgColor: $0.calendar.cgColor)
  }

After adding the above code, uncomment the commented-out lines related to the timelineContainer. Those three lines are necessary to make CalendarKit work properly but, until displayStart and displayEnd were defined, they would have resulted in confusing compiler errors.

Build and run the app, and send yourself another push notification. When you long-press into the notification, you should see a UI showing the time slot for the new event, as well as any events you might have planned at the same time.

You can play with the time in the payload to test out different appointments.

Accepting and declining

The UI is now displaying a snapshot of part of the calendar so that the end user can make an informed decision about whether or not to accept the invitation. What happens when they accept or reject, though? You’ll want to determine which option was chosen and then take some action, such as connecting to a REST endpoint to store the response.

If the invitation is declined, you’ll probably want to update your server so it can process the event, and eventually you’ll dismiss the UI. There’s now an issue to consider: The didReceive(_:completionHandler:) method, which responds to the action buttons, has no idea what the event is. You’ll fix that by adding another property to the class:

private var calendarIdentifier: Int?

Then set that in didReceive(_:) just after decoding the payload.

calendarIdentifier = id

You can now implement the didReceive(_:completionHandler:) method to start handling the actions by adding the following to the bottom of NotificationViewController:

func didReceive(
  _ response: UNNotificationResponse, 
  completionHandler completion: 
  @escaping (UNNotificationContentExtensionResponseOption) 
  -> Void) {
  
  guard let choice = 
    ActionIdentifier(rawValue: response.actionIdentifier) 
  else {
    // This shouldn't happen but definitely don't crash.  
    // Let the users report a bug that nothing happens
    // for this choice so you can fix it.
    completion(.doNotDismiss)
    return  
  }

  switch choice {
  case .accept, .decline:
    completion(.dismissAndForwardAction)
  case .comment:
    completion(.doNotDismiss)
  }
}

Are you getting a compiler error that ActionIdentifier is unknown? You know the drill! Add Custom UI to its target membership.

If the user chooses to enter a comment, bring up the keyboard and tell the completion handler that the UI window should stay active. If they accept or decline the invitation, the window can simply be dismissed.

You’re using a new option here, called dismissAndForward, which tells the UI to dismiss, while also forwarding the notification onto your primary app, triggering the userNotificationCenter(_:didReceive:withCompletionHandler:) method.

Because this app wants to display the responses to each invite in a table, it’s necessary to store the response in a Core Data entity. While it’s entirely possible to create a new entity in an extension target, it’s not easy to know that it happened in the main target. Both targets would use the same root context from the NSPersistentContainer, and Foundation’s notifications don’t cross app targets. For this reason, it’s simpler to leave the Core Data work to the primary app target. You’ll handle that in just a bit.

Commenting on the invitation

This one takes a little more work to get properly. You want to be able to comment without the notification being dismissed as soon as you do. In order to make that happen, you’ve got to tell iOS that you’re willing to become the first responder (i.e., provide a custom keyboard) and handle input by overriding canBecomeFirstResponder.

Add the following override to the top of NotificationViewController:

override var canBecomeFirstResponder: Bool {
  return true
}

When a keyboard appears from iOS, there’s no way for you to get access to the UITextField that is presented. Since you need to know when the Return button is pressed, it’s therefore necessary to replace the UITextField Apple provides with one of your own. The starter project has already created the keyboardInputAccessoryView for you for just this purpose.

By embedding the text field inside of another view, you can give some shading to the outer view, making the text field easier to see. Remember that you’re using full UIKit-based controls here, so you can add as many features as you need such as buttons and date pickers to suggest a new times. Just always keep the user experience in mind as you add more controls.

The delegate has to be set on the keyboardTextField so that you can catch when the user taps the Return button on the keyboard to dismiss it.

Add the following to the top of textFieldShouldReturn:

guard textField == keyboardTextField,
      let text = textField.text,
      let calendarIdentifier = calendarIdentifier else {
  return true
}

Server.shared.commentOnInvitation(with: calendarIdentifier,
                                  comment: text)
textField.text = nil

keyboardTextField.resignFirstResponder()
resignFirstResponder()

Something that’s not immediately obvious until after you’ve done some UI testing is that the comment text the end user typed won’t automatically disappear as the same UITextField is utilized each time the keyboard appears.

That’s why it’s necessary to set the text field’s text property to nil to properly clear it when you’re done.

Of course, for iOS to know that you want to actually do something with the UIView that you just created, you’ve got to tell it so!

Add this override to the top of the class:

override var inputAccessoryView: UIView? {
  return keyboardInputAccessoryView
}

All that’s left to do is to display the keyboard when the Comment button is tapped inside the switch in didReceive(_:completionHandler:):

case .comment:
  becomeFirstResponder()
  keyboardTextField.becomeFirstResponder()
  completion(.doNotDismiss)

Build and run.

Send yourself some events and you should see your snazzy new custom UI with usable buttons!

Final cleanups

After sending some notifications, it’ll quickly become apparent that nothing is happening in the main app when you accept or reject a notification. There is already code in ViewController.swift that shows the data, but you never actually create a Core Data entity anywhere. Time to resolve that issue!

Head over to AppDelegate.swift one last time. As mentioned, you’ll generate the Core Data entities from one of UNUserNotificationCenterDelegate’s methods. Add the following method to the UNUserNotificationCenterDelegate extension:

func userNotificationCenter(
  _ center: UNUserNotificationCenter,
  didReceive response: UNNotificationResponse,
  withCompletionHandler completionHandler: @escaping () 
  -> Void) {
  
  defer { completionHandler() }
  
  let formatter = ISO8601DateFormatter()
  let content = response.notification.request.content
  
  guard 
    let choice = 
    ActionIdentifier(rawValue: response.actionIdentifier),
    let userInfo = content.userInfo as? [String : Any],
    let title = userInfo["title"] as? String, !title.isEmpty,
    let start = userInfo["start"] as? String,
    let startDate = formatter.date(from: start),
    let end = userInfo["end"] as? String,
    let endDate = formatter.date(from: end),
    let calendarIdentifier = userInfo["id"] as? Int else {
      return
  }
}

Remember that the completion handler must be called, which is why it’s passed right into a defer block. The parsing is no different than before, but notice that there are a couple extra “dot walks” that have to happen to get to the userInfo property. Even though you know for a fact that everything will parse properly, it’s never a good idea to use force unwrapping if there’s another way; the guard syntax is a better choice here.

All that’s left to do is to update the server with the user’s decision and create a Core Data entity, which will then automatically be displayed in the table on the main view.

You can do this by adding the following code to the bottom of the above method:

switch choice {
case .accept:
  Server.shared.acceptInvitation(with: calendarIdentifier)
  createInvite(
    with: title,
    starting: startDate,
    ending: endDate,
    accepted: true)
  
case .decline:
  Server.shared.declineInvitation(with: calendarIdentifier)
  createInvite(
    with: title,
    starting: startDate,
    ending: endDate,
    accepted: false)
  
default:
  break
}

Build and run the app. Send yourself a few invites. When you open the app, you should see a list of accepted and declined appointments.

Where to go from here?

And, with that, your project is finished! Even so, there is still one final category of notifications to cover: local notifications, which you will explore in the next and final chapter.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.