Update: This was all about Xcode 12 beta 1. There is probably nothing still relevant in beta 5 which includes better built in Core dat support including ways to directly bind on objects and even fetch requests so NSFetchedResultsControllers should become unnecessary. Will try to post again when I have explored further.
This might be a bit of a ramble, it certainly isn’t a carefully designed tutorial but will be a mixture of a few general early thoughts and a few minor gotchas I’ve already hit in my experimentation. I’m still in the stumbling and exploring phase and certainly haven’t mastered SwiftUI or Combine to any great degree yet. This may mean it will be helpful to few people, it is probably too heavy on the jargon if you haven’t already had a good look at SwiftUI and is too light on detail and probably inaccurate if you have gone deeply in. Certainly the shelf life of this post feels short. Feel free to let me know anywhere that I am wrong, or have missed the best documentation or give any feedback either by Twitter or in the comments.
I’m less comfortable with SwiftUI than I was with Swift itself at the similar stage 5 years ago. I believe this is going to be great but there is a mental shift required which I haven’t yet achieved.
I’m in the early stages of rewriting my first ever app (Fast Lists) to create a new version with all the toys from WWDC 2019. Including but not limited to Cloudkit Core Data syncing, SwiftUI interface, support for Mac and Watch (not just iOS as before). Multi window support. I’m also hoping to add some drag and drop support later but I haven’t even started looking at that yet. Because I’m trying everything at once my progress is a bit stop start and random at the moment but it does mean that there is normally another piece to work on if I get stuck.
This is all based on experience with Version 11.0 beta (11M336w) (the first beta) so many of these issues may be resolved very soon. In a few places I note the feedback numbers that I have raised around things that are either bugs or potential areas for improvement.
Core Data seems a conceptual match for SwiftUI
While it might seem odd that the ball of dynamically typed global state might fit into the shiny new world of immutable interfaces on the face of it they are a good match precisely for that reason. The Core Data store is the single source of truth to be applied to the interfaces which can respond to changes on their updates. In practice it at least needs a bit of extra tooling around it.
Make your publisher send an initial value
Initially when I wrote my FetchedResultsControllerPublisher (see below - name may need revising as it is not a publisher although it provides publishers) I made the mistake of not calling send immediately after calling performFetch (or when instantiated if the performFetch is not required).
You may find this useful at least as inspiration:
Don’t Lose your Publisher
It seems when you set an @EnvironmentObject that the didChange publisher is accessed from the bindable object but it is not strongly retained. This means if you have a computed var for the didChange (possibly it just modifies another existing publisher by applying an operation to it or even just erasure to AnyPublisher) then it will be lost and updates will not be received by the UI. There may be good reasons why it shouldn’t be held strongly (I haven’t fully thought through the ownership model for Combine) but it certainly is an easy way to break things and is worth knowing about. — FB6143719
EnvironmentObject, State and let
I thought Environment Object was just a way of passing in information to SwiftUI views that was available through the hierarchy but it actually seems to be the essential way to get BindableObjects (externally changing objects generally) into the Swift UI system. I still don’t have my head fully round it but it seems that there are essentially three categories for the data in SwiftUI.
@EnvironmentObject - Externally varying, must be a bindable object.
@State - Locally varying owned by the view not attached to external data.
Constants - Data passed in as arguments that won’t further change after view creation (or a copy not to be updated unless the view itself is going to be replaced.)
Based on this understanding using EnvironmentObjects is the only way to pass in dynamic lists of content such as providers of data from NSFetchedResultsControllers and associated publishers.
Making NSManagedObjects Bindable
I went through a period trying to hide the managed object classes themselves behind protocols and it wasn’t working. When everything else is working I’ll go back to trying to do that, it may have been other issues rather than the protocols that were causing the problems. Assuming you plan to do as I am now doing and directly exposing the the managed objects you will probably need to make them BindableObjects.
I think that this is all that is required (unless there is state that isn’t in the core data model):
Now it is possible that I’ve missed something and it is a little more complex but for the moment this seems to be working. I think it is probably possible to apply this as an extension on NSManagedObject itself if you can use some Objective C hackery to store the publisher or make the necessary changes for it to be the publisher itself rather than leaning on the Passthrough publisher. I don’t think it should be too hard but I haven’t done it yet. It wouldn’t surprise me if Apple made every managed object conform to BindableObject.
Multi Scene Code
Beware the Xcode template do the willConnectTo method wrong and don’t use the provided UIWindowScene to make the window to use. They work until you enable multi scene in the Info.plist. The video shows how to do it right. It will end up looking something like this:
CloudKit Core Data
Note the videos or docs with instructions for how to apply remote content updates locally. It isn’t just a matter of creating an NSPersistentCloudKitContainer and then everything being complete magic. I haven’t done the required work yet and the results are disappointing. Will get to this soon but I needed some working UI first.
TextField Doesn’t Wrap
I think this is a first Xcode beta bug. I’m sure it will be fixed soon but wanted to flag in case it saves anyone time trying to battle with it. Better to wait until it is fixed for now than hack around it. (FB6136862)
The thing that I currently can’t see a good way to do is to use bindings to keep the UI up to date but to be able to provide validation on edits to the bound data. For example if I bind the text field to the property in the Core Data model I want it as a binding so that it gets updated when the data in the store changes but when a user starts typing into it I would prefer it was then linked to a local @State var until the onCommit gets fired at the end of editing when validation can be applied before it is written back into the database. I don’t currently see a mechanism for that unless I can use the validation in the model itself only. This does look possible but only allows validation based on the particular field without reference to any context. Should be good enough for my needs for now to prevent empty Strings being saved.
What would be nice is if the binding could allow customisation via inserting operations in the return path from the UI to the stored object that could alter the content or block the transmission. This is an idea that I haven’t yet thought through but wonder if it might plant seeds with anyone. I’ll think further as I learn more.
TextField Should have Optional String Binding init
It should be easy to bind a TextField to an optional string, that would make interfacing with Core Data easier. I worked around this by binding to a computed var that did the unwrapping on the getter to provide an empty string if the data in the store is nil but this shouldn't be necessary. Especially using CloudKit everything needs to be Optional I believe in the Core Data store.— FB6140122
I have a lot to learn. There are some rough edges (to be expected with a beta).
Let me know what you think.