OldTimes
OldTimes

Reputation: 388

@Bindable usage with protocols

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

Upvotes: 5

Views: 1155

Answers (2)

Y.Bonafons
Y.Bonafons

Reputation: 2349

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
    }
}

Upvotes: 1

lorem ipsum
lorem ipsum

Reputation: 29614

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
}

Upvotes: 7

Related Questions