magnuskahr

writing code

UnwrappingButton: Ensuring data in SwiftUI

One of the reasons why I love SwiftUI so much is the way that data is defined and how simple it is to pass data to any view. Comparing SwiftUI to UIKit regarding this, UIKit always felt hacky, being based on superclasses and inheritance.

With SwiftUI we define a struct and its properties. The compiler then offers us a perfectly valid initializer to create our view. Simple.

I always try to define my views and systems to only allow valid data, so I don't have to define UI for invalid or missing data. If possible, I never wanna inject invalid data into my system: Something that can be tricky when we allow users to pass in data. In this post I will describe how I typically handle this using "request"-structs whereafter I introduce the UnwrappingButton.

Handling user input

In SwiftUI we can have a simple form with a textfield and a button that when pressed, prints the value of the textfields.

struct InputView: View {
    @State private var title: String = ""
    var body: some View {
        Form {
            TextField("Title", value: $title)
            Button("Done") {
                print(title)
            }
        }
    }
}

However, say we need the title to be at least five characters long and can't contain numbers. This is a simple example, but I would typically handle this by creating a computed value:

var request: String? {
    guard title.count >= 5, !title.contains(where: \.isNumber) else {
        return nil
    }
    return title
}

And then in my button, work with the request instead:

Button("Done") {
    if let title = request {
        print(title)
    }
}

However, I would also like that my button only is enabled if the data is valid, so I add the .disabled(request == nil) modifier.

Now imagine we would need more than a simple string, what if the form had more textfields, pickers, and checkboxes?

Introducing request structs

Using computed properties to ensure the quality of our data is great I think. If it passes our requirements we return the data, or else we return nil. However, I propose that for multiple properties that need to pass each of their own requirements, that a Request struct is made. Say that we would need more than a title, but also a description and an age limit, we could create the following request:

struct CreateMovieRequest {
    let title: String
    let description: String
    let ageLimit: Int
}

Our computed property would now be changed to offer this request instead:

var request: CreateMovieRequest? {
    // Check title requirements
    // Check description requirements
    // Check age limit requirements
    return CreateMovieRequest(title: title, description: description, ageLimit: ageLimit)
}

Our button can now remain simple, and we can pass the request directly to our model:

Button("Done") {
    if let request = request {
        model.create(using: request)
    }
}
.disabled(request == nil)

This is not a complicated button to write, but it could be better:

  • What if we wouldn't need to handle the disabled state?
  • Unwrapping the request in the action is kind of ugly, what if we can ensure the availability of the request data?

Introducing the UnwrappingButton

The button we have had so far can be generalized pretty easy:

struct UnwrappingButton<Label: View, T>: View {

    let element: T?
    @ViewBuilder let label: Label
    let action: (T) -> Void
    
    var body: some View {
        Button {
            if let element = element {
                action(element)
            }
        } label: {
            label
        }
        .disabled(element == nil)
    }
}

And with this little extension easily have a Text label:

extension UnwrappingButton where Label == Text {
    init(element: T?, title: String, action: @escaping (T) -> Void) {
        self.element = element
        self.label = Text(title)
        self.action = action
    }
}

We can now replace our earlier button with the much simpler:

UnwrappingButton(element: request, title: "Done") { request in
    model.create(using: request)
}

That takes care of disabling the button whenever the request element is nil, and when it is not it serves us the unwrapped request in the handler for us to use when the button is tapped. A great thing about this is, that we rely on the built-in button, and still use button styles and such.

Unwrapping Bindings

Our UnwrappingButton works great, for most cases, but, I found that I needed a special case for Binding with an optional value, where I would like the button enabled when the wrapped value wasn't nil. To be specific about the situation, see the following:

struct SomeView: View {
    @Binding var value: Int?
    var body: some View {
        UnwrappingButton(element: $value, title: "Edit") { binding in
            // Here I need binding to be of type Binding<Int>
            // But this does not work atm.
        }
    }
}

To solve this issue, we will introduce a new enum:

extension UnwrappingButton {
    enum Unwrapping<T> {
        case element(value: T?, action: (T) -> Void)
        case binding(value: Binding<T?>, action: (Binding<T>) -> Void)
        
        var value: T? {
            switch self {
            case .element(let value, _):
                return value
            case .binding(let value, _):
                return value.wrappedValue
            }
        }
        
        func run() {
            switch self {
            case .element(let value, let action):
                if let value = value {
                    action(value)
                }
            case .binding(let value, let action):
                if let value = Binding(value) {
                    action(value)
                }
            }
        }
    }
}

It is a simple generic enum. It has two cases: One that takes an optional element and an action, and one that takes a binding with an optional element, and an action. It then has a computed property, that simply returns the element if present, and a function that unpacks said given value to feed and run the associated action.

With this, we can change the implementation of our button, to use Unwrapping as storage:

struct UnwrappingButton<Label: View, T>: View {
    
    let unwrapping: Unwrapping<T>
    @ViewBuilder let label: Label
    
    init(element: T?, action: @escaping (T) -> Void, @ViewBuilder label: () -> Label) {
        self.unwrapping = .element(value: element, action: action)
        self.label = label()
    }
    
    init(element: Binding<T?>, action: @escaping (Binding<T>) -> Void, @ViewBuilder label: () -> Label) {
        self.unwrapping = .binding(value: element, action: action)
        self.label = label()
    }
    
    var body: some View {
        Button {
            unwrapping.run()
        } label: {
            label
        }
        .disabled(unwrapping.value == nil)
    }
}

Along with our extension on Text-initialisers:

extension UnwrappingButton where Label == Text {
    init(element: T?, title: String, action: @escaping (T) -> Void) {
        self.unwrapping = .element(value: element, action: action)
        self.label = Text(title)
    }
    
    init(element: Binding<T?>, title: String, action: @escaping (Binding<T>) -> Void) {
        self.unwrapping = .binding(value: element, action: action)
        self.label = Text(title)
    }
}

And suddenly, we can have buttons that unwrap and feed the associated action the value, also if it comes from a binding.

Conclusion

I have gone through my steps of handling user-input:

  1. Ensure the data is valid, and offer it through a request-struct.
  2. Use the UnwrappingButton to simplify the guarantee of data.

To ensure the validity of data, there are many techniques we could use, and I just wanna mention the ability that the request-struct could handle the complexity and utilize the type system to manage much of the validation. And for the UnwrappingButton, I also just wanna note how I enjoy the user experience of it: The button is disabled as long as the input data is invalid - and that, I think is great.

You can find the full code of the unwrapping button here.