SwiftUI LazyVStack: a closer look

DevTechie Inc
Apr 6, 2022

LazyVStack is a container view which is used to layout child views in vertical order, just like VStack. ‘Lazy’ keyword distinguishes LazyVStack from VStack.

In VStack, all views are rendered and loaded in memory as soon as view is initialized and appears on screen.

In LazyVStack, only views that are visible on screen will be rendered. Views are loaded into the memory and rendered as they become visible which makes view rendering much more performant.

Let’s explore this difference with an example:

We will create a ScrollView and add VStack inside that. This VStack will contain ForEach loop to create 100 Text views with current time to seconds. In order to get time from Date object, we will create instance of DateFormatter and pass it in formatter property of Text view.

struct LazyVStackExample1: View {
    
    let dateFormatter: DateFormatter = {
            let formatter = DateFormatter()
            formatter.dateFormat = "HH:mm:ss z"
            return formatter
        }()
    
    var body: some View {
        ScrollView {
        VStack {
                ForEach(1...100, id: \.self) { _ in
                    Text(Date(), formatter: dateFormatter)
                }
            }
        }
    }
}
Take a note of time in all Text views is same down to seconds so essentially they are all created and loaded all at once.

Let’s change this example to LazyVStack by replacing VStack keyword with LazyVStack.

struct LazyVStackExample2: View {
    
    let dateFormatter: DateFormatter = {
            let formatter = DateFormatter()
            formatter.dateFormat = "HH:mm:ss z"
            return formatter
        }()
    
    var body: some View {
        ScrollView {
        LazyVStack {
                ForEach(1...100, id: \.self) { _ in
                    Text(Date(), formatter: dateFormatter)
                }
            }
        }
    }
}
Notice timestamps on Text as we scroll down to see more views. They are different meaning they are loaded when we started scrolling the view.

LazyVStack and VStack are similar in many ways with some subtle differences. For example, VStack only takes the space it needs to display the content whereas LazyVStack fills horizontal space as we can see with the example below.

We will create two cards, one with VStack and another with LazyVStack. All modifiers will be same for both containerviews except their name. Check out the difference.

struct LazyVStackExample3: View {
    
    var body: some View {
        VStack(spacing: 50) {
            Text ("VStack Example")
                .bold()
            
            VStack(spacing: 20) {
                Text("DevTechie Courses")
                    .font(.largeTitle)
                Text("Video Courses on iOS")
            }
            .foregroundColor(.white)
            .padding()
            .background(RoundedRectangle(cornerRadius: 20).fill(Color.orange))
            
            
            Text ("LazyVStack Example")
                .bold()
            LazyVStack(spacing: 20) {
                Text("DevTechie Courses")
                    .font(.largeTitle)
                Text("Video Courses on iOS")
            }
            .foregroundColor(.white)
            .padding()
            .background(RoundedRectangle(cornerRadius: 20).fill(Color.orange))
            
        }
    }
}
LazyVStack ended up filling space horizontally where as VStack only took the space needed to display its content.

Just like VStack, LazyVStack supports alignment for child views with just a little difference. Because LazyVStack takes entire width provided by the parent, alignment is more apparent. Take a look at the example below:

struct LazyVStackExample5: View {
    
    @State private var alignmentProp: HorizontalAlignment = .center
    
    var body: some View {
        VStack {
            Text("VStack")
                .bold()
                .font(.largeTitle)
            
            VStack(alignment: alignmentProp) {
                Capsule()
                    .fill(.orange)
                    .frame(width: 100, height: 50)
                
                Capsule()
                    .fill(.teal)
                    .frame(width: 200, height: 50)
                
                Capsule()
                    .fill(.mint)
                    .frame(height: 50)
                
                
            }
            .padding()
            .animation(.easeInOut, value: alignmentProp)
            
            Text("LazyVStack")
                .bold()
                .font(.largeTitle)
            
            LazyVStack(alignment: alignmentProp) {
                Capsule()
                    .fill(.orange)
                    .frame(width: 50, height: 50)
                
                Capsule()
                    .fill(.teal)
                    .frame(width: 100, height: 50)
                
                Capsule()
                    .fill(.mint)
                    .frame(width: 150, height: 50)
                
                
            }
            .padding()
            .animation(.easeInOut, value: alignmentProp)
            
            HStack {
                Button("Leading") {
                    alignmentProp = .leading
                }
                
                Button("Center") {
                    alignmentProp = .center
                }
                
                Button("Trailing") {
                    alignmentProp = .trailing
                }
            }
        }
    }
}
Notice that in case of VStack we have to make one capsule wide enough so rest capsule views can animate to alignment changes
Another difference between VStack and LazyVStack is that LazyVStack provides another parameter in its initializer called pinnedViews . PinnedViews is an optionSet that can be used to pin views to the bounds of ScrollView and can be used to have section header and footers. PinnedViews parameter is used with ScrollView and Sections.

