포스트

단어장 프로젝트 (2)

2일차 시작 우선 중요기능중 한가지를 먼저 구현하려고한다

그전에 Sparta에서 Daily 면접질문? 그런거에 대한 답을 먼저 하고 시작

오늘의 질문

  1. Storyboard를 이용해 UI를 구현하는 방법을 설명해주세요.

StoryBoard를 사용하여 UI를 구현할때는 Commnad + Shift + L을 눌러서 UIComponent를 추가할 수 있는 창을 띄우고 원하는 Component를 검색 후 드래그하여 추가한다.

이때 StoryBoard로 추가한 Component는 말그대로 보이기만 하고 아무런 기능이 없기에, 우리가 여기에 기능을 부여하거나, 외적인 부분을 코드로 접근을 하려고 하는 경우엔 VC로 Control을 누른채로 드래그를 해서 IBOutlet / IBAction을 만들어 주어야 한다.


기능 구현

1. DummyData 생성

단어를 추가하는 기능이 구현되기 전까지 마냥 기다릴수는 없다. DummyData를 만들어서 내가 담당한 기능을 구현해야한다.

약 20개정도 만들것이다.

우선 모델링을 해주고

1
2
3
4
struct DummyModel {
    var words: String
    var meaning: String
}

DummyGenerator라는 파일을 하나 만들어 주었다.

머리가 멍해져서 우선은 이렇게 구현했다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 func makeDummy () -> [DummyModel] {
        var array = [DummyModel]()
        let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        
        for _ in 0...19 {
            let int = (1...5).randomElement()!
            let secondInt = (1...5).randomElement()!
            let word = String((0..<int).map{ _ in letters.randomElement()! })
            let meaning = String((0..<secondInt).map{ _ in letters.randomElement()! })
            let dummy = DummyModel(words: word, meaning: meaning)
            array.append(dummy)
        }
        return array
    }

배열이 생성되는걸 확인했다.

simulator_screenshot_387B2CFC-6BEA-40A4-A0B7-DC0ABC011AB8

현재 문제점이라면, 배열의 개수만큼 셀이 전부 다 나오고 있다.

즉 해당부분을 다시 재수정해야한다는 말이 된다.

이전에 Udemy에서 공부했던 내용이 생각나서 그 글을 다시 보고있는데,

애초에 UIButton 통해서 만들었다.

그냥 셀같은 화면에 꽂혀서 그렇게했는데 생각이 바뀌었다.

UI디자인을 재수정해야한다.

컬렉션 뷰에서 UI 버튼으로 전부 교체 완료.

우선 이렇게 배열에 담으면 새롭게 문제를 만드는 과정이 필요하다.

모델링을 하나 더 해준다.

1
2
3
4
5
6
7
8
9
struct VocaQuizModel {
    
    let question: String
    let answer: String
    let incorrectFirst: String
    let incorrectSecond: String
    let incorrectThird: String
    
}

다음과 같이 모델링을 해준다.

그리고 DummyGenerator라는 클래스를 하나 만들어 준다.

테스트용 더미단어와 뜻을 만드는 녀석이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DummyGenerator {
    
    func makeDummy () -> [DummyModel] {
        var array = [DummyModel]()
        let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        
        for _ in 0...19 {
            let int = (1...5).randomElement()!
            let secondInt = (1...5).randomElement()!
            let word = String((0..<int).map{ _ in letters.randomElement()! })
            let meaning = String((0..<secondInt).map{ _ in letters.randomElement()! })
            let dummy = DummyModel(words: word, meaning: meaning)
            array.append(dummy)
        }
        return array
    }
    
}

이렇게 그냥 알파벳으로 대충 가져오게 한다.

2. 문제 배열 생성

그다음 다시 게임페이지 VC로 이동.

문제, 정답, 3개의 오답이 들어가야한다. 왜냐 4지선다이므로.

다음과 같이 코드를 작성한다.

문제를 만드는 함수이다.

우선 makeDummy를 통해 더미데이터를 가져온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private func generate() {
        
        for _ in 0...9 {
            var dummyData = dummyGenerator.makeDummy()
            var numberArray = (0...dummyData.count-1).map{ $0 }.shuffled() // 숫자를 섞음
            
            let getFourNumberArray = numberArray.prefix(4).map { numberArray[$0]  }

            let number1 = getFourNumberArray[0]
            let number2 = getFourNumberArray[1]
            let number3 = getFourNumberArray[2]
            let number4 = getFourNumberArray[3]
            
            
            let answerInfo = dummyData[number1]
            let question = answerInfo.words
            let answer = answerInfo.meaning
            let first = dummyData[number2].meaning
            let second = dummyData[number3].meaning
            let third = dummyData[number4].meaning
            
            let dummy = VocaQuizModel(question: question, answer: answer, incorrectFirst: first, incorrectSecond: second, incorrectThird: third)
            quizData.append(dummy)
        }

    }

