Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use scrollPosition with HStack (not LazyHStack) in SwiftUI

Tags:

swift

swiftui

I'm trying to implement scroll tracking using scrollTargetLayout and scrollPosition in SwiftUI. My code works as expected when I use LazyHStack, but it doesn't track the scroll position with a regular HStack.

Basically, I've noticed that my views don't always load properly when paging with the LazyHStack, but they appear fine with HStack.

Here's an example that works with HStack:

struct ContentView: View {
    @State var pageId: Item.ID?
    var items: [Item] = [.init(), .init(), .init(), .init()]
    var body: some View {
        ZStack {
            ScrollView(.horizontal) {
                HStack(spacing: 0) {
                    ForEach(items) { item in
                        Circle()
                            .foregroundStyle(.secondary)
                            .containerRelativeFrame(.horizontal, count: 1, spacing: 0)
                    }
                }
                .scrollTargetLayout()
            }
            .scrollTargetBehavior(.viewAligned)
            .scrollPosition(id: $pageId)
            
            if let pageId {
                Text(pageId.uuidString)
                    .font(.caption)
            }
        }
    }
}

However, if I try to use my own custom view it no longer works to track the scroll position:

struct CircleView: View {
    var body: some View {
        Circle()
    }
}

struct ContentView: View {
    @State var pageId: Item.ID?
    var items: [Item] = [.init(), .init(), .init(), .init()]
    var body: some View {
        ZStack {
            ScrollView(.horizontal) {
                HStack(spacing: 0) {
                    ForEach(items) { item in
                        CircleView()
                            .foregroundStyle(.secondary)
                            .containerRelativeFrame(.horizontal, count: 1, spacing: 0)
                    }
                }
                .scrollTargetLayout()
            }
            .scrollTargetBehavior(.viewAligned)
            .scrollPosition(id: $pageId)
            
            if let pageId {
                Text(pageId.uuidString)
                    .font(.caption)
            }
        }
    }
}
like image 515
Berry Blue Avatar asked Oct 13 '25 01:10

Berry Blue


1 Answers

I've dealt with this last week as well. In my case, I could use LazyHStack just fine, I just didn't want to. There are two scenarios (that I could find) where scrollPosition breaks:

  1. id is down the hierarchy, due to some logic:

     VStack{
     Text("\(selectedElem ?? 0)").font(.largeTitle)
     ScrollView(.horizontal, showsIndicators: false){
         HStack(spacing: 30){
             ForEach(0...100, id: \.self){ value in
                     if value % 2 == 0 {
                         Color(.green)
                             .frame(width: 10, height:100)
                     } else {
                         Color(.pink)
                             .frame(width: 10, height:100)
                     }
             }
         }
         .scrollTargetLayout()
    
     }
     .scrollPosition(id: $selectedElem)
     .scrollTargetBehavior(.viewAligned)
    

    }

  2. Using Custom Views, such as:

     VStack{
         Text("\(selectedElem ?? 0)").font(.largeTitle)
         ScrollView(.horizontal, showsIndicators: false){
             HStack(spacing: 30){
                 ForEach(0...100, id: \.self){ value in
                         SubView(id: value)
                 }
             }
             .scrollTargetLayout()
    
         }
         .scrollPosition(id: $selectedElem)
         .scrollTargetBehavior(.viewAligned)
     }
    

In either case, .id breaks and it does not get updated when scrolling. SwiftUI is aware of the id, because adding .onAppear modifier and setting selectedElem to 5, for example, will scroll the list at 5.

The solution I have found is to just wrap either of the cases (conditionals or custom Views) in a SwiftUI Container View (Z/H/V)Stack, like this:

VStack{
    Text("\(selectedElem ?? 0)").font(.largeTitle)
    ScrollView(.horizontal, showsIndicators: false){
        HStack(spacing: 30){
            ForEach(0...100, id: \.self){ value in
                ZStack{
                    SubView(id: value)
                }
            }
        }
        .scrollTargetLayout()
    }
    .scrollPosition(id: $selectedElem)
    .scrollTargetBehavior(.viewAligned)
}
like image 110
PrudentLeapCommander Avatar answered Oct 14 '25 14:10

PrudentLeapCommander