I know it’s an unpopular opinion, but i’ve really grown to like Core Data. It has it’s quirks, but it enables us to create a unidirectional data flow where all changes go through the managed object model, which drives the state of the view. If this already sounds like the perfect match for SwiftUIs reactive approach to rendering views, then that’s mostly true. Core Data itself still isn’t very swift-y, but we can leverage many new features in Swift 5.1 and Combine to create generic wrappers that can take any managed object and turn it into a view model that drives a SwiftUI view.
Let’s start with NSFetchedResultsController
. Subscribing to updates for many objects matching a fetch request has always been easier than subscribing to updates from a single managed object, thanks to NSFetchedResultsController
. It comes with a delegate that informs us about changes to the underlying data in a structured way, because it was designed to integrate with tables and collection views. But we won’t need most of that.
First we declare the view model.
class FetchedObjectsViewModel<ResultType: NSFetchRequestResult>:
NSObject, NSFetchedResultsControllerDelegate, BindableObject {
There is already a lot to unpack here:
ResultType
that defines the kind of Core Data entity of its fetched results controller. Since NSFetchedResultsController
fetches objects that conform to NSFetchRequestResult
our ResultType
is constraint accordingly.NSObject
, because it implements NSFetchedResultsControllerDelegate
, which enables us to listen to updates from the fetched results controller, but also requires us to conform to NSObjectProtocol
.BindableObject
protocol from the SwiftUI framework, which enables us to communicate changes about its underlying data to the view.This might sound a little complicated, but we’re also almost done already! The rest is boilerplate.
The view model stores the fetched results controller it gets passed during Initialization, becomes its delegate to get informed about changes about the underlying data and starts querying Core Data:
private let fetchedResultsController: NSFetchedResultsController<ResultType>
init(fetchedResultsController: NSFetchedResultsController<ResultType>) {
self.fetchedResultsController = fetchedResultsController
// Should be called from subclasses of NSObject.
super.init()
// Configure the view model to receive updates from Core Data.
fetchedResultsController.delegate = self
try? fetchedResultsController.performFetch()
}
It defines a PassthroughSubject
“didChange” that doesn’t pass any specific data and never fails:
// MARK: BindableObject
var didChange = PassthroughSubject<Void, Never>()
It implements the controllerDidChangeContent
method of NSFetchedResultsControllerDelegate
and calls the sends updates via “didChange” every time the underlying data changes:
// MARK: NSFetchedResultsControllerDelegate
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
didChange.send()
}
And since the backing fetched results controller is private to the view model, a little helper that ensures the view always receives an array, never nil
:
var fetchedObjects: [ResultType] {
return fetchedResultsController.fetchedObjects ?? []
}
}
Now we can create an @ObjectBinding
to the view model and simply iterate over its fetchedObjects
property to e.g. create a list item for each object. SwiftUIs List
and ForEach
require a unique identifier when passing an arbitrary array, and luckily instances of NSManagedObject
already come with one called objectID
.
struct ComicsView: View {
@ObjectBinding var viewModel: FetchedObjectsViewModel<ComicEntity>
var body: some View {
List {
ForEach(viewModel.fetchedObjects.identified(by: \.objectID)) { comic in
ComicsItemView(viewModel: ManagedObjectViewModel(managedObject: comic))
}
}
}
}
That last snippet already contains a teaser for the next post, which will be about creating view models for single instances of managed objects.
♦︎
Making rounded corners and borders with rounded corners in SwiftUI is pretty simple:
struct RootView: View {
var body: some View {
Text("Lorem ipsum.")
.padding()
.background(Color.orange)
.cornerRadius(8)
.border(Color.black, width: 2, cornerRadius: 8)
}
}
But, if you are anything like me, you want the new “continuous” corner style introduced in UIKit, the “squircle” or “superellipse” one that Apple has been using for some time now. Fortunately this is easy, too.
The cornerRadius()
modifier is just a special case of the clipShape()
modifier with a RoundedRectangle
. Similarly, the border()
modifier can be recreated by using an overlay()
with a stroke()
. Once you’ve done that, you can change the style of the RoundedRectangle
to .continuous
:
struct RootView: View {
var body: some View {
Text("Lorem ipsum.")
.padding()
.background(Color.orange)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
.overlay(RoundedRectangle(cornerRadius: 8, style: .continuous)
.stroke(Color.black, lineWidth: 2)
)
}
}
And that’s it 🙌
♦︎
As Ole Begemann points out in a new blog post, the completion handler of Apple’s URLSession
has three parameters, all of which are optional:
class URLSession {
func dataTask(with url: URL,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)
-> URLSessionDataTask
}
This presents us with a problem, as it is not inherently clear how to interpret certain cases. What does it mean if you receive data, but also an error? What if you don’t receiver either?
URLSession
isn’t the only class that does this, it’s all over Foundation
and UIKit
. The underlying implementation of these frameworks is still Objective-C, and was designed around the languages weird way of dealing with errors. Functions take an NSError
pointer as an inout parameter, which can be checked after the function returns. By convention, these functions also return a boolean value, indicating whether the NSError
pointer needs to be checked. From Apple’s documentation:
NSError *anyError;
BOOL success = [receivedData writeToURL:someLocalFileURL
options:0
error:&anyError];
if (!success) {
NSLog(@"Write failed with error: %@", anyError);
// present error to user
}
To fix this, Ole proposes a Result type. This is a really nice and Swift-y solution and i suspect at some point it will come to the standard library. I still wanted to show what i’ve been doing since before Swift even was a thing (so it’s even compatible with Objective-C). For me, the easist solution often is to simply use separate success and failure handlers. For example, to request JSON data:
extension URLSession {
func jsonTask<T: Decodable>(with request: URLRequest,
successHandler: @escaping (T) -> Void,
failureHandler: @escaping (Error) -> Void)
-> URLSessionDataTask {
return dataTask(with: request) { (data, response, error) in
if let data = data,
let object = try? JSONDecoder().decode(T.self, from: data) {
successHandler(object)
return
}
failureHandler(error
?? NSError(domain: "defaultErrorDomain", code: -1, userInfo: nil))
}
}
}
You could even go crazy and add more callbacks, if you want to handle more cases separately.
♦︎