함수안에 또 선언한게 많은데

요지는 이거다. 더미데이터를 가져와서

현재 단어의 추가한 개수만큼 숫자를 뽑아서 shuffle

그리고 거기서 4개의 숫자만 추출.

첫번째 숫자는 문제와, 정답을 위해서

나머지 3개의 숫자는 오답용이다.

이렇게 되면 하나의 dummy에는 문제, 정답, 3개의 오답이 생긴다.

이걸 10번 반복하여 총 10개의 임의의 문제가 생기게 된다.

3. 버튼에 임의로 띄우기

1
2
3
4
5
6
7
8
9
10
private func gameStart () {
        generate(count: 5)
        var answerList = [quizData[currentNumber].answer, quizData[currentNumber].incorrectFirst, quizData[currentNumber].incorrectSecond, quizData[currentNumber].incorrectThird]
        answerList.shuffle()
        
        gamePageBottomView.firstButton.setTitle(answerList[0], for: .normal)
        gamePageBottomView.secondButton.setTitle(answerList[1], for: .normal)
        gamePageBottomView.thirdButton.setTitle(answerList[2], for: .normal)
        gamePageBottomView.forthButton.setTitle(answerList[3], for: .normal)
}

quizData에서 퀴즈를 제외한 나머지를 다시 배열에 담고, 그걸 셔플을 하여 분배를 한다.

이렇게 되면 정답도 계속 랜덤으로 출제가 된다.

4. Button Addtarget 구현

이제 문제의 버튼을 눌렀을때 action이 필요하다.

responder를 사용해서 현재의 title을 가져올 수 있다면 되는데

현재 계속 nil이 뜨는상황이다.

자기전에 누워서 검색을 해보다가 addtarget아닌 addAction이 생긴걸 발견, 이건 closure를 통해 실행이 되므로, 뭔가 가능할것으로 판단이 들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lazy var firstButton: UIButton = {
        let button = UIButton()
        button.setTitle("첫번째", for: .normal)
        button.backgroundColor = .lightGray
        button.setTitleColor(.black, for: .normal)
        button.layer.cornerRadius = 15
        button.layer.borderWidth = 0.5
        button.addAction(UIAction(handler: { [weak self] _ in // added
            guard let title = button.titleLabel?.text else { return }
            if self?.checkAnswer(title: title) == true {
                button.backgroundColor = .blue
            } else {
                button.backgroundColor = .red
            }
        }), for: .touchUpInside)
        return button
    }()

이렇게 closure를 사용하게 되면 현재 버튼의 title을 사용할 수 있게된다.

그리고 값을 확인하는 함수는 다음과 같이 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func checkAnswer(title: String) -> Bool {
        var flag: Bool = false
        guard let currentVC = currentViewController as? GamePageViewController else { return flag }
        let currentQuestion = currentVC.gamePageBodyView.gameTitle.text
        let gameArray = currentVC.quizData
        let answer = gameArray.filter{$0.question == currentQuestion}.map{ $0.answer }.joined()
        
        if title == answer {
            flag = true
        } else {
            flag = false
        }
        
        return flag
    }

이렇게 해서 VC의 퀴즈데이터를 가져오고, 거기서 현재 문제와 일치하는 답을 가지고 온다.

그것과 현재 문제의 title값이 같은지를 보고 true or false를 리턴하게 된다.

Simulator Screenshot - iPhone 15 Pro - 2024-05-15 at 01 54 15

구현 완료.

5. 버튼클릭할때마다 문제 정답 갱신

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func checkAnswer(title: String) {
        guard let currentVC = currentViewController as? GamePageViewController else { return }
        let currentQuestion = currentVC.gamePageBodyView.gameTitle.text
        let gameArray = currentVC.quizData
        let answer = gameArray.filter{$0.question == currentQuestion}.map{ $0.answer }.joined()
        
        if title == answer {
            currentVC.currentNumber += 1
            currentVC.score += 1
            currentVC.gameStart()
        } else {
            currentVC.currentNumber += 1
            currentVC.gameStart()
        }
    }

lazy var firstButton: UIButton = {
        let button = UIButton()
        button.setTitle("첫번째", for: .normal)
        button.backgroundColor = .lightGray
        button.setTitleColor(.black, for: .normal)
        button.layer.cornerRadius = 15
        button.layer.borderWidth = 0.5
        button.addAction(UIAction(handler: { [weak self] _ in
            guard let title = button.titleLabel?.text else { return }
           self?.checkAnswer(title: title)
        }), for: .touchUpInside)
        return button
    }()

우선은 backgroundColor는 없앴다. 기능구현을 포커스를 두기위해서.

아마 타이머를 통해서 깜빡거리게 하면 될듯한데, 그렇게할경우 selector를 사용해서 하나하나 노가다를 해야할것같아서 잠시 보류한다.

이에따라 버튼의 addAction도 간소화.

