@Bindable usage with protocols
Asked Answered
A

2

5

Prior to iOS 17, I was used to creating SwiftUI views with a generic approach where a view is constrainted to one view model, and that ViewModel can be a protocol. This was very nice for testing and wrapping all the UI-related things to the ViewModels interface.

@MainActor
protocol CityQueryViewModelInterface: ObservableObject {
    var text: String { get set }
    func fetchWeather() async throws -> WeatherItem
}
struct CityQueryView<ViewModel: CityQueryViewModelInterface>: View {    
    @ObservedObject var viewModel: ViewModel
}

However, when trying to achieve this in iOS 17 using the Observable macro with @Bindable and protocol, I am getting an error

protocol CityQueryViewModelInterface: Observable {
    var text: String { get set }
    func fetchWeather() async throws -> WeatherItem
}
@Observable
final class CityQueryViewModel: CityQueryViewModelInterface {
struct CityQueryView<ViewModel: CityQueryViewModelInterface>: View {
    @Bindable var viewModel: ViewModel
}

'init(wrappedValue:)' is unavailable: The wrapped value must be an object that conforms to Observable

Next to the Bindable annotation

Attlee answered 10/1 at 16:32 Comment(0)
C
6

The error says

The wrapped value must be an object that conforms to Observable.

Notice the highlight on "object".

Bindable requires Observable and AnyObject so just add AnyObject to your protocol.

protocol CityQueryViewModelInterface: Observable, AnyObject {
    var text: String { get set }
    func fetchWeather() async throws -> WeatherItem
}
Carvalho answered 10/1 at 16:49 Comment(6)
I still see the error. I copy-pasted your protocol example, added a WeatherItem implementation and tried to declare it in a view and the error is still there. I am using Xcode 15.4. Any ideas about what could be wrong?Darnel
@Darnel ask a question with an MRE. This works so there is probably something else going on with your changes. You can tag me an I can take a lookCarvalho
I tried in a separate project and it worked. The issue was that I wasn't using generics in the view. I had implemented @BindableProtocol that doesn't require using generics. Thinking now what's cleanest, if using generics of using my property wrapper...Darnel
Whatever gives you a concrete type, SwiftUI does not work well with existentials @DarnelCarvalho
Why doesn't it work well with existentials in this case? I mean, in my approach I just use @BindableProtocol and it works. No need to use generics. Why do you think that's risky?Darnel
@Darnel I never said risky. Protocols don’t actually conform to themselves and SwiftUI depends on Hashable, Equatable, etc.Carvalho
A
0

I had the same issue and, if lorem ipsum's answer is correct, I would like to make it clear you must use a concrete type, not a protocol (as it has also been suggested in his answer's comments)

So, first, your protocol must inherit from AnyObject:

protocol MyInterface: AnyObject, Observable {
  var title: String { get set }
}

And then, you must use a concrete type passing by generics in your view like this:

struct MyView<ViewModel: MyInterface>: View {
    @Bindable private var viewModel: ViewModel
    
    init(viewModel: ViewModel) {
        self.viewModel = viewModel
    }
}
Arrear answered 19/8 at 19:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.