Search code examples
iosswiftxcodeswiftuiview

Swiftui items get duplicated in all views when added to a single custom view


I'm struggling with the following issue: I'm trying to build a very simple app that lets you add items in a dedicated view that can be collapsed. I managed to write a simple function that lets me add multiple of these custom collapsable views. It's my first app so I wanted to follow the MVVM protocol. I think I got confused along the way because now every item I add gets automatically added to all the custom collapsable views I made. Is there any way to fix this? I thought using the UUID would solve this issue.. I'm guessing that I have to customise the "saveButtonPressed" function, but I don't know how to tell it to only add the item to the view where I pressed the "plus" button..

Here are the Models for the individual items and the collapsable view:

struct ItemModel: Identifiable, Equatable {
let id: String
let title: String

init(id: String = UUID().uuidString, title: String) {
    self.id = id
    self.title = title

 }

  func updateCompletion() -> ItemModel {
  return ItemModel(id: id, title: title)
   }
   }

--

import Foundation

struct CollapsableItem: Equatable, Identifiable, Hashable {
let id: String
var title: String

init(id: String = UUID().uuidString, title: String) {
    self.id = id
    self.title = title

    
}

    func updateCompletion() -> CollapsableItem {
return CollapsableItem(id: id, title: title)
    }
}

These are my two ViewModels:

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

init() {
    getItems()
}

func getItems() {
    let newItems = [
        ItemModel(title: "List Item1"),
        ItemModel(title: "List Item2"),
        ItemModel(title: "List Item3"),
    ]
    items.append(contentsOf: newItems)
    }

func addItem(title: String) {
    let newItem = ItemModel(title: title)
    items.append(newItem)
}

func updateItem(item: ItemModel) {
    
    if let index = items.firstIndex(where: { $0.id == item.id}) {
        items[index] = item.updateCompletion()
    }
    }
    }

--

class CollapsedViewModel: ObservableObject  {
@Published var collapsableItems: [CollapsableItem] = []

@Published var id = UUID().uuidString


init() {
    getCollapsableItems()
}


func getCollapsableItems() {
    let newCollapsableItems = [
        CollapsableItem(title: "Wake up")
    ]
    collapsableItems.append(contentsOf: newCollapsableItems)
}

func addCollapsableItem(title: String) {
    let newCollapsableItem = CollapsableItem(title: title)
    collapsableItems.append(newCollapsableItem)
}




func updateCollapsableItem(collapsableItem: CollapsableItem) {
    
    if let index = collapsableItems.firstIndex(where: { $0.id == 
collapsableItem.id}) {
        collapsableItems[index] = 
 collapsableItem.updateCompletion()
    }
}

 }

The item view:

struct ListRowView: View {
@EnvironmentObject var listViewModel: ListViewModel
let item: ItemModel

var body: some View {
    HStack() {

        
        Text(item.title)
            .font(.body)
            .fontWeight(.bold)
            .foregroundColor(.white)
            .multilineTextAlignment(.center)
            .lineLimit(1)
            .frame(width: 232, height: 16)

        
    }
    .padding( )
    .frame(width: 396, height: 56)
    .background(.gray)
    .cornerRadius(12.0)

}
}

The collapsable view:

