Skip to content

Instantly share code, notes, and snippets.

@LePips
Last active May 14, 2024 03:19
Show Gist options
  • Save LePips/5c6b4546e9bd8f91c029f705347be974 to your computer and use it in GitHub Desktop.
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.
// 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