Instantly share code, notes, and snippets.
Last active
May 14, 2024 03:19
-
Star
(1)
1
You must be signed in to star a gist -
Fork
(1)
1
You must be signed in to fork a gist
-
Save LePips/5c6b4546e9bd8f91c029f705347be974 to your computer and use it in GitHub Desktop.
SwiftUI ViewModifier to create an offset fading-in/out navigation bar for a view based on a scroll view 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 | |
// Embedding view for content | |
struct NavBarOffsetView<Content: View>: UIViewControllerRepresentable { | |
@Binding | |
private var scrollViewOffset: CGFloat | |
private let start: CGFloat | |
private let end: CGFloat | |
private let content: () -> Content | |
init(scrollViewOffset: Binding<CGFloat>, start: CGFloat, end: CGFloat, @ViewBuilder content: @escaping () -> Content) { | |
self._scrollViewOffset = scrollViewOffset | |
self.start = start | |
self.end = end | |
self.content = content | |
} | |
init(start: CGFloat, end: CGFloat, @ViewBuilder body: @escaping () -> Content) { | |
self._scrollViewOffset = Binding(get: { 0 }, set: { _ in }) | |
self.start = start | |
self.end = end | |
self.content = body | |
} | |
func makeUIViewController(context: Context) -> NavBarOffsetHostingController<Content> { | |
NavBarOffsetHostingController(rootView: content()) | |
} | |
func updateUIViewController(_ uiViewController: NavBarOffsetHostingController<Content>, context: Context) { | |
uiViewController.scrollViewDidScroll(scrollViewOffset, start: start, end: end) | |
} | |
} | |
class NavBarOffsetHostingController<Content: View>: UIHostingController<Content> { | |
private var lastScrollViewOffset: CGFloat = 0 | |
private lazy var navBarBlurView: UIVisualEffectView = { | |
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial)) | |
blurView.translatesAutoresizingMaskIntoConstraints = false | |
return blurView | |
}() | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
// Customize to fit needs. My cases require a clear background. | |
view.backgroundColor = nil | |
view.addSubview(navBarBlurView) | |
navBarBlurView.alpha = 0 | |
NSLayoutConstraint.activate([ | |
navBarBlurView.topAnchor.constraint(equalTo: view.topAnchor), | |
navBarBlurView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), | |
navBarBlurView.leadingAnchor.constraint(equalTo: view.leadingAnchor), | |
navBarBlurView.trailingAnchor.constraint(equalTo: view.trailingAnchor), | |
]) | |
} | |
func scrollViewDidScroll(_ offset: CGFloat, start: CGFloat, end: CGFloat) { | |
let diff = end - start | |
let currentProgress = (offset - start) / diff | |
let offset = min(max(currentProgress, 0), 1) | |
self.navigationController?.navigationBar | |
.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label.withAlphaComponent(offset)] | |
navBarBlurView.alpha = offset | |
lastScrollViewOffset = offset | |
} | |
// Restore custom NavigationBar state | |
override func viewWillAppear(_ animated: Bool) { | |
super.viewWillAppear(animated) | |
self.navigationController?.navigationBar | |
.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label.withAlphaComponent(lastScrollViewOffset)] | |
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) | |
self.navigationController?.navigationBar.shadowImage = UIImage() | |
} | |
// Restore default NavigationBar for other views | |
override func viewWillDisappear(_ animated: Bool) { | |
super.viewWillDisappear(animated) | |
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label] | |
self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default) | |
self.navigationController?.navigationBar.shadowImage = nil | |
} | |
} | |
struct NavBarOffsetModifier: ViewModifier { | |
@Binding | |
var scrollViewOffset: CGFloat | |
// Begin fading in navbar at offset. If offset < start, navbar hidden | |
let start: CGFloat | |
// End fading in navbar at offset. If offset > end, navbar shown | |
let end: CGFloat | |
func body(content: Content) -> some View { | |
// Required .ignoresSafeArea() due to bug where view will hide under blank NavigationBar | |
NavBarOffsetView(scrollViewOffset: $scrollViewOffset, start: start, end: end) { | |
content | |
} | |
.ignoresSafeArea() | |
} | |
} | |
extension View { | |
func navBarOffset(_ scrollViewOffset: Binding<CGFloat>, start: CGFloat, end: CGFloat) -> some View { | |
self.modifier(NavBarOffsetModifier(scrollViewOffset: scrollViewOffset, start: start, end: end)) | |
} | |
} | |
// Usage | |
struct ContentView: View { | |
@State | |
private var scrollViewOffset: CGFloat = 0 | |
var body: some View { | |
NavigationView { | |
ScrollView { | |
VStack { | |
Color.clear | |
.frame(height: 500) | |
VStack { | |
ForEach(0..<100) { _ in | |
Text("Hello There") | |
} | |
// Prove restoration of native NavigationBar | |
// on new views | |
NavigationLink("Navigate") { | |
ScrollView { | |
VStack { | |
ForEach(0..<50) { _ in | |
Text("Hello There") | |
} | |
} | |
.frame(maxWidth: .infinity) | |
} | |
.navigationTitle("Another Test") | |
} | |
} | |
.background(Color.black) | |
} | |
.frame(maxWidth: .infinity) | |
} | |
.navigationTitle("Test") | |
.navigationBarTitleDisplayMode(.inline) | |
.ignoresSafeArea() | |
.scrollViewOffset($scrollViewOffset) | |
.navBarOffset($scrollViewOffset, start: 300, end: 350) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment