DEV Community

Dean Thompson
Dean Thompson

Posted on • Updated on

[WWDC2023] New Additions to ScrollView in iOS17 Part 1

SwiftUI

With the introduction of iOS 17, Apple has added a load of new ScrollView modifiers that promise to greatly enhance our app development experience. I thought it would be a great idea to test them out and report on how they work.

I'll be exploring four modifiers below.

  1. .containerRelativeFrame(_:alignment:)
  2. .scrollTargetLayout(isEnabled:)
  3. .scrollTargetBehavior(_:)
  4. .scrollPosition(id:)

If you like this article feel free to like it or give me a follow.
OK! Let's get into it!

1. .containerRelativeFrame(_:alignment:)

This new modifier allows us to specify a view's height, width, or both relative to its container view. In our example, we have used it on our ZStack. Here, we are setting its height and width relative to the container view, in this case, the ScrollView.

struct ScrollExamplePaging: View {
    var body: some View {
        ScrollView(.horizontal) {
            let strings: [String] = ["1", "2", "3", "4", "5"]
            LazyHStack(spacing: 16) {
                ForEach(strings, id: \.self) { string in
                    ZStack {
                        // ...
                    }
                    .padding(16)
                    .containerRelativeFrame(.horizontal)
                    .containerRelativeFrame(.vertical)
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This can be simplified to the following:

.containerRelativeFrame([.vertical, .horizontal])
Enter fullscreen mode Exit fullscreen mode

And looks like this:

Example01

This modifier is not limited to ScrollView. According to Apple's documents it can be used in the following contexts:

  • The window presenting a view on iPadOS or macOS, or the screen of a device on iOS.
  • A column of a NavigationSplitView
  • A NavigationStack
  • A tab of a TabView
  • A scrollable view like ScrollView or List

2. .scrollTargetLayout(isEnabled:)

The scroll target layout modifier is attached to the main view (container view) within a ScrollView, in this case, the ZStack. It lets the ScrollView determine where it should align to. It sets the scroll target for a given ScrollView. It is by default set to true.

struct ScrollExamplePaging: View {
    var body: some View {
        ScrollView(.horizontal) {
            let strings: [String] = ["1", "2", "3", "4", "5"]
            LazyHStack(spacing: 16) {
                ForEach(strings, id: \.self) { string in
                    ZStack {
                        // ...
                    }
                    .padding(16)
                    .containerRelativeFrame([.vertical, .horizontal])
                }
            }
            .scrollTargetLayout()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This modifier by itself will have no affect on the view, we must use it in conjunction with the modifiers in 3. and 4.

3. .scrollTargetBehavior(_:)

Now, let's move from setting our target in section 2 to defining our ScrollView's behavior – essentially telling how our ScrollView functions. This involves selecting behaviors like .paging or .viewAligned for our ScrollView. Let's delve deeper into each of these.

.paging

The .paging behavior transforms your ScrollView into a paginated interface, think TikTok's vertical scrolling or a smooth onboarding experience. This behavior makes use of the view's height and width to transition smoothly from one page to the next, ensuring a full view is always displayed without cutting any part of it.

When implementing .paging, it's important to ensure the LazyHStack's spacing is set to 0, given that this behavior requires the container view to occupy the full screen width.

Spacing set to 16 (paging behavior is disrupted)

Example04

Spacing set to 0 (normal paging behavior)

Example02

Here is the code:

struct ScrollExamplePaging: View {
    var body: some View {
        ScrollView(.horizontal) {
            let strings: [String] = ["1", "2", "3", "4", "5"]
            LazyHStack(spacing: 0) { // spacing is 0.
                ForEach(strings, id: \.self) { string in
                    ZStack {
                        RoundedRectangle(cornerRadius: 16)
                            .fill(.black.gradient)

                        Text(string)
                            .font(.system(size: 92))
                            .fontWeight(.bold)
                            .foregroundStyle(.white)

                    }
                    .padding(16)
                    .containerRelativeFrame([.vertical, .horizontal])
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.paging)
    }
}
Enter fullscreen mode Exit fullscreen mode

.viewAligned

For instances when you want to incorporate spacing or create more intricate layouts, the .viewAligned behavior comes in handy. This allows for customization of your ScrollView's start position, ensuring it aligns smoothly with the container view as you scroll. This functionality offers an added level of flexibility to your layouts.

Example05

Here is the code:

struct ScrollExamplePaging: View {
    var body: some View {
        ScrollView(.horizontal) {
            let strings: [String] = ["1", "2", "3", "4", "5"]
            HStack(spacing: 16) {
                ForEach(strings, id: \.self) { string in
                    ZStack {
                        RoundedRectangle(cornerRadius: 16)
                            .fill(.black.gradient)

                        Text(string)
                            .font(.system(size: 92))
                            .fontWeight(.bold)
                            .foregroundStyle(.white)

                    }
                    .frame(width: 300)
                    .containerRelativeFrame(.vertical)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
    }
}
Enter fullscreen mode Exit fullscreen mode

4. .scrollPosition(id:)

We can now keep track of the items being shown in our scroll view using scroll position. It is quite simple to use. Create a @State variable @State private var scrollPosition and then bind it to the modifier. .scrollPosition(id: $scrollPosition). Now as I scroll the ScrollView will keep track of which value it is showing. This means we are able to link this value with other Views.

Like so:

Example07

Here is the code:

struct ScrollViewExampleScrollPosition: View {
    @State private var scrollPosition: String?
    let strings: [String] = ["1", "2", "3", "4", "5"]
    var body: some View {
        GeometryReader { geo in
            let size = geo.size
            VStack {
                ScrollView(.horizontal) {
                    HStack(spacing: 16) {
                        ForEach(strings, id: \.self) { string in
                            ZStack(alignment: .center) {
                                RoundedRectangle(cornerRadius: 16)
                                    .fill(.black.gradient)

                                Text(string)
                                    .font(.system(size: 92))
                                    .fontWeight(.bold)
                                    .foregroundStyle(.white)

                            }
                            .frame(width: 300, height: 500)
                            .padding(.vertical, 16)
                            .padding(.horizontal, (size.width - 300) / 2)
                        }
                    }
                    .scrollTargetLayout()
                }
                .scrollTargetBehavior(.viewAligned)
                .scrollPosition(id: $scrollPosition)

                VStack {
                    if let scrollPosition {
                        Text(scrollPosition)
                            .font(.largeTitle)
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Or we could use buttons to jump to the number we want to see.

like so:

Example06

Here is the code:

struct ScrollViewExampleScrollPosition: View {
    @State private var scrollPosition: String?
    let strings: [String] = ["1", "2", "3", "4", "5"]
    var body: some View {
        GeometryReader { geo in
            let size = geo.size
            VStack {
                ScrollView(.horizontal) {
                    HStack(spacing: 16) {
                        ForEach(strings, id: \.self) { string in
                            ZStack(alignment: .center) {
                                RoundedRectangle(cornerRadius: 16)
                                    .fill(.black.gradient)

                                Text(string)
                                    .font(.system(size: 92))
                                    .fontWeight(.bold)
                                    .foregroundStyle(.white)

                            }
                            .frame(width: 300, height: 500)
                            .padding(.vertical, 16)
                            .padding(.horizontal, (size.width - 300) / 2)
                        }
                    }
                    .scrollTargetLayout()
                }
                .scrollTargetBehavior(.viewAligned)
                .scrollPosition(id: $scrollPosition)

                VStack {
                    ForEach(strings, id: \.self) { string in
                        Button("Scroll to \(string)") {
                            withAnimation {
                                scrollPosition = string
                            }
                        }
                        .buttonStyle(.borderedProminent)
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Thanks for checking out the post. In Part 2 I will check out ScrollView transitions! Follow to keep up-to-date.
All the best!

Dean Thompson

Follow me!
LinkedIn
Twitter
Instagram

References

Top comments (0)