Let’s take a look at pinnedView with an example. We will create DevTechie courses page here and will define model and sample data for that.

First, we will need course category so we can show courses sectioned by each category. This enum will be of String type so we can access its rawValue to display category name. We will make this enum to conform to CaseIterable protocol so we can iterate over all cases of enum.

enum CourseCategory: String, CaseIterable {
    case swiftUI = "SwiftUI"
    case machineLearning = "Machine Learning"
    case iOS = "iOS"
    case swift = "Swift"
}
Next, we will create course model, where model will conform to identifiable protocol so SwiftUI can uniquely identify each row.

struct DevTechieCourse: Identifiable {
    let id = UUID()
    var name: String
    var category: CourseCategory
}
DevTechieCourse model will also have an extension which will provide sample data to work with along with a function which will return filtered courses by supplied category. (Don’t be scared of sampleCourses static variable it’s repeated so we have enough rows to scroll through 😃)

extension DevTechieCourse {
    static func getCourses(by category: CourseCategory) -> [DevTechieCourse] {
        sampleCourses.filter { course in
            course.category == category
        }
    }
    
    static var sampleCourses: [DevTechieCourse] {
        [
            DevTechieCourse(name: "SwiftUI Deep Dive", category: .swiftUI),
            DevTechieCourse(name: "SwiftUI and Core Data", category: .swiftUI),
            DevTechieCourse(name: "Local Authentication in Swiftui", category: .swiftUI),
            DevTechieCourse(name: "Machine Learning in SwiftUI", category: .swiftUI),
            DevTechieCourse(name: "SwiftUI Deep Dive", category: .swiftUI),
            DevTechieCourse(name: "SwiftUI and Core Data", category: .swiftUI),
            DevTechieCourse(name: "Local Authentication in Swiftui", category: .swiftUI),
            DevTechieCourse(name: "Machine Learning in SwiftUI", category: .swiftUI),
            DevTechieCourse(name: "SwiftUI Deep Dive", category: .swiftUI),
            DevTechieCourse(name: "SwiftUI and Core Data", category: .swiftUI),
            DevTechieCourse(name: "Local Authentication in Swiftui", category: .swiftUI),
            DevTechieCourse(name: "Machine Learning in SwiftUI", category: .swiftUI),
            DevTechieCourse(name: "SwiftUI Deep Dive", category: .swiftUI),
            DevTechieCourse(name: "SwiftUI and Core Data", category: .swiftUI),
            DevTechieCourse(name: "Local Authentication in Swiftui", category: .swiftUI),
            DevTechieCourse(name: "Machine Learning in SwiftUI", category: .swiftUI),
            
            DevTechieCourse(name: "Machine Learning in iOS", category: .machineLearning),
            DevTechieCourse(name: "Computer Vision in iOS", category: .machineLearning),
            DevTechieCourse(name: "CreateML and CoreML", category: .machineLearning),
            DevTechieCourse(name: "Machine Learning in iOS", category: .machineLearning),
            DevTechieCourse(name: "Computer Vision in iOS", category: .machineLearning),
            DevTechieCourse(name: "CreateML and CoreML", category: .machineLearning),
            DevTechieCourse(name: "Machine Learning in iOS", category: .machineLearning),
            DevTechieCourse(name: "Computer Vision in iOS", category: .machineLearning),
            DevTechieCourse(name: "CreateML and CoreML", category: .machineLearning),
            DevTechieCourse(name: "Machine Learning in iOS", category: .machineLearning),
            DevTechieCourse(name: "Computer Vision in iOS", category: .machineLearning),
            DevTechieCourse(name: "CreateML and CoreML", category: .machineLearning),
            DevTechieCourse(name: "Machine Learning in iOS", category: .machineLearning),
            DevTechieCourse(name: "Computer Vision in iOS", category: .machineLearning),
            DevTechieCourse(name: "CreateML and CoreML", category: .machineLearning),
            
            DevTechieCourse(name: "iOS Development Fundamentals", category: .iOS),
            DevTechieCourse(name: "Protocol Oriented Programming", category: .iOS),
            DevTechieCourse(name: "iOS 15: What's New", category: .iOS),
            DevTechieCourse(name: "Codable Protocol in iOS", category: .iOS),
            DevTechieCourse(name: "iOS Development Fundamentals", category: .iOS),
            DevTechieCourse(name: "Protocol Oriented Programming", category: .iOS),
            DevTechieCourse(name: "iOS 15: What's New", category: .iOS),
            DevTechieCourse(name: "Codable Protocol in iOS", category: .iOS),
            DevTechieCourse(name: "iOS Development Fundamentals", category: .iOS),
            DevTechieCourse(name: "Protocol Oriented Programming", category: .iOS),
            DevTechieCourse(name: "iOS 15: What's New", category: .iOS),
            DevTechieCourse(name: "Codable Protocol in iOS", category: .iOS),
            DevTechieCourse(name: "iOS Development Fundamentals", category: .iOS),
            DevTechieCourse(name: "Protocol Oriented Programming", category: .iOS),
            DevTechieCourse(name: "iOS 15: What's New", category: .iOS),
            DevTechieCourse(name: "Codable Protocol in iOS", category: .iOS),
            
            DevTechieCourse(name: "Higher Order Function in Swift", category: .swift),
            DevTechieCourse(name: "Combine in Swift", category: .swift),
            DevTechieCourse(name: "Swift 5.5", category: .swift),
            DevTechieCourse(name: "Higher Order Function in Swift", category: .swift),
            DevTechieCourse(name: "Combine in Swift", category: .swift),
            DevTechieCourse(name: "Swift 5.5", category: .swift),
            DevTechieCourse(name: "Higher Order Function in Swift", category: .swift),
            DevTechieCourse(name: "Combine in Swift", category: .swift),
            DevTechieCourse(name: "Swift 5.5", category: .swift)
        ]
    }
}
Now when we have model and sample data ready, lets work on view. First we will create HeaderView which will server as section header view to display category name.

