I'm new to swift & swift UI so forgive me if there is an obvious answer to this question.
I'm trying to dynamically change the information within a view based on certain questions. Different types of questions require different responses. How do I properly down cast the Question object in QuestionResponseView as the proper subclass so that I can set the user's responses to the questions?
(Note: I still need to finish some design elements. I'm trying to solve this functionality issue before moving forward)
Thank you!
The Question object and it's subclasses:
struct Questions {
static let CheckinQuestions : [Question] = [
ValueResponseQuestion(questionText: "How many hours of sleep did you get last night?", initialValue: 6, maxValue: 12),
TextResponseQuestion(questionText: "What is taking up most of your headspace today?", responseText: "Family, Work, Children, Arguement with friend"),
TextResponseQuestion(questionText: "What do you feel you need in this moment? What is missing?", responseText: "Friendship, Money, Peace, Exercise"),
ValueResponseQuestion(questionText: "What is your stress level currently at?", initialValue: 50, maxValue: 100)
]
static var GroundingQuestions : [Question] = [
//TODO: (later)
]
}
class Question : ObservableObject {
var questionText : String
init(questionText: String) {
self.questionText = questionText
}
}
class ValueResponseQuestion : Question {
var responseValue : Double
var minValue : Double = 0
var maxVal : Double
init(questionText: String, initialValue: Double, maxValue: Double) {
responseValue = initialValue
maxVal = maxValue
super.init(questionText: questionText)
}
}
class TextResponseQuestion : Question {
var responseText : String
init(questionText: String, responseText: String){
self.responseText = responseText
super.init(questionText: questionText)
}
}
The view where the user answers the questions from the array in the previous file, and the answer's are supposed to be recorded when the button is pressed.
struct QuestionResponseView: View {
/** Variable that tracks the current question from the array of questions */
@State var questionNum : Int = 0
/** The current question (starting at the beginning of the array) */
@State var question : Question = Questions.CheckinQuestions[0]
//TODO: Do I need this, or can I simply save this value in the ValueResponseQuestion class?
/** The variable used to track value responses from slider */
@State private var responseVal: Double = 6.0
//TODO: Do I need this, or can I simply save this value in the TextResponseQuestion class?
/** The variable used to track text responses from the text box */
@State private var responseText: String = "Placeholder"
/** The progress bar at the top of the page that tracks the current question */
private var progressView : RoundedProgressBar
init() {
UITextView.appearance().backgroundColor = .clear
progressView = RoundedProgressBar(progressVal: 1)
}
var body: some View {
VStack{
Text("Let's start...").font(.title)
Spacer()
progressView
Spacer()
VStack{
Text("\(question.questionText)").padding()
if question is ValueResponseQuestion {
//TODO: Set value response
//How can I get/set this from the question object?
Slider(value: $responseVal, in: 0...12).padding()
} else if question is TextResponseQuestion {
//How can I get/set this from the question object?
TextEditor(text: $responseText).background(.thickMaterial).frame(width: 285, height: 250).cornerRadius(20).padding()
}
}.frame(width: 340, height: 490, alignment: .center).overlay(RoundedRectangle(cornerRadius: 40).stroke(.gray, lineWidth: 3))
Spacer()
Button{
//Record responses and change the question
//How do I downcast question to TextResponseQuestion so that I can access and set this value?
question.questionResponseText = responseText
print(question)
responseText = "Placeholder"
questionNum = 1
if questionNum < Questions.CheckinQuestions.endIndex {
question = Questions.CheckinQuestions[questionNum]
}
else {
}
progressView.increaseProgress()
} label: {
Text("Next Question").foregroundColor(.white).padding()
}.frame(width: 150, height: 30).background(Color(red: 0.376, green: 0.4, blue: 0.816)).cornerRadius(3)
}
}
}
struct QuestionResponseView_Previews: PreviewProvider {
static var previews: some View {
QuestionResponseView()
}
}
CodePudding user response:
classes that are ObservableObject
need to be observed so you can see/make changes. When you pass the initial value for the @ObservedObject
you can cast it to the specific type.
There are comments in the code too.
import SwiftUI
@available(iOS 15.0, *)
struct QuestionResponseView: View {
@State var repository: QuestionRepository = QuestionRepository()
@State var currentQuestionIndex: Int = 0
init() {
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
VStack{
if currentQuestionIndex == 0{
Text("Let's start...").font(.title)
}
Spacer()
Text("\(currentQuestionIndex 1)/\(repository.checkInQuestion.count)")
ProgressView("progress", value: Double(currentQuestionIndex)/Double(repository.checkInQuestion.count))
Spacer()
TabView(selection: $currentQuestionIndex){
ForEach($repository.checkInQuestion){ $question in
ScrollView{
VStack{
Text("\(question.questionText)").padding()
if question is ValueResponseQuestion {
//Once you check it you can cast it
ValueResponseQuestionView(question: question as! ValueResponseQuestion)
} else if question is TextResponseQuestion {
//Once you check it you can cast it
TextResponseQuestionView(question: question as! TextResponseQuestion)
}
}.frame(width: 340, height: 490, alignment: .center).overlay(RoundedRectangle(cornerRadius: 40).stroke(.gray, lineWidth: 3))
Spacer()
Button( action: {
if currentQuestionIndex >= repository.checkInQuestion.count - 1 {
currentQuestionIndex = 0
}else{
currentQuestionIndex = 1
}
}, label:{
Text("Next Question").foregroundColor(.white)
})
.frame(width: 150, height: 30)
.background(Color(red: 0.376, green: 0.4, blue: 0.816)).cornerRadius(3)
}.tag(repository.checkInQuestion.firstIndex(where: {
$0 == question
})!)
}
}.tabViewStyle(.page)
}
}
}
struct ValueResponseQuestionView: View{
//ObservableObjects have to be observed so you see changes
@ObservedObject var question: ValueResponseQuestion
var body: some View{
//TODO: Set value response
//How can I get/set this from the question object?
Slider(value: $question.responseValue, in: question.minValue...question.maxVal).padding()
}
}
@available(iOS 15.0, *)
struct TextResponseQuestionView: View{
//ObservableObjects have to be observed so you see changes
@ObservedObject var question: TextResponseQuestion
var body: some View{
TextEditor(text: $question.responseText).background(.thickMaterial).frame(width: 285, height: 250).cornerRadius(20).padding()
}
}
@available(iOS 15.0, *)
struct QuestionResponseView_Previews: PreviewProvider {
static var previews: some View {
QuestionResponseView()
}
}
struct QuestionRepository {
//static let == does not change
var checkInQuestion : [Question] = [
ValueResponseQuestion(questionText: "How many hours of sleep did you get last night?", initialValue: 6, maxValue: 12),
TextResponseQuestion(questionText: "What is taking up most of your headspace today?", responseText: "Family, Work, Children, Arguement with friend"),
TextResponseQuestion(questionText: "What do you feel you need in this moment? What is missing?", responseText: "Friendship, Money, Peace, Exercise"),
ValueResponseQuestion(questionText: "What is your stress level currently at?", initialValue: 50, maxValue: 100)
]
var GroundingQuestions : [Question] = [
//TODO: (later)
]
}
class Question : ObservableObject, Identifiable, Hashable {
static func == (lhs: Question, rhs: Question) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(questionText)
}
let id: UUID = UUID()
var questionText : String
init(questionText: String) {
self.questionText = questionText
}
}
class ValueResponseQuestion : Question {
@Published var responseValue : Double
@Published var minValue : Double = 0
@Published var maxVal : Double
init(questionText: String, initialValue: Double, maxValue: Double) {
responseValue = initialValue
maxVal = maxValue
super.init(questionText: questionText)
}
}
class TextResponseQuestion : Question {
@Published var responseText : String
init(questionText: String, responseText: String){
self.responseText = responseText
super.init(questionText: questionText)
}
}
CodePudding user response:
One solution to your question about casting is the following:
if let question = question as? TextResponseQuestion {
question.responseText = responseText
}
That being said, I'd reconsider your architecture if I were in your position, for a couple of reasons:
You're using classes for your model -- SwiftUI
@State
variables will work better with value types vs reference types.struct
s will be more likely to be what you're looking for.You're trying to store the answers along with the questions -- I'd consider splitting up the answers into a separate store
Assuming that you did #2, you could use enums with associated values instead of different subclasses for the different types of questions. In fact, you could do that even without #2, but I think separating the questions and answers may be cleaner.