Reputation: 38475
I have a model type which looks like this:
enum State {
case loading
case loaded([String])
case failed(Error)
var strings: [String]? {
switch self {
case .loaded(let strings): return strings
default: return nil
}
}
}
class MyApi: ObservableObject {
private(set) var state: State = .loading
func fetch() {
... some time later ...
self.state = .loaded(["Hello", "World"])
}
}
and I'm trying to use this to drive a SwiftUI View.
struct WordListView: View {
@EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List($api.state.strings) {
Text($0)
}
}
}
}
It's about here that my assumptions fail. I'm trying to get a list of the strings to render in my List
when they are loaded, but it won't compile.
The compiler error is Generic parameter 'Subject' could not be inferred
, which after a bit of googling tells me that bindings are two-way, so won't work with both my private(set)
and the var on the State enum being read-only.
This doesn't seem to make any sense - there is no way that the view should be able to tell the api whether or not it's loading, that definitely should be a one-way data flow!
I guess my question is either
or
Upvotes: 9
Views: 10147
Reputation: 816
Not exactly the same problem as I had, but the following direction can help you possibly find a good result when bindings are done with only reads.
You can create a custom binding using a computed property.
I needed to do exactly this in order to show an alert only when one was passed into an overlay.
Code looks something along these lines :
struct AlertState {
var title: String
}
class AlertModel: ObservableObject {
// Pass a binding to an alert state that can be changed at
// any time.
@Published var alertState: AlertState? = nil
@Published var showAlert: Bool = false
init(alertState: AnyPublisher<AlertState?, Never>) {
alertState
.assign(to: &$alertState)
alertState
.map { $0 != nil }
.assign(to: &$showAlert)
}
}
struct AlertOverlay<Content: View>: View {
var content: Content
@ObservedObject var alertModel: AlertModel
init(
alertModel: AlertModel,
@ViewBuilder content: @escaping () -> Content
) {
self.alertModel = alertModel
self.content = content()
}
var body: some View {
ZStack {
content
.blur(radius: alertModel.showAlert
? UserInterfaceStandards.blurRadius
: 0)
}
.alert(isPresented: $alertModel.showAlert) {
guard let alertState = alertModel.alertState else {
return Alert(title: Text("Unexected internal error as occured."))
}
return Alert(title: Text(alertState.title))
}
}
}
Upvotes: 0
Reputation: 35616
You don't actually need a binding for this.
An intuitive way to decide if you need a binding or not is to ask:
Does this view need to modify the passed value ?
In your case the answer is no. The List
doesn't need to modify api.state
(as opposed to a textfield or a slider for example), it just needs the current value of it at any given moment. That is what @State
is for but since the state is not something that belongs to the view (remember, Apple says that each state must be private to the view) you're correctly using some form of an ObservableObject
(through Environment).
The final missing piece is to mark any of your properties that should trigger an update with @Published
, which is a convenience to fire objectWillChange
signals and instruct any observing view to recalculate its body.
So, something like this will get things done:
class MyApi: ObservableObject {
@Published private(set) var state: State = .loading
func fetch() {
self.state = .loaded(["Hello", "World"])
}
}
struct WordListView: View {
@EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List(api.state.strings ?? [], id: \.self) {
Text($0)
}
}
}
}
Upvotes: 13