I'm looking for a clean solution to resolve this SwiftUI challenge.
The following code compiles but do not work since @State
property is outside the ContentView
scope.
import SwiftUI
struct ContentView: View {
var state: LocalState?
var body: some View {
if let state = state {
Toggle("Toggle", isOn: state.$isOn)
}
}
}
extension ContentView {
struct LocalState {
@State var isOn: Bool
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
VStack {
ContentView(
state: .init(isOn: false)
)
.border(Color.red)
ContentView()
.border(Color.red)
}
}
}
The following code doesn't compile since the following reasons:
Value of optional type 'ContentView.LocalState?' must be unwrapped to refer to member 'isOn' of wrapped base type 'ContentView.LocalState'
It seems that $
in $state.isOn
refer to the original state
and not to the unwrapped one.
import SwiftUI
struct ContentView: View {
@State var state: LocalState!
var body: some View {
if let state = state {
Toggle("Toggle", isOn: $state.isOn)
}
}
}
extension ContentView {
struct LocalState {
var isOn: Bool
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
VStack {
ContentView(
state: .init(isOn: false)
)
.border(Color.red)
ContentView()
.border(Color.red)
}
}
}
What I do NOT want is:
- use of failable initializer in ContentView.
- move
isOn
property outsideLocalState
.
How can I achieve those?
CodePudding user response:
This works for me:
var body: some View {
if let isOn = Binding($state)?.isOn {
Toggle("Toggle", isOn: isOn)
}
}
Breaking it down: $state
is a Binding<LocalState?>
, and we use the Binding
initialiser (hopefully that's not the failable initialiser that you don't want to use) to convert it to a Binding<LocalState>?
. Then we can use optional chaining and if let
to get a Binding<Bool>
out of it.
Related: How can I unwrap an optional value inside a binding in Swift?
CodePudding user response:
I believe this can be solved with two techniques. 1. using the Binding constructor that can create a non-optional binding from an optional. And 2. use of a constant binding in previews, e.g.
import SwiftUI
struct Config {
var isOn: Bool
}
struct ContentView: View {
@State var config: Config?
var body: some View {
if let config = Binding($config) { // technique 1
ContentView2(config: config)
}
}
}
struct ContentView2: View {
@Binding var config: Config
var body: some View {
Toggle("Toggle", isOn: $config.isOn)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView2(config: .constant(Config(isOn: false))) // technique 2
}
}
CodePudding user response:
$state
is syntactic sugar for _state.projectedValue
, which gives you a Binding<LocalState?>
. And from here on things are ugly.
You might be able to get away with a wrapped binding:
var wrappedIsOn: Binding<Bool> {
let stateBinding = $state
return Binding {
stateBinding.wrappedValue?.isOn ?? false
} set: {
stateBinding.wrappedValue?.isOn = $0
}
}
And then:
Toggle("Toggle", isOn: wrappedIsOn)
And alternative, inspired by @Sweeper's answer:
Toggle("Toggle", isOn: Binding($state)?.isOn ?? Binding.constant(false))