This is the first technical post in the series "CarShuffle: An Exploration of Practical iOS Architecture", where I will document how I approach new iOS projects, circa 2021. Read the intro to learn more about CarShuffle.
Introduction
When I develop for Apple platforms, I follow the Model-View-Controller (MVC) pattern. More specifically, because we live in a mobile-cloud world, I implement View-Controller-Model-Network patterns (VCM+N). I write it this way to clarify the layered nature of MVC, and to highlight the importance of getting mobile-cloud (M+N) data synchronization properly implemented before moving on to the higher layers.
Let’s step thru each of the layers, in the order in which I sketch them out, and I will share with you my current architecture choices. I won’t be diving deep into specific implementations, but will be offering tips and tricks that will hopefully be useful to those who have already walked thru similar code paths. I’ll include links to the source code for details.
Platform vs Open Source
But first, let’s talk open-souce.
Generally speaking, I prefer to use Apple’s technologies when possible. There are several benefits to this. First, Apple will (usually) maintain their kits and frameworks. Second, Apple can integrate new features across kits and frameworks, sometimes requiring little to no additional effort from developers.
That said, sometimes, Apple doesn’t provide a solution, or their solution is still green. In the interest of shipping earlier rather than later or never, I will commit to using an open-source project or two.
There are two key things to keep in mind, though. First, use open-source sparingly. The more you use, the more dependencies you have. You’re relying on others to update those libs with each new OS and device release. Second, make sure you know what those libs are doing, and make sure they aren’t leaking/stealing user info!
Storing Data
For the model layer, I use CoreData. Why? Because Apple uses it for all of their first-party apps.
For the cloud layer, I use CloudKit. Why? Again, because Apple uses it for all their first-party apps.
For synchronizing M+N, I don’t use NSPersistentCloudKitContainer. Why not? Because Apple doesn’t (yet) use it for their first party apps. As of iOS 15, NSPersistentCloudKitContainer supports private, public, and shared CloudKit databases, but anecdotal feedback on performance, reliability, and flexibility is mixed.
Instead, for M+N I use an open-source project called CloudCore, which I helped develop and currently maintain. It supports field-level synchronization, private public and shared databases, and fine-grain control over local vs sync’ed changes, among many other features. It does require some explicit configuration of your CoreData schema (I’d like to streamline this eventually), so YMMV.
Example: CarShuffle.AppDelegate.configureCloudCore()
At this first layer, I will write test code to add several records, confirm synchronization to iCloud, delete the app and re-install, and confirm sync again from iCloud.
I also follow a pattern where UI uses the persistentContainer.viewContext (of course), but all changes to data (adds/edits/deletes) are performed in persistentContainer.backgroundContexts. Why? This ensures that my UI code always updates in response to any data changes, whether they come from the user currently in the app, some background task dynamically updating content, the same user on another device syncing with this app, or another user making changes that are being shared with this user.
Example: CarShuffle.CarEditor.doSave()
Finally, I always use an app group, in anticipation of sharing CoreData with widgets and other extensions.
Showing Data
Next comes basic display of data. Whether its lists or grids, I tend towards UICollectionViews. SwiftUI is making solid gains, but I’m not quite ready to make the jump. So I use UICollectionViews paired with NSFetchedResultsControllers (FRCs) to display data.
If CoreData is hard to learn, FRCs are the prize worth pursuing. Simply put, thru the FRC delegate pattern, FRCs signals when fetched data changes, based on the predicates and sort descriptors you provide, to which you can dynamically update UI to match. One aspect of SwiftUI that I love is @FetchResults, which encapsulates NSFetchedResultsControllers into observables. Either way, dynamically observing changes in CoreData is the magic sauce that makes iOS apps shine.
Now, I’ve been grappling with the intersection of CollectionViews and FRCs for quite some time, and numerous bugs and nuances have made that intersection very challenging over the years. For instance, there was a time when the appearance or disappearance of sections caused crashes, and so we had to special-case handle that with a hard call to reloadData(). More recently, diffable data sources had a bug where updated managed objects weren’t being reported, and we all had to find inefficient workarounds to resolve this.
Thankfully, I’m happy to report that iOS 15 fixes all this, so your FRC delegate can simply implement the (… didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) protocol function and call diffableDataSource.apply(snapshot…) to update the list/collection view.
Example: CarShuffle.CarsListViewController
Editing Data
Once we have our foundation to save, sync, and show data, we need a way to create and edit it.
Out of the box, UIKit doesn’t provide much help here. SwiftUI has support for transforming Views into Forms, but it still feels very green to me.
Therefore, I turn once again to open-source and use Eureka for implementing editor UIs.
However you do it, one aspect to editing needs to be top of mind: Editors are for UI, but business logic should live in the model layer. I struggle with this all the time, but it's a struggle worth having.
Also note that, by the nature of our M+N synchronization layer, our view- and editor-controllers don’t access any network resources and don’t have to worry about caches or offline scenarios.
Conclusion
VCM+N provides the corner stones of a solid iOS app foundation. Once I can store, sync, show, and edit data, I'm ready to flesh out the app experience. I can change to the model layer, update the editor, and finally the display. Iteration can happen very quickly from here.
So that’s the foundation. Up next, I’ll probably talk about User Notifications.