포스트

단어장 프로젝트 (5)

게임 정답처리와 재시작할때 UI가 reset이 되도록 구현을 해야한다.

button 초기화

고민을 하다가 기존에 view가 로딩이 이미 되어서 리셋이 안되는거라면 새로 시작할때마다 addsubview를 하면 되지않을까 라는 생각이 들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private func gameStart () {
        let hangManBottomView = HangManBottomView()
        
        vStackView.addSubview(hangManBottomView)
        
        hangManBottomView.snp.makeConstraints {
            $0.top.equalTo(hangManBodyView.snp.bottom)
            $0.leading.equalToSuperview()
            $0.trailing.equalToSuperview()
            $0.bottom.equalToSuperview().offset(-60)
        }
        
        dummyList = dummyGenerator.makeDummy()
        makeWordLabel()
        
        updateMan()
    }

이렇게 게임 시작할때 추가하고 레이아웃을 잡게 한다.

시작해보면

May-17-2024 01-47-19

리셋이 잘된다.

예외처리

alert가 뜨고 취소를 한상태에서 버튼을 클릭하면 failCount가 증가하면서 out of range 에러가 발생한다.

이부분을 막기 위해서

변수를 추가한다.

var isGameEnd = false 게임이 종료되었는지 감지하는녀석이다.

그리고 게임 과 관련된 코드를 일부 수정했다.

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
func gameStart () {
        failCount = 0 // added
        isGameEnd = false // added
        let hangManBottomView = HangManBottomView()
        
        vStackView.addSubview(hangManBottomView)
        
        hangManBottomView.snp.makeConstraints {
            $0.top.equalTo(hangManBodyView.snp.bottom)
            $0.leading.equalToSuperview()
            $0.trailing.equalToSuperview()
            $0.bottom.equalToSuperview().offset(-60)
        }
        
        dummyList = dummyGenerator.makeDummy()
        makeWordLabel()
        
        updateMan()
    }
    
    func updateMan () {
        if failCount >= 7 {
            hangManBodyView.hangManImageView.image = UIImage(named: imageList[failCount])
            let alert = alertController.makeAlertWithCompletion(title: "게임종료", message: "게임이 끝났습니다.\n다시 시작하시겠습니까?\n취소하여도 버튼 터치시 재시작이 가능합니다.") { [weak self] _ in
                self?.gameStart()
                self?.isGameEnd = false
            }
            self.present(alert, animated: true)
            isGameEnd = true
        } else {
            hangManBodyView.hangManImageView.image = UIImage(named: imageList[failCount])
        }
    }

그리고 view쪽도 checkWord의 함수내용도 살짝 수정한다.

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
func checkWord(button: UIButton) {
        guard let currentVC = currentViewController as? HangManGameViewController else { return }
        
        if currentVC.dummyList[currentVC.gameCount].words.contains(button.currentTitle!.lowercased()) {
            if currentVC.isGameEnd == false {
                button.isEnabled = false
                button.setTitle("", for: .normal)
                button.backgroundColor = .blue
                button.setImage(UIImage(systemName: "checkmark"), for: .normal)
            } else {
                currentVC.gameStart()
            }
        } else {
            if currentVC.isGameEnd == false {
                button.isEnabled = false
                button.setTitle("", for: .normal)
                button.backgroundColor = .red
                button.setImage(UIImage(systemName: "xmark"), for: .normal)
                currentVC.failCount += 1
                currentVC.updateMan()
            } else {
                currentVC.gameStart()
            }
        }
    }

이렇게 하게되면 게임이 진행중일땐 버튼이 정답과 오답만 구분하고

게임끝나고선 유져가 취소를 눌러도 다시 버튼만 탭해도 게임이 재시작하게 된다.

하지만 Hierarchy를 보면 계속 메모리에 쌓이게 된다.

이걸 어떻게든 없애야한다.

CleanShot 2024-05-17 at 02 50 19@2x

단지 우리 눈에만 새로 보이는것일 뿐 메모리에는 남아있는 상황.

즉 강한 순환참조를 하고 있다는 말이 된다.

순환참조 해결

우선 해당문제를 해결하기 위해 nil을 부여를 해야할것같아서

var hangManBottomView: HangManBottomView? 이렇게 바꿔주었다.

