Push Nachrichten in iOS 13 funktionieren nicht – Grundsätzliches Problem in einigen Apps

Einige Benutzer der mein Lager Pro App haben mit angeschrieben, einige hatten Synchronisationsprobleme, die ich aus der Ferne beheben konnte. Es gab aber auch einen Fall, bei dem eine „Fernwartung“ nicht möglich war und auch die Ausbringung eines Updates für die App keinerlei Lösung bot.

Für Entwickler: Bisher konnte im AppDelegate immer wieder mit den Funktionen

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let current = UNUserNotificationCenter.current()
        
        current.requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in
            if let error = error {
                print("Fehler bei Erlaubnis \(error.localizedDescription)")
            }
        }
        
        DispatchQueue.main.async {
            print("Registriere Push Nachrichten")
            application.registerForRemoteNotifications()
            if application.isRegisteredForRemoteNotifications == true {
                NSLog("Hintergrundaktualisierung ist eingeschaltet")
            }
        }
        return true
    }

die Aktualisierung per Push Nachricht angefordert werden. Bis iOS 12 funktionierte dies problemlos und die Delegate Funktionen

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        print("Registered for remote notifications")
        let tokenParts = deviceToken.map { data -> String in
            return String(format: "%02.2hhx", data)
        }
        let token = tokenParts.joined()
        // 2. Print device token to use for PNs payloads
        print("Device Token: \(token)")
    }
    
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Remote notification registration failed")
        print(error)
    }

wurden ordentlich aufgerufen und das Gerät konnte für Push Nachrichten registriert werden.

Aktuell scheint es jedoch so zu sein, dass nur Geräte, bei denen eine SIM Karte installiert ist oder war diese Funktionen aufrufen. So ergab ein Versuch der Registrierung mit einem iPhone XS eine ordentliche Ausführung der Funktion. Das Gerät wird täglich mit einer SIM Karte verwendet.

Bei einem iPhone 7 ohne SIM Karte sah die Geschichte anders aus. Das Gerät war „frisch“ mit iOS 13.4.1 installiert und mit einem WLAN verbunden und aktiviert worden. Der identische Code auf dem iPhone 7 konnte zwar ausgeführt werden, allerdings wurden die Delegate Funktionen didRegisterForRemoteNotificationsWithDeviceToken und didFailToRegisterForRemoteNotificationsWithError nicht ausgeführt.

Nach mehreren Stunden Versuchen ergab dann die Konsole von Xcode bei Betrieb des Gerätes den folgenden Fehler:

Connection 55: default TLS Trust evaluation failed(-9807)

Diese Ausgabe erschien beim iPhone XS nicht. Es folgten noch weitere, nicht wirklich aussagekräftige Fehlermeldungen.

Nachdem dann eine SIM Karte im iPhone 7 installiert wurde, das iPhone neu gestartet wurde, die App gelöscht und per Xcode neu installiert wurde, funktionierte plötzlich die Registrierung und entsprechende Push Nachrichten wurden sofort empfangen.

Augenscheinlich muss es sich hierbei um einen Bug innerhalb von iOS 13 handeln. Was machen denn die Benutzer die beispielsweise ein iPad nur mit Wifi besitzen und die technisch schon keine Möglichkeit haben eine SIM Karte zu installieren?

Ein entsprechender Bug unter der Nummer FB7669283 wurde bei Apple eröffnet, wir dürfen also auf eine Antwort gespannt sein.

CoreData und CloudKit

Ich habe mich nun einige Tage mit CoreData und entsprechender Synchronisation über die Geräte innerhalb eines iTunes Accounts beschäftigt. Problem war: wie bekomme ich die Lagerbestände aus der mein Lager App synchronisiert um auf mehreren Geräten, möglichst in Echtzeit, Daten zu bearbeiten. Alles selber schreiben ist verdammt viel Arbeit. Zum Glück stand schon jemand vor einem ähnlichen Problem und hat ein entsprechendes Framework geschrieben.

Beim googeln und browsen bin ich auf Seam 3 gestossen, einem Framework das via Cocoapods eingebunden werden kann. Seam 3 ist auf GitHub unter https://github.com/paulw11/Seam3 zu finden und bringt auch ein paar Beispiele mit. Ich habe mir hier mal vorgenommen die Beispiele und Reihenfolgen zu erklären. Ich werde hier bewusst nicht über die Verwendung von Pods und das Implementieren eingehen, würde denke ich den Rahmen sprengen. Ich gehe davon aus, dass jemand, der auf diese Seite und den Beitrag stösst, weiss, wie er Pads benutzen muss.

