简体   繁体   English

SwiftUI:代码重用与视图组合

[英]SwiftUI: code reuse with view composition

Developing with SwiftUI I'm finding it difficult to reuse code composing views together.使用 SwiftUI 进行开发我发现很难一起重用代码组合视图。 I'll show you a simple example: let's say we have a textfield in our app with a specific UI.我将向您展示一个简单的示例:假设我们的应用中有一个带有特定 UI 的文本字段。 Let's call this textfield MyTextField .让我们将此文本字段称为MyTextField The UI might be:用户界面可能是:

在此处输入图像描述

Here is the code:这是代码:

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()
    }
}

Now, let's say we want to have another textfield with the same UI, but to use in secure contexts.现在,假设我们想要另一个具有相同 UI 的文本字段,但要在安全上下文中使用。 This textfield is called MySecureTextField .此文本字段称为MySecureTextField In this case I should use SecureField instead of TextField , but clearly I don't want to create an entire new view this way:在这种情况下,我应该使用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()
    }
}

How can I design a situation like this?我该如何设计这样的情况? I tried several approaches, but none of them seem the right one:我尝试了几种方法,但没有一种方法看起来是正确的:

1 - First attempt To have a sort of container view that takes the actual textfield as a parameter: 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()
    }
}

I could use TextFieldContainer this way:我可以这样使用TextFieldContainer

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

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

I don't like this solution: I don't want to specify the actual textfield, it should be implicit in the view itself ( MyTextField or MySecureTextField ).我不喜欢这个解决方案:我不想指定实际的文本字段,它应该隐含在视图本身( MyTextFieldMySecureTextField )中。 And this way I could even inject any kind of view inside the container and not just a textfield.这样我什至可以在容器中注入任何类型的视图,而不仅仅是一个文本字段。

2 - Second attempt To have a private container and two public views that use the container internally: 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)
        }
    }
}

and use them this way:并以这种方式使用它们:

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")
        }
    }
}

I don't really dislike this solution, but there is some code duplication on the properties.我并不是真的不喜欢这个解决方案,但是属性上有一些代码重复。 If there were a lot of properties there would be a lot of code duplication.如果有很多属性,就会有很多代码重复。 Also, if I changed some properties on TextFieldContainer I should change all the views consequently, it may be a lot of structs to change ( MyTextField , MySecureTextField , MyEmailTextField , MyBlaBlaTextField , and so forth).此外,如果我更改了TextFieldContainer上的某些属性,我应该因此更改所有视图,可能需要更改很多结构( MyTextFieldMySecureTextFieldMyEmailTextFieldMyBlaBlaTextField等)。

3 - My last attempt Use the same approach as in the second attempt here above, but using AnyView this way: 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
    }
}

It's not that different from the second try and my gut feeling is that I'm missing the right way (the SwiftUI-y way) to do this common task.这与第二次尝试没有什么不同,我的直觉是我错过了完成这项常见任务的正确方法(SwiftUI-y 方式)。 Can you point me to the right "design pattern" or maybe improve one of the solutions I described?您能否指出正确的“设计模式”或改进我描述的解决方案之一? Sorry for the long question.对不起,很长的问题。

You can use a simple if!你可以使用一个简单的 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 (self.secure) {
                SecureField("", text: $text)
            } else {
                TextField("", text: $text)
            }
            Divider()
        }
        .padding()
    }
}

Usage:用法:

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

Your first attempt is the correct approach, but instead of letting the caller provide the text field, add static methods for the different field types:您的第一次尝试是正确的方法,但不是让调用者提供文本字段,而是为不同的字段类型添加 static 方法:

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))
    }
}

Example use:示例使用:

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

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM