Unwrapping optional in SwiftUI View

Swift 5.3 - Xcode 12

Using conditional binding in a ViewBuilder is perfectly fine now:

HStack {
    if let height = profile.height { // <- This works now in Xcode 12
        TagBox(field: "height", value: String(height))
    }
    TagBox(field: "nationality", value: profile.nationality)
    Spacer()
}.padding(.horizontal)

So if the force unwrap makes you feel uneasy you can use the following custom view I made up:

struct OptionalView<Value, Content>: View where Content: View {
    private var content: Content

    init?(_ value: Value?, @ViewBuilder content: @escaping (Value) -> Content) {
        guard let value = value else { return nil }
        self.content = content(value)
    }

    var body: some View {
        content
    }
}

And use it like this:

OptionalView(profile.height) { height in
    TagBox(field: "height", value: String(height))
}

I wrote a post about this and making a chain of optional views here


There are two ways to work with optionals in this context:

The first one, if you don't want this view to show at all if profile.height is nil:

profile.height.map({ TagBox(field: "height", value: String($0))})

The second one, if you want this view to show, but with a default value instead:

TagBox(field: "height", value: String(profile.height ?? 0))

You can have if statements inside of a Group's view builder. I made an example to illustrate:

var label1 = "label 1"
@State var label2: String?
var label3: String? = "label 3"

var body: some View {
    VStack {
        Text(label1)
        Group {
            if label2 != nil {
                Text(label2!)
            }
            if label3 != nil {
                Text(label3!)
            }
        }
        Button(action: {
            self.label2 = "Button pressed!"
        }) {
            Text("Press me!")
        }
    }
}

Of course, in this example, you could just do Text(label2 ?? ""), but there would be a gap where the Text is until label2 has a value, and also, in more complex situations, this allows for more flexibility. If you run that code, you will see "Label 1" directly above "Label 3", and if you press the button, "Button pressed!" will appear in between "Label 1" and "Label 3".

Basically, if label2 is nil, there won't be a blank space where the label will be when it has a value, instead there will be no gap, and when label2 is assigned a value, everything above and below where it should be will shift to accommodate for it.

In the code you provided, you can change it to:

HStack {
    Group {
        if profile.height != nil {
            TagBox(field: "height", value: String(profile.height!))
        }
    }
    TagBox(field: "nationality", value: profile.nationality)
    Spacer()
}.padding(.horizontal)

The nice thing about this solution is that you can do more complex things in the scenario that the value is nil (or not), and even provide a default view in case the value is nil (In my example, this would be the equivalent of adding an else for either if statement with a Button, Text, etc. inside of the else's body).

One drawback is that you have to explicitly unwrap the optionals, which shouldn't be a problem because the if statement takes care of making sure it is not nil, but I still find that optional binding is generally a cleaner route for unwrapping optionals.

Edit:

If you really don't want to explicitly unwrap optionals, you can use ViewBuilders:

var label1 = "label 1"
@State var label2: String?
var label3: String? = "label 3"
var body: some View {
    VStack {
        Text(label1)
        Group {
            self.conditionalText(self.label2)
            if label3 != nil {
                Text(label3!)
            }
        }
        Button(action: {
            self.label2 = "Button pressed!"
        }) {
            Text("Press me!")
        }
    }
}
func conditionalText(_ text: String?) -> some View {
    if let text = text {
        return ViewBuilder.buildIf(Text(text))
    } else {
        print("Text is nil")
    }
    let view: Text? = nil
    return ViewBuilder.buildIf(view)
}

This method also highlights how you can have executable code inside of a Group as long as it is inside of a function that returns a view.

Tags:

Swift

Swiftui