0

Disclaimer: Android developer here

Been trying to make an existing android app work on iOS via KMM and I'd like to know if there's a way to force a view update in SwiftUI when only an update is made to an Item's paramaters in an array. (Something like the array[index].copy() function in Jetpack Compose

Here's the set up:

class MyViewModel: ObservableObject {


...
@Published var itemsArray = [Item]()

...

func increaseCount(itemIndex: Int)  {
        
        let count = itemsArray[itemIndex].quantity + 1
        itemsArray[itemIndex].quantity = count
print("item count is \(count)")
           
    }

func decreaseCount(itemIndex: Int)  {
        var count: Int
        if itemsArray[itemIndex].quantity < 2 {
             count = 1
        } else {
            count = Int(itemsArray[itemIndex].quantity - 1)
        }
        
        itemsArray[itemIndex].quantity = Int32(count) //Xcode insisted on Int32
    }

...
}

On the View in question:

struct SecondView: View {
    
    @ObservedObject var viewModel: MyViewModel

...
 var body: some View {

...
 ForEach (viewModel.itemsArray, id: \.self)   {
                        item in
                        let index = viewModel.itemsArray.firstIndex(of: item)
HStack {
                                    Button(action: {viewModel.decreaseCount(itemIndex: index!
                                    )}) {
                                        Image(systemName: "minus.circle")
                                            .foregroundColor(Color.gray)
                                    }
                                                                    
                                   
                                    Text("\(viewModel.itemsArray[index!].quantity)")
                                    
                                    Button(action: {viewModel.increaseCount(itemIndex: index!)}) {
                                        Image(systemName: "plus.circle")
                                            .foregroundColor(Color.black)
                                    }
                                }
}
}
}

I am also setting

extension Item: Identifiable{}

on the file containing the ViewModel

The print statements in the viewmodel get updated according but the UI does not. Any help will be appreciated.

EDIT: Just realised that if I remove the element at the specified index and re-insert it, the UI updates. Surely there has to be a more efficient/ elegant way to this is on Swift, right?

MrBE
  • 103
  • 2
  • 10

1 Answers1

1

without all relevent the code, I can only guess. Try something like this:

struct SecondView: View {
    @ObservedObject var viewModel: MyViewModel
    ...
    var body: some View {
        ...
        ForEach ($viewModel.itemsArray, id: \.self) { $item in  // <-- here
            HStack {
                Button(action: {
                    if item.quantity < 2 {    // <-- here
                        item.quantity = 1
                    } else {
                        item.quantity -= 1
                    }
                }) {
                    Image(systemName: "minus.circle").foregroundColor(Color.gray)
                }
                Text("\(item.quantity)")
                Button(action: { item.quantity += 1 }) {  // <-- here
                    Image(systemName: "plus.circle").foregroundColor(Color.black)
                }
            }
        }
    }
}

and no need for your increaseCount and decreaseCount code in your MyViewModel.

EDIT-1: here is my test code that shows the UI is updated when the buttons are tapped/clicked.

struct Item: Identifiable, Hashable {
    let id = UUID()
    var quantity = 0
}

class MyViewModel: ObservableObject {
    @Published var itemsArray = [Item(),Item(),Item()] // for testing some Items in the array
}

struct ContentView: View {
    @StateObject var viewModel = MyViewModel() // <-- here, or let
    
    var body: some View {
        SecondView(viewModel: viewModel)
    }
}

struct SecondView: View {
    @ObservedObject var viewModel: MyViewModel
    
    var body: some View {
        VStack(spacing: 33) {
            ForEach ($viewModel.itemsArray, id: \.self) { $item in  // <-- here
                HStack {
                    Button(action: {
                        if item.quantity < 1 {    // <-- here
                            item.quantity = 0
                        } else {
                            item.quantity -= 1
                        }
                    }) {
                        Image(systemName: "minus.circle").foregroundColor(Color.gray)
                    }
                    
                    Text("\(item.quantity)")
                    
                    Button(action: { item.quantity += 1 }) {  // <-- here
                        Image(systemName: "plus.circle").foregroundColor(Color.black)
                    }
                }
            }
        }
    }
}
  • I just tried this out. Sadly still, the UI wasn't getting updated – MrBE Jun 16 '22 at 08:12
  • updated my answer with the code I used to show my approach works. If your UI is not updating then it is due to other code that you are not showing. Maybe you are not passing the `viewModel` correctly into `SecondView`. You must have only 1 `viewModel` to pass around. – workingdog support Ukraine Jun 16 '22 at 09:18
  • Thing is, this a KMM project and as such, **Item** is actually a Kotlin class so setting up an additional struct might cause some conflicts. From a pure Swift standpoint, your answer works perfectly, that's why I've marked it as a solution. – MrBE Jun 16 '22 at 14:55
  • thanks. I did not realised `Item` was a kotlin class. Unfortunately I am not familiar with mixing kotlin and swift in the way you have it, in a `ObservableObject` array. Best of luck. – workingdog support Ukraine Jun 16 '22 at 22:43