Dean Thompson
Posted on June 20, 2023
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.
.containerRelativeFrame(_:alignment:)
.scrollTargetLayout(isEnabled:)
.scrollTargetBehavior(_:)
.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)
}
}
}
}
}
This can be simplified to the following:
.containerRelativeFrame([.vertical, .horizontal])
And looks like this:
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()
}
}
}
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)
Spacing set to 0 (normal paging behavior)
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)
}
}
.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.
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)
}
}
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:
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)
}
}
}
}
}
}
Or we could use buttons to jump to the number we want to see.
like so:
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)
}
}
}
}
}
}
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
Apple Docs for each modifier:
containerRelativeFrame(_:alignment:)
scrollTargetLayout(isEnabled:)
scrollTargetBehavior(_:)
scrollPosition(id:)Kavsoft's great video rounding up what's new in SwiftUI What's New in SwiftUI - iOS17
Posted on June 20, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.