How to align content of SwiftuUI ScrollView to top? (MacOS)

I have a ZStack view embedded in ScrollView. The height of ZStack often changes.

var body: some View {
      ScrollView([.vertical, .horizontal], showsIndicators: true) {
            VStack { // embedding in VStack doesn't change behaviour
                ZStack () {
                    ForEach(mapElements.indices, id:\.self) { i in
                        let element = mapElements[i]
                        MapElementView (mapElement: element)
                          .position(x: offset(x: element.x),
                                    y: offset(y: element.y))
                          .zIndex(5)
                        MakeConnections(mapElement: element)
                    }
                }.frame(width: calculateWidth(),
                        height: calculateHeight())
                }
                Rectangle() // Just to make VStack filled
            }
        }.frame(alignment: .topLeading)
   }

Some scroll views (ie. list) always align embedded objects to top even they are shorter than embedding scroll view. But in this case ZStack is always centred when view is refreshed.

Is it possible to define behaviour, so each change of a content will align its top to scroll view top, even if height of ZStack is smaller than ScrollView?

Answered by robnotyou in 716994022

It looks like having both axes in your ScrollView is forcing the content to the center.

Removing .horizontal sends it to the top.

So as a possible fix (here's a minimal example), try:

    var body: some View {
        ScrollView(.horizontal, showsIndicators: true) {
            ScrollView(.vertical, showsIndicators: true) {
                ZStack {
                    Text("content appears at top")
                }
            }
        }
    }
Accepted Answer

It looks like having both axes in your ScrollView is forcing the content to the center.

Removing .horizontal sends it to the top.

So as a possible fix (here's a minimal example), try:

    var body: some View {
        ScrollView(.horizontal, showsIndicators: true) {
            ScrollView(.vertical, showsIndicators: true) {
                ZStack {
                    Text("content appears at top")
                }
            }
        }
    }

Has anyone found a solution to getting the contents pinned into the top left that doesn't require embedding two ScrollViews within each other like this?

The problem with this solution is scrolling becomes "locked" to a single direction depending on whichever ScrollView captured the event first. So if scrolling horizontally, you must release the scroll, let it complete, then you are free to scroll vertically. The scroll view area loses diagonal scrolling as well.

I have found a solution. The idea is to read the size of the scroll view and use it as the minimum size of the view inside.

extension View {
    func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
        background(
            GeometryReader { geometryProxy in
                Color.clear
                    .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
            }
         )
        .onPreferenceChange(SizePreferenceKey.self, perform: onChange)
    }
}

struct ExampleView: View {
    @State var size: CGSize = .zero
    
    var body: some View {
        ScrollView([.horizontal, .vertical]) {
            Text("Hello World!")
                .frame(minWidth: size.width, minHeight: size.height, alignment: .topLeading)
        }
        .readSize(onChange: { size = $0 })
    }
}

Somewhat ugly, but it works.

How to align content of SwiftuUI ScrollView to top? (MacOS)
 
 
Q