iosswiftui

Hide toolbar item on scroll with .toolbarTitleDisplayMode(.inlineLarge)


I want to display a profile button like how iOS Health and App Store app do, and I thought it was done using .toolbarTitleDisplayMode(.inlineLarge) (available in iOS 17+). I get the desired look but the Image in the code below does not hide on scroll/when navigationTitle transitions to inline.

struct SomeView: View {
    var body: some View {
        NavigationStack {
            ScrollView {
                ForEach(0..<50) { i in
                    Text("Item \(i)")
                }
            }
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Image(systemName: "person.circle.fill")
                        .toolbarTitleDisplayMode(.inlineLarge)
                }
            }
            .navigationTitle("Profile")
        }
    }
}

Solution

  • I am not sure that your interpretation of .toolbarTitleDisplayMode(.inlineLarge) is correct. The documentation says:

    In iOS, this behavior displays the title as large inside the toolbar and moves any leading or centered toolbar items into the overflow menu of the toolbar.

    This does not seem to suggest, that an item with placement .topBarTrailing will be hidden when a large title is scrolled up.

    However, one way to get it working is to detect when the content has been scrolled up by a threshold amount and to hide the item when this happens.

    struct SomeView: View {
        @State private var isInlineTitle = false
    
        private func scrollDetector(topInsets: CGFloat) -> some View {
            GeometryReader { proxy in
                let minY = proxy.frame(in: .global).minY
                let isUnderToolbar = minY - topInsets - 40 < 0
                Color.clear
                    .onChange(of: isUnderToolbar) { _, newVal in
                        isInlineTitle = newVal
                    }
            }
        }
    
        var body: some View {
            GeometryReader { outer in
                let topInsets = outer.safeAreaInsets.top
                NavigationStack {
                    ScrollView {
                        VStack {
                            ForEach(0..<50) { i in
                                Text("Item \(i)")
                            }
                        }
                        .frame(maxWidth: .infinity)
                        .background {
                            scrollDetector(topInsets: topInsets)
                        }
                    }
                    .toolbar {
                        ToolbarItem(placement: .topBarTrailing) {
                            Image(systemName: "person.circle.fill")
                                .opacity(isInlineTitle ? 0 : 1)
                        }
                    }
                    .navigationTitle("Profile")
                }
                .animation(.easeInOut(duration: 0.2), value: isInlineTitle)
            }
        }
    }
    

    Animation