I was employed to completely rewrite the app from the ground up for a more reliable and fast synchronization and a more native user interface. I wrote, maintained and updated this app under employment of Crafter B.V. between September 2018 and August 2019. The design was done by my employer where I gave input and suggestions for modifications. Crafter develops work order planning and administration software for businesses with employees in the field. Visit their website here.

Crafter is a professional work order plannig, billing and administration software suite that includes a planning web application for people at the office and mobile apps for workers in the field. I have rebuilt this application from scratch in Swift. It uses a local Core Data database which synchronizes automatically with our API to allow mechanics to work with the app while they have no or poor internet connection. The iOS app is used by mechanics, painters and other craftsmen to see their list of daily tasks and fill in workorders and register their hours. The app contains many extra features such as logging gps routes and adding photos of objects on location.

Jim Koeleman, CEO of Crafter: (...) Rens delivered a properly working version of the app in no time and then expanded it in quality. Rens works meticulously. Can perform under time pressure if required. He does not lose sight of the quality of his work. Rens is leaving Crafter because of his departure for Berlin. Should Rens not leave for Berlin, we would still have liked to continue our cooperation.

How to reliably synchronize a Core Data database

Synchronizing a Core Data database is (at least at the time of the development of the app) not natively supported by the framework. There are several ways to implement database synchronization manually, and the method is more or less independent of the exact database framework. I will describe below how I implemented this for Crafter.

Easiest is to synchronize a single table. To do this with an API endpoint giving objects in JSON, apart from a last downloaded date stored for the entire table, every objects needs three extra attributes:

On synchronization, first all objects marked as "modified" are pushed from the app to the server. Then, all objects changed after the last-downloaded date are requested from the server by the app. The down-side is, that if changes are made by the app and on the server in between two synchronization cycles, the data on the server is over-written by the data in the app. However, this loss of data only occurs for this one object. It is possible to use a modified date in addition to the boolean flag. Either the server or the app could then decide what action to take if a conflict emerges.

Like most databases, the database in Crafter does not consist of a single entity. When synchronizing an entire table, it is important to focus especially on what to do with relationships between different objects. If an object from the API links to a second object, that second object will need to be synchronized first, as it should exist before importing it in Core Data. For one-to-many relationsships, the import works fastest if the "many" objects are imported first, and then the "one" object, fetching the many-objects by their server-id in a batch. To support partial synchronization in case of an interrupted internet connection, the last-downloaded date is stored per table.

To make sure the synchronization model works without any issues, even with cycles interruptions or object edits within a cycle, it is important that this last-downloaded date is given by the server and is requested before the downloading of new object versions start. Objects that are changed on the server (by the back-office web app or another instance of the app) are then downloaded again on the next cycle. Similarly, incomplete relationships that may have been caused by that, are also fixed in the next cycle. If the same version is re-downloaded again, that is obviously no problem.

What to include in objects can be chosen smartly to make use of the changes-in-app-overwrite-server-changes merge policy. For example, if an address street name is edited in the app, and the address number is changed on the cloud, it makes no sense to combine the two, resulting in an invalid and possibly even non-existent address. One address should therefore definitely not be split out over multiple objects. At the same time, it should be perfectly valid to have an in-office employee edit the workorder's address while the craftsman out in the fields adds equipment that he needs to take along to the same workorder.

If any combination of object values are invalid, this should be detected by the app, and not by the server. If the field for an e-mail address is not checked by the app, the server could check it and remove or change the value, which would be then be picked up by the app again in the same synchronization cycle. But it never rejects it, as in many cases it would be complicated or impossible to deal with a server-side rejection.

Core Data contexts

Core Data has a feature called "managed object contexts", which work like scratchpads on which objects can be edited seperately, and then discarded or merged. Any possible merge conflicts can be resolved by choosing a merge policy.

In the architecture of the iOS app of Crafter, the UI and synchronization logic work on seperated parent contexts. They are merged in such a way that the object changes made in UI overwrite the changes made by a synchronization, in the unfortunate case that both contexts are changed at the same time.

Any object-editing modally presented view controller works on a child context of the main UI context. If the view is cancelled, the changes are simply discarded. Changes from outside (from the cloud) are not merged into the view automatically. If the changes are requested to be saved, the child context is saved into its parent. Often, the parent and child contexts need to exchange objects, especially if connections between an edited object and an existing object need to be made. The exchanging of objects between contexts is possible, by looking up objects with their managed object id. It is never possible to create relationships between objects living on different contexts.