TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

@ObservedObject not changing on child view and it makes no sense

Forums > SwiftUI

I have a main view with a @StateObject

@StateObject private var restSyncProcessing : RestSyncProcessing

This object has two @Published properties

    @Published var syncItems = [SyncItem]()
    @Published var syncProgress = [SyncMessage]()

I then have two views on this main view that use @ObservedObject

    @ObservedObject var restSyncProcessing : RestSyncProcessing

Inside one view, it sill display syncProgress messages as they are added to the syncProgress. These are displayed in a scrollview. This works great.

            ScrollView {
                ForEach(restSyncProcessing.syncProgress) { item in
                    SyncLogView(syncProgress: item)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding()
            }

This scroll view uses a child view SyncLogView to display the messages.. it has a lot of code, so I don't want to paste all of it here. This view updates when the syncing is going on. No problem here.

The issue is whith my other child view which is in my main view which will display changes to the syncItems array. As items get sync'd both of these arrays are updated.

This works in my Child View:

            ScrollView{
                ForEach(restSyncProcessing.syncItems, id: \.id) { item in
                    HStack{
                        Text(item.syncItemTitle)
                            .bold()

                        Spacer()
                        if item.syncItemSyncing {
                            ProgressView()
                                .frame(height: 15)
                                .progressViewStyle(.circular)
                        }
                        else
                        {
                            Text(item.syncItemCount, format: .number)
                                .padding(.trailing, 10)
                                .bold()
                        }
                    }
                    .padding(4)
                }
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding(2)

But this doesn't! It will not update the UI

            ScrollView{
                ForEach(restSyncProcessing.syncItems, id: \.id) { item in
                    SyncItemView(item: item)
                }
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding(2)

My SyncItemView is the same as the view above:

              struct SyncItemView: View {
                  let item : SyncItem

                  var body: some View {
                      HStack{
                          Text(item.syncItemTitle)
                              .bold()
                          Spacer()
                          if item.syncItemSyncing {
                              ProgressView()
                                  .frame(height: 15)
                                  .progressViewStyle(.circular)
                          }
                          else
                          {
                              Text(item.syncItemCount, format: .number)
                                  .padding(.trailing, 10)
                                  .bold()
                          }
                      }
                      .padding(4)
                  }
              }

Why will the ForEach not work and update the items. They do for the other array I am changing and publishing?

Any ideas?

   

@bitco is out of sync

Why will the ForEach not work and update the items. They do for the other array I am changing and publishing?

This is a guess.

// This doesn't work! Why?
ScrollView{
    //                                                👇🏼 I am guessing this doesn't change
    ForEach(restSyncProcessing.syncItems, id: \.id) { item in SyncItemView(item: item) }
}

View Factory

What does a ForEach do?

I think of ForEach as a view factory. Its job is to make other views. In your case, you're asking ForEach to make SyncItemViews for as many items as there are in the restSyncProcessing.syncItems array.

If the number of items in this array doesn't change, the parent view will not reinvoke the ForEach factory to redraw views.

Maybe you can test this by adding a dashboard just above the ScrollView() ?

Text("Item count: \(restSyncProcessing.syncItems.count)" ) // <-- Does this value change?

Why does the other ForEach update?

But you have another chunk that seems to work. Yikes, why does this code work?

// So why does this work?
ForEach(restSyncProcessing.syncItems, id: \.id) { item in
    HStack{
        //            👇 Does this change?
        Text(item.syncItemTitle).bold()  // <- Does this change causing this ForEach to be re-invoked?

        // Do the item's internal properties change causing a redraw?
        //             👇🏼  Does this change?           
        if item.syncItemSyncing {  ProgressView().frame(height: 15).progressViewStyle(.circular)  }
        //                     👇🏼  Does this change?
        else { Text(item.syncItemCount, format: .number).padding(.trailing, 10)  }
    }
}

Another guess. The parent view redraws itself when elements inside it change. Here's a guess. While the item itself is not changing, some element inside the item is changing. This is what causes your view to redraw.

View Factory

Here's some other notes on ForEach.

See -> View Factory
or See -> View Factory
or See -> View Factory

Keep Coding!

Please return here and share what you learned with the rest of us.

   

Interesting... I think you are correct. I just like the seperation of concearns of that view so I can just work on it in a seperate preview to make it look the way I want.

The array is defined at the beginning of the view, only the data changes withing the array.

Is there a way that I can get the ForEach to understand that the array data has changed and needs to be drawn again?

   

I was able to make this work by doing the following. Do you see any issues with what I might be doing?

    private var _syncItemSyncing : Bool = false

    var syncItemSyncing : Bool
    {
        get { return _syncItemSyncing }
        set {
            id = UUID().uuidString
            _syncItemSyncing = newValue
        }
    }

I am of course doing my ForEach based on the id. From what you pointed out, my id never changed because I am using a class. As a struct it would generate a new id because it is recreated everytime you make a modification to a value inside the struct. So, as a class I just changed the id anytime the syncing was started or completed, which in turn will make the ForEach re-draw that item.

This works. The question is, is it safe?

   

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.