import SwiftUI
struct Foo {
@State private var list: [Int]? = [1, 2, 3]
func body() {
// make sure list is not nil
guard let _ = list else { return }
// Q0: why this doesn't work?
// property wrapper should work on local variable
@Binding var binding = $list
let bindingToOptionalArray = $list // here, the type is: Binding<Optional<Array<Int>>>
// Q1: how to mutate list through `bindingToOptionalArray`?
bindingToOptionalArray.projectedValue?.append(123)
// Q2: how to get the binding to "unwrapped" list: Binding<Array<Int>>, not Binding<Optional<Array<Int>>>?
let bindingToArray = ???
}
}
This pattern only works for property wrappers that have a init(wrappedValue:)
initializer. If Binding
had this initializer, then the compiler would translate the above code to Binding(wrappedValue: $list)
. State
does have this initializer, so that's why
@State var binding = $list
would work and it's why
@State private var list: [Int]? = [1, 2, 3]
works, but
@Binding private var list: [Int]? = [1, 2, 3]
doesn't work.
Read the docs for more information.
As a separate point, never use guard let _ = list else { return }
. Instead use a boolean test:
guard list != nil else { return }
Here is how to create a Binding<[Int]>
from a Binding<[Int]?>
:
func test() {
@Binding var binding: [Int]
// See https://developer.apple.com/documentation/swiftui/binding/init(_:)-5z9t9
guard let unwrappedBinding = Binding($list) else {
// if `list` is `nil`, then we return early
return
}
_binding = unwrappedBinding
}
Doesn't quite do what I want:
import SwiftUI
struct Foo {
@State private var list: [Int]? = [1, 2, 3]
func body() {
// make sure list is not nil
guard list != nil else { return }
let bindingToOptionalArray = $list // here, the type is: Binding<Optional<Array<Int>>>
guard let unwrappedBinding = Binding($list) else { return }
@Binding var bindingToArray: [Int]
_bindingToArray = unwrappedBinding
bindingToArray.append(123) // 👎👎 this should mutate `list` but is not
print(list) // Optional([1, 2, 3]), should be Optional([1, 2, 3, 123])
// I need to use binding like above
// but even not going through binding, how to mutate list?
// list.append(123) // no
list!.append(123) // this compile
print(list) // but the list is not changed
}
}
let foo = Foo()
foo.body()
It works as expected for me. When I mutate the binding, the original list
changes as well. The reason it's not working for you is because Foo
is not a View
. The State
property wrapper is only meant to be used in a View
. You should've gotten the following warning in your project:
Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
This works as expected, assuming it's part of an actual SwiftUI app:
import SwiftUI
struct ContentView: View {
@State private var list: [Int]? = [1, 2, 3]
func test() {
// make sure list is not nil
guard list != nil else { return }
guard let unwrappedBinding = Binding($list) else { return }
@Binding var bindingToArray: [Int]
_bindingToArray = unwrappedBinding
bindingToArray.append(123)
print(list as Any)
}
var body: some View {
Text("Hello, world!")
.onAppear(perform: test)
}
}
Are you not creating a SwiftUI app?
Yes I'm working on SwiftUI. I simplify things for posting question here. I can see your code does work in SwiftUI.View! And this works as expected:
list?.append(444)
I am testing thing in a Xcode Playground. Maybe this is why I don't see any warning about using @.State outside of View.
Thank you very much for your help!
Edit: I just realize:
guard let unwrappedBinding = Binding($list) else { return }
You are using this Binding.init?(Binding<Value?>). I wish it's .init(unwrap:)
instead of .init(_:)
. This would make it clear what it does.
I want to make an extension to Binding
that does the unwrapping:
extension Binding {
// want to be able to do this:
// $list.forceUnwrap()
// to get the unwrapped binding
func forceUnwrap<Wrapped>() -> Binding<Wrapped> where Optional<Wrapped> == Value {
guard let unwrappedBinding = Binding(self) else { // compile error: Cannot convert value of type 'Binding<Optional<Wrapped>>' to expected argument type 'Binding<Optional<Wrapped>?>'
fatalError("Cannot forceUnwrap binding of nil")
}
@Binding var binding: Wrapped // compile error: Property type 'Wrapped' does not match 'wrappedValue' type 'Optional<Wrapped>'
_binding = unwrappedBinding
return binding
}
}
is this doable? I just cannot figure out how to make this compile...
Never mind. I think the extension doesn't make sense.
guard let unwrappedBinding = Binding($list) else { return }
already does what I want.
Adding @Binding var binding: Wrapped
in your custom method Binding.forceUnwrap
makes no sense. What do you think the point of this statement is?
I realize my extension Binding
doesn't make sense. Please ignore what I asked. This functionality is already provided by Binding.init?(_ base: Binding<Value?>)