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.

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 😉

mein Lager – Lagerverwaltung 1.16

Mit dem neuesten Update wird es möglich sein die Barcodes für die Lagerplätze auch als QR Code zu generieren. Dadurch wird man flexibler sein bei der Länge der Lagerplatznamen da der QR Code mehr Informationen speichern kann.

Zusätzlich wurde ein Fehler bei der Skalierung der Produktbilder behoben. Zuvor konnte es vorkommen, dass Bilder verzerrt dargestellt wurden.

Das Update sollte in Kürze unter https://itunes.apple.com/de/app/mein-lager/id1448104110?l=de&ls=1&mt=8 zur Verfügung stehen.

mein Lager – Lagerverwaltung 1.15

Es geht Schlag auf Schlag, liegt aber daran, dass ich direkt nach dem Einsenden des letzten Updates bereits damit begonnen habe, ein neues Update zu erstellen.

Diesmal ist das Update etwas grösser ausgefallen und bringt folgende Änderungen mit sich:

  • Lagerplätze können nun angelegt werden
  • Zu jedem Lagerplatz wird automatisch ein Barcode generiert (Code 128) der per AirPrint ausgedruckt werden kann
  • Zuordnung von Artikeln zum Lagerplatz
  • Übersicht welche Artikel in welcher Menge am Lagerplatz liegen

Das Update ist in den letzten Zügen und wird in Kürze zur Überprüfung eingeschickt. Da Apple derzeit recht schnell prüft, gehe ich mal davon aus, dass die neue Version zum Wochenende hin dann über den AppStore unter https://itunes.apple.com/de/app/mein-lager/id1448104110 verfügbar sein wird.

mein Lager – Lagerverwaltung 1.14

In Kürze wird im AppStore ein Update für die Lagerverwaltung verfügbar sein. Neben der Behebung kleinerer Fehler steht hier die iPad Version im Vordergrund.

Die Version 1.13 wurde absichtlich ausgelassen, ein bisschen abergläubig bin ich ja schon 😉

Zusätzlich wurde auch ein direkter Link zum Kontaktformular hinterlegt damit Fragen schneller und einfacher beantwortet werden können.

Die App sollte in Kürze unter https://itunes.apple.com/de/app/mein-lager/id1448104110 zum Download bereit stehen.