BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

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.

Save 50% in my WWDC sale.

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.

Save 50% on all our books and bundles!

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: 4.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.