Nach der Installation von Seam 3 widmen wir uns zunächst unserem AppDelegate. Hier nehmen wir folgende Änderungen vor:

Erstmal erklären wir ihm, dass er das Framework verwenden soll, das machen wir direkt oben im Delegaten unterhalb von import UIKit

import Seam3

Als nächstes deklarieren wir noch die entsprechende Variable für Seam 3 um prinzipiell ein Abbild des ManagedContext zu erzeugen

var smStore: SMStore?

Danach schauen wir uns didFinishLaunchingWithOptions an und verändern ihn passend so:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
       //Die Kommunikation findet über Push Nachrichten statt. Wir müssen das Gerät also dafür vorbereiten
        application.registerForRemoteNotifications()
       
       //Kümmern wir uns nun um die Einbindung des Frameworks
        let storeDescriptionType = self.persistentContainer.persistentStoreCoordinator.persistentStores.first?.type
            if storeDescriptionType == SMStore.type {
                print("Store is SMStore")
                print()
                self.smStore = self.persistentContainer.persistentStoreCoordinator.persistentStores.first as? SMStore
            }
        context = self.persistentContainer.viewContext
        
        //Prüfen ob der Benutzer bereits in iTunes angemeldet ist
        self.validateCloudKitAndSync() {
            
        }
        return true
    }

Die Funktion zum Prüfen ob der User in iTunes bereits angemeldet ist, habe ich 1:1 aus dem Beispiel übernommen

func validateCloudKitAndSync(_ completion:@escaping (() -> Void)) {
        
        self.smStore?.verifyCloudKitConnectionAndUser() { (status, user, error) in
            guard status == .available, error == nil else {
                NSLog("Unable to verify CloudKit Connection \(error!)")
                return
            }
            
            guard let currentUser = user else {
                NSLog("No current CloudKit user")
                return
            }
            
            if let previousUser = UserDefaults.standard.string(forKey: "CloudKitUser") {
                if  previousUser != currentUser  {
                    do {
                        print("New user")
                        try self.smStore?.resetBackingStore()
                    } catch {
                        NSLog("Error resetting backing store - \(error.localizedDescription)")
                        return
                    }
                }
            }
            
            UserDefaults.standard.set(currentUser, forKey:"CloudKitUser")
            
            self.smStore?.triggerSync(complete: true)
            
            completion()
        }
        
    }

Nun kommt der kniffligere Teil, wir müssen den „normalen“ persistentContainer etwas abändern und erzeugen eigentlich eine Kopie des CoreData Models als SQL Datenbank

lazy var persistentContainer: NSPersistentContainer = {
        //Seam 3 registrieren
        SMStore.registerStoreClass()
        
        //Normale Initialisierung von CoreData
        let container = NSPersistentContainer(name: "MEINCOREDATAMODEL")
        let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        
        //Erstellen einer SQLLite Datenbank für Seam 3 zum Abgleich der Daten
        if let applicationDocumentsDirectory = urls.last {
            
            let url = applicationDocumentsDirectory.appendingPathComponent("MEINCOREDATAMODELSQL.sqlite")
            print(url)
            let storeDescription = NSPersistentStoreDescription(url: url)
            
            storeDescription.type = SMStore.type
            
            //Bezeichnung / URL aus den iCloud Optionen für die Speicherung der Daten innerhalb der Cloud
            storeDescription.setOption("ICLOUDURLDESCONTAINERS" as NSString, forKey: SMStore.SMStoreContainerOption)
            
            container.persistentStoreDescriptions=[storeDescription]
            
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
        }
        
        fatalError("Unable to access documents directory")
    }()

Zum Abschluss müssen wir dem Delegaten dann noch sagen, was er denn machen soll, wenn er eine Nachricht bekommt. Auch diese Funktion kann 1:1 aus dem Beispiel so übernommen werden

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        print("Received push")
        
        smStore?.handlePush(userInfo: userInfo) { (result) in
            completionHandler(result.uiBackgroundFetchResult)
        }
    }

Damit wäre unser Delegate erstmal bereit. Um nun die Daten abzugleichen bzw. bei Änderungen zu übertragen, wechseln wir in den entsprechenden ViewController und fügen auch dort erstmal das Framework oben zum Import ein

import Seam3

Um es mir ein bisschen einfacher zu machen, habe ich in den entsprechenden Controllern ein paar „Standarfunktionen“ implementiert die ich immer wieder in der gleichen Form verwende. Zunächst benutze ich eine Funktion die die Nachricht empfängt und eine Aktion auslöst

