在SwiftUI中创建包含NavigationLink项目的列表,其中每个NavigationLink再次包含单独的项目列表

eqqqjvef  于 2022-12-10  发布在  Swift
关注(0)|答案(1)|浏览(124)

[Pic 1 AS IS]

[Pic 2 TO BE]

Hi there, I am just starting to learn Swift an I would like my app users to build their own list of items (first level) where each item again contains a list of items (second level). Important is that each of the individually created lists in the second level is like no other of the individually created lists. (see picture)
Is anyone aware of which approach I need to take to solve this?
I am myself able to build the list within the list within the NavigationView, but how can I make each list individual?
Here is my code:

struct ItemModel: Hashable {
        let name: String
    }

struct ProductModel: Hashable {
    let productname: String
}

class ListViewModel: ObservableObject {
    @Published var items: [ItemModel] = []
    }

class ProductlistViewModel: ObservableObject {
    @Published var products: [ProductModel] = []
    }



struct ContentView: View {
        
        @StateObject private var vm = ListViewModel()
        @StateObject private var pvm = ProductlistViewModel()
        @State var firstPlusButtonPressed: Bool = false
        @State var secondPlusButtonPressed: Bool = false
        
        var body: some View {
            NavigationView {
               List {
                  ForEach(vm.items, id: \.self) { item in
                     NavigationLink {
                         DetailView() //The DetailView below
                             .navigationTitle(item.name)
                             .navigationBarItems(
                                  trailing:
                                      Button(action: {  
                               secondPlusButtonPressed.toggle()
   
                                        }, label: {                                                              
                                      NavigationLink {                                                    
                               AddProduct() //AddProduct below                                    
                               } label: {
                         Image(systemName: "plus")
                                                            }
        
                                            })
                                            )
                                
                            } label: {
                                Text(item.name)
                            }
                        }
                    }
                .navigationBarItems(        
                      trailing:
                          Button(action: {        firstPlusButtonPressed.toggle()
                             }, label: {
           NavigationLink {
                      AddItem() //AddItem below
                             } label: {                        Image(systemName: "plus")
             }
                                      })
                                      )
            }
            .environmentObject(vm)
            .environmentObject(pvm)
        }
    }

struct AddItem: View {
    
    @State var textFieldText: String = ""
    @Environment(\.presentationMode) var presentationMode
    @EnvironmentObject var vm: ListViewModel

    var body: some View {
        
        NavigationView {
            
        VStack {
            
            TextField("Add an item...", text: $textFieldText)
            
            Button(action: {
                vm.addItem(text: textFieldText)
                presentationMode.wrappedValue.dismiss()
                
            }, label: {
                Text("SAVE")
            })                
            }
        }
    }
}

struct DetailView: View {
    
    @StateObject private var pvm = ProductlistViewModel()
    @Environment(\.editMode) var editMode
    
    var body: some View {
        
        NavigationView {
            List {
                ForEach(pvm.products, id: \.self) { product in
                    Text(product.productname)
                }
            }
        }
        .environmentObject(pvm)
    }
}
struct AddProduct: View {
    
    @State var textFieldText: String = ""
    @Environment(\.presentationMode) var presentationMode
    @EnvironmentObject var pvm: ProductlistViewModel

    var body: some View {
        
        NavigationView {
            
            VStack {
            
            TextField("Add a product", text: $textFieldText)
            
            Button(action: {
                pvm.addProduct(text: textFieldText)
                presentationMode.wrappedValue.dismiss()
                
            }, label: {
                Text("SAVE")
            })
                  
            }
        }
    }
}
igetnqfo

igetnqfo1#

This is going to be long but here it goes. The issue is the whole ViewModel setup. You detail view now is only using the product view model, you need to rethink your approach.
But what makes the whole thing "complicated" is the 2 different types, Item and Product which you seem to want to combine into one list and use the same subviews for them both.
In swift you have protocol that allows this, protocols require struct and class "conformance".

