62

I'm trying to code a simple login page on my app. I started using SwiftUI on my newly updated macOS Catalina. The Apple documentation is still lacking a lot. I need to center a VStack vertically on a Scrollview occupying the whole page with a "limit" on it's width of 400.

Something like this:

ScrollView(.vertical) {
    VStack {
        Text("Hello World")
    }
    .frame(maxWidth: 400, alignment: .center)
}

It was easy with UIScrollView, just needed to set the ContentView to fill height and width and then centering a Vertical StackLayout inside the Content View but now with SwiftUI I just wonder.

The goal is something like this (Credit to the author)

enter image description here

If someone is wondering why I want everything inside a scrollview, it's because my form is quite big and I expect the user to use both landscape and portrait view so I really need the content to be scrollable. Bear in mind also that in an iPad, the form doesn't fill the whole screen, that's why I want it centered vertically.

2 Answers 2

142

iOS 18+

iOS 18 adds defaultScrollAnchor(_:for:), which can be used to achieve this (described here by Keith Harrison).

ScrollView {
    // Form content
    VStack {
        Text("Form goes here")
    }
    // Set your max width (optional)
    .frame(maxWidth: 400)
    .padding()
    .background(Color.yellow)
}
// Center the scroll view's content
.defaultScrollAnchor(.center, for: .alignment)
// Disable bounce if the content fits (optional)
.scrollBounceBehavior(.basedOnSize)

iOS 17+

As of iOS 17, you can achieve this without a GeometryReader by using containerRelativeFrame to reserve space in a ZStack.

As a bonus you can also disable scroll bouncing with .scrollBounceBehavior(.basedOnSize).

ScrollView {
    ZStack {
        // Reserve space matching the scroll view's frame
        Spacer().containerRelativeFrame([.horizontal, .vertical])

        // Form content
        VStack {
            Text("Form goes here")
        }
        // Set your max width (optional)
        .frame(maxWidth: 400)
        .padding()
        .background(Color.yellow)
    }
}
// Disable bounce if the content fits (optional)
.scrollBounceBehavior(.basedOnSize)

iOS 13+

You can vertically center content in a scroll view by using GeometryReader to get the parent view's dimensions and setting the scroll view's content's minHeight to the parent's height.

When the content is too big to fit vertically it'll just scroll like normal.

For example:

var body: some View {
    GeometryReader { geometry in
        ScrollView(.vertical) {
            // Form content
            VStack {
                Text("Form goes here")
            }
            // Set your max width (optional)
            .frame(maxWidth: 400)
            .padding()
            .background(Color.yellow)

            // Make the scroll view full-width
            .frame(width: geometry.size.width) 

            // Set the content’s min height to the parent
            .frame(minHeight: geometry.size.height)
        }
    }
}

Preview

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks Alex, that helped with a very simple approach :)
Best solution for this case
Also note that if you have defined the ScrollView with a direction you should also define the same axis for the scrollBounceBehaviour to work. If you have a .horizontal scrolling : .scrollBounceBehavior(.basedOnSize, axes: [.horizontal])
Only the ScrollAnchorRole requires iOS 18, without it this can be used on iOS 17. @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) nonisolated public func defaultScrollAnchor(_ anchor: UnitPoint?) -> some View @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) nonisolated public func defaultScrollAnchor(_ anchor: UnitPoint?, for role: ScrollAnchorRole) -> some View
The for: .alignment is required to match the same behaviour though. Without it, when you have content that is taller than the scroll view, it starts out vertically centred with the top and bottom of the content out of view.
Spacer().containerRelativeFrame([.horizontal, .vertical]) // Thank You!
36

I've build a more generic view based on @Alex answer

/// Custom vertical scroll view with centered content vertically
///
struct VScrollView<Content>: View where Content: View {
  @ViewBuilder let content: Content
  
  var body: some View {
    GeometryReader { geometry in
      ScrollView(.vertical) {
        content
          .frame(width: geometry.size.width)
          .frame(minHeight: geometry.size.height)
      }
    }
  }
}

You can use it anywhere in your app like this

  var body: some View {
    VScrollView {
      VStack {
        Text("YOUR TEXT HERE")
      }
    }
  }

1 Comment

I would like to know why the ScrollView doesn't knows the height, I looked into the docs but can't find it, do you know it?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.