Previously we looked at how to use GeometryReader
to create varying effects based on where a view is on the screen. That code all works fine, and you'll certainly see it in lots of apps, but SwiftUI provides some helpful alternatives that can be much easier.
First, let's look again at some previous code – this creates a simple CoverFlow-style effect, where we can swipe horizontally to see views moving in 3D space:
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(1..<20) { num in
GeometryReader { proxy in
Text("Number \(num)")
.font(.largeTitle)
.padding()
.background(.red)
.rotation3DEffect(.degrees(-proxy.frame(in: .global).minX) / 8, axis: (x: 0, y: 1, z: 0))
.frame(width: 200, height: 200)
}
.frame(width: 200, height: 200)
}
}
}
That code uses GeometryReader
to read each view's position in the scroll view, but we've needed to add an explicit width and height to make stop our GeometryReader
from automatically expanding to take up all available space.
SwiftUI gives us an alternative called visualEffect()
, and it has a very specific purpose and a very specific restriction: it lets us apply effects that change the way something looks, which in practice means it can't do anything that affects the actual layout position or frame of a view.
This modifier works in a very interesting way: we pass it a closure to run, and we'll be given the content we're modifying as well as a GeometryProxy
for it. That content we're modifying is our view, but we can't just apply any modifiers we want like we normally would – again, we can't do anything that affects the layout position of the view.
Fortunately, that still leaves lots of modifiers for us to use, including some that might surprise you – we can use rotationEffect()
, rotation3DEffect()
, and even offset()
, because although they effect how views are drawn, they don't change the frame of the view.
So, we can rewrite our code using visualEffect()
like this:
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(1..<20) { num in
Text("Number \(num)")
.font(.largeTitle)
.padding()
.background(.red)
.frame(width: 200, height: 200)
.visualEffect { content, proxy in
content
.rotation3DEffect(.degrees(-proxy.frame(in: .global).minX) / 8, axis: (x: 0, y: 1, z: 0))
}
}
}
}
Although the code is only a little shorter, this is a much neater solution than using GeometryReader
because we no longer need to add a second frame()
modifier to stop things taking up the full screen – this scroll view can fit alongside other parts of our SwiftUI layout without screwing things up.
What we have now is a lot nicer, but with just two extra modifiers we can make this effect work a lot better.
The first is scrollTargetLayout()
, which I'd like you to apply to the HStack
. That tells SwiftUI we want to make each view inside this HStack
a scroll target – something that is considered important when it comes to scrolling around.
The second is .scrollTargetBehavior(.viewAligned)
, which I'd like you to apply to the ScrollView
. That tells SwiftUI it should make this scroll view move smoothly between all scroll targets, which we just defined as being every view inside our HStack
.
If you put those two together, the result is lovely: we can now scroll smoothly between our text views, and whenever we let go SwiftUI will automatically ensure one view snaps to the left edge.
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.