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

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.

Finally Obviously this is part III, you need to go back to part I and part IIfor it to make any sense, indeed you need to look at the notifications series before do that else your struggle.

So what is the status so far. We took noob from the notification project, added some cryptographic methods, added some CloudKit methods and a neat SwiftUI interface.

In this instalment we’ll be adding a little quite a bit more code to our Cloud.swift class and update our ContentView.swift to use it.

We’ll start with the Cloud.swift file. I included some code you already have to help you position this correctly.

import Foundation
import CloudKit
import Combine
let pingPublisher = PassthroughSubject<String, Never>()
let dataPublisher = PassthroughSubject<String, Never>()
let cloudPublisher = PassthroughSubject<String, Never>()
...func searchAndUpdate(name: String, publicK:String, device: String) {
print("searching ",name)
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.recordID)
self!.updateRec(record: result, publicK: publicK, device: device)
}
if results.count == 0 {
print("no name ",name)
}
}
}
func updateRec(record: CKRecord, publicK: String, device: String) {
// print("updating ",record)
let saveRecordsOperation = CKModifyRecordsOperation()
record.setValue(publicK, forKey: "key")
record.setValue(device, forKey: "device")
saveRecordsOperation.recordsToSave = [record]
saveRecordsOperation.savePolicy = .allKeys
saveRecordsOperation.modifyRecordsCompletionBlock = { savedRecords,deletedRecordID, error in
if error != nil {
print("error")
} else {
// print("saved ",savedRecords?.count)
}
}
publicDB.add(saveRecordsOperation)
}

The first method searches thru your icloud database and finds the user record with the name it is called with as a parameter. It then calls the second method which saves the record together with a public key that also supplied with the call. Both are used with the first pickerview. Than we have this code also for the cloud.swift file.

func search(name: String) {
print("searching ",name)
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 publicK = result.object(forKey: "key") as! String
DispatchQueue.main.async {
dataPublisher.send(publicK)
}
}
if results.count == 0 {
print("no name ",name)
return
}
}
return
}
func keepRec(name: String, sender:String, senderDevice:String) {
print("searching ",name)
let predicate = NSPredicate(format: "name = %@ AND sender = %@", name, sender)
let query = CKQuery(recordType: "mediator", 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
}
if let results = results {
if results.count > 0 {
self!.keepRec2(record: results.first!, sender: sender, senderDevice: senderDevice)
} else {
self!.saveRec2(name: name, sender: sender, senderDevice: senderDevice)
}
}
}
}
func keepRec2(record: CKRecord, sender:String, senderDevice:String) {
record.setObject(senderDevice as CKRecordValue, forKey: "devices")
let modifyRecordsOperation = CKModifyRecordsOperation(
recordsToSave: [record],
recordIDsToDelete: nil)
modifyRecordsOperation.modifyRecordsCompletionBlock = { records, recordIDs, error in
if let _ = error {
print("error ",error)
} else {
DispatchQueue.main.async {
print("success ")
}
}
}
publicDB?.add(modifyRecordsOperation)
}
func saveRec2(name: String, sender:String, senderDevice:String) {
let record = CKRecord(recordType: "mediator")
record.setObject(name as CKRecordValue, forKey: "name")
record.setObject(sender as CKRecordValue, forKey: "sender")
record.setObject(senderDevice as CKRecordValue, forKey: "senderDevice")
let modifyRecordsOperation = CKModifyRecordsOperation(recordsToSave: [record],recordIDsToDelete: nil)
modifyRecordsOperation.modifyRecordsCompletionBlock = { records, recordIDs, error in
if let err = error {
print("error ",error)
} else {
DispatchQueue.main.async {
print("success ")
}
}
}
publicDB?.add(modifyRecordsOperation)
}

This methods are all called within the scope of the second pickerview. The first searches for a given user and returns their public key. And keepRec, keepRec2 and saveRec2 all maintain the mediator database.

Before I cross over to the ContentView.swift, here is a quick. update needed to the RSA.swift code too. Basically the original code shown in an earlier article returned SecKey, but we needed a String. So I updated these two routines.

func getPublicKey() -> Data? {
var error: UnsafeMutablePointer<Unmanaged<CFError>?>?
let publicK = SecKeyCopyExternalRepresentation(self.publicKey!, error)
return publicK! as Data
}
func getPrivateKey() -> CFData? {
var error: UnsafeMutablePointer<Unmanaged<CFError>?>?
let privateK = SecKeyCopyExternalRepresentation(self.privateKey!, error)
return privateK
}

Which of course neatly takes back to ContentView.swift in which we need to replace the debugPrint statements with this code with the .onTapGesture [shown].

.onTapGesture {
let success = rsa.generateKeyPair(keySize: 2048, privateTag: "ch.cqd.noob", publicTag: "ch.cqd.noob")
if success {
let publicK = rsa.getPublicKey()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let token = appDelegate.returnToken()
let publicKS = publicK?.base64EncodedString()
print("Update ",self.users[self.selected],publicKS!,token)
cloud.searchAndUpdate(name: self.users[self.selected], publicK: publicKS!, device: token)
self.sender = self.users[self.selected]
messagePublisher.send(self.sender + " Logged In")
self.disableUpperWheel = true
}
}.disabled(disableUpperWheel)
.onReceive(resetPublisher) { (_) in
self.disableUpperWheel = false
self.disableLowerWheel = false
}

In this code we generate public/private key pair and then call the search function to get the icloud record with the same name and update the public key within it. We also get a copy of the local token and file it for debugging purposes. We added another text field to show a message when it registers a tap and send it a message and finally we disable the pickerView after you made your selection.

.onReceive(dataPublisher) { (data) in
debugPrint(self.users[self.selected2])
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let token = appDelegate.returnToken()
self.sendingTo = self.users[self.selected2]
// self.sending person selected in second PickerView
// self.sender person selected in first PickerView sending message
// token device sender [this device] is running on encypted with sending person public key
debugPrint("debug ",self.sendingTo!,self.sender!,token)
messagePublisher.send("Sending To " + self.sendingTo)
cloud.keepRec(name: self.sender, sender: self.sendingTo, senderDevice: token)
self.disableLowerWheel = true
}.disabled(disableLowerWheel)
Text(message).onReceive(messagePublisher) { (data) in
self.message = data
}

Again, quite a lot going on here. We again get the local token. Find the user you’re planning to send it too and update the mediation record with your details. Disable this pickerview after the selection. Send a message to the new text field. And of course define the new text field. We need to add these bits of code to the top and the bottom of our contentView.swift too.

...let messagePublisher = PassthroughSubject<String, Never>()
let resetPublisher = PassthroughSubject<Void, Never>()
struct ContentView: View {...extension UIWindow {
open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
print("Device shaken")
resetPublisher.send()
}
}
}

The extension is to reset the pickerviews if someone shakes the device. We disabled them after the first tap cause they would jump when I tried to send a message. We’re almost there, read on.

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