그리고나서

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
func gameStart () {
        failCount = 0
        isGameEnd = false
        
        hangManBottomView = HangManBottomView()
        
        vStackView.addSubview(hangManBottomView!)
        
        hangManBottomView!.snp.makeConstraints {
            $0.top.equalTo(hangManBodyView.snp.bottom)
            $0.leading.equalToSuperview()
            $0.trailing.equalToSuperview()
            $0.bottom.equalToSuperview().offset(-60)
        }
        
        dummyList = [dummyGenerator.makeDummy().shuffled().first!]
        
        makeWordLabel()
        
        updateMan()
    }
    
    func updateMan () {
        if failCount >= 7 {
            hangManBodyView.hangManImageView.image = UIImage(named: imageList[failCount])
            let alert = alertController.makeAlertWithCompletion(title: "게임종료", message: "게임이 끝났습니다.\n다시 시작하시겠습니까?\n취소하여도 버튼 터치시 재시작이 가능합니다.") { [weak self] _ in
                self?.hangManBottomView = nil
                self?.gameStart()
                self?.isGameEnd = false
            }
            self.present(alert, animated: true)
            isGameEnd = true
        } else {
            hangManBodyView.hangManImageView.image = UIImage(named: imageList[failCount])
        }
    }

그리고 시작할때 객체를 인스턴스화 하고 추가를 했는데도 불구하고

게임이 끝난뒤에 nil을 해도 메모리에 남아있었다.

무엇이 문제인지 몰랐다.

Stackoverflow을 통해 nil이 아닌

self?.hangManBottomView?.removeFromSuperview()이것을 통해 날려야 한다는걸 알게 되었다.

하지만 이해가 가지않는건 nil로도 날릴 수 있는게 아닌가? 라는게 내 생각이었다.

튜터님께 여쭤보니 애초에 Stackview를 참조하기에, count가 1이 증가한 상태로 시작. 하므로 nil을 하더라도 view 계층에 남아있는 이상은 순환참조가 발생할 수 밖에없다는 것. view쪽은 nil보다는 removeFromSuperview를 사용하도록 하자.

우선 버튼은 해결

하지만 아직 label이 남아있다.

아무래도 tag를 부여했으나 누적이 되면서 기존의 tag값이 날아간것같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
private func makeWordLabel () {
        for i in 1 ... dummyList[0].words.count {
            print(dummyList[0].words)
            print(dummyList[0].words.count)
            label = LabelFactory().hangManLabel(title: "_", size: 20, tag: i, isBold: true)
            print("label tag: \(String(describing: label?.tag))")
            hangManBodyView.wordFrameView.addSubview(label!)
            
            label!.snp.makeConstraints {
                $0.leading.equalTo(hangManBodyView.wordFrameView.snp.leading).offset(i * 20)
            }
        }
    }

label이 변수명이 하나라서 그런듯하다.

하지만 hierarchy를 확인해보니

1
2
3
4
5
6
Printing description of $19:
<UILabel: 0x104b2a640; frame = (40 0; 11.6667 24); text = '_'; userInteractionEnabled = NO; tag = 2; backgroundColor = UIExtendedGrayColorSpace 0 0; layer = <_UILabelLayer: 0x600002650f60>>
Printing description of $20:
<UILabel: 0x104b29bb0; frame = (20 0; 11.6667 24); text = '_'; userInteractionEnabled = NO; tag = 1; backgroundColor = UIExtendedGrayColorSpace 0 0; layer = <_UILabelLayer: 0x600002650d80>>
Printing description of $21:
<UILabel: 0x104b2b0d0; frame = (60 0; 11.6667 24); text = '_'; userInteractionEnabled = NO; tag = 3; backgroundColor = UIExtendedGrayColorSpace 0 0; layer = <_UILabelLayer: 0x600002651140>>

tag가 다 살아있다.

하지만

레이블을 지우기 위해 함수를 만들고 print를 찍어보았으나.

1
2
3
4
5
6
7
private func resetLabel () {
        for i in 1 ... dummyList[0].words.count {
            print("resetLabel : \(String(describing: label?.viewWithTag(i)))")
            label?.viewWithTag(i)?.removeFromSuperview()
        }
    }
    

결과는 이렇다. 분명히 Hierarchy에는 남아있으나, 찾지를 못하는건가 싶다.

1
2
3
4
5
6
7
resetLabel : nil
resetLabel : nil
resetLabel : nil
resetLabel : nil
resetLabel : nil
resetLabel : nil
resetLabel : Optional(<UILabel: 0x106539ef0; frame = (140 0; 11.6667 24); text = '_'; userInteractionEnabled = NO; tag = 7; backgroundColor = UIExtendedGrayColorSpace 0 0; layer = <_UILabelLayer: 0x6000026376c0>>)