func syncwithcloud()
    {
        NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: SMStoreNotification.SyncDidFinish), object: nil, queue: nil) { notification in
            
            print("Sync fertig")
            if notification.userInfo != nil {
                let appDelegate = UIApplication.shared.delegate as! AppDelegate
                appDelegate.smStore?.triggerSync(complete: true)
            }
            
            self.context?.refreshAllObjects()
            
            DispatchQueue.main.async {
                self.reloaddata()
            }
        }
        
    }

Hier wird also nach der erfolgten Synchronisation ein reload der vorhandenen Daten erzwungen. Dadurch bekommt man die Daten mehr oder weniger in Echtzeit ins View.

Die Reload Funktion ist nicht wirklich spektakulär, sie nullt ein Array, lädt eigentlich nur den FetchRequest neu und aktualisiert ein TableView

@objc func reloaddata()
    {
        artikels = []
        loaddata()
        self.tableView.reloadData()
    }

Zum Schluss binden wir das ganze dann nur noch in viewDidAppear ein und es kann losgehen

override func viewDidAppear(_ animated: Bool) {
        self.syncwithcloud()
    }

Nach dem Kompilieren und starten sollte dann auf den Geräten Meldungen wie die folgenden kommen

019-02-01 12:54:35.894926+0100 MyApp [23176:291129] OK will update existing object from CKRecord Lagerplatz, recordID=260AD8A2-EAB8-4BF8-AA25-2863328F03A7
2019-02-01 12:54:35.903374+0100 MyApp [23176:291129] OK will update existing object from CKRecord Lieferanten, recordID=3B8FE4A3-50AF-43AF-B75D-14D058ADF67F
2019-02-01 12:54:35.906518+0100 MyApp [23176:291129] OK will update existing object from CKRecord Artikel, recordID=1D23BD8D-2522-4CBC-879F-788536328950
2019-02-01 12:54:35.910490+0100 MyApp[23176:291129] OK will update existing object from CKRecord Artikel, recordID=1E31BDA7-0B6B-4739-8F85-67D238B050E2
2019-02-01 12:54:35.915084+0100 MyApp[23176:291129] OK will update existing object from CKRecord Artikel, recordID=B50F1ED0-8880-4FB1-819D-1E88A06D0A42
2019-02-01 12:54:35.916876+0100 MyApp[23176:291129] OK will update existing object from CKRecord Bestand, recordID=87C0535F-12B3-425A-B9EA-FFF93A3EEBF3
2019-02-01 12:54:35.919183+0100 MyApp [23176:291129] OK will update existing object from CKRecord Protokoll, recordID=104F7BAD-8B90-43A6-99A5-87002A47A89B

Normalerweise sollten nun die Daten synchronisiert werden, that’s all 😉

QRCode Generierung in Swift

Da ich selbst nach einer Funktion zum Generieren von QR Codes direkt in Swift ohne Webdienst gesucht habe, um eine „richtige“ Offline Applikation zu generieren, dachte ich, andere könnten es vielleicht auch brauchen 😉

Erstellt folgende Funktion:

func generiereQRCode(from string: String) -> UIImage? {
    let data = string.data(using: String.Encoding.ascii)

    if let filter = CIFilter(name: "CIQRCodeGenerator") {
        filter.setValue(data, forKey: "inputMessage")
        let transform = CGAffineTransform(scaleX: 3, y: 3)

        if let output = filter.outputImage?.transformed(by: transform) {
            return UIImage(ciImage: output)
        }
    }

    return nil
}

Danach könnt ihr den QR Code mit folgendem Aufruf generieren und beispielsweise in ein UIImageView setzen

let image = generiereQRCode(from: "Ich werde umgewandelt zum QR Code")

Auch „normale“ Barcodes können mit der Funktion erstellt werden. Dazu muss lediglich die Zeile

if let filter = CIFilter(name: "CIQRCodeGenerator") {

durch

if let filter = CIFilter(name: "CICode128BarcodeGenerator") {

ersetzt werden. Der Rest der Funktion kann so bleiben. Theoretisch kann man die verschiedenen Barcode Typen auch als UserDefault setzen und von dort laden um sie beispielsweise über die Einstellungen zu verwalten. Ein Beispiel dafür findet Ihr in der mein Lager – Lagerverwaltungsapp 😉

Wir benutzen Cookies um die Nutzerfreundlichkeit der Webseite zu verbessen. Durch Deinen Besuch stimmst Du dem zu.