swiftuirefreshable

Refreshable closure not having access to model? Is this a bug?


I've come along a strange behaviour of .refreshable that is illustrated with the code below:

struct ContentView: View {
    
    @State private var cntrl=ContentViewController()
    
    @State private var counter=0
    
    var body: some View {
        VStack {
            Button {
                counter+=1
                cntrl.setValue(counter)
            } label: {
                Text("Increase value")
            }
            
            
            Text("Selected value local: \(counter)")
            Text("Selected value cntrl: \(cntrl.model.selectedValue)")
            NewView(model: cntrl.model)
        }
        
    }
    
    
}

struct NewView:View {
    let model:Model
    
    @State private var valueSetByFunc:Int=0
    var body: some View {
        VStack {
            Button {
                whatIsTheValue()
            } label: {
                Text("Call func")
            }
            List {
                Text("Value set by func: \(valueSetByFunc)")
            }.refreshable {
                whatIsTheValue()
            }
        }
    }
    
    private func whatIsTheValue() {
        valueSetByFunc=model.selectedValue
    }
}

struct Model {
    var selectedValue=0
    
    mutating func setValue(_ value:Int) {
        self.selectedValue=value
    }
}

@Observable
class ContentViewController {
    var model=Model()
    
    func setValue(_ value:Int) {
        self.model.setValue(value)
    }
}

Hello, I am trying to understand the following behaviour of .refreshable. If you try the code posted, and click on "increase value", you will get respective fields being updated correctly. If you then click on the "call func" button, that updates the valueSetByFunc, it correctly updates the value in the "NewView" by calling whatIsTheValue(). But if you "pull to refresh", that calls the same function whatIsTheValue(), suddenly the value returned is different.

Does anybody know if this is correct behaviour?


Solution

  • You need to pass a Binding to your NewView. Try this approach to update the cntrl.model in all views:

    struct ContentView: View {
        @State private var cntrl = ContentViewController()
        @State private var counter = 0
        
        var body: some View {
            VStack {
                Button {
                    counter += 1
                    cntrl.setValue(counter)
                } label: {
                    Text("Increase value")
                }
                Text("Selected value local: \(counter)")
                Text("Selected value cntrl: \(cntrl.model.selectedValue)")
                NewView(model: $cntrl.model) // <-- here $
            }
        }
    }
    
    struct NewView:View {
        @Binding var model: Model  // <-- here
        @State private var valueSetByFunc: Int = 0
        
        var body: some View {
            VStack {
                Button {
                    whatIsTheValue()
                } label: {
                    Text("Call func")
                }
                List {
                    Text("Value set by func: \(valueSetByFunc)")
                }.refreshable {
                    whatIsTheValue()
                }
            }
        }
        
        private func whatIsTheValue() {
            valueSetByFunc = model.selectedValue
        }
    }
    

    An alternative that also works, is to keep your original code but change Model to:

    @Observable class Model {
        var selectedValue = 0
        
        func setValue(_ value: Int) {
            selectedValue = value
        }
    }