4

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)
            }
        }
    }
}
10
  • Possible duplicate? Commented May 8, 2024 at 8:59
  • Their solution seemed to be switching from a regular VStack to LazyVStack. In my case, I'm hoping to use a regular stack layout. I added a link to the documentation. It seems to imply that it supports a regular stack layout, but it doesn't seem to be working. Commented May 8, 2024 at 9:17
  • That's not the only thing the answer suggested. You should remove the id modifier. Does that fix the issue? Commented May 8, 2024 at 9:20
  • The paragraph in the other answer about setting .id is relevant. I found that your example works when using an HStack if you change it to ForEach(1..<5, id: \.self) and comment out the .id modifier on the Circle. Commented May 8, 2024 at 9:21
  • @BenzyNeez Thanks! Removing the .id modifier seems to do the trick. Any idea why that doesn't work with HStack there? Commented May 8, 2024 at 9:27

1 Answer 1

4

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)
}
Sign up to request clarification or add additional context in comments.

1 Comment

I had the exact same issue but unfortunately wrapping it in a Stack didn't work for me.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.