Last active
March 17, 2023 21:37
-
-
Save LePips/f2ea32ee2748b6baef0e896f69f665aa to your computer and use it in GitHub Desktop.
SwiftUI ViewModifier to create a parallax and stretchy background header based on ScrollView offset
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Assumes usage of: | |
// - https://gist.github.com/LePips/3640ad0cd9b6e2ceb407e9d0e9e32b5c | |
// - https://gist.github.com/LePips/5c6b4546e9bd8f91c029f705347be974 | |
// | |
// Last gist not required, but nice to have for headers. | |
struct BackgroundParallaxHeaderModifier<Header: View>: ViewModifier { | |
@Binding | |
var scrollViewOffset: CGFloat | |
let height: CGFloat | |
let multiplier: CGFloat | |
let header: () -> Header | |
init(_ scrollViewOffset: Binding<CGFloat>, | |
height: CGFloat, | |
multiplier: CGFloat = 1, | |
@ViewBuilder header: @escaping () -> Header) { | |
self._scrollViewOffset = scrollViewOffset | |
self.height = height | |
self.multiplier = multiplier | |
self.header = header | |
} | |
func body(content: Content) -> some View { | |
content.background(alignment: .top, content: { | |
header() | |
.offset(y: scrollViewOffset > 0 ? -scrollViewOffset * multiplier : 0) | |
.scaleEffect(scrollViewOffset < 0 ? (height - scrollViewOffset) / height : 1, anchor: .top) | |
.mask(alignment: .top) { | |
Color.black | |
.frame(height: max(0, height - scrollViewOffset)) | |
} | |
.ignoresSafeArea() | |
}) | |
} | |
} | |
extension View { | |
func backgroundParallaxHeader<Header: View>(_ scrollViewOffset: Binding<CGFloat>, | |
height: CGFloat, | |
multiplier: CGFloat = 1, | |
@ViewBuilder header: @escaping () -> Header) -> some View { | |
self.modifier(BackgroundParallaxHeaderModifier(scrollViewOffset, | |
height: height, | |
multiplier: multiplier, | |
header: header)) | |
} | |
} | |
// Example | |
struct ContentView: View { | |
@State | |
private var scrollViewOffset: CGFloat = 0 | |
var body: some View { | |
NavigationView { | |
ScrollView { | |
VStack { | |
// Show background parallax header | |
Color.clear | |
.frame(height: 300) | |
VStack { | |
ForEach(0..<100) { _ in | |
Text("Hello There") | |
} | |
} | |
.background(Color.black) | |
} | |
.frame(maxWidth: .infinity) | |
} | |
.navigationTitle("Test") | |
.navigationBarTitleDisplayMode(.inline) | |
.ignoresSafeArea() | |
.scrollViewOffset($scrollViewOffset) | |
.navBarOffset($scrollViewOffset, start: 270, end: 320) | |
.backgroundParallaxHeader($scrollViewOffset, height: 300, multiplier: 0.5) { // Will offset slower than scroll speed | |
Color.red | |
.frame(height: 300) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment