Help me overcome problems with Binding and optional array

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 }
1 Like

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 

}
4 Likes

:pray:

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?>)

Terms of Service

Privacy Policy

Cookie Policy