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)
}
}
}
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
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))
}
}
}
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
}
}
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
}
}
}
Love to hear your thoughts and feedback.
Top comments (0)