blub
blub

Reputation: 712

Hide navigation bar on scroll in SwiftUI?

Hiding the navigation bar on scroll was supported in Swift with navigationController?.hidesBarsOnSwipe = true

To be clear, I'd like it to only be hidden on scroll, so .navigationBarHidden(true) would not suffice.

I tried accessing the NavigationController as described in this Stackoverflow answer, (I added nc.hidesBarsOnSwipe = true) and while it compiled, it did not work.

Is this supported in SwiftUI?

Upvotes: 11

Views: 12100

Answers (3)

damagucci_Juice
damagucci_Juice

Reputation: 1

It'd be great this code will help you. Good Luck bro.

Under iOS 18

  • preparation
import SwiftUI

extension View {
    @ViewBuilder
    func hideNavBarOnSwipe(_ isHidden: Bool) -> some View {
        self.modifier(NavBarModifier(isHidden: isHidden))
    }
}


private struct NavBarModifier: ViewModifier {
    var isHidden: Bool
    @State private var isNavBarHidden: Bool?

    func body(content: Content) -> some View {
        content
            .onChange(of: isHidden) { newValue in
                isNavBarHidden = newValue
            }
            .onDisappear {
                isNavBarHidden = nil
            }
            .background {
                NavigationControllerExtractor(isHidden: isNavBarHidden)
            }
    }

}

private struct NavigationControllerExtractor: UIViewRepresentable {
    var isHidden: Bool?

    func makeUIView(context: Context) -> UIView {
        UIView()
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
            if let hostView = uiView.superview?.superview,
               let parentController = hostView.parentController,
               let isHidden {
                parentController.navigationController?.hidesBarsOnSwipe = isHidden
            }
        }
    }
}

private extension UIView {
    var parentController: UIViewController? {
        sequence(first: self) { view in
            view.next
        }
        .first { responder in
            return responder is UIViewController
        } as? UIViewController
    }
}

  • Usage
import SwiftUI

struct ContentView: View {
    @State private var hideNavBar: Bool = true

    var body: some View {
        NavigationStack {
            List(1...50, id: \.self) { index in 
                Text("\(index)")
            }
            .listStyle(.plain)
            .navigationTitle("Hide Nav Bar")
            .hideNavBarOnSwipe(hideNavBar)
        }
    }
}

Refer.

Kavsoft's youtube

Upvotes: 0

Lakshith Nishshanke
Lakshith Nishshanke

Reputation: 136

I've come across the same problem. Here's how i solved it.

  1. get the scroll offset of the view
  2. hide or view nav bar according to the offset

1. getting the scroll position

Please see here for how to do this. I'll add a sample code here.

struct ScrollViewOffsetPreferenceKey: PreferenceKey {
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}

import SwiftUI

struct ObservableScrollView<Content>: View where Content : View {
    @Namespace var scrollSpace
    @Binding var scrollOffset: CGFloat
    let content: () -> Content
    
    init(scrollOffset: Binding<CGFloat>,
         @ViewBuilder content: @escaping () -> Content) {
        _scrollOffset = scrollOffset
        self.content = content
    }
    
    var body: some View {
        ScrollView {
                content()
                    .background(GeometryReader { geo in
                        let offset = -geo.frame(in: .named(scrollSpace)).minY
                        Color.clear
                            .preference(key: ScrollViewOffsetPreferenceKey.self,
                                        value: offset)
                    })
            
        }
        .coordinateSpace(name: scrollSpace)
        .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in
            scrollOffset = value
        }
    }
}

2. hide or view nav bar according to the offset

Now we can use the above created observable scroll view.

@State var scrollOffset: CGFloat = CGFloat.zero
@State var hideNavigationBar: Bool = false

var body: some View {
        NavigationView {
            ObservableScrollView(scrollOffset: self.$scrollOffset) {
                Text("I'm observable")
            }
            .navigationTitle("Title")
            .onChange(of: scrollOffset, perform: { scrollOfset in
                let offset = scrollOfset + (self.hideNavigationBar ? 50 : 0) // note 1
                if offset > 60 { // note 2
                    withAnimation(.easeIn(duration: 1), {
                        self.hideNavigationBar = true
                    })
                }
                if offset < 50 {
                    withAnimation(.easeIn(duration: 1), {
                        self.hideNavigationBar = false
                    })
                }
            })
            .navigationBarHidden(hideNavigationBar)
        }
    }

Note 1: Assume that the height of the navigation title is 50. (This will change depending on the style.) When the nav bar dissapears, scroll offset drops by that height instantly. To keep the offset consistant add the height of the nav bar to the offset if it's hidden.

Note 2: I intentionally let a small difference between two thresholds for hiding and showing instead of using the same value, Because if the user scrolls and keep it in the threshold it won't flicker.

Upvotes: 1

arsenius
arsenius

Reputation: 13286

NavigationView seems to be relatively buggy still. For example, by default a ScrollView will ignore the title area and just scroll beneath it.

It looks to me like you can get this working by using displayMode: .inline and StackNavigationViewStyle() together.

struct ContentView: View {
    var body: some View {
        NavigationView {
            ScrollView {
                ForEach(0...20, id: \.self) { count in
                    (count % 2 == 0 ? Color.red : Color.blue)
                        .frame(height: 44.0)
                }
            }
            .background(NavigationConfigurator { nc in // NavigationConfigurator is from the OP's post: https://stackoverflow.com/a/58427754/7834914
                nc.hidesBarsOnSwipe = true
            })
            .navigationBarTitle("Hello World", displayMode: .inline)
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}

Before Scroll: Before Scroll

After Scroll: After Scroll

Upvotes: 4

Related Questions