如何创建一个带有可选辅助 View 参数的 SwiftUI 视图?

Posted

技术标签:

【中文标题】如何创建一个带有可选辅助 View 参数的 SwiftUI 视图?【英文标题】:How do you create a SwiftUI view that takes an optional secondary View argument? 【发布时间】:2019-10-14 22:01:30 【问题描述】:

我正在尝试创建一个自定义 SwiftUI 视图,该视图的行为类似于默认视图,我可以在其中使用方法或可选的初始化程序参数向视图添加额外内容。

SomeCustomView(title: "string argument") 
    // some view


SomeCustomView(title: "hello") 
    // some view
.sideContent 
    // another view


// This style is acceptable too
SomeCustomView(title: "hello", sideContent:  /* another view */ ) 
    // some view

我怎样才能修改这个视图结构使其表现得像上面的例子?

struct SomeCustomView<Content>: View where Content: View 
    let title: String
    let content: Content

    init(title: String, @ViewBuilder content: () -> Content) 
        self.title = title
        self.content = content()
    

    var body: some View 
        VStack 
            Text(title)
            content
        
    

理想情况下,我有两个不同的主体“模板”,我可以根据是调用 sideContent 方法还是设置了 sideContent 参数来切换它们。例如,

var body: some View 
    VStack 
        Text(title)
        content
    


// or

var otherBody: some View 
    HStack 
        VStack 
            Text(title)
            content
        
        sideContent
    

【问题讨论】:

为什么不创建一个群组,这样您就可以说if 并根据某些属性包含或不包含侧视图? @matt 我尝试过类似的方法,但我不知道如何接受可能是 nilEmptyView 的任意可选视图。 我明白你的意思了。 【参考方案1】:

2021 年 11 月更新(适用于 Xcode 11.x、12.x 和 13.x)

经过一番思考和反复试验,我想通了。事后看来有点明显。

struct SomeCustomView<Content>: View where Content: View 
    let title: String
    let content: Content

    init(title: String, @ViewBuilder content: @escaping () -> Content) 
        self.title = title
        self.content = content()
    

    // returns a new View that includes the View defined in 'body'
    func sideContent<SideContent: View>(@ViewBuilder side: @escaping () -> SideContent) -> some View 
        HStack 
            self     // self is SomeCustomView
            side() 
        
    

    var body: some View 
        VStack 
            Text(title)
            content
        
    

不管有没有方法调用都可以。

SomeCustomView(title: "string argument") 
    // some view


SomeCustomView(title: "hello") 
    // some view
.sideContent 
    // another view

以前的代码有细微的错误:body 应该是self

    func sideContent<SideContent: View>(@ViewBuilder side: @escaping () -> SideContent) -> some View 
        HStack 
            body // <--- subtle bug, updates to the main View are not propagated 
            side() 
        
    

感谢 Jordan Smith 很久以前就指出了这一点。

【讨论】:

优秀的解决方案。像魅力一样工作,非常符合 SwiftUI 范式。 这不适用于 xcode 11 和 swift ui 1.0,如果您尝试在没有 sideContent 闭包参数的情况下调用它,它会说“无法推断通用参数'内容'” @AntonShevtsov 尝试将&lt;SideContent: View&gt; 添加到函数中?在 Xcode 11.6 中为我工作 这里有一个微妙但重要的错误:通过使用'body',我们在当前时间点运行body view builder,并忽略了创建body 的视图。对于像这里显示的那样的静态视图,这很好 - 但对于任何更动态的东西(例如,也许你有一些 @State 变量或首选项键),返回的视图将不再按预期运行,因为它不再封装任何状态与视图相关联。我建议不要使用正文,而是使用视图本身,它可以继承任何状态等。【参考方案2】:

我为容器视图遵循的一种模式是使用条件扩展一致性来支持不同变体的初始化程序。

这是一个带有可选页脚的简单面板视图示例。

struct Panel<Content: View, Footer: View>: View 
    
    let content: Content
    let footer: Footer?
    
    init(@ViewBuilder content: () -> Content, footer: (() -> Footer)? = nil) 
        self.content = content()
        self.footer = footer?()
    
    
    var body: some View 
        VStack(spacing: 0) 
            content

            // Conditionally check if footer has a value, if desirable.
            footer
        
    


// Support optional footer
extension Panel where Footer == EmptyView 
    init(@ViewBuilder content: () -> Content) 
        self.content = content()
        self.footer = nil
    


我相信这类似于 Apple 为支持所有内置类型的变体所做的工作。例如,这是 Button 的标头的 sn-p。

@available(ios 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension Button where Label == PrimitiveButtonStyleConfiguration.Label 

    /// Creates an instance representing the configuration of a
    /// `PrimitiveButtonStyle`.
    @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
    public init(_ configuration: PrimitiveButtonStyleConfiguration)


@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension Button where Label == Text 

    /// Creates an instance with a `Text` label generated from a localized title
    /// string.
    ///
    /// - Parameters:
    ///     - titleKey: The key for the localized title of `self`, describing
    ///       its purpose.
    ///     - action: The action to perform when `self` is triggered.
    public init(_ titleKey: LocalizedStringKey, action: @escaping () -> Void)

    /// Creates an instance with a `Text` label generated from a title string.
    ///
    /// - Parameters:
    ///     - title: The title of `self`, describing its purpose.
    ///     - action: The action to perform when `self` is triggered.
    public init<S>(_ title: S, action: @escaping () -> Void) where S : StringProtocol

【讨论】:

这个!谢谢你:)【参考方案3】:

我建议使用ViewModifyer 而不是自定义视图。这些工作如下:

struct SideContent<SideContent: View>: ViewModifier 

    var title: String
    var sideContent: (() -> SideContent)?

    init(title: String) 
         self.title = title
    

    init(title: String, @ViewBuilder sideContent: @escaping () -> SideContent) 
         self.title = title
         self.sideContent = sideContent
    

    func body(content: Content) -> some View 
        HStack 
          VStack 
             Text(title)
             content
           
           sideContent?()
        
    

这个可以作为SomeView().modifier(SideContent(title: "asdasd") Text("asdasd"))使用,不过如果省略边,还是需要指定它的类型SomeView().modifier(SideContent&lt;EmptyView&gt;(title: "asdasd"))

更新

删除它简化的标题,正如你提到的。

struct SideContent<SideContent: View>: ViewModifier 

    var sideContent: (() -> SideContent)

    init(@ViewBuilder sideContent: @escaping () -> SideContent) 
        self.sideContent = sideContent
    

    func body(content: Content) -> some View 
        HStack 
            content
            sideContent()
        
    

另外,您可以为Title 进行修饰符。

struct Titled: ViewModifier 

    var title: String

    func body(content: Content) -> some View 
        VStack 
            Text(title)
            content
        
    


SomeView()
   .modifier(Titled(title: "Title"))
   .modifier(SideContent  Text("Side") )

【讨论】:

如果你想使用“模板”,要么使用Group,要么在表达式末尾转换为AnyView,这样类型就会匹配。 这种技术有效,但有点难看。您可以将title 参数保留在SomeView 中并避免使用SideContent&lt;EmptyView&gt;

以上是关于如何创建一个带有可选辅助 View 参数的 SwiftUI 视图?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用可选参数创建端点? (节点、快递、MongoDB)

如何创建一个带有输入的闭包结构参数,在 SwiftUI 中返回“some View”而不是“AnyView”?

如何在 argparse 中使用带有 nargs='*' 参数的可选位置参数?

带有可选参数的 Postgresql 函数

Bash - 带有可选参数和缺少逻辑的函数

discord.py 如何接受可选参数