如何创建一个带有可选辅助 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 我尝试过类似的方法,但我不知道如何接受可能是 nil
或 EmptyView
的任意可选视图。
我明白你的意思了。
【参考方案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 尝试将<SideContent: View>
添加到函数中?在 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<EmptyView>(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<EmptyView>
以上是关于如何创建一个带有可选辅助 View 参数的 SwiftUI 视图?的主要内容,如果未能解决你的问题,请参考以下文章
如何创建一个带有输入的闭包结构参数,在 SwiftUI 中返回“some View”而不是“AnyView”?