Search code examples
swiftuiswiftui-navigationview

Navigation Title with custom view with large and small title transition


With navigationTitle we can set a specific Title to the navigation view ex: "Rows" .

So when the view is loaded we get large text title and when it scrolled the title font reduces and aligns to the center.

However is it possible to have a custom view in place of title text? And have the same effect of large to small text transition when scrolled?

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                ForEach(0..<100, id: \.self) { index in
                    Text("Row Item \(index)")
                }
            }.listStyle(.plain)
            .padding()
            .navigationTitle("Rows")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Large Text when loaded

enter image description here

Small Text when scrolled.

enter image description here


Solution

  • I came across the same problem recently. Here is what I came up with:

    import SwiftUI
    
    struct LabeledScrollView<Content: View, Label: View>: View {
        /// Scale of the label when shown outside of the toolbar
        let labelScale: Double = 1.4
        @ViewBuilder var content: Content
        @ViewBuilder var label: Label
        @State var showToolbarTitle: Bool = false
        var body: some View {
            ScrollView {
                LazyVStack(alignment: .leading) {
                    label
                    .scaleEffect(labelScale, anchor: .topLeading)
                    .overlay {
                        GeometryReader { geo in
                            EmptyView()
                                .onChange(of: geo.frame(in: .named("container"))) { newValue in
                                    /// 5.0 constant value is about the offset of text from the bottom of the label view - it ensures the toolbar label show right when the text moves out of view behind the toolbar - can remove if there is not any space between the bottom of the view and the content inside
                                    let heightOfViewShowing = ((newValue.height * labelScale) + newValue.origin.y) - 5.0
                                    if heightOfViewShowing <= 0.0 {
                                        withAnimation {
                                            showToolbarTitle = true
                                        }
                                    } else {
                                        withAnimation {
                                            showToolbarTitle = false
                                        }
                                    }
                                }
                        }
                    }
                    .padding(.bottom)
                    content
                }
                .padding()
            }
            .toolbar {
                ToolbarItem(placement: .principal) {
                    label
                    .opacity(showToolbarTitle ? 1.0 : 0.0)
                }
            }
            .navigationBarTitleDisplayMode(.inline)
            .coordinateSpace(name: "container")
        }
    }
    
    struct LabeledScrollView_Previews: PreviewProvider {
        static var previews: some View {
            /// Preview placed in NavigationStack to show toolbar
            NavigationStack {
                LabeledScrollView {
                    ForEach(1...3, id: \.self) { i in
                        Text("Item \(i)")
                    }
                } label: {
                    HStack {
                        Image(systemName: "checkmark")
                            .foregroundColor(.green)
                        Text("Custom Title View")
                    }
                    .font(.body.weight(.semibold))
                }
                .toolbar(.visible, for: .navigationBar)
            }
        }
    }
    

    While I used a LazyVStack for presenting content, you could adapt to use a List as well.