SwiftUI:我如何使用多态性使一个模型类创建自己的视图



我正在尝试构建一个小应用程序,您可以在其中使用SwifUI创建带有问题的测试,但是找到如何使一切工作出来是很难像我这样的新手。该应用程序将在主可滚动视图中显示问题列表,这些问题可以是不同类型的,如真或假,文本,多项选择等,可以是活动的或不活动的。

我认为所有不同类型的问题都采用相同的协议会很好。该协议还将定义一个函数或计算属性,负责使用存储在不同属性中的值显示其on视图。但是,当试图修改任何与View交互的参数时问题就出现了。假设我想添加一个切换按钮来激活或响应这个问题,修改这个问题的一个值。使用我实现的不同解决方案,我没有得到正在重建/更新的视图。

我尝试了一些事情来实现这一点,比如用@State或@Binding包装那些应该更新其值的属性。我还尝试将这些属性转换为ObservableObjects,添加采用ObservableObject协议的新类,但它不起作用。唯一可行的方法是,对于任何类型的问题,创建一个带有可观察ViewModel的视图。稍后,在显示所有问题的视图中,我必须创建一个Switch,其中包含所有不同的可能性。

我不喜欢这个解决方案的地方是,如果我想添加一个新类型的问题,我必须修改这个主视图,为这个新类型的问题添加一个额外的情况,这是违反开闭原则的。

你有什么建议小伙分配这个责任给任何问题类,而不是主视图?

提前谢谢你:)

通常你不希望一个模型知道视图。这是倒退。但是,我们经常希望将具体的选择逻辑隐藏在单个调用之后,该调用根据所提供的数据执行不同的操作(类似于通过函数重载实现的多态性)。

关键思想是将工厂模式与访问者模式结合使用,以保持与开闭原则的和谐。

当我们对常规对象做这类事情时,我们通常使用工厂方法为输入数据返回适当的子类。在这个工厂方法中通常有一个switch语句。工厂接口允许我们遵循开闭原则,这样当我们添加新问题类型时,ContentView不会改变。很有可能,下面的结果与您使用视图模型方法得到的结果非常相似。

在SwiftUI中,工厂式视图的最佳方法是创建一个QuestionView,然后知道如何为每个问题对象创建正确的具体视图。我希望default + fatalError()能让你考虑到enum在这里是如何有用的。

struct QuestionView: View {
    let question: Question
    
    var body: some View {
        switch question {
        case let q as TextQuestion:
            TextQuestionView(question: q)
        case let q as DoubleQuestion:
            DoubleQuestionView(question: q)
        default:
            fatalError("Unknown question type")
        }
    }
}

然后在你的主视图中,它将是多态的,对实际的Question实例动态反应。你可以这样使用它:

struct ContentView: View {
    @State private(set) var questions: [Question]
    var body: some View {
        NavigationView {
            Form {
                ForEach(questions, id: .key) { question in
                    QuestionView(question: question)
                }
                
                Button("Submit") {
                    let answers = Answers()
                    for question in questions {
                        question.record(answers: answers)
                    }
                    print(answers)
                }
            }
            .navigationTitle("Questions")
        }
    }
}

提取答案的合理方法是使用访问者模式,其结构类似于Encodableencode(to encoder: Encoder)。我们期望每个专业视图与其特定的Question对象通信,然后期望Question对象包含func record(answers: Answers)的实现。时间到了,在questions上循环,让他们记录下他们的答案。(注意,我们可以在不改变Question子类的情况下添加各种Answers实现,以保持开闭原则)。

Question对象类似于视图模型,它们是ObservableObjects。你可以看到他们是如何记录他们的回答的。

要使其工作,它们不能是具有关联类型的协议。

class TextQuestion: Question, ObservableObject {
    @Published var answer = ""
    
    override func record(answers: Answers) {
        answers.addAnswer(key: key, value: answer)
    }
}
class MeasurementQuestion: Question, ObservableObject {
    let unit: String
    @Published var answer = 0.0
    
    init(key: String, question: String, unit: String) {
        self.unit = unit
        super.init(key: key, question: question)
    }
    
    override func record(answers: Answers) {
        answers.addAnswer(key: key, value: answer)
    }
}

然后每个单独的问题子类型视图将观察自己的Question实例:

struct TextQuestionView: View {
    @ObservedObject private(set) var question: TextQuestion
    
    var body: some View {
        Section(question.question) {
            TextField("Answer", text: $question.answer)
        }
    }
}
struct MeasurementQuestionView: View {
    @ObservedObject private(set) var question: MeasurementQuestion
    
    var body: some View {
        Section(question.question) {
            HStack {
                TextField("Answer", value: $question.answer, format: .number)
                Text(question.unit)
            }
        }
    }
}

你可以简单地将你的问题列表添加到预览中,看看它是如何工作的:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(questions: Example.questions)
    }
}
struct Example {
    static let questions: [Question] = [
        TextQuestion(
            key: "name",
            question: "What...is your name?"
        ),
        TextQuestion(
            key: "quest",
            question: "What...is your quest?"
        ),
        [
            TextQuestion(
                key: "assyria",
                question: "What...is the capital of Assyria?"
            ),
            TextQuestion(
                key: "color",
                question: "What...is your favorite colour?"
            ),
            MeasurementQuestion(
                key: "swallow",
                question: "What is the air-speed velocity of an unladen swallow?",
                unit: "kph"
            )
        ].randomElement()!
    ]
}

我不确定我是否喜欢对于玩具项目以外的任何东西的实现-我更喜欢更强的层分隔。然而,这确实设置了一个多态视图,以适应数据实现类型。关键思想是将工厂模式与访问者模式结合使用。

最新更新