//Protocols are needed so you can use reuse views
protocol ObjectModelProtocol: Hashable, Identifiable{
    var id: UUID {get}
    var name: String {get set}
    init(name: String)
}
//Protocols are needed so you can use reuse views
protocol ListModelProtocol: Hashable, Identifiable{
    associatedtype O : ObjectModelProtocol
    var id: UUID {get}
    var name: String {get set}
    //Keep the individual items with the list 
    var items: [O] {get set}
    init(name: String, items: [O])
}
extension ListModelProtocol{
    mutating func addItem(name: String) {
        items.append(O(name: name))
    }
}

Then your models start looking something like this. Notice the conformance to the protocols.

//Replaces the ListViewModel
struct ListItemModel: ListModelProtocol{
    let id: UUID
    var name: String
    var items: [ItemModel]
    
    init(name: String, items: [ItemModel]){
        self.id = .init()
        self.name = name
        self.items = items
    }
}
//Replaces the ProductlistViewModel
struct ListProductModel: ListModelProtocol{
    let id: UUID
    var name: String
    var items: [ProductModel]
    init(name: String, items: [ProductModel]){
        self.id = .init()
        self.name = name
        self.items = items
    }
}
//Uniform objects, can be specialized but start uniform
struct ItemModel: ObjectModelProtocol {
    let id: UUID
    var name: String
    init(name: String){
        self.id = .init()
        self.name = name
    }
}
//Uniform objects, can be specialized but start uniform
struct ProductModel: ObjectModelProtocol {
    let id: UUID
    var name: String
    init(name: String){
        self.id = .init()
        self.name = name
    }
}
class ModelStore: ObservableObject{
    @Published var items: [ListItemModel] = [ListItemModel(name: "fruits", items: [.init(name: "peach"), .init(name: "banana"), .init(name: "apple")])]
    @Published var products: [ListProductModel] = [ListProductModel(name: "vegetable", items: [.init(name: "tomatoes"), .init(name: "paprika"), .init(name: "zucchini")])]
    
}

Now your views can look something like this

struct ComboView: View {
    @StateObject private var store = ModelStore()
    @State var firstPlusButtonPressed: Bool = false
    @State var secondPlusButtonPressed: Bool = false

    var body: some View {
        NavigationView {
            List {
                //The next part will address this
                ItemLoop(list: $store.items)
                ItemLoop(list: $store.products)
                
            }
            .toolbar(content: {
                ToolbarItem {
                    AddList(store: store)
                }
            })
        }
    }
}

struct ItemLoop<LM: ListModelProtocol>: View {
    @Binding var list: [LM]
    var body: some View{
        ForEach($list, id: \.id) { $item in
            NavigationLink {
                DetailView<LM>(itemList: $item)
                    .navigationTitle(item.name)
                    .toolbar {
                        NavigationLink {
                            AddItem<LM>( item: $item)
                        } label: {
                            Image(systemName: "plus")
                        }
                    }
            } label: {
                Text(item.name)
            }
        }
    }
}

struct AddList: View {
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var store: ModelStore
    var body: some View {
        Menu {
            Button("add item"){
                store.items.append(ListItemModel(name: "new item", items: []))
            }
            Button("add product"){
                store.products.append(ListProductModel(name: "new product", items: []))
            }
        } label: {
            Image(systemName: "plus")
        }
        
    }
}
struct AddItem<LM>: View where LM : ListModelProtocol {
    @State var textFieldText: String = ""
    @Environment(\.presentationMode) var presentationMode
    @Binding var item: LM

    var body: some View {
        VStack {
            TextField("Add an item...", text: $textFieldText)
            Button(action: {
                item.addItem(name: textFieldText)
                presentationMode.wrappedValue.dismiss()

            }, label: {
                Text("SAVE")
            })
        }

    }
}

