SwiftUI Bindings with CoreData

If you’ve been playing with SwiftUI for a while, you’re likely familiar with the liberal use of @State and @Binding throughout the library. For instance, consider the following simple to-do item editor:

enum Priority: Int, CaseIterable { 
    case low, normal, high, urgent
    
    var title: LocalizedStringKey {
        switch self {
        case .low: return "Low"
        case .normal: return "Normal"
        case .high: return "High"
        case .urgent: return "Urgent"
        }
    }
}

struct Item: Identifiable {
    var id: UUID
    var title: String
    var priority: Priority = .normal
    var dueDate: Date = .distantPast
    var notes: String = ""
}

struct Editor: View {
    @State var item: Item
    
    var body: some View {
        Form {
            TextField("Title", text: $item.title)
            Picker("Priority", selection: $item.priority) {
                ForEach(Priority.allCases, id: \.self) { priority in
                    Text(priority.title).tag(priority)
                }
            }
            DatePicker("Due Date", selection: $item.dueDate,
                       displayedComponents: .date)
            TextField("Notes", text: $item.notes)
        }
    }
}

There’s a single @State property containing a struct type, and this happily hands out the bindings required by TextField, Picker, and DatePicker. However, if the properties in the Item type were optional, the compiler wouldn’t be so happy:

@State var title: String?
...
// Error: "Cannot convert value of type 'Binding<String?>' to expected
// argument type 'Binding<String>'"
TextField("Title", text: $title)

This is actually what you’ll find if you try to use a CoreData managed object type, because all its object-type properties are nullable:

class Item: NSManagedObject {
    @NSManaged var title: String?
    @NSManaged var priority: Int
    @NSManaged var dueDate: Date?
    @NSManaged var notes: String?
    
    var priorityEnum: Priority {
        get { Priority(rawValue: priority) ?? .normal }
        set { priority = newValue.rawValue }
    }
}

struct Editor: View {
    @ObservedObject var item: Item
    
    var body: some View {
        Form {
            // Errors everywhere!
            TextField("Title", text: $item.title)
            Picker("Priority", selection: $item.priority) {
                ForEach(Priority.allCases, id: \.self) { priority in
                    Text(priority.title).tag(priority)
                }
            }
            DatePicker("Due Date", selection: $item.dueDate,
                       displayedComponents: .date)
            TextField("Notes", text: $item.notes)
        }
    }
}

Unwrapping optional bindings

What are we to do with this? Well, there’s an initializer on Binding that looks like it’ll help—it takes a Binding<Optional<Value>> and returns a Binding<Value>.

/// Creates an instance by projecting the base optional value to its
/// unwrapped value, or returns `nil` if the base value is `nil`.
public init?(_ base: Binding<Value?>)

Unfortunately, as the comment says, it only works if the value isn’t currently nil. So, you need to assign a default there first in order to use this. It’ll work, certainly, but quickly becomes awkward, not least because you probably want to use your binding within an @ViewBuilder block, and you can’t put general code in those—the compiler complains, because it needs every statement to evaluate with a return type expected by the @ViewBuilder in use.

@State var title: String? = nil
var body: some View {
    Form {
        if title == nil {
            // Error: "'()' is not convertible to 'String?'"
            title = ""
        }
        TextField("Title", text: Binding($title)!)
    }
}

Now, this works if you lift the setter up out of the Form block, but that becomes unwieldy with more than a couple of values:

@State var title: String? = nil
@State var notes: String? = nil
// etc...
var body: some View {
    if title == nil {
        title = ""
    }
    if notes == nil {
        notes = ""
    }
    // etc...
    Form {
        TextField("Title", text: Binding($title)!)
        TextField("Notes", text: Binding($notes)!)
        // etc...
    }
}

You’re looking at a minimum four lines of code for each additional optional property. There’s a better way, though: you can add a new initializer to Binding that’s similar to the existing unwrapping one, but which takes a default value to assign first so it will guarantee a non-nil result:

extension Binding {
    init(_ source: Binding<Value?>, _ defaultValue: Value) {
        // Ensure a non-nil value in `source`.
        if source.wrappedValue == nil {
            source.wrappedValue = defaultValue
        }
        // Unsafe unwrap because *we* know it's non-nil now.
        self.init(source)!
    }
}

This initializer thus inlines the if value == nil rule above, leading to cleaner code and a one-line initialization:

@State var title: String? = nil
@State var notes: String? = nil
var body: some View {
    Form {
        TextField("Title", text: Binding($title, ""))
        TextField("Notes", text: Binding($notes, ""))
    }
}

Assigning nil or non-nil to a non-optional binding

A further situation occurs, though. What if nil is actually a reasonable value? Perhaps you would rather have a nil value than an empty string. Which of these looks better?

if !item.title.isEmpty {
    // use item.title
}

// or

if let title = item.title {
    // use title
}

The answer is entirely subjective, of course, but if let is generally considered a “swifty” way to do things.

