Oscar
Oscar

Reputation: 2335

How can we have "one source of truth" when SwiftUI doesn't support optionals?

I need to display and allow the editing of a class whose members are mostly optional. This does not appear to be a scenario that SwiftUI envisions, despite Apple propagating the "single source of truth" mantra in its SwiftUI tutorials. What is the expected approach?

Take this class, for example, which represents a user:

class User : Equatable, Codable, ObservableObject
{
    var ID: String
    var username:  String
    var firstName: String?
    var lastName: String?
    var EMail: String?
    var phoneNbr: String?
    var avatarURL: String?
    var mediaServiceID: String?
    var validated: Bool = false
...
}

I can't directly show a form to fill this thing out in SwiftUI, because you can't bind text fields to optionals. Most of the published workarounds to that involve using let to create a non-optional variable if the member isn't nil; but this is unworkable because that won't populate a nil member if someone enters text in the text field. Apple docs talk about custom binding, which would probably work to populate an optional member. That means doing this in the SwiftUI view to set the object's username, for example:

@State private var      tempUser = User()

private var username: Binding<String>
{
    Binding { tempUser.username ?? "" }
    set: { newName in
        tempUser.username = newName
    }
}

But that means setting up one of these verbose methods for every single optional member of every class I want to show in a UI. At that point I might as well just make a shadow structure that's all non-optionals that I can bind directly to. Or just make all the members non-optional and just face the fact that optionals are done with in the age of SwiftUI.

Or is there some succinct approach I'm missing here? Thanks for any insight.

Upvotes: 1

Views: 324

Answers (2)

meomeomeo
meomeomeo

Reputation: 942

TextField Picker Toggle... doesn't support optionals by default. Apple has good reasons for this design choice: you cannot set a value back to nil without conflicting with the default value.

For example, to support an Optional String, a quick hack by getting default value on nil is good enough in most case. But it never be fit completely.

To fully support an Optional String, you need to define how to handle a nil value, which can make your TextField appear awkward.

import SwiftUI

struct DisplayView: View {
    @State var user: User = .init(id: "1", name: "John")
    
    var body: some View {
        Form {
            Section {
                EditorView(user: $user)
            } header: {
                HStack {
                    Text("\(user.id). \(user.name)")
                    Spacer()
                    if let note = user.description {
                        Text(note)
                    } else {
                        Image(systemName: "person.fill.questionmark")
                    }
                    
                }
            }
        }
    }
}

struct EditorView: View {
    @Binding var user: User
    
    var body: some View {
        TextField("user name", text: $user.name)
        OptionalStringEditor(optionalText: $user.description, title: "note")
    }
}

struct OptionalStringEditor: View {
    @Binding var optionalText: String?
    var title: String
    var defaultValue: String = ""
    
    private var text: Binding<String> {
        Binding {
            optionalText ?? defaultValue
        } set: {
            optionalText = $0
        }
    }
    
    var body: some View {
        if optionalText != nil {
            HStack {
                TextField(title, text: text)
                Button("remove \(title)") { optionalText = nil }
            }
        } else {
            Button("add \(title)") { optionalText = defaultValue }
        }
    }
}

struct User {
    let id: String
    var name: String
    var description: String?
}

#Preview { DisplayView() }

In the end, there are only two choices: an awkward UI or a half-way binding. Apple's response to this dilemma is simply NO.

There are many features currently missing from SwiftUI, and while I believe many will be added in the future, an Optional TextField is likely not among them.

Claiming that SwiftUI does not support a feature without suggesting a solution might indicate a lack of understanding of how it should be implemented. While it's easy to criticize the framework, focusing on solutions rather than blaming could have a more positive impact on your coding skills.

Upvotes: -1

Rob Napier
Rob Napier

Reputation: 299355

I need to display and allow the editing of a class whose members are mostly optional.

That has always been a bad design, not just SwiftUI. In what way is nil different from empty? From your binding, you're indicating they're the same. If that's the case, then it should never have been Optional in the first place. It should just have been a normal String and made empty.

You're correct that SwiftUI can make this kind of bad design even harder than it's been in the past, but it was always hard. Redesign your type to only use Optionals for things where nil has a distinct meaning.

Of particular interest is that ID is optional. How can this type work with no ID?

To the headline question of whether Optionals are over, that absolutely is not the case. Optionals are as much a critical part of Swift as they've ever been. But representing "empty" as two different values (an empty thing like "", and also nil) is not something SwiftUI (or Swift) encourages. It will tend to be a pain.

That said, macros and property wrappers can write the code for you if you need it. I recommend fixing your data before going down that path, however.

Upvotes: 0

Related Questions