Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
A recent episode from the SwiftCraft podcast featured an interesting interview with Matteo Manferdini, who has been developing in iOS since the first iPhone, was creating Mac apps before then.
It offers what is currently a contrarian take on iOS architecture, but one that resonates with me — being in favour of MVC.
In the rapidly changing software industry it’s essential to keep an extremely open mind, because the lines are really thin between fad and revolutionary. Although I’ve been exploring MVP, MVVM, and VIPER in recent years, none of them have been compelling enough to convince me that they’re less complicated and cleaner than MVC can be.
I’ve been meaning to write a blog post expanding on Matteo’s Lotus MVC pattern, why a ruthlessly lean MVC is perhaps the cleanest solution, and how it’s all tied into a programming culture where we’re making slightly complicated concepts into incredibly complicated solutions.
The Lotus MVC pattern is still a little bit too close to VIPER for me, but it’s the closest literature I’ve come across to argue for a lean MVC that is capable of organically growing, rather than setting up unnecessary boilerplate code from start. I’ll expand on my thoughts on Lotus MVC, and how it ties into a pattern that I’m calling FUMVC (part joke, part serious) to combat against the trend to double down on complexity, at another time.
When classic software engineering concepts of high cohesion & low coupling are applied along with SOLID principles, MVC in iOS tends to strike a balance between a particularly well abstracted yet easy to follow architecture. This is beneficial to both rapid prototyping or MVP’s, or longer term legacy code (organic MVC growth).
The abstraction between the models and the view controller Matteo referred to was a Model Controller, and I couldn’t help but think that it’s the small and manageable pattern that I usually adopt for any Core Data work. Except I usually call it something like a ModelManager or ModelCoordinator.
Trying Different Abstractions
Over the years I’ve swung between a few different patterns working with Core Data, that could probably be summed up by the following phases:
- When I started iOS dev I was using more of a model controller pattern, with a central model object doing a combination of performing the CRUD operations (or more pedantically in iOS — add, fetch, save, and delete), and setting up the NSManagedObjectContext, NSPersistentStoreCoordinator, NSManagedObjectModel Core Data stack, and any heavyweight migration that was needed. This was unfortunately, also usually a singleton.
- On one particular project, a frontend developer provided some feedback on the API interface calls that he thought made more sense for the iOS frontend. Based on this, I experimented with adopting a pattern where each model’s class or extension contained their own CRUD operations. They could therefore be used like the following modelType.create(withProperty: String), or modelType.fetch(byProperty: String, containingValue: String). There was a lot of boilerplate here, and all for not much gain. The initial intention with things like the fetch call, was to be an alternative to specifying predicate strings, and using customized sort descriptors from enumerations and the like. But on reflection, I believe that every developer should be familiar with predicate strings, as the syntax is universally appicable to SQL queries, and every developer should familiarize themselves with SQL queries, even when using an ORM.
- Since iOS 10 when the NSPersistentContainer simplified the setup involved with the Core Data stack, and prior to that when generics became a thing with the rise of Swift, I’ve been favouring an approach where 1 or 2 smaller abstractions coordinate between the data models and the view controllers. Generics makes the prospect of reducing boilerplate code particularly attractive, and with the NSPersistentContainer gently guiding you to create a new background MOC every time you need a background MOC, this also helps in mentally discouraging you from implementing it as a singleton.
A slide from WWDC 2016, the introduction of NSPersistentContainer.
On that note, I thought I could share the above latest implementation of a Core Data ModelController that I’m favouring — the purpose being that anyone can drop this into their codebase no matter how your data models are set up!
The Model Controller
I’ve uploaded this to a new repository, which will be used for the greater purpose of reusable FUMVC abstractions. This is the 1st contribution to FUMVC, so the library is currently really small.
The ModelController shouldn’t try to everything, if it’s primary purpose is to be highly reusable. I’ve kept the code to a minimum for this reason.
I usually use conventionally verbose Apple influenced function names, such as something like addDataStorageEntry. Yet lately I’ve been influenced by my work in React apps and its naming that seems to be influenced by intermediate operations — which seems to be the naming conventions that Apple are moving to as well. So the functions are defined as:
- add(type:)
- total(type:)
- fetch(type: predicate: sort:)
- save()
- delete(by objectID:)
- delete(type: predicate:)
What the sample code in this blog post doesn’t do — it only uses one managed object context (the view context used for the main thread), and it doesn’t implement thread handling, as it would detract away from the main topic. Although I have updated the Github repository with some thread handling, and cover it a little bit below. But for now, let’s dive into each of the above functions…
The ModelController properties just consists of an NSPersistentContainer, and 2 NSManagedObjectContexts for the main thread and a background thread.
There is a convenience initializer with a single purpose to set the model name via dependency injection instead of being coupled to the Core Data abstraction. The NSPersistentContainer is lazy because of this. The private modelName’s default value should set the model name to the bundle, a likely safe scenario should you opt for the designated initializer. The modelName is set via the convenience initializer rather than a setter, because its single purpose it to be needed on instantiation and nowhere else.
The ModelController+Add takes care of the creation of a new NSManagedObject subclass. If Recipe is one of your model entities, you can use it by modelController.add(Recipe.self).
I’ve included a total function as well as a fetch function in ModelController+Fetch, as they’re both fetch related. This is where your NSFetchRequests are done, and the predicate strings and sort descriptors are optional, as you might want to use fetch to simply retrieve all records.
How to use total and fetch:
let total = modelController.total(Recipe.self)
let predicate = NSPredicate(format: "name LIKE %@", "Fetcheroni Pizza")let fetched = modelController.fetch(Recipe.self, predicate: predicate)
The ModelController+Save is pretty standard. Even if you don’t use a Core Data abstraction, it’s very likely that you’ve written a save() method to check for hasChanges and wrap a try/catch.
Finally, the ModelController+Delete. There are 2 delete methods here — one with the standard interface pattern used in the fetch method, with the type and predicate passed in to find the object you want to delete.
The other checks for the NSManagedObjectID, which is an issue that is consistently overlooked in deleting Core Data objects. Accessing the object via a fetch is no guarantee that the same object will be deleted on the same thread that it was accessed on, so deleting by ID is the recommended approach by Apple. The predicate method also wraps the delete(by objectID:) method for this reason.
How to use:
modelController.delete(by: recipe.objectID)
let predicate = NSPredicate(format: "name LIKE %@", "Deep Pan Pizza")modelController.delete(Recipe.self, predicate: predicate)
Thread handling
There are advantages to using either perform(_:) and performAndWait(_:). Both are functions that accept a closure to enable you to use a managed object context on the correct thread it’s associated with.
Note, that this is slightly different to using DispatchQueue.global() async(_:) or sync(_:), where sure, while it may be true that they would be executed on a different thread, you are not necessarily enabling the context to match to the thread it’s associated with.
Getting back to perform(_:) and performAndWait(_:), the only difference between the 2 is that perform(_:) continues the flow of execution. So in a function where you want to return a value for some reason that doesn’t depend on any asynchronous operations, you would use perform(_:) to continue execution and return something not based on that block.
If on the other hand, what your function is returning depends on the block, you could use performAndWait(_:) to allow the block to finish executing before the function returns. More is explained here.
We could go with either approach in the ModelController, or we could even go with a combination. For now, I’ve updated the repo with the perform(_:) approach (the above sample code has no thread handling), and have added a completion block as a function parameter to allow the continuation of execution via the completion (as opposed to the return approach with performAndWait(_:)).
I just think by limiting the usage of performAndWait(_:) until it’s really needed, reduces the chances of blocking the main UI thread. As after all, the ModelController, as with most good abstractions, shouldn’t restrict the flexibility in how a calling component should use it — it’s up to the parent object to decide whether to block the main thread or not, not some mystery box behaviour in an encapsulated interface.
However, maintaining flexibility in providing performAndWait(_:) returns and perform(_:) completion blocks comes at a little bit of a cost of clean code. For this reason, if the ModelController will eventually add performAndWait(_:) behaviour into its implementation, I’d want to do this via Max Howell’s PromiseKit. This uses the JavaScript concept of promises, to reduce the messiness of nested completion blocks, as well as propagating error handling.
The ModelController repo also currently doesn’t utilize the background context, and providing this functionality would be the next obvious implementation. It doesn’t need to blow out any more than that though, this should be a really simple and lean reusable library as the first port of call for any Core Data codebase. I feel as if reusable libraries are best that way, and any extra functionality should be an extension on that library or just customized for the particular codebase being worked on. Otherwise it doesn’t get reused, and that’s the whole point of libraries.
Where To Next?
I found accessing the type as a value along the lines of EntityName.self a bit redundant, and one step working against this from an elegant API. I’m not sure if this is possible, but it would be great if you could add an enumeration for the Core Data model types, along the lines of Benedikt Terhechte’s blog post on the section of using custom data types, by implementing the StringLiteralConvertible protocol. On another note, this is a great reference for everything to do with enumerations in Swift, and I highly recommend bookmarking it.
The most pressing issue would be providing an elegant and minimal interface to the background managed object context. This could be done via an extension or option protocol on the ModelController though, as I think that having a background MOC is something that most developers rush into without a need for it.
I will also be blogging an update on the greater concepts of FUMVC, and how the repository is likely to take shape.
How a Model Controller works with Core Data in Swift was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.