atineoSE / LampMonitor

A sample iOS app to explore effects of lazy stacks and grids in SwiftUI

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Interactions of lazy stacks and lazy grids in SwiftUI

Experiment to observe the intreractions of lazy stacks and lazy grids in SwiftUI.

The app concept is to list a number of lamp switching records in different rooms.

The scrolling list is implemented as a:

  • List and

  • ScrollView with a LazyVStack.

The row has two different implementations:

  • based on VStacks and HStacks, as seen here
  • based on LazyVGrid with 2 columns, as seen here

This video capture shows the scrolling behavior when using each of the 4 combinations, namely:

  • List with VStack/HStack-based rows → ✅
  • List with LazyVGrid-based rows → ✅
  • ScrollView+LazyVStack with VStack/HStack-based rows → ✅
  • ScrollView+LazyVStack with LazyVGrid-based rows →❌

Scrolling of lamp records

Combining lazy stacks with lazy grids seems to result in a glitchy experience for the scrolling of rows. However, the same lazy grids work alright inside a List.

Here is a slow-motion capture of the scrolling behavior when combining a LazyVStack-based list with LazyVGrid-based rows.

Slow-motion scroll when combining lazy stack and lazy grid

Is this a SwiftUI bug?

Is this a SwiftUI bug or the result of an unsupported combination of UI elements?

In WWDC'2020 session on "Stack, Grids, and Outlines in SwiftUI", it is said:

On the other hand, making the stacks [...] lazy doesn't actually confer any benefits. The content is all visible at once as soon as the view lands on screen. So, everything has to be loaded at once, regardless of the container's default behavior. As a rule, if you aren't sure which type of stack to use, use VStack or HStack.

The intended use of a lazy stack is to help with performance bottlenecks related to content that is not visible all at once on the screen. In this example, each row in the lamp record is visible all at once, so we don't really need the lazy behavior benefits.

In that case, we can use VStack and HStack instead. However, there is no regular (i.e non-lazy) counterpart for a Grid element. All we have is a LazyVGrid and LazyHGrid. It looks like we can't leverage these lazy layouting primitives for inner UI elements inside of other lazy containers, despite the principle that states that:

SwiftUI's layout primitives were designed with composition in mind

The fix

It turns out that the use exposed above of the LazyVGrid is actually incorrect: instead of serving as a layouting primitive inside a row, the lazy grid wants to be given an arbitrarily large collection of elements to layout in the given column arrangement, which naturally profits from its lazy qualities.

Thanks to Chris Barker for pointing out the correct pattern!

This is what the list looks like now:

Smooth scrolling of a LazyVGrid

The actual code for the LazyVGrid looks like this:

        LazyVGrid(columns: columns, alignment: .leading, spacing: 8.0) {
            ForEach(lampRecords) { lampRecord in
                Text(lampRecord.date.formatted)
                    .font(.body)
                    .bold()
                Text("New state")
                    .font(.body)
                    .bold()
                Text(lampRecord.lamp.rawValue)
                    .font(.body)
                    .bold()
                lampImage(for: lampRecord)
                Divider().padding([.trailing], -8.0)
                Divider()
            }
        }

Note the double divider with extra negative padding to simulate the effect of an unbroken divider between the "rows".

If we further think about "tapping the row", things get tricky... since we actually have no rows but a grid! This is an indication that the LazyVGrid is not the right primitive for this kind of list-like interfaces.

Thanks for reading and happy coding! 🧑‍💻👩‍💻

About

A sample iOS app to explore effects of lazy stacks and grids in SwiftUI


Languages

Language:Swift 100.0%