In the world of SwiftUI, List
and LazyVStack
, as two core lazy containers, offer robust support for developers to display large amounts of data. However, their similar performance in certain scenarios often causes confusion among developers when making a choice. This article aims to analyze the characteristics and advantages of these two components to help you make a better decision.
Note: The
LazyVStack
mentioned in this article primarily refers to the combined use ofScrollView
andLazyVStack
, typically used in conjunction withForEach
to dynamically provide data. It’s worth mentioning that the features ofLazyVStack
discussed here are largely applicable toLazyHStack
and partially to otherLazy
series containers. We will focus on a macro-level comparison and discussion, rather than the specific details of API usage.
Underlying Implementation: Different Origins, Divergent Architectures
Throughout the evolution of SwiftUI, List
and LazyVStack
have played different roles and hold distinct positions. List
, as a veteran lazy container from the first version of SwiftUI, was not only the sole official lazy component at the time but also set the tone for the data loading processes of many subsequent lazy containers. In contrast, LazyVStack
debuted in the second year alongside other lazy containers such as LazyHStack
, LazyVGrid
, and LazyHGrid
. Although both components perform similarly in certain scenarios, their underlying architectures are vastly different.
Essentially, List
is a clever encapsulation of UIKit/AppKit components by Apple. From iOS 13 to iOS 15, it relied on the UITableView
; starting with iOS 16, its implementation shifted to the more flexible UICollectionView
. In stark contrast, LazyVStack
and other Lazy+
series containers are native SwiftUI implementations, and their underlying mechanics do not depend on any specific UIKit/AppKit components.
This fundamental difference in implementation is not just a technical detail but is also the root cause of their differing performance in several key aspects. Whether it’s performance, feature richness, customization flexibility, or layout logic, List
and LazyVStack
each exhibit unique characteristics due to their distinct underlying architectures. Understanding this is crucial for developers when choosing and using these two components.
Styling and Presets: Feature-Rich vs. Minimalist Flexibility
LazyVStack
, like its cousin VStack
, focuses on the fundamental function of layout. As a pure layout container in SwiftUI, it strictly adheres to preset layout rules, orderly arranging subviews without arbitrarily adding extra effects. This minimalist design offers developers the greatest degree of freedom.
In contrast, the design philosophy behind List
is markedly different. It is not just a container but a feature-rich UI component. List
comes with multiple preset visual templates, allowing developers to easily switch between different styles using listStyle
. However, the advantages of List
go beyond visual effects; it also offers a series of unique interactive capabilities:
- Swipe actions (
swipeActions
) are exclusive toList
subviews. - The
onDelete
andonMove
functionalities ofForEach
are only effective withinList
. - Built-in edit mode support makes gesture-based item reordering possible, a unique feature in SwiftUI.
For scenarios requiring swipe menus, or system-like delete and move editing features, List
is undoubtedly the best choice. Not only is its code concise, but it is also deeply optimized for the entire Apple ecosystem. Additionally, the rich construction methods of List
(including integration with ForEach
and data binding) significantly enhance development efficiency, especially for beginners or prototype development.
However, the preset features of List
also introduce certain limitations. Although List
has received more preset templates with updates to SwiftUI, as of iOS 18, Apple has yet to open up complete customization capabilities for List
styles. In contrast, LazyVStack
is like a blank canvas; although it lacks preset modes, it offers unlimited possibilities for developers’ creativity.
In summary, Apple’s positioning of these two components is distinctly different: List
is a multifunctional container with default styles and behaviors, while LazyVStack
is a purely flexible layout tool.
Layout and Customization: Balancing Flexibility and Constraints
LazyVStack
, as a lazy container in SwiftUI, has unique characteristics in terms of layout, particularly when it comes to handling the height of subviews. Unlike VStack
, LazyVStack
uses the ideal size of the subviews when their height is not explicitly specified. This feature is very evident in practice:
struct ContentView: View {
var body: some View {
LazyVStack {
Rectangle()
}
}
}
In the code above, the Rectangle
will only display as a rectangle with a height of 10 (the default ideal size for shapes), rather than filling the available space as it would in a VStack
.
This size determination logic aligns with ScrollView
in terms of layout in the scrollable direction. Even when nested in a VStack
, the Rectangle
still maintains a height of 10:
ScrollView {
VStack {
Rectangle()
}
}
For a deeper understanding of SwiftUI’s layout size determination mechanism, I recommend reading SwiftUI Layout: The Mystery of Size.
As a pure layout container, LazyVStack
offers developers tremendous freedom, allowing for a more precise recreation of design styles. In contrast, the presentation of List
is influenced by multiple factors, including the selected style, the container environment, and the operating system platform. Developers have limited ability to intervene in the styling of List
, and its layout tends to be more formulaic.
Adjusting the appearance and behavior of List
primarily relies on specialized view decorators provided by Apple. Although these decorators are enriched with updates to SwiftUI versions, some complex layouts may be difficult to achieve or require workaround methods in earlier versions of SwiftUI.
Due to differences in underlying implementation, List
has limitations in handling dynamic changes in row height. For example:
struct ContentView: View {
@State var high = false
var body: some View {
List{
Toggle(isOn: $high.animation()){}
Rectangle()
.frame(height:high ? 200 : 100)
}
}
}
For scenarios involving dynamic height changes, it is recommended to avoid using List
.
Moreover, List
has restrictions on the types of transition animations for subviews (rows). For scenarios that require special animations and transition effects, LazyVStack
often provides greater flexibility and better performance.
Collaboration with Other Containers: The Advantage of Context Awareness
Despite some developers’ reservations about the implementation of List
, believing it does not fully meet specific needs, leading them to consider wrapping UIKit/AppKit components themselves to create better-suited alternatives. While this approach can indeed better meet personalized needs in some cases, we should not overlook the considerable efforts Apple has made to adapt List
for multi-platform compatibility and specific requirements.
SwiftUI includes an important yet undisclosed mechanism—context awareness. Official containers and components can intelligently sense their environment and automatically adjust their presentation styles based on different contexts. This unique capability is one of the significant advantages of List
over LazyVStack
.
List
works seamlessly with navigation containers:
- Supports special display modes such as Sidebars.
- Navigation containers provide excellent support for the data sources and row tags bound to
List
. - Some components, such as
NavigationLink
, have a unique style when displayed withinList
.
These features not only significantly reduce development workload but also achieve an interactive experience that LazyVStack
cannot match.
For a deeper understanding of the collaboration between
List
and navigation containers, I recommend reading The New Navigation System in SwiftUI and Adaptive Programmatic Navigation in SwiftUI.
However, this context awareness and default behavior can sometimes pose challenges:
- By default,
List
responds only to the action of oneButton
element in a row view (this can be resolved by adjusting thebuttonStyle
). - In early versions of SwiftUI, due to a lack of comprehensive programmatic navigation capabilities, controlling the style of
NavigationLink
was quite challenging.
In summary, when developers want to fully utilize the automatic sensing capabilities of system components to create interfaces consistent with the system style, List
is undoubtedly the better choice. It not only provides a familiar interactive experience for users but also allows developers to achieve higher code reuse across different platforms.
Scroll Control: Native and Workaround Solutions
In recent updates to SwiftUI, Apple has significantly enhanced scroll control capabilities by introducing a series of new APIs. However, these new features are mainly targeted at ScrollView
, with the development of List
lagging somewhat in this area. Currently, the official scroll control methods provided for List
are still limited to ScrollViewReader
.
Despite this, the underlying implementation of List
is based on mature UIKit components, offering developers an alternative route. By using third-party libraries such as SwiftUI-Introspect, developers can directly access the APIs of underlying UIKit components, thus enabling more control options. This method is not only applicable to scroll control but can also be used for customizing display styles.
Even so, starting with iOS 17, the combination of ScrollView
and LazyVStack
has far surpassed List
in terms of scroll control capabilities and convenience. Considering LazyVStack
’s inherent advantages in layout flexibility and animation support, when precise subview scrolling or varying visual effects based on subview positions are needed, LazyVStack
is undoubtedly the superior choice.
For a deeper understanding of the latest developments in scroll control APIs, I recommend reading Deep Dive into the New Features of ScrollView in SwiftUI 5 and The Evolution of SwiftUI Scroll Control APIs and Highlights from WWDC 2024.
Performance: Challenges and Trade-offs
Although SwiftUI has been around for six years, the performance of List
and LazyVStack
when handling large datasets still leaves room for improvement. Even with medium-sized datasets, due to differences in underlying implementations, both exhibit distinct performance characteristics.
LazyVStack
is essentially a VStack
with lazy loading capabilities, maintaining a complete container height (the total height of subviews plus spacing). To achieve lazy loading, it employs a dynamic height estimation method, calculating the overall height based on the number and height of subviews near the visible area. This mechanism leads to two significant issues:
- Rapidly scrolling to a specific position requires instantiating and calculating the height of all subviews prior to that position (i.e., evaluating their
body
), which can lead to noticeable performance degradation. - When there is a large height difference between subviews, rapid scrolling or large jumps may cause a white screen phenomenon (failure to calculate all necessary subviews in time).
In contrast, List
does not maintain a concept of complete content height at the SwiftUI level. During fast scrolling or extensive jumps, it intelligently selects the necessary subviews for instantiation and height calculation, significantly improving scrolling and jumping efficiency.
It is worth noting that using the id
modifier in List
may cause subviews to lose their lazy loading capabilities, which is not an issue with LazyVStack
. Therefore, when providing a data source for List
, it is recommended that the data type adhere to both the Identifiable
and Hashable
protocols, to avoid using the id
modifier as a scroll control tag.
Overall, with the same amount of data, List
typically demonstrates higher efficiency than LazyVStack
.
For a deeper understanding of performance optimization strategies, I recommend reading Tips and Considerations for Using Lazy Containers in SwiftUI and Demystifying SwiftUI List Responsiveness: Best Practices for Large Datasets.
Conclusion
The notion that “everything exists for a reason” is fully embodied in the design philosophies of List
and LazyVStack
. These two components each have their unique features, providing different solutions for SwiftUI developers. When choosing which one to use, developers need to consider several key factors comprehensively:
- Performance: Special attention should be paid to scenarios requiring extensive jumps and significant differences in subview heights.
- Scroll Control: The precision and the ability to detect subview positions.
- Layout Flexibility: The capability to adapt to complex UI designs and compatibility with animations and transitions.
- Preset Functionality: Whether there is a need to utilize built-in features provided by the system.
- Cross-platform Compatibility: Performance across different Apple ecosystems.
- Collaboration with Other SwiftUI Components: Especially in navigation and data binding.
There is no one-size-fits-all solution; the best choice often depends on the specific project requirements and development scenarios. A deep understanding of the strengths and weaknesses of these components will help developers make wise decisions when facing specific challenges.
In practice, there may be situations where both are used together, or they are used separately on different pages to maximize their respective strengths. The key is to flexibly choose and apply these tools based on specific application needs, target user groups, and performance requirements.
As SwiftUI continues to evolve, the capabilities and applicable scenarios of these components may change. Staying informed about new features and best practices will help maintain efficiency and innovation in SwiftUI development.