0

Searched around and have not found an answer. Believe I know what the issue is, but not sure how to resolve it.

I have a swiftUI list that displays a context menu when a certain type of row is selected(not all rows qualify, this works as it should. When the context menu is displayed, the label is generated by the index of the array populating the lists object property.

The context menu selection performs performs a task that should result in the context menu label changing. And sometimes it works, other times it does not. This is resolved by scrolling that particular row off screen and scrolling back too it (has to be far enough away). The Object array is from a singleton data store passed as an environment object.

I believe this is related to the size of the array and the data being lazy loaded in swiftUI lists. I also would use the List selection property for this, but the context menu being populated by the row does not update the lists selected row.

A snippet example of my code is below.

@EnvironmentObject var singletonStore: MyObjectStore
@State private var selectedRow: Int?

var body: some View {
    VStack {
            
                    List(singletonStore.myArray.indices, id: \.self, selection: $selectedRow) { index in
                        
                        LazyVGrid(columns: gridColumns) {
                            ItemGridView(item: $singletonStore.myArray[index], opacityOffset: getRowOpacity(index: index))
                        }
                        .contextMenu {
                            if singletonStore.myArray[index].thisDate == nil {
                                if singletonStore.myArray[index].thisNeedsDone > 0 {
                                    Button {
                                        selectedRow = index
//these functions will add or remove a users id or initials to the appropriate property, and this updates values in my list view.

                                        if singletonStore.myArray[index].id != nil {
                                            //do this
                                        } else {
                                            //do that
                                        }
                                    } label: {
                                        Label{                     
//This is where my issue is - even though the items in the list view are updating, the label of the context menu is not updating until the row is reloaded


Text(singletonStore.myArray[index].initials != nil ? "This Label" : "That Label") } icon: {
                                                Image(systemName: "aqi.medium")
                                            }
                                    }
                                }
                            }
                        } //context menu close
                    } // list close
                    .listStyle(PlainListStyle())
        }    
}

My closures may be off, but its because I modified the code significantly on this platform to make is easier to follow. Nothing removed would affect this issue.

if there was a way to have opening the context menu update the lists selected row, that would solve the issue and I could use the selected row for the index of the singletonStores array objects property, but I may be approaching the problem from thinking the index is incorrect when the actual issue is the context menu is not being reloaded with the environment objects new information. Any help is always appreciated!

EDIT:

After some tinkering I further found that the issue must be related to the context menu itself not refreshing its data. I separated my views and used a @Viewbuilder function to return the needed view for the button - however it still does not refresh the context menus data.

EDIT 2:

currently (and subject to change) my SingletonStore class loads the data from another network class and publishes that data in the form of an array

    final class SingletonStore: ObservableObject {

     static private(set) var shared = singletonStore()

     static func reset() {
             shared = StagingStore()
     }
    

     @Published var myArray: [CustomObject] = []

     private func getMyData() {
           //uses other class and methods to retrieve and set data
          //works and updates view on refresh
     }
}

My View is called from a different View that is just a Tab bar controller, that code looks as follows:

    struct ContainerView: View {

    @StateObject var singletonStore = SingletonStore.shared

    var body: some View {
        TabView{
            GenericView().environmentObject(singletonStore)
                .tabItem {
                    Label("This View", systemImage: "camera.metering.matrix")
                }
 
    }
}
Charles
  • 95
  • 11
  • I believe you should bind the Text's text to a published property in view model, and when you select a row call a method in view model, that would change the text according to to whatever row you have selected, once the published property changes that would redraw the UI. That should fix it – Saket Kumar Sep 29 '22 at 17:03
  • 1
    Instead of iterating over the indexes, why don't you iterate over the array itself? Just use `List(singletonStore.myArray, ...) { index in`, then read it using `ItemGridView(item: item, ...)`. – HunterLion Sep 29 '22 at 17:05
  • @saketkumar could you further describe this? How I am thinking of it is if I did bind the Text's property to a published property in the view model it would update that text for every case where the context menu is displayed. Unless I opted to use an array that changed sized to be equal to myarrays size. Then I could fulfill and toggle each object. – Charles Sep 29 '22 at 17:29
  • @HunterLion I used .myArray.indicies and an index to access each object because of issues presented by the list being lazy loaded and (even though each item in myArray is unique) certain actions referencing the incorrect row that was being recycled. I actually would prefer to use the object itself and drop the need for the index, and I have gone back to that way a couple times to verify it still will not work – Charles Sep 29 '22 at 17:31
  • @Charles: What you show per cell, what you hide per cell that entirely depend on the datasource and binding. I can help with a code snippet. Just drop me the model screenshot, or I will have to assume that too. – Saket Kumar Sep 29 '22 at 17:39
  • @SaketKumar the information is added above – Charles Sep 29 '22 at 18:54
  • Am working on it, will let you know soon. – Saket Kumar Sep 29 '22 at 19:15
  • Yup found it, answering it now! – Saket Kumar Sep 29 '22 at 19:37

1 Answers1

1

I have created a demo project inspired by your sample code above. In order to reproduce the issue I had to improvise some.

List is binded to a collection, when any of the item change, view hierarchy gets built and changes reflects.

Code for reference is as follow. Notice I am calling a view model method from button action, which makes a change in the collection that is binded.

import Foundation

class ContentViewModel: ObservableObject {
    @Published var myArray: [Item] = []
    init() {
        for i in 0...100 {
            let obj = Item(id: UUID().uuidString, thisDate: Date.now, thisNeedsDone: i, initials: "That Label")
            myArray.append(obj)
        }
       
    }
    
    func updateTheRow(item: Item) {
        
        if let indexOfItem = myArray.firstIndex(where: { obj in
            obj.id == item.id
        })
        {
            myArray[indexOfItem] = item
        }
    }
}


struct Item: Identifiable, Equatable, Hashable {
    var id: String
    var thisDate: Date?
    var thisNeedsDone: Int
    var initials: String?
}





import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()
    let columns = [
           GridItem(.adaptive(minimum: 80))
       ]
    var body: some View {
        VStack {
            
            List(viewModel.myArray, id: \.self) { item in
                
                LazyVGrid(columns: columns) {
                    VStack{
                        Text("My bad")
                    }
                }
                .contextMenu {
                    if item.thisDate != nil {
                        if item.thisNeedsDone > 0 {
                            Button {
                                //these functions will add or remove a users id or initials to the appropriate property, and this updates values in my list view.
                                var modifiedItem = item
                                modifiedItem.initials = "Modified Label"
                                viewModel.updateTheRow(item: modifiedItem)
                            } label: {
                                Label{
                                    //This is where my issue is - even though the items in the list view are updating, the label of the context menu is not updating until the row is reloaded
                                    Text(item.initials!) } icon: {
                                        Image(systemName: "aqi.medium")
                                    }
                            }
                        }
                    }
                } //context menu close
            } // list close
            .listStyle(PlainListStyle())
        }
    }
}
Saket Kumar
  • 1,157
  • 2
  • 14
  • 30
  • I am reviewing you code against mine and the only difference I can see is that I am passing a StateObject of ContentViewModel from a parent view to what would be your ContentView into an EnvironmentObject. – Charles Sep 29 '22 at 22:43
  • 1
    After refactoring my code to eliminate lazy loading (allowing me to not use indices on the singleton array, a major part of the issue here) and removing the environment object passing from the container tab view to the content view, I was able to structure everything similarly here. You did a great job in your assumptions on datatype and stored properties. – Charles Sep 30 '22 at 16:33
  • I tried, glad that you found a way around. – Saket Kumar Sep 30 '22 at 18:24