我正在尝试构建一个小应用程序,您可以在其中使用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")
}
}
}
提取答案的合理方法是使用访问者模式,其结构类似于Encodable
的encode(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()!
]
}
我不确定我是否喜欢对于玩具项目以外的任何东西的实现-我更喜欢更强的层分隔。然而,这确实设置了一个多态视图,以适应数据实现类型。关键思想是将工厂模式与访问者模式结合使用。