Search code examples
swiftprotocolsself

Mutate a self variable from Protocol


protocol: MyProtocol {
    var showView: Bool { get set }
}

struct MyView: View, MyProtocol {
    var showView = false
    var body: some View {
        ZStack(){
            AnotherView(myProtocol: self)
            if showView {
                // code
            }
        }
    }
}

struct AnotherView: View {
    var myProtocol: MyProtocol
    var body: some View {
        ZStack(){
            Text("Text")
                .onTapGesture{
                    myProtocol.showView = true  // xcode complains on this line
                }
        }
    }
}

I have this code above. I want when the Text is tapped, the showView variable changes value and then do whatever I want here if showView {}

This is not my actual code, it's just a minimal reproduction example, and I'm forced to use protocol because AnotherView is a reusable view to be used from multiple sides, so I want to change the value of showView of only the current parent view

But XCode keeps complaining, saying:

Cannot assign to property: 'self' is immutable

How can I change the value of the parent View's Protocol's property from the child view?


Solution

  • You should check that your “minimal reproduction example“ produces the error you describe. Yours does not, because of syntax errors in the declaration of MyProtocol. The correct declaration is

    protocol MyProtocol {
        var showView: Bool { get set }
    }
    

    Correcting that results in the error you describe.

    @vadian gives the correct answer in a comment. Mutable state in a SwiftUI view needs to be stored in some sort of DynamicProperty, most commonly by using one of the property wrappers @State, @Binding, or @ObservedObject.

    But it is difficult to understand how to fix your example, because in your example, you have a MyView that passes itself to AnotherView and expects the AnotherView to mutate the passed-in MyView. That is not really how SwiftUI is designed to work. In SwiftUI, mutable state needs to be more cleanly separated from views.

    Perhaps this captures your intended effect:

    struct MyViewState: MyProtocol {
        var showView = false
    }
    
    struct MyView: View {
        @State var state: any MyProtocol = MyViewState()
        
        var body: some View {
            ZStack(){
                AnotherView(myProtocol: $state)
                
                if state.showView {
                    // code
                }
            }
        }
    }
    
    struct AnotherView: View {
        @Binding var myProtocol: any MyProtocol
        var body: some View {
            ZStack {
                Text("Text")
                    .onTapGesture {
                        myProtocol.showView = true  // xcode complains on this line
                    }
            }
        }
    }
    

    Note that, because AnotherView expects a MyProtocol existential, I had to explicitly declare state to be an existential (any MyProtocol) in MyView. You can learn more about existentials by watching WWDC 2022: Embrace Swift generics and WWDC 2022: Design protocol interfaces in Swift.

    It's probably better to avoid using existentials here and instead make AnotherView generic, like this:

    struct MyView: View {
        @State var state = MyViewState()
        
        var body: some View {
            ZStack(){
                AnotherView(myProtocol: $state)
                
                if state.showView {
                    // code
                }
            }
        }
    }
    
    struct AnotherView<My: MyProtocol>: View {
        @Binding var myProtocol: My
        var body: some View {
            ZStack {
                Text("Text")
                    .onTapGesture {
                        myProtocol.showView = true  // xcode complains on this line
                    }
            }
        }
    }