Integrating NSFetchedResultsController with SwiftUI

25 Jun 2019

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:

  • The view model is a generic type with a type parameter 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.
  • The view model is a subclass of 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.
  • Lastly the view model implements the 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.

♦︎

Continuous corners in SwiftUI

23 Jun 2019

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 🙌

♦︎

Schroedinger's completion handler

26 Mar 2018

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?

Error handling in Objective-C

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
}

Possible solutions

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.

♦︎