Connecting @Published and @Binding

Viewed 85

I have a @Published parameter in my viewmodel like here:

struct TestView: View {
    let viewModel: ViewModel

    var body: some View {
        Text("aaa")
            .overlay(Color.clear.modifier(GeometryGetter(rect: viewModel.rect)))
    }
}

extension TestView {
    class ViewModel {
        @Published var rect: CGRect = .zero
    }
}

I want to connect this parameter to an inner view, so I thought I'd use @Binding like here:

public struct GeometryGetter: ViewModifier {
    @Binding var rect: CGRect
    
    public init(rect: Binding<CGRect>) {
        self._rect = rect
    }
    
    public func body(content: Content) -> some View {
        return GeometryReader { proxy -> Color in
            DispatchQueue.main.async {
                self.rect = proxy.frame(in: .global)
            }
            return Color.clear
        }
    }
}

But I'm getting an error:

Cannot convert value of type 'Published<CGRect>.Publisher' to expected argument type 'Binding<CGRect>'

What is the correct way to implement it?

2 Answers

You need to make view model as ObservableObject and in view add corresponding wrapper, then it is possible to pass binding to published property, like

struct TestView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        Text("aaa")
            .overlay(Color.clear.modifier(GeometryGetter(rect: $viewModel.rect)))
    }
}

extension TestView {
    class ViewModel: ObservableObject {
        @Published var rect: CGRect = .zero
    }
}

We don't use View Model objects in SwiftUI for view data. The View struct should be our primary encapsulation mechanism for view data and sometimes we use custom structs when we have multiple related vars and methods that we want to test independently. The use of @State and @Binding gives us all the benefits of reference types like objects but we can use super fast value types with the benefits of value semantics - learning this concept is fundamental to understanding SwiftUI. Thus replacing your class with a struct, your code should look something along the lines of this (not sure about your modifier though):

struct TestViewConfig {
    var rect: CGRect = .zero
    var someOtherVar = false

    func someFuncYoudLikeToTest() {

    }

    mutating func someFuncThatMakesAChange() {
        rect = .zero
    }
}

struct TestView: View {
    @State var config = TestViewConfig() // everytime TestView is recreated, e.g. when init in a parent body, it is supplied the old version of config so the changes are not lost between updates.

    var body: some View {
        Text("aaa")
            .overlay(Color.clear.modifier(GeometryGetter(rect: $config.rect)))
    }
}


public struct GeometryGetter: ViewModifier {
    @Binding var rect: CGRect // gives us write access to the config stored in @State
    
    public func body(content: Content) -> some View {
        return GeometryReader { proxy -> Color in
            rect = proxy.frame(in: .global)
            return Color.clear
        }
    }
}

For more info on this custom struct pattern watch WWDC 2020 Data Essentials in SwiftUI at 4:06

Related