问题
I want to create MyViewModel
which gets data from network and then updates the arrray of results. MyView
should subscribe to the $model.results
and show List
filled with the results.
Unfortunately I get an error about "Type of expression is ambiguous without more context".
How to properly use ForEach
for this case?
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
@Published var results: [String] = []
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.results = ["Hello", "World", "!!!"]
}
}
}
struct MyView: View {
@ObservedObject var model: MyViewModel
var body: some View {
VStack {
List {
ForEach($model.results) { text in
Text(text)
// ^--- Type of expression is ambiguous without more context
}
}
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(model: MyViewModel())
}
}
P.S. If I replace the model with @State var results: [String]
all works fine, but I need have separate class MyViewModel: ObservableObject
for my purposes
回答1:
The fix
Change your ForEach
block to
ForEach(model.results, id: \.self) { text in
Text(text)
}
Explanation
SwiftUI's error messages aren't doing you any favors here. The real error message (which you will see if you change Text(text)
to Text(text as String)
and remove the $
before model.results
), is "Generic parameter 'ID' could not be inferred".
In other words, to use ForEach
, the elements that you are iterating over need to be uniquely identified in one of two ways.
- If the element is a struct or class, you can make it conform to the Identifiable protocol by adding a property
var id: Hashable
. You don't need theid
parameter in this case. - The other option is to specifically tell
ForEach
what to use as a unique identifier using theid
parameter. Update: It is up to you to guarentee that your collection does not have duplicate elements. If two elements have the same ID, any change made to one view (like an offset) will happen to both views.
In this case, we chose option 2 and told ForEach
to use the String element itself as the identifier (\.self
). We can do this since String conforms to the Hashable protocol.
What about the $
?
Most views in SwiftUI only take your app's state and lay out their appearance based on it. In this example, the Text views simply take the information stored in the model and display it. But some views need to be able to reach back and modify your app's state in response to the user:
- A Toggle needs to update a Bool value in response to a switch
- A Slider needs to update a Double value in response to a slide
- A TextField needs to update a String value in response to typing
The way we identify that there should be this two-way communication between app state and a view is by using a Binding<SomeType>
. So a Toggle requires you to pass it a Binding<Bool>
, a Slider requires a Binding<Double>
, and a TextField requires a Binding<String>
.
This is where the @State
property wrapper (or @Published
inside of an @ObservedObject
) come in. That property wrapper "wraps" the value it contains in a Binding
(along with some other stuff to guarantee SwiftUI knows to update the views when the value changes). If we need to get the value, we can simply refer to myVariable
, but if we need the binding, we can use the shorthand $myVariable
.
So, in this case, your original code contained ForEach($model.results)
. In other words, you were telling the compiler, "Iterate over this Binding<[String]>
", but Binding
is not a collection you can iterate over. Removing the $
says, "Iterate over this [String]," and Array is a collection you can iterate over.
来源:https://stackoverflow.com/questions/58069967/how-to-bind-an-array-and-list-if-the-array-is-a-member-of-observableobject