새벽이라 머리가 돌아가지 않아서 아이디어만 gpt에게 구했다.

확실히 UI를 다시 보여주는 이런부분에 내가 많이 취약하다는것을 알게된다.

이번 팀프로젝트하면서 더 많은걸 배우게 된다.

가장 큰 아이디어는 바로 uilabel을 배열에 저장시켜주고, 지울때도 그 배열에서 꺼내서 지우면 되는것이었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var labelList = [UILabel]() // 

private func makeWordLabel () {
        for i in 1 ... dummyList[0].words.count {
            label = LabelFactory().hangManLabel(title: "_", size: 20, tag: i, isBold: true)
            hangManBodyView.wordFrameView.addSubview(label!)
            labelList.append(label!) // added
            
            label!.snp.makeConstraints {
                $0.leading.equalTo(hangManBodyView.wordFrameView.snp.leading).offset(i * 20)
            }
        }
    }

private func resetLabel () {
        for label in labelList {
            print("resetLabel : \(label)")
            label.removeFromSuperview()
        }
        labelList.removeAll()
    }

이렇게 하면 UIlabel도 reset이 된다.

정답 처리.

버튼을 눌렀을때 단어에서 알파벳의 위치를 가져와서 그 위치에 해당하는 label에 그 단어를 리턴시켜주면 될듯하다.

우선 다음과 같이 만들었다.

1
2
3
func getIndex(alphabet: Character) {
        print(answer.firstIndex(of: alphabet))
    }
1
2
3
4
Optional(Swift.String.Index(_rawBits: 15))
Optional(Swift.String.Index(_rawBits: 65799))
Optional(Swift.String.Index(_rawBits: 131335))
Optional(Swift.String.Index(_rawBits: 196871))

우선을 값을 가져오는것으로 확인.

Stackoverflow에 인덱스 값을 인트로 변환할수 있는 글이 있어 사용해보려한다.

시도를 해보려다가 굳이 이렇게 할필요가 있을까? 라는 생각이 들었고,

단어를 배열로 바꿔서 하면 더 쉽다고 판단 하여 다음과 같이 구성을 했다.

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
func guessAnswer(alphabet: Character) {
        let words = answer.map { $0 }
        
        for i in words.indices {
            if words[i] == alphabet {
                labelList[i].text = String(alphabet)
                score += 1
                monitorScore()
            }
        }
        
    }

func monitorScore() {
        if score == answer.count {
            let alert = alertController.makeAlertWithCancelCompletion(title: "축하합니다.", message: "정답을 맞추셨습니다\n다시 시작하시겠습니까?") { [weak self] _ in
                self?.hangManBottomView?.removeFromSuperview()
                self?.resetLabel()
                self?.gameStart()
                self?.isGameEnd = false
            }
            self.present(alert, animated: true)
            isGameEnd = true
        }
    }    

배열을 만들고 for문을 쓰면 알파벳이 중복이 되더라도 다 걸러낼수가 있기에 이 방법이 훨씬 낫다고 판단했다.

그리고 score가 정답의 글자수와 같다면 정답이라고 인식하게 하였다.

이렇게 실행하면 정답인상태에서 취소를 누르고 다시 시작할때 기존 값이 남아있게 된다.

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
 func gameStart () {
        failCount = 0
        score = 0
        isGameEnd = false
        
        if !labelList.isEmpty { // added
            resetLabel()
        }
        
        hangManBottomView = HangManBottomView()
        
        vStackView.addSubview(hangManBottomView!)
        
        hangManBottomView!.snp.makeConstraints {
            $0.top.equalTo(hangManBodyView.snp.bottom)
            $0.leading.equalToSuperview()
            $0.trailing.equalToSuperview()
            $0.bottom.equalToSuperview().offset(-60)
        }
        
        answer = dummyGenerator.makeDummy().shuffled().first!.words
        print(answer)
        makeWordLabel(word: answer)
        
        updateUI()
    }

그래서 취소를 대비하여 labelList가 비어있지 않으면 한번 리셋을 하게 해주었다.

May-17-2024 05-58-52

완료.

Hangman 좀 빡셌는데, 그래도 부족한점 두개를 배워간다.

또 한단계 발전할 수 있는 Insight를 얻어간다.

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