Home > Enterprise >  Struggling to set values of subclass (down casting?)
Struggling to set values of subclass (down casting?)

Time:12-10

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

Image of view just for kicks: Image of view just for kicks:

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:

  1. You're using classes for your model -- SwiftUI @State variables will work better with value types vs reference types. structs will be more likely to be what you're looking for.

  2. You're trying to store the answers along with the questions -- I'd consider splitting up the answers into a separate store

  3. 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.

  • Related