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

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 X, you need to go back to the beginning for it to make any sense, indeed you need to look at the notifications series before do that else your struggle. Most of the parts are just 4 minutes long including code.

Where are we. In this article we going to be focusing on SwiftUI for the most part, with most of the background coding in place.

We setup our app such that we can register upto eight parties, identify their devices and send messages to each other, after an initial setup phase. It’s the setup phase we’re going to focus on here.

Firstly I want to setup a popup. It will look a little different on the iPad vs the iPhone, a feature of SwiftUI for now.

struct PopUp : View {
@Binding var code: String
@State var input: String
@State var status: String = ""
var body : some View {
VStack {
Text("and the Code is ...")
Text("\(self.code)")
TextField("Code?", text: $input, onEditingChanged: { (editing) in
if editing {
self.input = ""
}
}, onCommit: {
if self.code == self.input {
UIApplication.shared.windows[0].rootViewController?.dismiss(animated: true, completion: {
putemthruPublisher.send()
})
} else {
self.status = "Sorry Code Incorrect"
}
}).frame(width: 128, height: 128, alignment: .center)
Button(action: {
UIApplication.shared.windows[0].rootViewController?.dismiss(animated: true, completion: {})
}) {
Text("Cancel")
}
Button(action: {
UIApplication.shared.windows[0].rootViewController?.dismiss(animated: true, completion: {})
}) {
Text("OK")
}
Text(status)
}
}
}

Code that looks much like the other we’re been adding to the main view, except that the binding declaration. It declares a property that is visible on both the parent and child views in our case. Lets feed the value we used as a title in the previous article to this one as default text.

.onReceive(enableMessaging, perform: { (data, secret) in
print("Granted")
self.confirm = data
self.showingGrant = true
self.code = secret
let alertHC = UIHostingController(rootView: PopUp(code: self.$code, input: ""))
alertHC.preferredContentSize = CGSize(width: 256, height: 256)
alertHC.modalPresentationStyle = .formSheet
UIApplication.shared.windows[0].rootViewController?.present(alertHC, animated: true)
}).onReceive(putemthruPublisher, perform: { (_) in
self.disableMessaging = false
// cloud.saveAuthRequest2PrivateDB(name: self.sendingTo, token: self.confirm!)
messagePublisher.send("Code good, start sending ...")print("good2Go")
})

I have commented out the saveAuthRequest2PrivateDB method cause it will bypass the new code after you run it just once, not ideal for testing :) Put in the code and test it.

Ok what next. We added an admin screen to let us create more people, but we left the kluge in, so we were restricted to just eight people. Lets change the app so that I can add as many people as I like. Almost all the changes are in ContentView.swift. Firstly since structs area not mutable, we need to create a micro class to contain our users.

class Users: ObservableObject {
var name:[String] = []
}

Next we’ll create some more State variables to help us manage it.

@State var people = Users()
@State var display = true
@State var selected3 = 0
@State var selected4 = 0

Next we need to replace all the references to the array with the ones to the new structure. A replacement I don’t really want to paste here piecemeal cause as you surely have discover debugging ContentView.swift you make any errors is very difficult. I going to go for broke and paste the entire thing in one go. It is a monster bit of code, but you can thank me later for it.

