【问题标题】:How do I make a preferenceKey accept a View in SwiftUI?如何让preferenceKey 接受SwiftUI 中的视图?
【发布时间】:2021-05-04 21:52:16
【问题描述】:

我正在尝试构建一个自定义 NavigationView,但我正在努力解决如何实现自定义“.navigationBarItems(leading: /* insert views /, trailing: / insert Views */ )”。我假设我必须使用一个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: { })
    }
}

这是我尝试创建一个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)
    }
}

【问题讨论】:

  • navigationBarItems() 正在将deprecated 切换到.toolbar 模式。 Passing a View 在 SO 中已被多次询问
  • 谢谢,但不完全是。在上面的示例代码中,我自己将视图作为变量传递。这不是我正在努力解决的问题。它使用 View 作为让我难过的preferenceKey。我尝试像通常在视图中那样将视图传递给我的preferenceKey,但它没有按我预期的那样工作。

标签: swiftui


【解决方案1】:

可能的做法是使用AnyView,比如

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

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

extension View {
    func topMenuItems<LView: View, TView: View>(leading: LView, trailing: TView) -> some View {
        self
          .preference(key: TopMenuItemsLeading.self, value: AnyView(leading))
          .preference(key: TopMenuItemsTrailing.self, value: AnyView(trailing))
    }
}

【讨论】:

  • 太棒了,谢谢。这种方法的唯一缺点是 AnyView 使用起来有点重。有没有办法以某种方式初始化视图?
  • 你测量过它的重量还是只是相信某人的传说? ;)
  • 哈哈!我的意思是“性能重”。我自己不是专家,但引用 Paul Hudson 的话:“现在,这里的合乎逻辑的结论是问为什么我们不一直使用 AnyView,如果它可以让我们避免某些 View 的限制。答案很简单:性能。 [...] 因此,当定期更改发生时,可能需要做更多的工作来更新我们的用户界面,因此通常最好避免使用 AnyView,除非您特别需要它。”
  • 关键词“除非你特别需要”
  • 我尝试使用它,但由于 AnyView 不相等,我无法让 onPreferenceChange 工作。因此,我最终改用了自定义对象。我已经在这里发布了我的解决方案。
【解决方案2】:

您可以像这样声明 TopView 的初始化程序以采用 Views

struct TopMenu<Left: View, Right: View>: View {
    
    let leading: Left
    let trailing: Right
    
    init(left: Left,
         right: Right) {
        self.leading = left
        self.trailing = right
    }
    //etc.

然后像定义navigationBarItems修饰符一样声明修饰符:

extension View {
    
    func topMenuItems<L, T>(leading: L, trailing: T) -> some View where L : View, T : View {
        VStack(alignment: .center, spacing: 0) {
            TopMenu(left: leading, right: trailing)
            self
        }
    }
    func topMenuItems<L>(leading: L) -> some View where L : View {
        VStack(alignment: .center, spacing: 0) {
            TopMenu(left: leading, right: EmptyView())
            self
        }
    }
    func topMenuItems<T>(trailing: T) -> some View where T : View {
        VStack(alignment: .center, spacing: 0) {
            TopMenu(left: EmptyView(), right: trailing)
            self
        }
    }

}

【讨论】:

  • 是的,但这只会在视图层次结构中向下起作用,对吗?我需要专门将视图从子视图传递到 topMenu。
  • 为什么你认为它不起作用?它适用于导航栏项目。
  • 嗯,我承认我不是专家,但我认为当向层次结构传递信息时,preferenceKeys 是绑定的唯一替代方案。我会测试一下,如果可行,我会将其标记为答案,因为它比我想出的要简单,
  • 好的,我测试了它,除非我误解了你的建议,否则它不会像我的解决方案那样更新同一个 TopMenu。它只是在我添加修饰符的任何视图周围添加一个新的 TopMenu。如果您尝试我添加到问题中的解决方案,可能会更容易看到我想要实现的目标。抱歉,如果不够清楚。
【解决方案3】:

好的,所以这里有一些很好的部分答案,但没有一个真正达到我的要求,即使用preferenceKey将视图传递到视图层次结构。本质上是 .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
        })
        
    }
}
`````

【讨论】:

  • 纯粹的天才!非常感谢您抽出宝贵时间分享您的解决方案
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多