It seems I am entirely losing a concept of MVVM data flow: I have a simple example view, displaying array of Doc items, supplied by ViewModel class that has to be a singleton, accessed from multiple places. Each item has name and a button, that should change it's "isFav" value, and reflect the change through color change of the said button.
I tried changing it via vm function, via struct mutating func, the direct way, but each one I come up with gives me a different error. What am I doing wrong?
import SwiftUI
struct Doc: Identifiable
{
let id: UUID
var name: String
var isFav: Bool = false
init(_ name: String)
{
self.id = UUID()
self.name = name
}
init()
{
self.id = UUID()
self.name = id.uuidString.dropLast(13).lowercased()
}
mutating func updateIsFav()
{
self.isFav.toggle()
}
}
class ViewModel: ObservableObject
{
static var shared = ViewModel()
private init() {}
@Published var array: [Doc] = []
func updateIsFav(for doc: Doc)
{
if let index = array.firstIndex(where: { $0.id == doc.id} )
{
self.array[index].isFav.toggle()
}
}
}
struct ContentView: View
{
@ObservedObject var model = ViewModel.shared
var body: some View
{
ZStack
{
Color.black.ignoresSafeArea()
ScrollView
{
VStack
{
ForEach( model.array, id: \.id )
{
doc in
HStack
{
Text( doc.name )
.foregroundColor(.white)
Button(action:
{
doc.updateIsFav() // Cannot use mutating member on immutable value: 'doc' is a 'let' constant
doc.isFav.toggle // Cannot reference 'mutating' method as function value
model.updateisFav(for: doc) // Cannot call value of non-function type 'Binding<Subject>'
},
label:
{
Image(systemName: "star")
.foregroundColor(doc.isFav ? .yellow : .white)
}
)
}
}
}
}
}
.onAppear()
{
model.array.append(Doc())
model.array.append(Doc())
model.array.append(Doc())
model.array.append(Doc())
model.array.append(Doc())
model.array.append(Doc())
model.array.append(Doc())
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View
{
ContentView()
.previewDevice("iPhone 13")
}
}
In order to mutate a Doc
in your Button
's action, you need a Binding<Doc>
.
You can get a Binding<[Doc]>
from your existing model
property using the syntax $model.array
. If you pass this Binding<[Doc]>
to ForEach
, then ForEach
will pass a Binding<Doc>
to each child.
Thus:
struct ContentView: View {
@ObservedObject var model = ViewModel.shared
var body: some View {
List {
ForEach($model.array, id: \.id) { $doc in
// ^ NEW! ^ NEW! ⬅️
HStack {
Text(doc.name)
Spacer()
Button {
doc.updateIsFav()
// also works now: doc.isFav.toggle()
} label: {
Image(systemName: doc.isFav ? "star.fill" : "star")
.foregroundColor(.yellow)
}
}
}
}.onAppear {
model.array.append(Doc())
model.array.append(Doc())
model.array.append(Doc())
model.array.append(Doc())
model.array.append(Doc())
model.array.append(Doc())
model.array.append(Doc())
}
}
}