Cannot convert value of type 'Published<Bool>.Publisher' to expected argument type 'Binding<Bool>'
Asked Answered
K

7

75

When trying to compile the following code:

class LoginViewModel: ObservableObject, Identifiable {
    @Published var mailAdress: String = ""
    @Published var password: String = ""
    @Published var showRegister = false
    @Published var showPasswordReset = false

    private let applicationStore: ApplicationStore

    init(applicationStore: ApplicationStore) {
        self.applicationStore = applicationStore
    }

    var passwordResetView: some View {
        PasswordResetView(isPresented: $showPasswordReset) // This is where the error happens
    }
}

Where PasswordResetView looks like this:

struct PasswordResetView: View {
    @Binding var isPresented: Bool
    @State var mailAddress: String = ""
    
    var body: some View {
            EmptyView()
        }
    }
}

I get the error compile error

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

If I use the published variable from outside the LoginViewModel class it just works fine:

struct LoginView: View {
    @ObservedObject var viewModel: LoginViewModel

    init(viewModel: LoginViewModel) {
      self.viewModel = viewModel
    }
    
    var body: some View {
            PasswordResetView(isPresented: self.$viewModel.showPasswordReset)
    }
}

Any suggestions what I am doing wrong here? Any chance I can pass a published variable as a binding from inside the owning class?

Thanks!

Kacey answered 6/8, 2020 at 11:14 Comment(3)
Projected values (something started with $) can be different in different contexts. Binding projected value in your second case is generated by @ObservedObject, if first case @Published generates publisher projected value. The question is what are you trying to do and why do you put View insider view model?Hoashis
I am following the MVVM principles that were described on the Ray Wenderlich Sitze (raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios). There they put all the routing logic inside the viewmodel of a view (including the instanciation and configuration of views + their models). Thats basically what I am trying to do here.Kacey
Better to follow SwiftUI principals, MVVM isn't really suited to SwiftUI which already solves everything.Apology
H
4

Here is possible approach - the idea to make possible observation in generated view and avoid tight coupling between factory & presenter.

Tested with Xcode 12 / iOS 14 (for older systems some tuning might be needed)

protocol ResetViewModel {
    var showPasswordReset: Bool { get set }
}

struct PasswordResetView<Model: ResetViewModel & ObservableObject>: View {
    @ObservedObject var resetModel: Model

    var body: some View {
        if resetModel.showPasswordReset {
            Text("Show password reset")
        } else {
            Text("Show something else")
        }
    }
}

class LoginViewModel: ObservableObject, Identifiable, ResetViewModel {
    @Published var mailAdress: String = ""
    @Published var password: String = ""
    @Published var showRegister = false
    @Published var showPasswordReset = false

    private let applicationStore: ApplicationStore

    init(applicationStore: ApplicationStore) {
        self.applicationStore = applicationStore
    }

    var passwordResetView: some View {
        PasswordResetView(resetModel: self)
    }
}
Hoashis answered 6/8, 2020 at 17:48 Comment(1)
That's a very smart idea by just putting the ObservedObject in the view and with that preventing any type issues. I use it to update a progress bar which is monitoring a published progress value in another thread and works perfectly.Carmacarmack
E
139

Not sure why the proposed solutions here are so complex, when there is a very direct fix for this.

Found this answer on a similar Reddit question:

The problem is that you are accessing the projected value of an @Published (which is a Publisher) when you should instead be accessing the projected value of an @ObservedObject (which is a Binding), that is: you have globalSettings.$tutorialView where you should have $globalSettings.tutorialView.

Ericaericaceous answered 12/2, 2022 at 11:51 Comment(9)
This should be the accepted answer!Polston
That makes so much sense I don't know why I didn't think about it. lolChangchangaris
Can't believe this actually workedFillian
Most people probably have globalSettings.$tutorialView because that's what XCode suggests while you're typing.Androw
Besides other answers giving more ideas, this one is a direct answer to the question asked.Unlookedfor
Also don't forget to add @ObservedObject to the object (view model)Mikesell
You are a god among menOak
not working within the ObservableObject itself, because cant use $self.tutorialView .Checkrein
The ugliest thing that xcode offer it as autocompletion :facepalm:Lightfoot
A
16

** Still new to Combine & SwiftUI so not sure if there is better way to approach **

You can initalize Binding from publisher.

https://developer.apple.com/documentation/swiftui/binding/init(get:set:)-6g3d5

let binding = Binding(
    get: { [weak self] in
        (self?.showPasswordReset ?? false)
    },
    set: { [weak self] in
        self?.showPasswordReset = $0
    }
)

PasswordResetView(isPresented: binding)

Afflux answered 6/8, 2020 at 11:44 Comment(3)
Thanks. I saw that as well but it looks a bit clunky. But I will try it out now :-)Kacey
I get Cannot find 'self' in scopeInnocent
I was about to give up on using binding in favor of some closure, but this was exactly what I needed! I had a view model with a published property that it needed to pass as a binding to something else and this did the trick. Thank you!Latimer
O
10

I think the important thing to understand here is what "$" does in the Combine context.

What "$" does is to publish the changes of the variable "showPasswordReset" where it is being observed.

when it precedes a type, it doesn't represent the type you declared for the variable (Boolean in this case), it represents a publisher, if you want the value of the type, just remove the "$".

"$" is used in the context where a variable was marked as an @ObservedObject, (the ObservableObject here is LoginViewModel and you subscribe to it to listen for changes in its variables market as publishers)

