SwiftUI:如何使用多态性使模型类创建自己的View

eqoofvh9  于 2022-12-10  发布在  Swift
关注(0)|答案(1)|浏览(161)

我正在尝试开发一个小应用程序,你可以在其中使用SwifUI创建带问题的测试,但是对于像我这样的新手来说,找到如何让一切都顺利进行是越来越困难的。这个应用程序会在一个主滚动视图中显示一个问题列表,这些问题可以是不同的类型,如对或错、文本、多项选择等......并且可以是活动的或不活动的。
我认为所有不同类型的问题采用相同的协议会很棒。这个协议还将定义一个函数或计算属性,尝试修改与该视图交互的任何参数时出现问题。让'比如说我想添加一个切换按钮来激活或React问题,修改问题的一个值。使用我实现的不同解决方案,我没有得到正在重建/更新的视图。
我尝试了几种方法来实现这一点,比如用@State或@Binding Package 那些应该更新其值的属性。我还尝试将这些属性转换为ObservableObject,添加采用ObservableObject协议的新类,但是没有效果。唯一看起来有效的方法是,对于任何类型的Question,创建一个视图,带有一个可观察的ViewModel。稍后,在显示所有问题的视图中,我必须创建一个包含所有不同可能性的Switch。
我不喜欢这个解决方案的地方是,如果我想添加一个新类型的问题,我必须修改这个主视图,为这个新类型的问题包括一个额外的情况,这违反了开放-封闭原则。
你有什么建议的家伙分配这个责任给任何问题类,而不是主视图?
提前感谢您:)

yuvru6vn

yuvru6vn1#

通常你不想让模型知道视图,这是逆向的,但是我们经常想把具体的选择逻辑隐藏在一个单独的调用后面,这个调用根据提供的数据做不同的事情(类似于通过函数重载的多态性)。
其关键思想是将Factory PatternVisitor Pattern结合使用,以保持开闭原理的和谐。
当我们对常规对象执行此类操作时,我们通常使用工厂方法来返回输入数据的正确子类。Inside that factory method usually sits a switch statement.工厂接口允许我们荣誉开-闭原则,这样当我们添加新的问题类型时,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并告诉他们记录他们的答案。(注意,我们可以添加各种Answers实现,而不更改Question子类,这与开-闭原则保持一致)。
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)
            }
        }
    }
}

您只需将您的list of questions添加到预览中,然后查看它的工作方式:

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

除了玩具项目之外,我不确定我是否喜欢这个实现--我更喜欢更强的层分离。然而,这确实设置了一个多态视图,可以根据数据实现类型进行调整。关键思想是结合使用Factory PatternVisitor Pattern

相关问题