1

I try to recreate the .toolbar modifier Apple uses for their NavigationView. I created an own implementation of a NavigationStackView but also want to use a .toolbar modifier.

I got something to work using environment objects and custom view modifiers, but when I don't apply the .toolbar modifier this won't work because no environment object is set.

Is there a better way to do this? How does Apple do this?

Example:

import Foundation
import SwiftUI

class ToolbarData: ObservableObject {
    @Published var view: (() -> AnyView)? = nil
    
    init(_ view: @escaping () -> AnyView) {
        self.view = view
    }
}

struct NavigationStackView<Content: View>: View {
    @ViewBuilder var content: () -> Content
    @EnvironmentObject var toolbar: ToolbarData
    
    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 0) {
                if (toolbar.view != nil) {
                    toolbar.view!()
                }
            }
            Spacer()
            content()
            Spacer()
        }
    }
}

struct NavigationStackToolbar<ToolbarContent: View>: ViewModifier {
    var toolbar: ToolbarContent
        
    func body(content: Content) -> some View {
        content
            .environmentObject(ToolbarData({
                AnyView(toolbar)
            }))
    }
}

extension NavigationStackView {
    func toolbar<Content: View>(_ content: () -> Content) -> some View {
        modifier(NavigationStackToolbar(toolbar: content()))
    }
}

struct NavigationStackView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationStackView {
            Text("Test")
        }
        .toolbar {
            Text("Toolbar")
        }
    }
}

Current solution:

import Foundation
import SwiftUI

private struct ToolbarEnvironmentKey: EnvironmentKey {
    static let defaultValue: AnyView = AnyView(EmptyView())
}

extension EnvironmentValues {
    var toolbar: AnyView {
        get { self[ToolbarEnvironmentKey.self] }
        set { self[ToolbarEnvironmentKey.self] = newValue }
    }
}

struct NavigationStackView<Content: View>: View {
    @ViewBuilder var content: () -> Content    
    @Environment(\.toolbar) var toolbar: AnyView
    
    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 0) {
                toolbar
            }
            Spacer()
            content()
            Spacer()
        }
    }
}

extension NavigationStackView {
    func toolbar<Content: View>(_ content: () -> Content) -> some View {
        self
            .environment(\.toolbar, AnyView(content()))
    }
}

struct NavigationStackView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationStackView {
            Text("Test")
        }
        .toolbar {
            Text("Toolbar")
        }
    }
}
Manu
  • 922
  • 6
  • 16
  • 1
    You can make your own `.Environment` (not `.EnvironmentObject`) variables to use just like Apple does. Also, you want to avoid using `AnyView` as that can mess with SwiftUI view updating system. – Yrb Feb 09 '22 at 21:57
  • That's a very good hint, thank you. I added my current solution to the question above. But how do I get rid of `AnyView` in this case? – Manu Feb 10 '22 at 19:35
  • 1
    That should be a new question. You shouldn't ask a new question, in the comments of a different question. Rework your code using the `.Environment` and post a new question if you can't solve the `AnyView` issue. – Yrb Feb 10 '22 at 19:41

0 Answers0