2

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?

11
  • every time you use "MyListItemDetailView", you re-create a new MyListViewItemDetailModel. Is that what you want to do? Could this be the source of your issue? Commented Aug 7, 2021 at 0:10
  • When I just about got it working, I didn't find any issues with it, but that could be just to fix all the issues to even make it compile Commented Aug 7, 2021 at 0:21
  • @workingdog I think that it's no problem. So, when I update the item inside MyListViewItemDetailModel, it doesn't apply to the MyListView and MyListItemView. Commented Aug 7, 2021 at 1:05
  • @George Do you think there is no issue? It's true that the values are not synchronized between the views. Commented Aug 7, 2021 at 1:06
  • @Yun Can you be more clear than 'not synchronized between the views'? For example the NavigationLink shows the right stuff in the destination, and I can add new items. Bear in bind I had to edit a lot to get it to compile so I may have different running code. Commented Aug 7, 2021 at 1:42

2 Answers 2

1

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.

Sign up to request clarification or add additional context in comments.

11 Comments

I need to edit the item in MyListItemDetailView, so I created MyListItemDetailViewModel. What do you think about it?
In my answer, in MyListItemDetailView, you can edit the item using for example the TextField I provided. The changes will be reflected in the views. Have you tried my code?
I mean that I need to call some API here, so I created a viewModel class. I am not sure how can I do it
should not be any problems. You can call all your api in MyListItemDetailView and update the item.
So, you mean without create a viewmodel?
|
1

Been searching for an answer for this for a while now- posting incase it helps someone else.

Passing down your model down to a child view as bindable solves this issue. Works with pickers, lists and textFields:

SelectionView(settingsStore: settingsStore)

struct SelectionView: View {
   @Bindable var settingsStore: SettingsStore

   var body: some View {
     List(settingsStore.locations, id: \.self, selection: $settingsStore.selectedLocation) { location in

Adding as a @State var on your view is fine for demonstrating how those UI elements work- but in any practical application you are going to need that selected data on the model/state.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.