17
votes

Goal

Get data to display in a scrollView

Expected Result

data shown in scrollview

Actual Result

a blank view

Alternative

use List, but it is not flexible (can't remove separators, can't have multiple columns)

Code

struct Object: Identifiable {
    var id: String
}

struct Test: View {
    @State var array = [Object]()

    var body: some View {
//        return VStack { // uncomment this to see that it works perfectly fine
        return ScrollView(.vertical) {
            ForEach(array) { o in
                Text(o.id)
            }
        }
        .onAppear(perform: {
            self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
        })
    }
}
5
I tested this on Xcode 11.1, iOS 13.1, Swift 5 and am getting the expected result (although the scroll view is on top rather than center). - sfung3
You also don't need the keyword return - sfung3
interesting, I'm on Xcode 11.2, iOS 13.2, Swift 5 - youjin
yeah I had a print statement before, hence the return - youjin
I filed FB8101550 with Apple using this code. Thanks for documenting this bug and making a small reproduction case. For posterity: github.com/jgale/SwiftUIScrollViewUpdateBug - Jeremy

5 Answers

21
votes

A not so hacky way to get around this problem is to enclose the ScrollView in an IF statement that checks if the array is empty

if !self.array.isEmpty{
     ScrollView(.vertical) {
          ForEach(array) { o in
               Text(o.id)
          }
     }
}
2
votes

I've found that it works (Xcode 11.2) as expected if state initialised with some value, not empty array. In this case updating works correctly and initial state have no effect.

struct TestScrollViewOnAppear: View {
    @State var array = [Object(id: "1")]

    var body: some View {
        ScrollView(.vertical) {
            ForEach(array) { o in
                Text(o.id)
            }
        }
        .onAppear(perform: {
            self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
        })
    }
}
2
votes

One hacky workaround I've found is to add an "invisible" Rectangle inside the scrollView, with the width set to a value greater than the width of the data in the scrollView

struct Object: Identifiable {
    var id: String
}

struct ContentView: View {
    @State var array = [Object]()

    var body: some View {
        GeometryReader { geometry in
            ScrollView(.vertical) {
                Rectangle()
                    .frame(width: geometry.size.width, height: 0.01)
                ForEach(array) { o in
                    Text(o.id)
                }
            }
        }
        .onAppear(perform: {
            self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
        })
    }
}
1
votes

The expected behavior occurs on Xcode 11.1 but doesn't on Xcode 11.2.1

I added a frame modifier to the ScrollView which make the ScrollView appear

struct Object: Identifiable {
    var id: String
}

struct ContentView: View {
    @State var array = [Object]()

    var body: some View {
        ScrollView(.vertical) {
            ForEach(array) { o in
                Text(o.id)
            }
        }
        .frame(height: 40)
        .onAppear(perform: {
            self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
        })
    }
}
1
votes

Borrowing from paulaysabellemedina, I came up with a little helper view that handles things the way I expected.

struct ScrollingCollection<Element: Identifiable, Content: View>: View {
    let items: [Element]
    let axis: Axis.Set
    let content: (Element) -> Content

    var body: some View {
        Group {
            if !self.items.isEmpty {
                ScrollView(axis) {
                    if axis == .horizontal {
                        HStack {
                            ForEach(self.items) { item in
                                self.content(item)
                            }
                        }
                    }
                    else {
                        VStack {
                            ForEach(self.items) { item in
                                self.content(item)
                            }
                        }
                    }
                }
            }
            else {
                EmptyView()
            }
        }
    }
}