SwiftUI get rid of AnyView for environment variables

Viewed 120

I try to recreate the toolbar modifier for my own implementation of a NavigationStackView. It works now using environment variables.

Now I would like to get rid of using AnyView. I thought about using a computed property with type some View but can't get this to work.

Example:

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")
        }
    }
}
1 Answers

Actually it is not needed to pass view via Environment (and it is not very appropriate tool for that) - we can hide everything within a private implementation of NavigationStackView and use it directly, like

struct NavigationStackView<Content: View>: View {
    private var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    private struct _NavigationStackView<Content: View, Toolbar: View>: View {
        private var content: () -> Content
        private var toolbar: () -> Toolbar

        init(@ViewBuilder content: @escaping () -> Content, @ViewBuilder toolbar: @escaping () -> Toolbar) {
            self.content = content
            self.toolbar = toolbar
        }

        var body: some View {
            VStack(spacing: 0) {
                HStack(spacing: 0) {
                    toolbar()
                }
                Spacer()
                content()
                Spacer()
            }
        }
    }

    var body: some View {
        _NavigationStackView(content: content, toolbar: {})
    }

    func toolbar<Toolbar: View>(@ViewBuilder _ toolbar: @escaping () -> Toolbar) -> some View {
        _NavigationStackView(content: self.content, toolbar: toolbar)
    }
}

Tested with Xcode 13.2 / iOS 15.2

Related