SwiftUI:视图组合的代码重用

Posted

技术标签:

【中文标题】SwiftUI:视图组合的代码重用【英文标题】:SwiftUI: code reuse with view composition 【发布时间】:2019-09-25 20:09:39 【问题描述】:

使用 SwiftUI 开发我发现很难一起重用代码组合视图。我将向您展示一个简单的示例:假设我们的应用中有一个带有特定 UI 的文本字段。我们将此文本字段称为MyTextField。用户界面可能是:

代码如下:

struct MyTextField: View 
    @Binding var text: String
    var label: String

    var body: some View 
        VStack 
            HStack 
                Text(label)
                Spacer()
            
            TextField("", text: $text) //here we have a simple TextField
            Divider()
        
        .padding()
    

现在,假设我们想要另一个具有相同 UI 的文本字段,但要在安全上下文中使用。此文本字段称为MySecureTextField。在这种情况下,我应该使用SecureField 而不是TextField,但显然我不想以这种方式创建一个全新的视图:

struct MySecureTextField: View 
    @Binding var text: String
    var label: String

    var body: some View 
        VStack 
            HStack 
                Text(label)
                Spacer()
            
            SecureField("", text: $text) //this time we have a SecureField here
            Divider()
        
        .padding()
    

我该如何设计这样的情况?我尝试了几种方法,但似乎没有一个是正确的:

1 - 第一次尝试 拥有一种将实际文本字段作为参数的容器视图:

struct TextFieldContainer<ActualTextField>: View where ActualTextField: View 
    private let actualTextField: () -> ActualTextField
    var label: String

    init(label: String, @ViewBuilder actualTextField: @escaping () -> ActualTextField) 
        self.label = label
        self.actualTextField = actualTextField
    

    var body: some View 
        VStack 
            HStack 
                Text(label)
                Spacer()
            
            actualTextField()
            Divider()
        
        .padding()
    

我可以这样使用TextFieldContainer

struct ContentView: View 
    @State private var text = ""

    var body: some View 
        TextFieldContainer(label: "Label") 
            SecureField("", text: self.$text)
        
    

我不喜欢这个解决方案:我不想指定实际的文本字段,它应该隐含在视图本身中(MyTextFieldMySecureTextField)。通过这种方式,我什至可以在容器中注入任何类型的视图,而不仅仅是文本字段。

2 - 第二次尝试 拥有一个私有容器和两个在内部使用该容器的公共视图:

private struct TextFieldContainer<ActualTextField>: View where ActualTextField: View 
    //...
    //the same implementation as above
    //...


struct MyTextField: View 
    @Binding var text: String //duplicated code (see MySecureTextField)
    let label: String //duplicated code (see MySecureTextField)

    var body: some View 
        TextFieldContainer(label: label) 
            TextField("", text: self.$text)
        
    


struct MySecureTextField: View 
    @Binding var text: String //duplicated code (see MyTextField)
    let label: String //duplicated code (see MyTextField)

    var body: some View 
        TextFieldContainer(label: label) 
            SecureField("", text: self.$text)
        
    

并以这种方式使用它们:

struct ContentView: View 
    @State private var text = ""
    @State private var text2 = ""

    var body: some View 
        VStack 
            MyTextField(text: $text, label: "Label")
            MySecureTextField(text: $text2, label: "Secure textfield")
        
    

我并不是真的不喜欢这个解决方案,但是属性上有一些代码重复。如果有很多属性,就会有很多代码重复。另外,如果我更改了TextFieldContainer 上的某些属性,我应该更改所有视图,因此可能需要更改很多结构(MyTextFieldMySecureTextFieldMyEmailTextFieldMyBlaBlaTextField 等等)。

3 - 我的最后一次尝试 使用与上述第二次尝试相同的方法,但以这种方式使用AnyView

struct MySecureTextField: View 
    private let content: AnyView

    init(text: Binding<String>, label: String) 
        content = AnyView(TextFieldContainer(label: label) 
            SecureField("", text: text)
        )
    

    var body: some View 
        content
    


struct MyTextField: View 
    private let content: AnyView

    init(text: Binding<String>, label: String) 
        content = AnyView(TextFieldContainer(label: label) 
            TextField("", text: text)
        )
    

    var body: some View 
        content
    

这与第二次尝试没有什么不同,我的直觉是我错过了执行这项常见任务的正确方法(SwiftUI-y 方法)。您能否指出正确的“设计模式”或改进我描述的解决方案之一?对不起,很长的问题。

【问题讨论】:

【参考方案1】:

你可以使用简单的 if!

struct MyTextField: View 
    @Binding var text: String
    var label: String
    var secure: Bool = false

    var body: some View 
        VStack 
            HStack 
                Text(label)
                Spacer()
            

            if secure 
                SecureField("", text: $text)
             else 
                TextField("", text: $text)
            

            Divider()
        
        .padding()
    

用法:

MyTextField(text: $text, label: "Label") // unsecure
MyTextField(text: $text, label: "Label", secure: true) // secure

【讨论】:

感谢 Quinn,我赞成您的回答,因为这是一种简单而干净的方法,在某些简单的情况下可能值得。 只是一个建议:约定是在编写 swift 时省略括号,仅在需要时明确使用 self。如果你愿意,你可以在你的身体中使用if secure @ethoooo 是的,你是对的,我在 kotlin 和 swift 之间跳来跳去,以至于我总是习惯用括号... 【参考方案2】:

您的第一次尝试是正确的方法,但不是让调用者提供文本字段,而是为不同的字段类型添加静态方法:

struct TextFieldContainer<FieldView>: View where FieldView: View 

    var label: String

    var body: some View 
        VStack 
            HStack 
                Text(label)
                Spacer()
            
            fieldView
            Divider()
        
        .padding()
    

    fileprivate init(label: String, fieldView: FieldView) 
        self.label = label
        self.fieldView = fieldView
    

    private let fieldView: FieldView


extension TextFieldContainer where FieldView == TextField<Text> 
    static func plain(label: String, text: Binding<String>) -> some View 
        return Self(label: label, fieldView: TextField("", text: text))
    


extension TextFieldContainer where FieldView == SecureField<Text> 
    static func secure(label: String, text: Binding<String>) -> some View 
        return Self(label: label, fieldView: SecureField("", text: text))
    

使用示例:

struct ContentView: View 
    @State private var text = ""

    var body: some View 
        VStack 
            TextFieldContainer.plain(label: "Label", text: $text)
            TextFieldContainer.secure(label: "Label", text: $text)
        
    

【讨论】:

谢谢 Rob,这正是我正在寻找的通用解决方案。 嗨,Rob,很抱歉再次打扰您:您知道为什么在预览上方复制粘贴代码不再起作用吗?如果我使用您在另一个视图中创建的文本字段,则没有问题(预览有效),但文本字段本身的预览(在文本字段编码的文件中)会出现错误:replaced function 'secure(label:text:)' of type '&lt;τ_0_0 where τ_0_0 == SecureField&lt;Text&gt;&gt; (BBTextField&lt;SecureField&lt;Text&gt;&gt;.Type) -&gt; (String, Binding&lt;String&gt;) -&gt; some View' could not be found。谢谢。

以上是关于SwiftUI:视图组合的代码重用的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 将 Swift 代码作为参数传递给可重用视图

如何在 SwiftUI 视图上使用组合

在 SwiftUi 中水平组合和打破垂直滚动视图?

SwiftUI之深入解析如何创建和组合视图

SwiftUI:超过 4 个组合的文本视图不能与视图修饰符一起使用

SwiftUI如何将列表项重构为子视图