SwiftUI MVVM Binding List Item

Viewed 584

I am trying to create a list view and a detailed screen like this:

struct MyListView: View {
   @StateObject var viewModel: MyListViewModel = MyListViewModel()

   LazyVStack {
      // https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/
      ForEach(viewModel.items.identifiableIndicies) { index in
         MyListItemView($viewModel.items[index])
      }
   }
}

class MyListViewModel: ObservableObject {
   @Published var items: [Item] = []
   ...
}

struct MyListItemView: View {
   @Binding var item: Item

   var body: some View {
      NavigationLink(destination: MyListItemDetailView(item: $item), label: {
         ...
      })
   }
}

struct MyListItemDetailView: View {
   @Binding var item: Item
   @StateObject var viewModel: MyListViewItemDetailModel

   init(item: Binding<Item>) {
      viewModel = MyListViewItemDetailModel(item: item)
   }

   var body: some View {
      ...
   }
}

class MyListViewItemDetailModel: ObservableObject {
   var item: Binding<Item>

   ...
}

I am not sure what's wrong with it, but I found that item variables are not synced with each other, even between MyListItemDetailView and MyListItemDetailViewModel. Is there anyone who can provide the best practice and let me know what's wrong in my implmentation?

1 Answers

I think you should think about a minor restructure of your code, and use only 1 @StateObject/ObservableObject. Here is a cut down version of your code using only one StateObject source of truth:

Note: AFAIK Binding is meant to be used in View struct not "ordinary" classes.

PS: what is identifiableIndicies?

import SwiftUI

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct Item: Identifiable {
    let id = UUID().uuidString
    var name: String = ""
}

struct MyListView: View {
    @StateObject var viewModel: MyListViewModel = MyListViewModel()
    
    var body: some View {
        LazyVStack {
            ForEach(viewModel.items.indices) { index in
                MyListItemView(item: $viewModel.items[index])
            }
        }
    }
}

class MyListViewModel: ObservableObject {
    @Published var items: [Item] = [Item(name: "one"), Item(name: "two")]
}

struct MyListItemView: View {
    @Binding var item: Item
    
    var body: some View {
        NavigationLink(destination: MyListItemDetailView(item: $item)){
            Text(item.name)
        }
    }
}

class MyAPIModel {
    func fetchItemData(completion: @escaping (Item) -> Void) {
        // do your fetching here
        completion(Item(name: "new data from api"))
    }
}

struct MyListItemDetailView: View {
    @Binding var item: Item
    let myApiModel = MyAPIModel()
    
    var body: some View {
        VStack {
            Button(action: fetchNewData) {
                Text("Fetch new data")
            }
            TextField("edit item", text: $item.name).border(.red).padding()
        }
    }
    
    func fetchNewData() {
        myApiModel.fetchItemData() { itemData in
            item = itemData
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            MyListView()
        }.navigationViewStyle(.stack)
    }
}

EDIT1:

to setup an API to call some functions, you could use something like this:

class MyAPI {
    func fetchItemData(completion: @escaping (Item) -> Void) {
        // do your stuff
    }
}

and use it to obtain whatever data you require from the server.

EDIT2: added some code to demonstrate the use of an API.

Related