DEV Community

Daniel Tavares
Daniel Tavares

Posted on

Simpler way to deal with hardcoded ViewModifers values in SwiftUI

Alt Text

How often do you you have to deal with 2 values based on a condition in your SwiftUI views?

.background(isShowingSomething ? Color.red : Color.blue)
.font(isShowingSomething ? .title : .callout)
.padding(isShowingSomething ? 20 : 50)

This can become cumbersome specially if you have a lot of view modifiers that rely on said values.

struct NonToggledExampleView: View {
    @State var isShowingMenu = false

    var body: some View {
        ZStack {
            if isShowingMenu {
                Color.red.edgesIgnoringSafeArea(.all)
            } else {
                Color.blue.edgesIgnoringSafeArea(.all)
            }

            VStack {
                Text("Hello from Toggled")
                    .font(isShowingMenu ? .title : .callout)
                Button(action: {
                    isShowingMenu.toggle()
                }, label: {
                    Text("Toggle Me")
                })
            }.background(isShowingMenu ? Color.yellow : Color.green)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In order to update those values you will need to navigate your code, remember what they are and update accordingly. Resume preview and you are on your way.

One way of simplifying this is to extract those values into variables.

private var backgroundColorOn = Color.red
private var backgroundColorOff = Color.blue
Enter fullscreen mode Exit fullscreen mode

This will grow exponentially the more options you have.

Introducing Toggled

We can do better than this, so I've created Toggled. Toggled allows you to specify both values at initialization in order to make it easy to write/read/update your SwiftUI hardcoded values.

struct ToggledExampleView: View {
    @State var isShowingMenu = false
    @State var backgroundColor = Toggled<Color>(values: (on: .red, off: .blue))
    @State var font = Toggled<Font>(values: (on: .title, off: .callout))
    @State var stackBackgroundColor = Toggled<Color>(values: (on: .yellow, off: .green))
    @State var stackPadding = Toggled<CGFloat>(values: (on: 20, off: 0))

    var body: some View {
        ZStack {
            backgroundColor.value(state: isShowingMenu)
                .edgesIgnoringSafeArea(.all)
            VStack {
                Text("Hello from Toggled")
                    .fixedSize()
                    .font(font.value(state: isShowingMenu))
                Button(action: {
                    withAnimation {
                        isShowingMenu.toggle()
                    }
                }, label: {
                    Text("Toggle Me")
                })
            }
            .padding(stackPadding.value(state: isShowingMenu))
            .background(stackBackgroundColor.value(state: isShowingMenu))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Source Code

protocol ToggleInterface {
    associatedtype ValueType
    var values: (on: ValueType, off: ValueType) { get set }
    func value(state: Bool) -> ValueType
}

struct Toggled<T>: ToggleInterface {
    typealias ValueType = T
    var values: (on: T, off: T)
    func value(state: Bool) -> T {
        guard state else {
            return values.off
        }
        return values.on
    }
}
Enter fullscreen mode Exit fullscreen mode

Using toggled will eliminate the need for the ternary/if when you want to use the value, it makes it easier to reason with the options you have in your view and also you can make it dynamic by turning into a binding.

Futher improvements

We could take this a step further and create some sort of utility to dynamically change those values at run time.

DebugToggledViewRangeValue(title: "$stackPadding", toggled: $stackPadding, range: 0...100)

struct DebugToggledViewColor: View {
    var title: String
    var toggled: Binding<Toggled<Color>>

    var body: some View {
        VStack(spacing: 0) {
            Text(title)
         HStack {
            ColorPicker("On", selection: toggled.onValue)
            Spacer()
            ColorPicker("Off", selection: toggled.offValue)
        }
        }
    }
}

struct DebugToggledViewRangeValue<T>: View where T : BinaryFloatingPoint, T.Stride : BinaryFloatingPoint {
    var title: String
    var toggled: Binding<Toggled<T>>
    var range: ClosedRange<T>

    var body: some View {
        VStack(spacing: 0) {
            Text(title)
            HStack {
                VStack {
                    Text("On")
                    Slider(value: toggled.onValue, in: range)
                }
                VStack {
                    Text("Off")
                    Slider(value: toggled.offValue, in: range)
                }
            }
        }
    }
}

extension Binding where Value: ToggleInterface {
    var onValue: Binding<Value.ValueType> {
        return Binding<Value.ValueType> {
            wrappedValue.values.on
        } set: { newValue in
            wrappedValue.values.on = newValue
        }
    }

    var offValue: Binding<Value.ValueType> {
        return Binding<Value.ValueType> {
            wrappedValue.values.off
        } set: { newValue in
            wrappedValue.values.off = newValue
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Love to hear your thoughts and feedback.

GIST

Top comments (0)