struct ContentView: View {
       @ObservedObject var loginViewModel: LoginViewModel...

in that context (the ContentView for example) the changes of "showPasswordReset" are going to be 'Published' when its value is updated so the view is updated with the new value.

Optimum answered 25/4, 2021 at 0:13 Comment(0)
H
4

Here is possible approach - the idea to make possible observation in generated view and avoid tight coupling between factory & presenter.

Tested with Xcode 12 / iOS 14 (for older systems some tuning might be needed)

protocol ResetViewModel {
    var showPasswordReset: Bool { get set }
}

struct PasswordResetView<Model: ResetViewModel & ObservableObject>: View {
    @ObservedObject var resetModel: Model

    var body: some View {
        if resetModel.showPasswordReset {
            Text("Show password reset")
        } else {
            Text("Show something else")
        }
    }
}

class LoginViewModel: ObservableObject, Identifiable, ResetViewModel {
    @Published var mailAdress: String = ""
    @Published var password: String = ""
    @Published var showRegister = false
    @Published var showPasswordReset = false

    private let applicationStore: ApplicationStore

    init(applicationStore: ApplicationStore) {
        self.applicationStore = applicationStore
    }

    var passwordResetView: some View {
        PasswordResetView(resetModel: self)
    }
}
Hoashis answered 6/8, 2020 at 17:48 Comment(1)
That's a very smart idea by just putting the ObservedObject in the view and with that preventing any type issues. I use it to update a progress bar which is monitoring a published progress value in another thread and works perfectly.Carmacarmack
I
1

You could also reimplement @Published to have ability to take @Binding right from projectedValue (aka $).

Usage:

class ViewModel: ObservableObject {
  @Relay var email: String = ""

  func doSomething() { 
    let binding = $email.binding
  }
}

Implementation:

// Reimplementation of @Published with support of binding
@propertyWrapper
struct Relay<Value> {
  private var publisher: Publisher

  public init(wrappedValue: Value) {
    publisher = Publisher(wrappedValue)
  }

  public var projectedValue: Publisher {
    publisher
  }

  private var observablePublisher: ObservableObjectPublisher? {
    get { publisher.observablePublisher }
    set { publisher.observablePublisher = newValue }
  }

  public var wrappedValue: Value {
    get { publisher.subject.value }
    set { publisher.subject.send(newValue) }
  }

  public struct Publisher: Combine.Publisher {
    typealias Output = Value
    typealias Failure = Never

    var subject: CurrentValueSubject<Value, Never>
    weak var observablePublisher: ObservableObjectPublisher?

    public func receive<S>(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
      subject.subscribe(subscriber)
    }

    init(_ output: Output) {
      subject = .init(output)
    }

    var binding: Binding<Value> {
      .init(
        get: { subject.value },
        set: {
          observablePublisher?.send()
          subject.send($0)
        }
      )
    }
  }

  public static subscript<OuterSelf: ObservableObject>(
    _enclosingInstance observed: OuterSelf,
    wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value>,
    storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
  ) -> Value {
    get {
      if observed[keyPath: storageKeyPath].observablePublisher == nil {
        observed[keyPath: storageKeyPath].observablePublisher = observed.objectWillChange as? ObservableObjectPublisher
      }

      return observed[keyPath: storageKeyPath].wrappedValue
    }
    set {
      if let willChange = observed.objectWillChange as? ObservableObjectPublisher {
        willChange.send() // Before modifying wrappedValue
        observed[keyPath: storageKeyPath].wrappedValue = newValue
      }
    }
  }
}

Thanks a lot to the guy who explained internals of the @Published here https://fatbobman.com/en/posts/adding-published-ability-to-custom-property-wrapper-types/

Inerrant answered 14/3 at 22:17 Comment(0)
L
-1

For error that states: "Cannot convert value of type 'Binding' to expected argument type 'Bool'" solution is to use wrappedValue as in example below.

If you have MyObject object with property isEnabled and you need to use that as vanilla Bool type instead of 'Binding' then do this myView.disabled($myObject.isEnabled.wrappedValue)

Laconism answered 20/10, 2020 at 15:42 Comment(0)
B
-1

Here is a different way of building the LoginView. This particular approach does not need a View Model since all the things can be performed in the view. Authentication can be performed in AuthenticationService (ApplicationStore in your case).

struct LoginView: View {
    
    @State private var mailAddress: String = ""
    @State private var password: String = ""
    @State private var showRegister = false
    @State private var showPasswordReset = false
    
    let authenticationService: AuthenticationService
    
    var passwordResetView: some View {
        PasswordResetView(isPresented: $showPasswordReset)
    }
    
    var body: some View {
        VStack {
            Button("Login") {
                authenticationService.login("username", "password")
            }
        }
    }
}

If there are too many @State variables then you can group them into a struct as shown below:

struct LoginConfig {
    
    var mailAddress: String = ""
    var password: String = ""
    var showRegister = false
    var showPasswordReset = false
}

struct LoginView: View {
    
    @State private var loginConfig = LoginConfig()
    
    let authenticationService: AuthenticationService
    
    var passwordResetView: some View {
        PasswordResetView(isPresented: $loginConfig.showPasswordReset)
    }
    
    var body: some View {
        VStack {
            Button("Login") {
                authenticationService.login("username", "password")
            }
        }
    }
}
Bruell answered 14/3 at 22:38 Comment(2)
4 State inside another State? Using State in a non-View struct? That is not correct.Premillennialism
Thanks! My mistake. Inside the LoginConfig you don't need @State. Updated.Bruell

© 2022 - 2024 — McMap. All rights reserved.