2

I have an app that consist of 3 layer of views. The func of the app is to let student to look at the question, choice the answer, and have the app to check if the answer is correct (this is the func I am struggling right now).

  1. ContentView - generate a random id (ranId) and pass it to the ChoiceView. ChoiceView is a component in this view.

  2. ChoiceView* - to combine the different ChoiceRowView after a ForEach loop and pass the String as a label to Choice RowView from the ranId

  3. ChoiceRowView* - control how each row look like in the ChoiceView and to check the answer for each answer.

I tried to use the Protocol-delegate method, but I can't figure out how to use it with the ForEach loop. I understand there are other ways to do it, like didSet, but I would rather stay with the protocol-delegate method for now. Thanks!!

Code in ChoiceView.swift

import SwiftUI   

struct ChoiceView: View {
        
        var ranId_CV : Int // random id pass from ContentView
        
        var delegate : ChoiceRowView?
        
        func test_CV() {
            
            delegate?.checkAnswer()
        }
        
        var body: some View {
            
            ScrollView {
            HStack{
                VStack(alignment: .leading){
                ForEach (qandaData[ranId_CV].choices.components(separatedBy: "\n"), id: \.self){ item in
                    ChoiceRowView(label: String(item), answer: String(qandaData[self.ranId_CV].answer))
                    }
                }
                .fixedSize(horizontal: false, vertical: true)
                Spacer()
            }
            .padding([.top, .leading, .bottom, .trailing])
            Button(action: test_CV) { Text("Test_CV") }   // this should work, but it doesn't
            }
        }
    }

struct ChoiceView_Previews: PreviewProvider {
    static var previews: some View {
        ChoiceView(ranId_CV: 125)
    }
}

Code in ChoiceRowView.swift


    import SwiftUI

    protocol checkAnswerProtocol {
        func checkAnswer()
    }

    struct ChoiceRowView: View, checkAnswerProtocol {    
        
        var label: String
        var answer: String
        @State var isChecked : Bool = false
        @State var fgColor : String = "black"
        
        func buttonPress() {

            isChecked = !isChecked
                
            if isChecked == false {
                
                fgColor = "black"
                
            } else {
                
                fgColor = "blue"
                
            }
            
    //            selectedAnswer = selectedAnswer.filter { $0 != item_BP.prefix(1) }
        }
        
        func checkAnswer() {
            
            print("executed checkAnswer")
            
            switch (isChecked, answer.contains(String(label.prefix(1)))) {
            
            //selected = isChecked = true, qandaData.answer contain prefix(1)  => change to green
            case (true, true) : fgColor = "green"
            
            //selected = isChecked = true, incorrect => change to red
            case (true, false) : fgColor = "red"
                
            //unselected = isChecked = false, correct => change to red
            case (false, true) : fgColor = "red"
            //unselected = isChecked = false, incorrect => remain unchange
            default: print("do nothing")
            }

        }

        
        var body: some View {
            Button(action: buttonPress) {
                    HStack{
                        Image(systemName: isChecked ? "checkmark.square": "square")
                        Text(label)
                    }
                    .foregroundColor(Color(String(fgColor)))
                Button(action: checkAnswer) { Text("Test") }
            }
        }
            
    }
        

    struct ChoiceRow_Previews: PreviewProvider {
        static var previews: some View {
            ChoiceRowView(label: "label", answer: "AB")
        }
    }

Sample of qandaData[ranId_CV]


    "id": 1,
    "question": "Which ones are odd number?",
    "choices": "A. 1\nB. 2\nC. 3\nD. 4",
    "answer": "AC"

5
  • Generally, your views shouldn't implement business logic. That belongs in your model. For example, you could have a Question struct that can tell you whether an answer is correct or not. That said, I can't see anywhere where you are setting the delegate. Commented Aug 31, 2020 at 6:10
  • @Paulw11 thanks for your quick comment. I have created the protocol and declare a delegate in the ChoiceView. I have a sense of where it is goings wrong, and I believe it is related to the ForEach? Commented Aug 31, 2020 at 6:25
  • You have declared a delegate property in choice view, but I can't see anywhere where you assign an object to that property, so it will be nil Commented Aug 31, 2020 at 6:32
  • @Paulw11 I have this in my ChoiceView ` var delegate : ChoiceRowView? func test_CV() { delegate?.checkAnswer() }` How exactly can I include the nil object? I am very new to app development, this is my first app, please excuse me... Commented Aug 31, 2020 at 6:51
  • That declares a delegate property and tries to invoke a method in that property, but you never assign anything to that property. You need to assign some object that conforms to the protocol to that property; an instance of ChoiceRowView based on your code, but as I said your views shouldn't be delegates in SwiftUI. Delegation isn't the right pattern to use here Commented Aug 31, 2020 at 7:01

1 Answer 1

1

In this post I will try to explain some fundamentals about SwiftUI which I used to solve your problem. First, SwiftUI is a little different from UIKit, it is a declarative programming, state driven so UI Components are not referenced manually for manipulation or update instead, each view has different states and will render accordingly when it's state changes.

In SwiftUI, Apple provides us with built-in state management property wrappers @State and @Binding. SwiftUI will store them and update each UI that component is linked to.

