如何让一个首选项键接受 SwiftUI 中的视图?

lmu*_*nck 5 swiftui

我正在尝试构建自定义 NavigationView,并且正在努力解决如何实现自定义“.navigationBarItems(前导:/ *插入视图/,尾随:/插入视图*/)”。我认为我必须使用preferenceKey,但我不知道如何让它接受视图。

我的顶部菜单看起来像这样:

import SwiftUI

struct TopMenu<Left: View, Right: View>: View {
    
    let leading: Left
    let trailing: Right
    
    init(@ViewBuilder left: @escaping () -> Left, @ViewBuilder right: @escaping () -> Right) {
        self.leading = left()
        self.trailing = right()
    }
    
    var body: some View {
        
        VStack(spacing: 0) {
            
            HStack {
                
                leading
                
                Spacer()
                
                trailing
                
            }.frame(height: 30, alignment: .center)
            
            Spacer()
            
        }
        .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
        
    }
}

struct TopMenu_Previews: PreviewProvider {
    static var previews: some View {
        TopMenu(left: { }, right: { })
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我尝试创建一个preferenceKey来更新它,我显然错过了一些非常基本的东西:

struct TopMenuItemsLeading: PreferenceKey {
    static var defaultValue:View
    
    
    static func reduce(value: inout View, nextValue: () -> View) {
        value = nextValue()
    }
}

struct TopMenuItemsTrailing: PreferenceKey {
    static var defaultValue:View
    
    
    static func reduce(value: inout View, nextValue: () -> View) {
        value = nextValue()
    }
}

extension View {
    func topMenuItems(leading: View, trailing: View) -> some View {
        self.preference(key: TopMenuItemsLeading.self, value: leading)
        self.preference(key: TopMenuItemsTrailing.self, value: trailing)
    }
}
Run Code Online (Sandbox Code Playgroud)

lmu*_*nck 11

好的,这里有一些很好的部分答案,但没有一个真正实现了我所要求的,即使用首选项键将视图传递到视图层次结构。本质上是 .navigationBarItems 方法正在做的事情,但是使用我自己的自定义视图。

然而,我找到了一个解决方案,所以这里是(如果我错过了任何明显的捷径,我深表歉意。这是我第一次使用preferenceKeys做任何事情):

import SwiftUI

struct TopMenu: View {
    
    @State private var show:Bool = false
    
    var body: some View {
        VStack {
            TopMenuView {
                
                Button("Change", action: { show.toggle() })
                
                Text("Hello world!")
                    .topMenuItems(leading: Image(systemName: show ? "xmark.circle" : "pencil"))
                    .topMenuItems(trailing: Image(systemName: show ? "pencil" : "xmark.circle"))
            }
        }
    }
}

struct TopMenu_Previews: PreviewProvider {
    static var previews: some View {
        TopMenu()
    }
}

/*

To emulate .navigationBarItems(leading: View, trailing: View), I need four things:
 
    1) EquatableViewContainer - Because preferenceKeys need to be equatable to be able to update when a change occurred
    2) PreferenceKeys - That use the EquatableViewContainer for both leading and trailing views
    3) ViewExtenstions - That allow us to set the preferenceKeys individually or one at a time
    4) TopMenu view - That we can set somewhere higher in the view hierarchy.
 
 */

// First, create an EquatableViewContainer we can use as preferenceKey data
struct EquatableViewContainer: Equatable {
    
    let id = UUID().uuidString
    let view:AnyView
    
    static func == (lhs: EquatableViewContainer, rhs: EquatableViewContainer) -> Bool {
        return lhs.id == rhs.id
    }
}

// Second, define preferenceKeys that uses the Equatable view container
struct TopMenuItemsLeading: PreferenceKey {
    static var defaultValue: EquatableViewContainer = EquatableViewContainer(view: AnyView(EmptyView()) )
    
    static func reduce(value: inout EquatableViewContainer, nextValue: () -> EquatableViewContainer) {
        value = nextValue()
    }
}

struct TopMenuItemsTrailing: PreferenceKey {
    static var defaultValue: EquatableViewContainer = EquatableViewContainer(view: AnyView(EmptyView()) )
    
    static func reduce(value: inout EquatableViewContainer, nextValue: () -> EquatableViewContainer) {
        value = nextValue()
    }
}

// Third, create view-extensions for each of the ways to modify the TopMenu
extension View {
    
    // Change only leading view
    func topMenuItems<LView: View>(leading: LView) -> some View {
        self
            .preference(key: TopMenuItemsLeading.self, value: EquatableViewContainer(view: AnyView(leading)))
    }
    
    // Change only trailing view
    func topMenuItems<RView: View>(trailing: RView) -> some View {
        self
            .preference(key: TopMenuItemsTrailing.self, value: EquatableViewContainer(view: AnyView(trailing)))
    }
    
    // Change both leading and trailing views
    func topMenuItems<LView: View, TView: View>(leading: LView, trailing: TView) -> some View {
        self
            .preference(key: TopMenuItemsLeading.self, value: EquatableViewContainer(view: AnyView(leading)))
    }
}


// Fourth, create the view for the TopMenu
struct TopMenuView<Content: View>: View {
    
    // Content to put into the menu
    let content: Content
    
    @State private var leading:EquatableViewContainer = EquatableViewContainer(view: AnyView(EmptyView()))
    @State private var trailing:EquatableViewContainer = EquatableViewContainer(view: AnyView(EmptyView()))
    
    
    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        
        VStack(spacing: 0) {
            
            ZStack {
                
                HStack {
                    
                    leading.view
                    
                    Spacer()
                    
                    trailing.view
                    
                }
                
                Text("TopMenu").fontWeight(.black)
            }
            .padding(EdgeInsets(top: 0, leading: 2, bottom: 5, trailing: 2))
            .background(Color.gray.edgesIgnoringSafeArea(.top))
            
            content
            
            Spacer()
            
        }
        .onPreferenceChange(TopMenuItemsLeading.self, perform: { value in
            leading = value
        })
        .onPreferenceChange(TopMenuItemsTrailing.self, perform: { value in
            trailing = value
        })
        
    }
}
`````
Run Code Online (Sandbox Code Playgroud)

  • 纯粹的天才!非常感谢您花时间分享您的解决方案 (2认同)