UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

ScrollView effects using visualEffect() and scrollTargetBehavior()

Paul Hudson    @twostraws   

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.

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.