DEV Community

Cover image for List or LazyVStack: Choosing the Right Lazy Container in SwiftUI
Fatbobman( 东坡肘子 )
Fatbobman( 东坡肘子 )

Posted on • Originally published at fatbobman.com

List or LazyVStack: Choosing the Right Lazy Container in SwiftUI

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 of ScrollView and LazyVStack, typically used in conjunction with ForEach to dynamically provide data. It's worth mentioning that the features of LazyVStack discussed here are largely applicable to LazyHStack and partially to other Lazy series containers. We will focus on a macro-level comparison and discussion, rather than the specific details of API usage.


Don’t miss out on the latest updates and excellent articles about Swift, SwiftUI, Core Data, and SwiftData. Subscribe to Fatbobman’s Swift Weekly and receive weekly insights and valuable content directly to your inbox.


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 to List subviews.
  • The onDelete and onMove functionalities of ForEach are only effective within List.
  • 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()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

list-row-animation-issue

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:

  1. Supports special display modes such as Sidebars.
  2. Navigation containers provide excellent support for the data sources and row tags bound to List.
  3. Some components, such as NavigationLink, have a unique style when displayed within List.

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 one Button element in a row view (this can be resolved by adjusting the buttonStyle).
  • 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:

  1. 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.
  2. 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:

  1. Performance: Special attention should be paid to scenarios requiring extensive jumps and significant differences in subview heights.
  2. Scroll Control: The precision and the ability to detect subview positions.
  3. Layout Flexibility: The capability to adapt to complex UI designs and compatibility with animations and transitions.
  4. Preset Functionality: Whether there is a need to utilize built-in features provided by the system.
  5. Cross-platform Compatibility: Performance across different Apple ecosystems.
  6. 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.

The original article was published on my blog Fatbobman's Blog.

Top comments (0)