So, now you have a binding which will ensure nil is translated into an ‘empty’ value of some kind, but you’d really like to take any empty value and represent it as nil while still holding a Binding<Value> rather than Binding<Value?>. This, it turns out, is also possible thanks to the regular initializer for Binding, which uses closures to get and set the underlying value:

/// Initializes from functions to read and write the value.
public init(get: @escaping () -> Value, set: @escaping (Value) -> Void)

As long as the underlying Value is Equatable, it’s quite easy to use those to implement the replace-nil functionality we need:

init(_ source: Binding<Value?>, replacingNilWith nilValue: Value) {
    self.init(
        get: { source.wrappedValue ?? nilValue },
        set: { newValue in
            if newValue == nilValue {
                source.wrappedValue = nil
            }
            else {
                source.wrappedValue = newValue
            }
    })
}

Here the source binding is held by the two closures. The getter just uses the ?? operator to return the specified ‘nil value’ if necessary, and the setter explicitly checks if it’s been given that nil value, and transparently assigns nil to the source binding if so. This works well for CoreData types (avoid allocating byte storage if a ‘nil’ marker will do), and also for truly optional items. An example would be the ‘notes’ in our to-do item. A title is expected, but notes may be present on quite a small number of items, so this is what we’d use in that case:

Form {
    // Items should have titles, so use a 'default value' binding
    TextField("Title", text: Binding($item.title, "New Item"))
    
    // Notes are entirely optional, so use a 'replace nil' binding
    TextField("Notes", text: Binding($item.notes, replacingNilWith: ""))
}

Now your title will start out automatically with a value of "New Item", and if everything is deleted from the notes field that property will be set to nil.

Binding nil/non-nil status directly

The ‘due date’ in our to-do item is another item which is legitimately nil when not used—not all to-do items have an explicit deadline—but there’s no reasonable ‘empty’ value for a date. We might use the epoch, or Date.distantPast, but the user has no easy way of inputting exactly that value to say “don’t use a date.” Instead you’d likely give your user a Toggle that turns the date on or off: when they turn it on, a default value is applied and a Date Picker created. Turning it off would take away the picker and set the value to nil.

This is also quite easy to assemble:

init<T>(isNotNil source: Binding<T?>, defaultValue: T) where Value == Bool {
    self.init(get: { source.wrappedValue != nil },
              set: { source.wrappedValue = $0 ? defaultValue : nil })
}

This creates a Binding<Bool> from a Binding<T?> and a default value for T. The getter returns whether the Binding<T?> holds nil or not, and the setter assigns either nil or the supplied default value. This is quite simple to use, too:

Toggle("Has Due Date",
       isOn: Binding(isNotNil: $item.dueDate, defaultValue: Date()))
if item.dueDate != nil {
    DatePicker(DatePicker("Due Date", selection: Binding($item.dueDate)!,
                       displayedComponents: .date))
}

Here you’ve used the Binding(isNotNil:defaultValue:) for the Toggle, and a regular unwrapping binding for the DatePicker.

All together now…

This leads to a CoreData-backed form binding to optional properties that looks almost as concise as the pure struct format:

enum Priority: Int, CaseIterable { 
    case low, normal, high, urgent
    
    var title: LocalizedStringKey {
        switch self {
        case .low: return "Low"
        case .normal: return "Normal"
        case .high: return "High"
        case .urgent: return "Urgent"
        }
    }
}

class Item: NSManagedObject {
    @NSManaged var title: String?
    @NSManaged var priority: Int
    @NSManaged var dueDate: Date?
    @NSManaged var notes: String?
    
    var priorityEnum: Priority {
        get { Priority(rawValue: priority) ?? .normal }
        set { priority = newValue.rawValue }
    }
}

struct Editor: View {
    @ObservedObject var item: Item
    
    var body: some View {
        Form {
            TextField("Title", text: Binding($item.title, "New Item"))
            
            Picker("Priority", selection: Binding($item.priority, .normal)) {
                ForEach(Priority.allCases, id: \.self) { priority in
                    Text(priority.title).tag(priority)
                }
            }
            
            Toggle("Has Due Date", isOn: Binding(isNotNil: $item.dueDate, 
                                                 defaultValue: Date()))
            if item.date != nil {
                DatePicker("Due Date", selection: Binding($item.dueDate!),
                           displayedComponents: .date)
            }
            
            TextField("Notes", text: Binding($item.notes, replacingNilWith: ""))
        }
    }
}

Available Now!

These extensions to Binding are available now via the Swift Package Manager. It uses Swift v5.1 and tags its contents with the appropriate availability modifiers for macOS, iOS, tvOS, and watchOS. Including it is simple:

let package = Package(
    ...
    dependencies: [
        ...
        .package(url: "https://github.com/AlanQuatermain/AQUI.git", from: "0.1.0")
    ],
    swiftLanguageVersions: [
        ...
        .version("5.1")
    ]
)

Using it in code is straightforward too:

import SwiftUI
import AQUI

...
    TextField("Notes", text: Binding($cdObject.notes, replacingNilWith: ""))
...

© 2009-2019. All rights reserved.

Powered by Hydejack v9.1.6