Using iOS Notifications, Cryptography and iCloud to build your own Chat App XI

I should start this article with a disclaimer, it based on iOS 13, Swift 5 and Xcode 11.x. If you reading this and those numbers look dated, be forewarned.

I should also warn you that notifications and iCloud code, involve Apple’s infrastructure which means you will need an Apple Developers account to use them.

Obviously this is part XI. It is an ongoing series in which I blogging the development of whatsapp type app using the above tech. To get the most of it you need to start at the beginning.

What are we doing. The fact that two users need to be logged in at the same time to start off the conversation worries me, its not practical. What can we do about it.

I tried a local notification, that didn’t work. I need an alert that will appear even when I not running the app. One that I can respond too. We need a notification content extension.

As before this is a reasonably major update, so you should backup what you have so that if it all goes horribly wrong, you can start again.

What is the plan. We going to change the alert that pops up in the app the first time someone tries to talk to you. We’re going to change it to a notification with an action. We should do three actions. An accept action, which will launch the app so you can configure it. An later action, which will send back a message to say later. And a deny action, that will block any more requests from said user.

We need to make changes to RemoteNotifications, Could.swift and our AppDelegate. Obviously we also need to add a content extension, which is a special piece here. Lets start with Cloud.swift. Strictly speaking we need to tidy up ContentView.swift too, but I never promised we pass the Joel Spolsky test when I started did I.

func authRequest2(auth:String, name: String, device:String) {
// Search the directory
let predicate = NSPredicate(format: "name = %@", name)
let query = CKQuery(recordType: "directory", predicate: predicate)
publicDB.perform(query,inZoneWith: CKRecordZone.default().zoneID) { [weak self] results, error in
guard let _ = self else { return }
if let error = error {
DispatchQueue.main.async { print("error",error) }
return
}
guard let results = results else { return }
for result in results {
print("results ",result)
let token = result.object(forKey: "device") as? String
if token != nil {
poster.postNotification(token: token!, message: auth, type: "alert", request: "request", device:device)
}
}
if results.count == 0 {
print("no name ",name)
}
}
}

There is only one line changed here, although I show the entire method for completeness. We need to post a special alert. Lets move across to RemoteNotification.swift.

func postNotification(token:String, message:String, type: String, request: String, device:String) {
var jsonObject:[String:Any]?
if type == "background" {
let secret = UserDefaults.standard.string(forKey: "secret")
jsonObject = ["aps":["content-available":1],"request":request,"user":message,"device":device, "secret":secret]
}
if type == "alert" {
if request == "ok" {
jsonObject = ["aps":["sound":"bingbong.aiff","badge":1,"alert":["title":"Noob","body":message]]]
}
if request == "request" {
let secret = UserDefaults.standard.string(forKey: "secret")
jsonObject = ["aps":["sound":"bingbong.aiff","badge":1,"category":"noobCategory","alert":["title":"Noob","body":message],"request":request,"user":message,"device":device, "secret":secret]]
}
if request == "later" {
let secret = UserDefaults.standard.string(forKey: "secret")
jsonObject = ["aps":["sound":"bingbong.aiff","badge":1,"category":"noobCategory","alert":["title":"Noob","body":message],"request":request,"user":message,"device":device, "secret":secret]]
}
}

Which is looking horribly complex, I blame JSON… although it isn’t really. In short this send one of three messages, depending in the parameters coming in. The one coming in from our cloud.swift is the last one here. And it contains a category which we can use to associate actions with it.

Feeling guily about the Spolsky comment so I decided to tidy up the AppDelegate, make amends. You can comment out a few lines in the didRecieveRemoteNotitifications.

func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
debugPrint("Received: \(userInfo)")
let request = userInfo["request"] as? String
// let user = userInfo["user"] as? String
let device = userInfo["device"] as? String
let secret = userInfo["secret"] as? String
// if request == "request" {
// DispatchQueue.main.async {
// print("token ",device)
// popPublisher.send((device!,user!))
// }
// }
if request == "grant" {
DispatchQueue.main.async {
print("token ",token)
enableMessaging.send((device!,secret!))
}
}
completionHandler(.newData)
}

This was the code that was triggering the request to talk alert in the ContentView.swift. And we going to add this shed load of code to an extension to our AppDelegate class. Note the first method was already there, I just moved it.

extension AppDelegate: UNUserNotificationCenterDelegate {func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
recieptPublisher.send()
completionHandler([.alert, .badge, .sound])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
print("yo buddy")
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
print("reponse ",response.notification.request.content.subtitle)
let action = response.actionIdentifier
let request = response.notification.request
if action == "accept" {
print("content ",request.content.userInfo)
let userInfo = request.content.userInfo["aps"]! as! Dictionary<String, Any>
let device = userInfo["device"] as? String
poster.postNotification(token: device!, message: "Granted", type: "background", request: "grant",device:token)
}
if action == "later" {
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [request.identifier])
let userInfo = request.content.userInfo["aps"]! as! Dictionary<String, Any>
let device = userInfo["device"] as? String
let user = userInfo["user"] as? String
poster.postNotification(token: device!, message: "Later", type: "background", request: "later",device:token)
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [request.identifier])
}
if action == "deny" {
// Block the user
}completionHandler()
}
func registerCategories() {
let acceptAction = UNNotificationAction(identifier: "accept", title: "Accept", options: [.foreground])
let laterAction = UNNotificationAction(identifier: "later", title: "Later", options: [.foreground])
let denyAction = UNNotificationAction(identifier: "deny", title: "Deny", options: [.destructive])
let noobCategory = UNNotificationCategory(identifier: "noobCategory", actions: [acceptAction,laterAction,denyAction], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([noobCategory])
}
}

What does all this do. In short it defines the actions we need to associate with that category we defined earlier. The accept, later and deny buttons that will be available in the notification.

It also includes the action to associate with our accept and later buttons.

And of course a minor change to the ContentView.swift file.

.alert(isPresented:$showingAlert) {
Alert(title: Text("Not now"), message: Text("Talk Later"), dismissButton: .default(Text("Ok")))
}

Which advices the user requesting to talk that you can’t set him up right now, so he/she should try later.

Coding for 35+ years, enjoying using and learning Swift/iOS development. Writer @ Better Programming, @The StartUp, @Mac O’Clock, Level Up Coding & More

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store