Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI ForEach not correctly updating in scrollview

Tags:

ios

swiftui

I have a SwiftUI ScrollView with an HStack and a ForEach inside of it. The ForEach is built off of a Published variable from an ObservableObject so that as items are added/removed/set it will automatically update in the view. However, I'm running into multiple problems:

  1. If the array starts out empty and items are then added it will not show them.
  2. If the array has some items in it I can add one item and it will show that, but adding more will not.

If I just have an HStack with a ForEach neither of the above problems occur. As soon as it's in a ScrollView I run into the problems.

Below is code that can be pasted into the Xcode SwiftUI Playground to demonstrate the problem. At the bottom you can uncomment/comment different lines to see the two different problems.

If you uncomment problem 1 and then click either of the buttons you'll see just the HStack updating, but not the HStack in the ScrollView even though you see init print statements for those items.

If you uncomment problem 2 and then click either of the buttons you should see that after a second click the the ScrollView updates, but if you keep on clicking it will not update - even though just the HStack will keep updating and init print statements are output for the ScrollView items.

import SwiftUI
import PlaygroundSupport
import Combine

final class Testing: ObservableObject {
    @Published var items: [String] = []

    init() {}

    init(items: [String]) {
        self.items = items
    }
}

struct SVItem: View {
    var value: String

    init(value: String) {
        print("init SVItem: \(value)")
        self.value = value
    }

    var body: some View {
        Text(value)
    }
}

struct HSItem: View {
    var value: String

    init(value: String) {
        print("init HSItem: \(value)")
        self.value = value
    }

    var body: some View {
        Text(value)
    }
}

public struct PlaygroundRootView: View {
    @EnvironmentObject var testing: Testing

    public init() {}

    public var body: some View {
        VStack{
            Text("ScrollView")
            ScrollView(.horizontal) {
                HStack() {
                    ForEach(self.testing.items, id: \.self) { value in
                        SVItem(value: value)
                    }
                }
                .background(Color.red)
            }
            .frame(height: 50)
            .background(Color.blue)
            Spacer()
            Text("HStack")
            HStack {
                ForEach(self.testing.items, id: \.self) { value in
                    HSItem(value: value)
                }
            }
            .frame(height: 30)
            .background(Color.red)
            Spacer()
            Button(action: {
                print("APPEND button")
                self.testing.items.append("A")
            }, label: { Text("APPEND ITEM") })
            Spacer()
            Button(action: {
                print("SET button")
                self.testing.items = ["A", "B", "C"]
            }, label: { Text("SET ITEMS") })
            Spacer()
        }
    }
}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = UIHostingController(
    // problem 1
    rootView: PlaygroundRootView().environmentObject(Testing())

    // problem 2
    // rootView: PlaygroundRootView().environmentObject(Testing(items: ["1", "2", "3"]))
)

Is this a bug? Am I missing something? I'm new to iOS development..I did try wrapping the actual items setting/appending in the DispatchQueue.main.async, but that didn't do anything.

Also, maybe unrelated, but if you click the buttons enough the app seems to crash.

like image 534
Jon Carl Avatar asked Dec 13 '19 04:12

Jon Carl


3 Answers

Just ran into the same issue. Solved with empty array check & invisible HStack

ScrollView(showsIndicators: false) {
    ForEach(self.items, id: \.self) { _ in
        RowItem()
    }

    if (self.items.count == 0) {
        HStack{
            Spacer()
        }
    }
}
like image 159
TheLegend27 Avatar answered Nov 16 '22 01:11

TheLegend27


It is known behaviour of ScrollView with observed empty containers - it needs something (initial content) to calculate initial size, so the following solves your code behaviour

@Published var items: [String] = [""]

In general, in such scenarios I prefer to store in array some "easy-detectable initial value", which is removed when first "real model value" appeared and added again, when last disappears. Hope this would be helpful.

like image 29
Asperi Avatar answered Nov 16 '22 03:11

Asperi


For better readability and also because the answer didn't work for me. I'd suggest @TheLegend27 answer to be slightly modified like this:

if self.items.count != 0 {

   ScrollView(showsIndicators: false) {
       ForEach(self.items, id: \.self) { _ in
           RowItem()
       }
   }

}
like image 29
Simon Avatar answered Nov 16 '22 03:11

Simon