Reputation: 692
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: 10
Views: 11923
Reputation: 136
I've come across the same problem. Here's how i solved it.
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
Reputation: 13246
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())
}
}
Upvotes: 3