We have a simple SwiftUI app for macOS. Nothing fancy, just LazyVGrid with numbers inside ScrollView. And we run it on a MacBook Pro M2 Max 16' fullscreen. I can hit about 50FPS. Scrolling is not smooth. How can I get 120FPS on 120Hz Pro motion display?
Here is the code:
struct GridTest: View {
@State var data = (1...30000).map { "\($0)" }
//doesn't matter if this is adaptive or fixed..it is still slow
//there is no way to hit 120Hz on mac?
let columns = [GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90)),
GridItem(.fixed(90))
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(data, id: \.self) {item in
Text(item).frame(width: 100, height: 30)
}
}.frame(width: 1700)
}
}
}//GridTest
FPS measured with Quartz Debug. Can be downloaded from Apple web in more section, Additional_Tools_for_Xcode_15.
It seems you have confused the term lazy
with the term reusable
(also known as recyclable
).
Lazy
means it won't be initialized at the assignment point. Instead, it will be initialized at the first call. So, for example, using a range of 1...30_000_000
with a LazyVGrid
, will NOT immediately take up 8GB of your RAM, but it definitely WILL take it all when you call all of them gradually, like by scrolling to the end.
But Recycling
, on the other hand, means creating a limited number of objects and using them again and again instead of having a huge number of objects.
You should use the lazy
method when you don't need all items instantly and use Recycling
when the elements can be reusable.
In your case (I have bumped the numbers to make it extreme), the following line has a heavy impact and takes around 500MB memory right at the point:
@State var data = (1...30_000_000).map { "\($0)" } // š Using `map` will cause all 30,000 items to be created instantly!
It can be lazy
since we don't need them all at once, like:
@State var data = (1...30_000_000).lazy.map { "\($0)" } // š Using `lazy.map` will postpone the map operation of each individual element
And you can use a View that recycles its content like the native SwiftUI List
or a wrapped counter part from the AppKit a UIKit like CollectionView which is totally fine and standard to use and has a lot of built-in performance friendly APIs.
For example You can do the math an calculate how many items you need for each row and chunk the data into separate reusable rows:
struct GridTest: View {
@State var data = (1...30_000_000).lazy.map(String.init) // š Using a lazy collection
let columns = Array(repeating: GridItem(.fixed(90)), count: 15)
var verticalCount: Int { columns.count }
var chuckedRange: Range<Int> { 0..<data.count/verticalCount }
var body: some View {
List(chuckedRange, id: \.self) { rowNumber in // š Using a recycling View
LazyVGrid(columns: columns) {
let range = (0..<verticalCount).map { $0 + rowNumber*verticalCount }
ForEach(range, id: \.self) { index in
Text(data[ClosedRange<Int>.Index.inRange(index)])
}
}
}
}
}
You can also reduce the number of final rendering elements by making compositingGroups
and even make them render offscreen by using drawingGroup
modifier on each line but it should be benchmarked for the reliable results!
LazyVGrid(columns: columns) { ... }
.compositingGroup()
.drawingGroup()
SwiftUI is just a tool to make the basic UI tasks easy. If you have some advanced UI logic to implement (like showing millions of data in an infinite canvas), you should implement some more GPU friendly mechanism like using Canvas
, Metal
or etc. with prefetching, caching and even async loading mechanisms like what the AsyncDisplayKit used to do.
Also
There was a WWDC session (Unfortunately it was a long time ago and I don't remember the session number) that described how Apple implements the Photos App collection view with hundreds of Images to work at maximum FPS and even interactive by holding finger on small pixels to preview the image.
That can be good start if someone can mention the link.
I Hope this helps
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With