var body: some View {
VStack {
if showAdmin {
Button(action: {
print("saving to icloud")
cloud.seekAndTell(names: self.people.name)
}) {
Image(systemName: "icloud.and.arrow.up")
}.onReceive(turnOffAdmin) { (_) in
self.showAdmin = false
self.showUpperWheel = true
}
}
Text("noobChat").onAppear() {
cloud.getDirectory()
if self.showAdmin {
self.showUpperWheel = false
}
let name = UserDefaults.standard.string(forKey: "name")
if name != nil && name != "" {
self.showUpperWheel = false
self.showAdmin = false
self.sender = name
messagePublisher.send(self.sender + " Owner")
self.disableMessaging = false
}
}.onReceive(recieptPublisher) { (_) in
messagePublisher.send("Message Recieved")
}.onReceive(pongPublisher) { ( _ ) in
self.showAdmin = true
}.alert(isPresented:$showingAlert) {
Alert(title: Text("Can we talk?"), message: Text("\(alertMessage!)"), primaryButton: .destructive(Text("Sure")) {
poster.postNotification(token: self.confirm!, message: "Granted", type: "background", request: "grant",device:token)
}, secondaryButton: .cancel(Text("No")))
}
if self.showAdmin {
HStack {
Button(action: {
let finder = self.people.name.firstIndex(of: self.name)
if finder == nil {
self.display = false
self.people.name.append(self.name)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.display = true
}
}
self.name = ""
}) {
Image(systemName: "plus.circle")
}
TextField("Nobody?", text: self.$name, onEditingChanged: { (editing) in
if editing {
self.name = ""
}
}, onCommit: {
let finder = self.people.name.firstIndex(of: self.name)
if finder == nil {
self.display = false
self.people.name.append(self.name)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.display = true
}
}
self.name = ""
})
Button(action: {
let finder = self.people.name.firstIndex(of: self.name)
self.display = false
self.people.name.remove(at: finder!)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.display = true
}
}) {
Image(systemName: "minus.circle")
}
}.padding()
}
if showUpperWheel {
if self.display {
Picker(selection: $selected3, label: Text("")) {
ForEach(0 ..< self.people.name.count) {
Text(self.people.name[$0])
}
}.pickerStyle(WheelPickerStyle())
.padding()
.onTapGesture {
// *** 1ST ***
self.sender = self.people.name[self.selected3]
UserDefaults.standard.set(self.sender, forKey: "name")
let success = rsa.generateKeyPair(keySize: 2048, privateTag: "ch.cqd.noob", publicTag: "ch.cqd.noob")
if success {
let privateK = rsa.getPrivateKey()
let publicK = rsa.getPublicKey()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let token = appDelegate.returnToken()
var timestamp = UInt64(floor(Date().timeIntervalSince1970 * 1000))
let random = String(timestamp, radix: 16)
UserDefaults.standard.set(random, forKey: "secret")
cloud.searchAndUpdate(name: self.sender, publicK: publicK!, privateK: privateK!, token: token, shared: random)
}
messagePublisher.send(self.sender + " Logged In")
self.showUpperWheel = false
}.disabled(disableUpperWheel)
.onReceive(resetPublisher) { (_) in
self.disableUpperWheel = false
self.disableLowerWheel = false
self.showUpperWheel = true
}
}
}
if !showAdmin {
TextField("Message", text: $yourMessageHere, onCommit: {
self.output = self.yourMessageHere
if self.confirm != nil {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let token = appDelegate.returnToken()
poster.postNotification(token: self.confirm!, message: self.yourMessageHere, type: "alert", request: "ok", device: token)
}
})
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.disabled(disableMessaging)
.onReceive(disablePublisher) { (_) in
self.disableMessaging = true
}
}
if self.display {
Picker(selection: $selected4, label: Text("")) {
ForEach(0 ..< self.people.name.count) {
Text(self.people.name[$0])
}
}.pickerStyle(WheelPickerStyle())
.padding()
.onReceive(pingPublisher) { (data) in
self.display = false
self.people.name = data
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.display = true
}
}
.onTapGesture {
// *** 2ND ***
self.sendingTo = self.people.name[self.selected4]
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let token = appDelegate.returnToken()
cloud.authRequest(auth:self.sender, name: self.sendingTo!, device: token)
}.onReceive(popPublisher) { (token,data) in
self.alertMessage = data
self.confirm = token
self.showingAlert = true
self.disableMessaging = true
}.onReceive(enableMessaging, perform: { (data, secret) in
print("Granted")
self.confirm = data
self.showingGrant = true
self.code = secret
let alertHC = UIHostingController(rootView: PopUp(code: self.$code, input: ""))
alertHC.preferredContentSize = CGSize(width: 256, height: 256)
alertHC.modalPresentationStyle = .formSheet
UIApplication.shared.windows[0].rootViewController?.present(alertHC, animated: true)
}).onReceive(putemthruPublisher, perform: { (_) in
self.disableMessaging = false
cloud.saveAuthRequest2PrivateDB(name: self.sendingTo, token: self.confirm!)
messagePublisher.send("")
print("good2Go")
})
.onReceive(shortProtocol, perform: { (data) in
print("Granted")
self.confirm = data
self.disableMessaging = false
}).disabled(disableLowerWheel)
}
if showAdmin {
Button(action: {
print("saving to icloud")
cloud.seekAndTell(names: self.people.name)
}) {
Image(systemName: "icloud.and.arrow.up")
}
}
Text(message).onReceive(messagePublisher) { (data) in
self.message = data
}
}
}
}

As I said I don’t think it useful to go thru this with too much detail. The most important change evident here. You don’t need to cut’n’paste the code below anywhere.

Button(action: {
let finder = self.people.name.firstIndex(of: self.name)
if finder == nil {
self.display = false
self.people.name.append(self.name)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.display = true
}
}
self.name = ""
}) {
Image(systemName: "plus.circle")
}

Within it you can see the way I have managed to fix it. Basically given you cannot change a pickerView while it is displaying, the code hides it. Adds an entry and then re-displays it. Cause it is now a class and not a struct I can mutate it. I did try and flip this with a toggle, which worked but also produced an error, better a version with no errors. On that note I also changed one method in cloud.swift.

let pingPublisher = PassthroughSubject<[String], Never>()...func getDirectory() {
let predicate = NSPredicate(value: true)
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 }
var names:[String] = []
for result in results {
let name = result.object(forKey: "name") as? String
names.append(name!)
}
DispatchQueue.main.async {
pingPublisher.send(names)
}
if results.count == 0 {
DispatchQueue.main.async { pongPublisher.send() }
}
}
}

This used update names one by one in the array. Since we introduced the fix which turns the picker on and off, I thought it preferable to update all the names on one go.

Where do we go from here. I tried it out and didn’t like the challenge key, which was a nightmare to type in. I didn’t like the solution to the encryption dance, which we fixed, but we didn’t. We need a means of having users with the same names. All problems, for which I need to find solutions… 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