DEV Community

Cover image for Easy SwiftUI View bindings Using OptionSet and Sourcery
Daniel Tavares
Daniel Tavares

Posted on

Easy SwiftUI View bindings Using OptionSet and Sourcery

Photo by Alexander Sinn

Have you ever had a SwiftUI view with lots of toggles?

struct LotsOfStateView: View {
    //1
    @State var showBackground = false
    //2
    @State var showBorder = false
    //3
    @State var reduceMotionEnabled = false
    //4
    @State var largeFont = false
    //5
    @State var receiveEmailNotifications = false

    var body: some View {
        VStack {
            Toggle(isOn: $showBackground) {
                Text(LocalizedStringKey("Show Background"))
            }
            Toggle(isOn: $showBorder) {
                Text(LocalizedStringKey("Show Border"))
            }
            Toggle(isOn: $reduceMotionEnabled) {
                Text(LocalizedStringKey("Reduce Motion Enabled"))
            }
            Toggle(isOn: $largeFont) {
                Text(LocalizedStringKey("Large Font"))
            }
            Toggle(isOn: $receiveEmailNotifications) {
                Text(LocalizedStringKey("Receive Notifications"))
            }
        }
        .padding()
        .font(.system(size: 22, weight: .light, design: .rounded))
    }
}
Enter fullscreen mode Exit fullscreen mode

Dealing with those toggles is pretty straight forward task, but it can become cumbersome the more you add.

A technique of reducing the amount of Bool values is to compact them into one storage unit. We can essentially convert them into a series of zeros and ones. Sounds familiar?

There are only 10 types of people in the world – those who understand binary, and those who don’t.

So if we were to convert the @State above to something more manageable we could use Swift's OptionSet. They are very similar to enums.

I'm sure you have set a reminder before. If you set the frequency of the reminder (Monday, Wednesday and Saturday) for example. You are essentially creating a set of 3 values.

This basic idea of being able to create a combination of desired values is perfect to remove the amount of boilerplate state we have in your view.

If we were to convert the above @State into an OptionSet we could do this:

struct Options: OptionSet {
    var rawValue: UInt
    static let none = Self([])
    static let showBackground = Self(rawValue: 1 << 0)
    static let showBorder = Self(rawValue: 1 << 1)
    static let reduceMotionEnabled = Self(rawValue: 1 << 2)
    static let largeFont = Self(rawValue: 1 << 3)
    static let receiveEmailNotifications = Self(rawValue: 1 << 4)
}
Enter fullscreen mode Exit fullscreen mode

Then in our views we could remove all the @State in favour of one.

@State var viewOptions = Options.none
Enter fullscreen mode Exit fullscreen mode

If you look at the rawValue of the OptionSet, it's an UInt. You are essentially storing the union of values into 1 single storage value. (Series of 101001, on/off)

This is what your view would look like.

struct LessStateVariablesView: View {
    @State var viewOptions = Options.none