struct CollapsedView2<Content: View>: View {
@State var collapsableItem: CollapsableItem
@EnvironmentObject var collapsedViewModel: CollapsedViewModel
@State private var collapsed: Bool = true
@EnvironmentObject var listViewModel: ListViewModel
@State var label: () -> Text
@State var content: () -> Content
@State private var show = true

var body: some View {
    ZStack{
        VStack {
            HStack{
                Button(
                    action: { self.collapsed.toggle() },
                    label: {
                        HStack() {
                            Text("Hello")
                                .font(.title2.bold())
                            Spacer()
                            Image(systemName: self.collapsed ? "chevron.down" : 
                       "chevron.up")
                        }
                        .padding(.bottom, 1)
                        .background(Color.white.opacity(0.01))
                    }
                )
                .buttonStyle(PlainButtonStyle())
                
                
                Button(action: saveButtonPressed, label: {
                    Image(systemName: "plus")
                        .font(.title2)
                        .foregroundColor(.white)
                })
            }
            VStack {
                self.content()
            }
            
            ForEach(listViewModel.items) { item in ListRowView (item: item)
                
            }
            .lineLimit(1)
            .fixedSize(horizontal: true, vertical: true)
            .frame(minWidth: 396, maxWidth: 396, minHeight: 0, maxHeight: collapsed ? 
            0 : .none)
            .animation(.easeInOut(duration: 1.0), value: show)
            .clipped()
            .transition(.slide)
            
        }
    }
    
}
func saveButtonPressed() {
    listViewModel.addItem(title: "Hello")
}
        }
        
            

and finally the main view:

struct ListView: View {

@EnvironmentObject var listViewModel: ListViewModel
@EnvironmentObject var collapsedViewModel: CollapsedViewModel

var body: some View {
    ZStack{
            ScrollView{
                VStack{
                    HStack{
                        Text("MyFirstApp")
                            .font(.largeTitle.bold())
                        
                        Button(action: newCollapsablePressed, label: {
                            
                            Image(systemName: "plus")
                                .font(.title2)
                                .foregroundColor(.white)
                            
                        })
                    }
                    .padding()
                    .padding(.leading)
                    
                    ForEach(collapsedViewModel.collapsableItems) { collapsableItem in 
                    CollapsedView2 (collapsableItem: collapsableItem,                      
                    label: { Text("") .font(.title2.bold()) },
                                                                                                      
                    content: {
                        
                        HStack {
                            Text("")
                            Spacer() }
                        .frame(maxWidth: .infinity)
                        
                        
                    })
                        
                        
                        
                    }
                    .padding()
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .statusBar(hidden: false)
            .navigationBarHidden(true)
        }
}

func newCollapsablePressed() {
    collapsedViewModel.addCollapsableItem(title: "hello2")
}
}


       

Would love to understand how I could fix this!


Solution

  • There is the anwser for your comment about add item in each CollapsedView2. Because ListViewModel is not ObservableObject (ListViewModel is difference from each CollapsableItem). You should use "@State var items: [ItemModel]".

    struct CollapsedView2<Content: View>: View {
        @State var collapsableItem: CollapsableItem
    //    @State var listViewModel = ListViewModel()
        @State var collapsed: Bool = true
        @State var label: () -> Text
        @State var content: () -> Content
        @State private var show = true
        @State var items: [ItemModel] = []
        @State var count = 1
    
        var body: some View {
                VStack {
                    HStack{
                        Text("Hello")
                            .font(.title2.bold())
                        
                        Spacer()
                        
                        Button( action: { self.collapsed.toggle() },
                                label: {
                            Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
                        }
                        )
                        .buttonStyle(PlainButtonStyle())
    
                        Button(action: saveButtonPressed, label: {
                            Image(systemName: "plus")
                                .font(.title2)
    //                            .foregroundColor(.white)
                        })
                    }
                    VStack {
                        self.content()
                    }
    
                    ForEach(items) { item in
                        ListRowView (item: item)
                    }
                    .lineLimit(1)
                    .fixedSize(horizontal: true, vertical: true)
                    .frame(minHeight: 0, maxHeight: collapsed ? 0 : .none)
                    .animation(.easeInOut(duration: 1.0), value: show)
                    .clipped()
                    .transition(.slide)
                }
        }
    
        func saveButtonPressed() {
            addItem(title: "Hello \(count)")
            count += 1
        }
        
        func addItem(title: String) {
            let newItem = ItemModel(title: title)
            items.append(newItem)
        }
    
        func updateItem(item: ItemModel) {
    
            if let index = items.firstIndex(where: { $0.id == item.id}) {
                items[index] = item.updateCompletion()
            }
        }
    }