그리고 게임시작 함수는 보강을하고, 업데이트라는 함수를 새로 만들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func gameStart () {
        if currentNumber > quizData.count - 1 { // added & modified
            gamePageBodyView.gameTitle.text = "게임이 종료 되었습니다."
        } else {
            update()
            var answerList = [quizData[currentNumber].answer, quizData[currentNumber].incorrectFirst, quizData[currentNumber].incorrectSecond, quizData[currentNumber].incorrectThird]
            answerList.shuffle()
            
            gamePageBottomView.firstButton.setTitle(answerList[0], for: .normal)
            gamePageBottomView.secondButton.setTitle(answerList[1], for: .normal)
            gamePageBottomView.thirdButton.setTitle(answerList[2], for: .normal)
            gamePageBottomView.forthButton.setTitle(answerList[3], for: .normal)
        }
    }
    
func update() { //added
        gamePageBodyView.gameTitle.text = quizData[currentNumber].question
        gamePageHeaderView.scoreLabel.text = "Score: \(score) 점"
        
    }

gameStart에는 update를 하되 outofrange를 고려하여 카운트가 마지막에 다다르고 한번 더 클릭하면 게임 종료의 글자가 뜨게 했다.

완료.

May-15-2024 02-10-13

역시 자기전에 검색이나 아이디어는 항상 옳다.

6. 색으로 정답 오답 표현하기.

이대로 끝내기에 좀 찝찝해서 이것까지 구현을 해둔다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
lazy var firstButton: UIButton = {
        let button = UIButton()
        button.setTitle("첫번째", for: .normal)
        button.backgroundColor = .lightGray
        button.setTitleColor(.black, for: .normal)
        button.layer.cornerRadius = 15
        button.layer.borderWidth = 0.5
        button.addAction(UIAction(handler: { _ in
            guard let title = button.titleLabel?.text else { return }
            if self.checkAnswer(title: title) == true { // modified
                button.backgroundColor = .green
                Timer.scheduledTimer(timeInterval: 0.2,target: self, selector: #selector(self.updateBackground), userInfo: nil, repeats: false)
            } else {
                button.backgroundColor = .red
                Timer.scheduledTimer(timeInterval: 0.2,target: self, selector: #selector(self.updateBackground), userInfo: nil, repeats: false)
            }
            
        }), for: .touchUpInside)
        return button
    }()

 func checkAnswer(title: String) -> Bool { // modified
        var flag = false
        guard let currentVC = currentViewController as? GamePageViewController else { return flag }
        let currentQuestion = currentVC.gamePageBodyView.gameTitle.text
        let gameArray = currentVC.quizData
        let answer = gameArray.filter{$0.question == currentQuestion}.map{ $0.answer }.joined()
        
        if title == answer {
            currentVC.currentNumber += 1
            currentVC.score += 1
            currentVC.gameStart()
            flag = true
        } else {
            currentVC.currentNumber += 1
            currentVC.gameStart()
        }
        return flag
    }   

우선 checkAnswer를 bool로 리턴하게 해서 정답과 오답일때의 리턴을 다르게하고

addAction에서는 리턴값이 참과 거짓에 따라 색을 바꾼다.

이때 타이머를 통해서 원래의 색을 돌아오게 한다.

원래 의도한건 자체의 색깔 하나만 리턴하게 하고싶었으나, selector의 objc함수에서 호출한 당사자의 버튼을 특정할수가 없다.

그래서 전부 다 바뀌게 했다

1
2
3
4
5
@objc func updateBackground () {
        [firstButton, secondButton, thirdButton, forthButton].forEach { button in
            button.backgroundColor = .lightGray
        }
    }

그리고 마지막 게임때 정답을 맞췄는데 스코어가 올라가지 않아서 코드를 한줄 추가해준다.

게임이 끝나고 버튼이 눌려지면 안되므로 버튼을 disable로 바꾼다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func gameStart () {
        if currentNumber > quizData.count - 1 {
            gamePageBodyView.gameTitle.text = "게임이 종료 되었습니다."
            gamePageHeaderView.scoreLabel.text = "Score: \(score) 점" // added
            [gamePageBottomView.firstButton, gamePageBottomView.secondButton, gamePageBottomView.thirdButton, gamePageBottomView.forthButton].forEach { button in // added
                button.isEnabled = false
            }
        } else {
            update()
            var answerList = [quizData[currentNumber].answer, quizData[currentNumber].incorrectFirst, quizData[currentNumber].incorrectSecond, quizData[currentNumber].incorrectThird] 
            answerList.shuffle()
            
            gamePageBottomView.firstButton.setTitle(answerList[0], for: .normal)
            gamePageBottomView.secondButton.setTitle(answerList[1], for: .normal)
            gamePageBottomView.thirdButton.setTitle(answerList[2], for: .normal)
            gamePageBottomView.forthButton.setTitle(answerList[3], for: .normal)
        }
    }

완료

May-15-2024 02-47-23

큰고비를 하나 넘겼다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.