struct DetailView<LM>: View where LM : ListModelProtocol{
    @Environment(\.editMode) var editMode
    @Binding var itemList: LM
    var body: some View {
        VStack{
            TextField("name", text: $itemList.name)
                .textFieldStyle(.roundedBorder)
            List (itemList.items, id:\.id) { item in
                Text(item.name)
            }
        }
        .navigationTitle(itemList.name)
        .toolbar {
            NavigationLink {
                AddItem(item: $itemList)
            } label: {
                Image(systemName: "plus")
            }
        }
    }
}

If you notice the List in the ComboView you will notice that the items and products are separated into 2 loop. That is because SwiftUI requires concrete types for most views, view modifiers and wrappers.
You can have a list of [any ListModelProtocol] but at some point you will have to convert from an existential to a concrete type. In your case the ForEach in de DetailView requires a concrete type.

class ModelStore: ObservableObject{
    @Published var both: [any ListModelProtocol] = [
        ListProductModel(name: "vegetable", items: [.init(name: "tomatoes"), .init(name: "paprika"), .init(name: "zucchini")]),
        ListItemModel(name: "fruits", items: [.init(name: "peach"), .init(name: "banana"), .init(name: "apple")])
    ]
}
struct ComboView: View {
    
    @StateObject private var store = ModelStore()
    @State var firstPlusButtonPressed: Bool = false
    @State var secondPlusButtonPressed: Bool = false

    var body: some View {
        NavigationView {
            List {
                ConcreteItemLoop(list: $store.both)
            }
            .toolbar(content: {
                ToolbarItem {
                    AddList(store: store)
                }
            })
        }
    }
}
struct ConcreteItemLoop: View {
    @Binding var list: [any ListModelProtocol]
    var body: some View{
        ForEach($list, id: \.id) { $item in
            NavigationLink {
                if let concrete: Binding<ListItemModel> = getConcrete(existential: $item){
                    DetailView(itemList: concrete)
                } else if let concrete: Binding<ListProductModel> = getConcrete(existential: $item){
                    DetailView(itemList: concrete)
                }else{
                    Text("unknown type")
                }
            } label: {
                Text(item.name)
            }
        }
    }
    func getConcrete<T>(existential: Binding<any ListModelProtocol>) -> Binding<T>? where T : ListModelProtocol{
        if existential.wrappedValue is T{
            return Binding {
                existential.wrappedValue as! T
            } set: { newValue in
                existential.wrappedValue = newValue
            }

        }else{
            return nil
        }
    }
}

struct AddList: View {
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var store: ModelStore
    var body: some View {
        Menu {
            Button("add item"){
                store.both.append(ListItemModel(name: "new item", items: []))
            }
            Button("add product"){
                store.both.append(ListProductModel(name: "new product", items: []))
            }
        } label: {
            Image(systemName: "plus")
        }
        
    }
}

I know its long but this all compiles so you should be able to put it in a project and disect it.
Also, at the end of all of this you can create specific views for the model type.

struct DetailView<LM>: View where LM : ListModelProtocol{
    @Environment(\.editMode) var editMode
    @Binding var itemList: LM
    var body: some View {
        VStack{
            TextField("name", text: $itemList.name)
                .textFieldStyle(.roundedBorder)
            List (itemList.items, id:\.id) { item in
                VStack{
                    switch item{
                    case let i as ItemModel:
                        ItemModelView(item: i)
                    case let p as ProductModel:
                        Text("\(p.name) is product")
                    default:
                        Text("\(item.name) is unknown")
                    }
                }
            }
        }
        .navigationTitle(itemList.name)
        .toolbar {
            NavigationLink {
                AddItem(item: $itemList)
            } label: {
                Image(systemName: "plus")
            }
        }
    }
}
struct ItemModelView: View{
    let item: ItemModel
    var body: some View{
        VStack{
            Text("\(item.name) is item")
            Image(systemName: "person")
        }
    }
}

相关问题