【问题标题】:SwiftUI: code reuse with view compositionSwiftUI:视图组合的代码重用
【发布时间】: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 方法)。您能否指出正确的“设计模式”或改进我描述的解决方案之一?对不起,很长的问题。

【问题讨论】:

    标签: ios swift swiftui


    【解决方案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。谢谢。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-03
    • 1970-01-01
    • 2022-07-11
    • 2022-11-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多