[WWDC2023] New Additions to ScrollView in iOS17 Part 1
Tags: swift, swiftui, ios, tutorial
Published: 2023-06-20 06:31:22

![SwiftUI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e5xr4nr86flzbwbz3jue.png)

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`.

```swift
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)
                }
            }
        }
    }
}
```

This can be simplified to the following:

```swift
.containerRelativeFrame([.vertical, .horizontal])
```

And looks like this:

![Example01](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1k1f2d3a8mbywngqljy7.gif)

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. 

```swift
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()
        }
    }
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ke834ujgdcx6d4omplv.gif)

#### Spacing set to 0 (normal paging behavior)

![Example02](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wpjlbh75647bji6gpg6j.gif)

Here is the code:

```swift
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)
    }
}
```

### `.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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h8c799obo2j7cc4cigiy.gif)

Here is the code:

```swift
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)
    }
}
```

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9w1l1q996y1d7a8fvj0a.gif)

Here is the code:

```swift
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)
                    }
                }
            }
        }
    }
}
```

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

like so:

![Example06](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q9bs95ojz1vtxjl3qeod.gif)

Here is the code:

```swift
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)
                    }
                }
            }
        }
    }
}
```

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](https://www.linkedin.com/in/deanwthompson/)
[Twitter](https://twitter.com/DeanWThompson)
[Instagram](https://www.instagram.com/thompson.dean)

## References

- Apple Docs for each modifier:
[containerRelativeFrame(_:alignment:)](https://developer.apple.com/documentation/deviceactivity/deviceactivityreport/containerrelativeframe(_:alignment:))
[scrollTargetLayout(isEnabled:)](https://developer.apple.com/documentation/swiftui/view/scrolltarget(isenabled:))
[scrollTargetBehavior(_:)](https://developer.apple.com/documentation/appintents/siritipview/scrolltargetbehavior(_:))
[scrollPosition(id:)](https://developer.apple.com/documentation/familycontrols/familyactivitytitleview/scrollposition(id:)?changes=latest_minor&language=_2)

- Kavsoft's great video rounding up what's new in SwiftUI [What's New in SwiftUI - iOS17](https://www.youtube.com/watch?v=1UHShfxysPM&t=498s)

- [【SwiftUI】iOS17の新APIを使用してScrollViewを操る](https://qiita.com/SNQ-2001/items/b34ccca02ed82c67e8a1)