Skip to content

Instantly share code, notes, and snippets.

@shial4
Last active May 10, 2023 03:22
Embed
What would you like to do?
SwiftUI Horizontal List - loads only visible elements. Efficiency for large collections.

Hey guys! Created SwiftUI HList. Where you can put thousand of elements inside a horizontal list but only visible elements are are loaded. You can preview how does it work in action using swift playground.

struct ClippedContentView: View {    
    var body: some View {
        HList(numberOfItems: 10000, itemWidth: 80) { index in
            Text("\(index)")
        }
        .frame(width: 200, height: 60)
        .border(Color.black, width: 2)
        .clipped()
    }
}

above code would generate list view (black frame) with clipped contend. It means it doesn't draw stuff beyond frame. ezgif-5-4d391e503230

Bellow example, shows you how the visible content is generated and how many views is loaded. Plus you can see how it does reuse elements.

struct ContentView: View {
    var body: some View {
        HList(numberOfItems: 10000, itemWidth: 80) { index in
            Text("\(index)")
        }
        .frame(width: 200, height: 60)
        .border(Color.black, width: 2)
    }
}

ezgif-5-c711d3474691

Let me know what you think! I hope it will solve you performance issues while loading thousand of elements in horizontal lists.

//: A UIKit based Playground for presenting user interface
import SwiftUI
import PlaygroundSupport
struct HList<Content: View>: View {
@State private var scrollOffset: CGFloat = 0
@State private var previousScrollOffset: CGFloat = 0
private let numberOfItems: Int
private let itemWidth: CGFloat
private let cellBuilder: (Int) -> Content
init(numberOfItems: Int, itemWidth: CGFloat, viewForCell cellBuilder: @escaping (Int) -> Content) {
self.numberOfItems = numberOfItems
self.itemWidth = itemWidth
self.cellBuilder = cellBuilder
}
var body: some View {
GeometryReader { geometry in
self.listView(geometry)
}
}
private func listView(_ geometry: GeometryProxy) -> some View {
let screenWidth: CGFloat = geometry.size.width
let canvasWidth: CGFloat = itemWidth * CGFloat(numberOfItems)
let itemsPerWindow: CGFloat = min(screenWidth / itemWidth, CGFloat(numberOfItems))
let initialWindowOffset: CGFloat = (canvasWidth / 2) - ((itemsPerWindow / 2) * itemWidth)
let startIndex = Int(floor(abs(scrollOffset) / itemWidth))
let endIndex = min(startIndex + Int(ceil(itemsPerWindow)) + 1, numberOfItems)
let visibleRange = startIndex ..< endIndex
let leadingPadding: CGFloat = CGFloat(startIndex) * itemWidth
let trailingPadding: CGFloat = canvasWidth - (CGFloat(endIndex) * itemWidth)
return HStack(spacing: 0) {
Spacer().frame(width: leadingPadding)
ForEach(visibleRange, id: \.hashValue) { index in
self.cellBuilder(index).frame(width: self.itemWidth).border(Color.red, width: 1)
}
Spacer().frame(width: max(-initialWindowOffset, trailingPadding))
}
.offset(x: initialWindowOffset)
.offset(x: scrollOffset)
.gesture(DragGesture().onChanged({ value in
self.scrollOffset = max((-2 * initialWindowOffset), min(0, self.previousScrollOffset + value.translation.width))
}).onEnded({ value in
withAnimation() {
self.scrollOffset = max((-2 * initialWindowOffset), min(0, self.previousScrollOffset + value.predictedEndTranslation.width))
self.previousScrollOffset = self.scrollOffset
}
}))
}
}
struct ContentView: View {
var body: some View {
HList(numberOfItems: 10000, itemWidth: 80) { index in
Text("\(index)")
}
.frame(width: 200, height: 60)
.border(Color.black, width: 2)
.clipped()
}
}
PlaygroundPage.current.setLiveView(ContentView())
@halilyuce
Copy link

Thanks for this solution mate! It's a really big problem and needed. I can recommend a fix for gestures. If you change .gesture with .simultaneousGesture, users can use tap gesture or other gestures too for items.

@BhushanaPannaga
Copy link

hey , i am facing an issue while implementing this HList, can get some help?

@shial4
Copy link
Author

shial4 commented Jul 28, 2022

@BhushanaPannaga how can I help you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment