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 😉