HeaderView will take title string as parameter and with the combination of a few modifiers, we will turn it into a cool looking header 😎

struct HeaderView: View {
    
    var title: String
    
    var body: some View {
        Text(title)
            .font(.largeTitle)
            .bold()
            .frame(maxWidth: .infinity, maxHeight: 50)
            .background(.ultraThinMaterial)
    }
}
Last but not the least, we will create our LazyVStackExample4 view which will have our LazyVStack with pinnedViewsfor sectionHeaders option. We will wrap this view inside a ScrollView so we can scroll though the list. By iterating over enum values, we will get section for each course and display course’s name accordingly.

struct LazyVStackExample4: View {
    
    var body: some View {
        NavigationView {
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 10, pinnedViews: [.sectionHeaders]) {
                    ForEach(CourseCategory.allCases, id: \.self) { category in
                        Section(header: HeaderView(title: category.rawValue)) {
                            ForEach(DevTechieCourse.getCourses(by: category)) { course in
                                Text("▶ \(course.name)")
                                    .font(.title2)
                                    .padding()
                            }
                        }
                    }
                }
            }
            .navigationTitle("DevTechie Courses")
        }
    }
}
It’s time to build and run 😄

Just like sectionHeaders, we can also pin sectionFooters.

We will add another View for footer:

struct FooterView: View {
    
    var title: String
    
    var body: some View {
        Text(title)
            .bold()
            .frame(maxWidth: .infinity)
            .frame(height: 50)
            .background(.ultraThinMaterial)
    }
}
Next we will add footer view to our section along with a few more changes as shown in bold text below:

struct LazyVStackExample4: View {
    
    var body: some View {
        NavigationView {
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 10, pinnedViews: [.sectionHeaders, .sectionFooters]) {
                    ForEach(CourseCategory.allCases, id: \.self) { category in
                        let courses = DevTechieCourse.getCourses(by: category)
                        Section(header: HeaderView(title: category.rawValue), footer: FooterView(title: "Total \(courses.count) courses.")) {
                            ForEach(courses) { course in
                                Text("▶ \(course.name)")
                                    .font(.title2)
                                    .padding()
                            }
                        }
                    }
                }
            }
            .navigationTitle("DevTechie Courses")
            .edgesIgnoringSafeArea(.bottom)
        }
    }
}
With that, we have reached the end of this article. Thank you once again for reading, if you liked it, don’t forget to subscribe our newsletter.