mein Lager – Lagerverwaltung 1.18

Über eine Rezension habe ich diverse Wüsche für ein Update erhalten die, so wie ich denke, auch sinnvoll sind. Ich habe versucht die Wünsche umzusetzen und ein entsprechendes Update vorbereitet, welches Anfang der Woche verfügbar sein sollte. Unter anderem wurde dabei auch ein neues Logo für die App hinzugefügt.

Folgende Änderungen werden enthalten sein:

  • Drucken von Beständen aus der Artikelansicht nun möglich
  • Drucken des Protokolls jetzt möglich
  • Drucken von Beständen je Lagerplatz nun möglich
  • Bugfix aktueller Bestand wurde beim Buchen von Beständen nicht übergeben, Buchung wurde jedoch korrekt ausgeführt
  • Löschen von Buchungsprotokollen zu einem bestimmten Zeitpunkt möglich

Die App wird unter https://itunes.apple.com/de/app/mein-lager/id1448104110 verfügbar sein und sollte in Kürze den Genehmigungsprozess abgeschlossen haben

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 😉

mein Lager – Lagerverwaltung 1.17

Im aktuellen Update 1.17 gibt es nur kleinere Fehlerbehebungen. Einmal wurde ein Fehler beim Scannen von Barcodes behoben der bei kleineren Displays auftreten konnte. Auf der anderen Seite wurde noch ein Fehler bei der Vergabe der nächsten Artikelnummer für neue Artikel behoben.

Zusätzlich wurde eine neue Version als separate App zur Prüfung eingeschickt, die die Synchronisation zwischen allen Geräten innerhalb eines iTunes Accounts erlaubt. Zusätzlich ist diese Version frei von Werbeanzeigen.

Die aktuellste Version der Single Variante wird in Kürze unter https://itunes.apple.com/de/app/mein-lager/id1448104110 verfügbar sein.