Search code examples
swiftuiswiftui-sheet

Odd Behaviour: LazyVStack and Sheets on iOS 17


Some of my apps that use a LazyVStack and a sheet for detail views show an odd behaviour as of iOS 17 where the modal does not show on rows that are loaded lazily, i.e. that are not displayed initially. Tap gesture doesn't trigger the modal or only when scrolling further, respectively.

The test code below works as expected on a device running iOS 16 or lower. Use iOS 17 instead and scroll down to a section further down (3 or 4 onwards). Tapping on a number will most probably not trigger the modal immediately or at all. It might get triggered after a short delay when you scroll up or down again, though.

Can anyone confirm this issue? Did they change anything in how lazy content is handled in iOS 17? Any workaround I can use?

Thanks for any feedback.

import SwiftUI

struct ListView: View {
    
    @State private var sections = [1, 2, 3, 4, 5, 6]
    
    @State var number: ModalDetail?
    
    var body: some View {
        
        VStack(spacing: 0) {
            
            ScrollView {
                LazyVStack(spacing: 10, pinnedViews: .sectionHeaders) {
                    ForEach(sections, id: \.self) { section in
                        Section {
                            ForEach(1..<21) { number in
                                Text("\(number)")
                                    .onTapGesture {
                                        self.number = ModalDetail(body: "Section \(section) / \(number)")
                                    }
                            }
                        } header: {
                            Text("\(section)")
                                .font(Font.title.lowercaseSmallCaps())
                                .fontWeight(.medium)
                                .frame(minWidth: 0, maxWidth: .infinity)
                                .padding(.vertical, 8)
                                .padding(.horizontal, 8)
                                .background(.gray)
                        }
                        .sheet(item: $number, content: { detail in
                            self.modal(detail: detail.body)
                        })
                    }
                }
            }
            Spacer()
        }
        .font(.title)
    }
    func modal(detail: String) -> some View {
        Text(detail)
    }
}

struct ModalDetail: Identifiable {
    var id: String {
        return body
    }
    
    let body: String
}

Solution

  • “The SwiftUI cookbook for navigation” from WWDC 2022 has some relevant information:

    So let's add a navigationDestination modifier. But where should I attach it? I'm tempted to attach it directly to the link, but this is wrong for two reasons. Lazy containers, like List, Table, or, here, LazyVGrid, don't load all of their views immediately. If I put the modifier here, the destination might not be loaded, so the surrounding NavigationStack might not see it. Second, if I put the modifier here, it will be repeated for every item in my grid. Instead, I'll attach the modifier to my ScrollView.

    Although you’re using sheet, not navigationDestination, both reasons apply to your scenario too. You need to attach the modifier to a view that’s not loaded lazily, and you need to attach it to just one view.

    You could move it out to the ScrollView:

    VStack(spacing: 0) {            
        ScrollView {
            LazyVStack(spacing: 10, pinnedViews: .sectionHeaders) {
                ForEach(sections, id: \.self) { section in
                    Section {
                        // blah blah blah
                    } header: {
                        // blah blah blah
                    }
                    // 👉 REMOVED FROM HERE
                }
            }
        }
        // 👉 INSERTED HERE
        .sheet(item: $number, content: { detail in
            self.modal(detail: detail.body)
        })
        Spacer()
    }
    .font(.title)
    

    Or you could attach it to the VStack:

    VStack(spacing: 0) {            
        ScrollView {
            LazyVStack(spacing: 10, pinnedViews: .sectionHeaders) {
                ForEach(sections, id: \.self) { section in
                    Section {
                        // blah blah blah
                    } header: {
                        // blah blah blah
                    }
                    // 👉 REMOVED FROM HERE
                }
            }
        }
        Spacer()
    }
    .font(.title)
    // 👉 INSERTED HERE
    .sheet(item: $number, content: { detail in
        self.modal(detail: detail.body)
    })