7

I have a horizontal scroll view with lists. When scrolling horizontally, how to make the lists to snap to the edges.

struct RowView: View {
    var post: Post
    var body: some View {
        GeometryReader { geometry in
            VStack {
                Text(self.post.title)
                Text(self.post.description)
            }.frame(width: geometry.size.width, height: 200)
            //.border(Color(#colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1)))
            .background(Color(#colorLiteral(red: 0.721568644, green: 0.8862745166, blue: 0.5921568871, alpha: 1)))
            .cornerRadius(10, antialiased: true)
            .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
        }
    }
}

struct ListView: View {
    var n: Int
    @State var posts = [Post(id: UUID(), title: "1", description: "11"),
                        Post(id: UUID(), title: "2", description: "22"),
                        Post(id: UUID(), title: "3", description: "33")]

    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                ForEach(0..<self.n) { n in
                    RowView(post: self.posts[0])
                    //.border(Color(#colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1)))
                    .frame(width: geometry.size.width, height: 200)
                }
            }
        }
    }
}

struct ContentView: View {
    init() {
        initGlobalStyles()
    }

    func initGlobalStyles() {
        UITableView.appearance().separatorColor = .clear
    }

    var body: some View {
        GeometryReader { geometry in
            NavigationView {
                ScrollView(.horizontal) {
                    HStack {
                        ForEach(0..<3) { _ in
                            ListView(n: 1000)  // crashes
                                .frame(width: geometry.size.width - 60)
                        }
                    }.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0))
                }
            }
        }
    }
}

When I give the value of ListView(n: 1000), the view is crashing. The app launches and a white screen is shown for some time and then I get a black screen.

2019-10-06 15:52:57.644766+0530 MyApp[12366:732544] [Render] CoreAnimation: Message::send_message() returned 0x1000000e

How to fix this? My assumption is that it would be using something like dequeue cells like UITableView, but not sure why it's crashing.

2
  • Allocating this count of UI elements consumes a lot of memory. In this case, I suggest you use UITableView. Commented Oct 6, 2019 at 10:40
  • Will changing scroll view to list view make any difference? Commented Oct 6, 2019 at 11:02

3 Answers 3

8

There are a couple of issues with the code provided. The most important is you are not using a List just a ForEach nested in a ScrollView, which is like equivalent of placing 1000 UIViews in a UIStack - not very efficient. There is also a lot of hardcoded dimensions and quite a few of them are duplicates but nevertheless add a significant burden when the views are calculated.

I have simplified quite a lot and it runs with n = 10000 without crashing:

struct ContentView: View {

    var body: some View {
        GeometryReader { geometry in
            NavigationView {
                ScrollView(.horizontal) {
                    HStack {
                        ForEach(0..<3) { _ in
                            ListView(n: 10000)
                                .frame(width: geometry.size.width - 60)
                        }
                    }   .padding([.leading], 10)
                }
            }
        }
    }
}

struct ListView: View {

    var n: Int

    @State var posts = [Post(id: UUID(), title: "1", description: "11"),
                        Post(id: UUID(), title: "2", description: "22"),
                        Post(id: UUID(), title: "3", description: "33")]

    var body: some View {
        List(0..<self.n) { n in
            RowView(post: self.posts[0])
                .frame(height: 200)
        }
    }
}

struct RowView: View {

    var post: Post

    var body: some View {
        HStack {
            Spacer()
            VStack {
                Spacer()
                Text(self.post.title)
                Text(self.post.description)
                Spacer()
            }
            Spacer()
        }   .background(RoundedRectangle(cornerRadius: 10)
                            .fill(Color(#colorLiteral(red: 0.721568644, green: 0.8862745166, blue: 0.5921568871, alpha: 1))))
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

I want to emphasize how much using RoundedRectangle over cornerRadius modifier on the view itself means in performance. My List just went from lagging to completely smooth.
7

ScrollView don't reuse anything. But List do.

so change this:

ScrollView {
    ForEach(0..<self.n) { n in
        ,,,
    }
}

to this:

List(0..<self.n) { n in
    ,,,
}

SwiftUI 2.0

You can use Lazy stacks like the LazyVStack and the LazyHStack. So even if you use them with ScrollView, It will be smooth and performant.

💡 But you may still stay with the List because its items are reusable.

2 Comments

confirmed. Tried with ScrollView and the CPU skyrocketed to 200%.
Looking at the Xcode > Memory Graph while using both LazyVStack and List, I see that memory is not being deallocated after cells became invisible. So I am not sure about List "reuse" feature. Do we have any official docs about this behaviour ?
5

I've created SwiftUI horizontal list which loads views only for visible objects + extra elements as a buffer. Moreover, it exposes the offset parameter as binding so you can follow it or modify it from outside.

You can access source code here HList

Give it a go! This example is prepared in the swift playground.

Example use case

struct ContentView: View {
    @State public var offset: CGFloat = 0
    
    var body: some View {
        HList(offset: self.$offset, numberOfItems: 10000, itemWidth: 80) { index in
            Text("\(index)")
        }
    }
}

To see content being reused in action you could do something like this

struct ContentView: View {
    @State public var offset: CGFloat = 0
    
    var body: some View {
        HList(offset: self.$offset, numberOfItems: 10000, itemWidth: 80) { index in
            Text("\(index)")
        }
        .frame(width: 200, height: 60)
        .border(Color.black, width: 2)
        .clipped()
    }
}

If you do remove .clipped() at the end you will see how the extra component is reused while scrolling when it moves out of the frame.

Update

While the above solution is still valid, however, it is a custom component. With WWDC2020 SwiftUI introduces lazy components such as LazyHStack but keep in mind that:

The stack is “lazy,” in that the stack view doesn’t create items until it needs to render them onscreen.

Meaning, elements are loaded lazy but after that, they are kept in the memory.

My custom HList only refers to visible components. Not the one which already has appeared.

4 Comments

Thanks for the update! It seems like LazyHStack doesn't actually re-use cells like the optimized UITableView and UICollectionView. It creates items when they're needed but doesn't actually recycle them. From my understanding, your HList does re-use - correct? Isn't that the more optimized approach to take?
Hi @apunn, I believe so yes! My HList hold number of items needed to be visible at the screen plus your offset (buffer) you can set. I can really say that I'm using it everywhere in my apps. One improvement what you could do is to distinguish HStack and LazyHStack between iOS 14 and lower. Let me know if you need any help!
Thanks for your reply! Does this mean that LazyHStack is not re-using cells? I wonder why Apple made it this way. Because List re-uses cells, why don't the LazyHStack/LazyHGrids re-use cells?
LazyHStack is pure SwiftUI component so it will reuse views if the id or equatableview will tell it that it is the same. It will redraw it otherwise. List it build on top of Table for iOS. that's why it does reuse them.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.