Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Performance issue when creating a list of 1000 elements in SwiftUI

Tags:

ios

swift

swiftui

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.

like image 737
johndoe Avatar asked Oct 06 '19 10:10

johndoe


3 Answers

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))))
    }
}
like image 147
LuLuGaGa Avatar answered Nov 15 '22 05:11

LuLuGaGa


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.

like image 39
Mojtaba Hosseini Avatar answered Nov 15 '22 05:11

Mojtaba Hosseini


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.

like image 34
Shial Avatar answered Nov 15 '22 05:11

Shial