Vertical ScrollView Height and Paging Offset Issues in SwiftUI

Hi everyone,

I'm currently working on an iOS app using SwiftUI, and I'm facing an issue with a vertical ScrollView. My goal is to have the ScrollView take up all the safe area space plus the top inset (with the bottom inset being an ultra-thin material) and enable paging behavior. However, I'm encountering two problems:

  • The initial height of the ScrollView is too high (dragging the view (even without changing the page) adjusts the size).
  • The paging offset of the ScrollView is incorrect (page views are not aligned).

I’ve tried many things and combinations in desperation, including padding, safeAreaPadding, contentMargins, frame, fixedSize, containerRelativeFrame with callback, custom layout, and others, but nothing seems to work.

If by any chance someone can help me find a solution, I’d greatly appreciate it.

I suspect there are issues with the height defined by the ScrollView when some safe areas are ignored, leading to strange behavior when trying to set the height. It's challenging to explain everything in a simple post, so if someone from the SwiftUI team could look into this potential bug, that would be incredibly helpful.

Thank you!

import SwiftUI

struct ScrollViewIssue: View {
    @State private var items: [(String, Color)] = [
        ("One", .blue),
        ("Two", .green),
        ("Three", .red),
        ("Four", .purple)
    ]
    
    var body: some View {
        ZStack(alignment: .top) {
            ScrollView(.vertical) {
                VStack(spacing: 0) {
                    ForEach(items, id: \.0) { item in
                        ItemView(text: item.0, color: item.1)
                            .containerRelativeFrame([.horizontal, .vertical])
                    }
                }
            }
            .printViewSize(id: "ScrollView")
            .scrollTargetBehavior(.paging)
            .ignoresSafeArea(edges: .top)

            VStack {
                Text("Title")
                    .foregroundStyle(Color.white)
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.black.opacity(0.2))
                
                Spacer()
            }
        }
        .printViewSize(id: "ZStack")
        .safeAreaInset(edge: .bottom) {
            Rectangle()
                .frame(width: .infinity, height: 0)
                .overlay(.ultraThinMaterial)
        }
    }
}

struct ItemView: View {
    let text: String
    let color: Color

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(color.opacity(0.2))
                .overlay(
                    color,
                    in: RoundedRectangle(cornerRadius: 20)
                        .inset(by: 10 / 2)
                        .stroke(lineWidth: 10)
                )
            
            Text(text)
        }
        .printViewSize(id: "item")
    }
}

struct ViewSizeKey: PreferenceKey {
    static var defaultValue = CGSize()
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

extension View {
    func printViewSize(id: String) -> some View {
        background(
            GeometryReader { proxy in
                Color.clear
                    .preference(key: ViewSizeKey.self, value: proxy.size)
            }
            .onPreferenceChange(ViewSizeKey.self) { value in
                print("\(id) size:\(value)")
            }
        )
    }
}

#Preview {
    ScrollViewIssue()
}
Answered by mat21 in 802962022

Seems .scrollTargetBehavior(.viewAligned(limitBehavior: .alwaysByOne)) available in iOS 18 do the job (do not forget to add .scrollTargetLayout())

timeline:

  • initial state
  • moving the item without changing page change the height of the item
  • scrolling to page 2
  • scrolling to page 3
  • scrolling to page 4 (last)

Accepted Answer

Seems .scrollTargetBehavior(.viewAligned(limitBehavior: .alwaysByOne)) available in iOS 18 do the job (do not forget to add .scrollTargetLayout())

As stated in this answer, the offset issue might also be caused by the default spacing of the containers used inside the ScrollView.

https://stackoverflow.com/a/77421751/19954370

Vertical ScrollView Height and Paging Offset Issues in SwiftUI
 
 
Q