You can have a good read about Swift UI : here or here


I tried not to change many things and kept the properties and class names the same.

Code in ChoiceView

import SwiftUI

struct ChoiceView: View {

var ranId_CV : Int // random id pass from ContentView

var delegate : ChoiceRowView? // delegates can't be used in SwiftUI

@State var qandaData : [Question] = [
    Question(id : 1 , question : "Which ones are odd number?", rawChoices : "A. 1\nB. 2\nC. 3\nD. 4" , answer : "AC"),
    Question(id : 2 , question : "This is another question", rawChoices : "A. 1\nB. 2\nC. 3\nD. 4" , answer : "B")
]

// Array of Line objects containing all the Questions and their states
@State var lines : [Line] = []

var body: some View {
    ScrollView {
        Text(self.qandaData[ranId_CV].question)
        HStack{
            VStack(alignment: .leading){
                ForEach (self.lines.indices, id: \.self){ index in
                    ChoiceRowView(line: self.$lines[index])
                }
            }
            .fixedSize(horizontal: false, vertical: true)
            Spacer()
        }
        .padding([.top, .leading, .bottom, .trailing])
        .onAppear(perform: {
            self.qandaData[self.ranId_CV].choices.forEach { (label) in
                self.lines.append(Line(label: label , answer: self.qandaData[self.ranId_CV].answer))
            }
        })
        
        Button(action: {
            self.test_CV()
        }, label: {
            Text("Test_CV")
        })
    }
}

func test_CV() {
    //        delegate?.checkAnswer()
    
    self.lines.indices.forEach { (index) in
        self.lines[index].state = .none // restore correction state
    }
    
    self.lines.indices.forEach { (index) in
        if self.lines[index].selection == .selected {
            self.qandaData[self.ranId_CV].answer.forEach({ (char) in
                if self.lines[index].label.prefix(1).contains(char) && self.lines[index].selection == .selected {
                    self.lines[index].state = .valid
                }else{
                    self.lines[index].state = .invalid
                }
                
            })
        }
    }
}

}

Walkthrough part 1 :

  1. When the view appear, the selected by the random Int Question propositions will be converted into a Line object each.
  2. ForEach will loop through the array of Lines and render them depend on each of their state.
  3. Line's states are stored in an enum which makes them simple to trigger.
  4. Using the @State wrapper will tell SwiftUI to update the concerned views whenever changes happens to that array of objects.

Code in ChoiceRowView

import SwiftUI

protocol checkAnswerProtocol {
    func checkAnswer()
}

struct ChoiceRowView: View {

@Binding var line : Line

//    var label: String
//    var answer: String
@State var isChecked : Bool = false
@State var fgColor : Color = .black


func buttonPress() {

    self.line.selection = isChecked ? .selected : .unselected
    
    isChecked = !isChecked
   
    
    if isChecked == false {
        fgColor = .black
    } else {
        fgColor = .blue
    }
    
    //            selectedAnswer = selectedAnswer.filter { $0 != item_BP.prefix(1) }
}

func checkAnswer() {
    
    print("executed checkAnswer")
    
    switch (isChecked, self.line.label.contains(String(self.line.label.prefix(1)))) {
        
    //selected = isChecked = true, qandaData.answer contain prefix(1)  => change to green
    case (true, true) : fgColor = .green
        
    //selected = isChecked = true, incorrect => change to red
    case (true, false) : fgColor = .red
        
    //unselected = isChecked = false, correct => change to red
    case (false, true) : fgColor = .red
    //unselected = isChecked = false, incorrect => remain unchange
    default: print("do nothing")
    }
    
}


var body: some View {
    HStack{
        Button(action: {
            self.buttonPress()
            
        }) {
            
            Image(systemName: isChecked ? "checkmark.square": "square")
            Text(self.line.label)
        }
        .foregroundColor(self.line.state != .none  ? (self.line.state == .valid ? .green : .red) : .blue )
    }
}

}

Walkthrough part 2 :

  1. Using the wrapper @Binding will tell the view that the property does come from the caller or the parent and whenever it's value changes, the view will get updated accordingly.

  2. Using enum to store a view's state is a good practice and make your code more readable.

  3. The entity Question is used, so we can manipulate data correctly and avoid dealing with raw strings.

  4. With no views references in code, delegates are not any more part of this programming paradigm.

    import Foundation
    
    struct Question {
    let id: Int
    let question, answer: String
    let choices : [String]
    
    init(id : Int , question : String, rawChoices : String , answer : String) {
        self.id = id
        self.question = question
        self.choices = rawChoices.components(separatedBy: "\n")
        self.answer = answer
    }
    }
    

And the UI object

import Foundation
struct Line {
        let id = UUID()
        let label : String
        let answer : String
        var state : QState = .none
        var selection : Selecction = .unselected
        
    }
    
    enum QState {
        case valid,invalid, none
    }
    
    enum Selecction {
        case selected, unselected
    }

Finally this is the project structure

enter image description here

Or clone the project from my Github

Feel free to ask me any question I will be glad to help you and don't forget to +1 if I answered your question.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.