    var body: some View {
        ZStack {
            if $viewOptions.bindValue(.showBackground) {
                Color.red.edgesIgnoringSafeArea(.all)
            }
            VStack {
                Toggle(isOn: $viewOptions.bind(.showBackground)) {
                    Text(LocalizedStringKey("Show Background"))
                }
                Toggle(isOn: $viewOptions.bind(.showBorder)) {
                    Text(LocalizedStringKey("Show Border"))
                }
                Toggle(isOn: $viewOptions.bind(.reduceMotionEnabled)) {
                    Text(LocalizedStringKey("Reduce Motion Enabled"))
                }
                Toggle(isOn: $viewOptions.bind(.largeFont)) {
                    Text(LocalizedStringKey("Large Font"))
                }
                Toggle(isOn: $viewOptions.bind(.receiveEmailNotifications)) {
                    Text(LocalizedStringKey("Receive Notifications"))
                }
            }
            .padding()
            .font(.system(size: 22, weight: .light, design: .rounded))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we've shifted all the previous @State and encapsulated into an OptionSet. The behaviour is exactly the same but instead of having 5 bools, we have 1 and all the true and false is stored as 1 single UInt.

Converting an OptionSet into Binding

Now that we have our options, we need a way of interacting with them in our SwiftUI View. If you wanted to use a toggle for example we need a Binding.

A property wrapper type that can read and write a value owned by a source of truth.

Binding is a struct with a Generic value Value, so we could write an extension in order to interact with our new OptionSet.

extension Binding where Value: OptionSet, Value == Value.Element {
    func bindedValue(_ options: Value) -> Bool {
        return wrappedValue.contains(options)
    }

    func bind(
        _ options: Value,
        animate: Bool = false
    ) -> Binding<Bool> {
        return .init { () -> Bool in
            self.wrappedValue.contains(options)
        } set: { newValue in
            let body = {
                if newValue {
                    self.wrappedValue.insert(options)
                } else {
                    self.wrappedValue.remove(options)
                }
            }
            guard animate else {
                body()
                return
            }
            withAnimation {
                body()
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

By doing so we can now plug it in our SwiftUI view.

We have now access to bindedValue to pull out its Bool representation and also bind where you can pass to the Toggle to update its value. There is also a convenient animate option in order to animate its changes.

Removing Boiler Plate (Sourcery to the rescue)

Writing an OptionSet is fairly trivial, but I often forget the syntax and the correct rawValue needed for each item.

I have created an Stencil template file which can be used with Sourcery in order to make creating those OptionSet very simple and easy.

Sourcery is a code generator for Swift language, built on top of Apple's own SwiftSyntax. It extends the language abstractions to allow you to generate boilerplate code automatically.
Sourcery

What we can do now is to create an enum with your cases and make sure you conform to OptionsBinding.

protocol OptionsBinding {}

enum MyEnum: OptionsBinding {
    case showBackground
    case showBorder
    case reduceMotionEnabled
    case largeFont
    case receiveEmailNotifications
}
Enter fullscreen mode Exit fullscreen mode

Sourcery will then extend that enum and add the equivalent OptionSet for you.

extension MyEnum {
    struct Options: OptionSet {
        var rawValue: UInt
        static let none = Self([])
        static let showBackground = Self(rawValue: 1 << 0)
        static let showBorder = Self(rawValue: 1 << 1)
        static let reduceMotionEnabled = Self(rawValue: 1 << 2)
        static let largeFont = Self(rawValue: 1 << 3)
        static let receiveEmailNotifications = Self(rawValue: 1 << 4)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now you are free to use MyEnum.Options wherever you need.

I hope this will help some folks to reduce some of the Bool boilerplate in your views and also make it easy to reason with your options by having a nice enum to use.

Stencil template

{% for type in types.enums where type.cases.count > 0 and type.based.OptionsBinding or type|annotated:"OptionsBinding" %}
extension {{ type.name }} {
    struct Options: OptionSet {
        var rawValue: UInt
        static let none = Self([])
        {% for i in 0...type.cases.count %}
            {% if not forloop.last %}
        static let {{type.cases[i].name}} = Self(rawValue: 1 << {{i}})
      {% endif %}
    {% endfor %}
    }
}
{% endfor %}

extension Binding where Value: OptionSet, Value == Value.Element {
    func bindValue(_ options: Value) -> Bool {
        return wrappedValue.contains(options)
    }

    func bind(
        _ options: Value,
        animate: Bool = false
    ) -> Binding<Bool> {
        return .init { () -> Bool in
            self.wrappedValue.contains(options)
        } set: { newValue in
            let body = {
                if newValue {
                    self.wrappedValue.insert(options)
                } else {
                    self.wrappedValue.remove(options)
                }
            }
            guard animate else {
                body()
                return
            }
            withAnimation {
                body()
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

References / Useful Links

Source Code
Custom OptionSet
OptionSet
Sourcery

Discussion (0)