포스트

단어장 프로젝트 (7)

게임을 클릭하면 ModalView가 떠오르고

단어장과, 출제 단어를 선택할수있게 한다.

우선 ModalVC가 필요하다.

이전에 ModalVC를 몇번 구현해봤으나.

새로운 방식이 있다 하여 그걸로 해본다.

출처는 여기.

ModalVC를 띄울 presentationController 만들기

우선은 Controller를 하나 만들어준다.

하나 독특하다면 UIPresentationController 이녀석을 상속 받는다.

1
2
3
4
5
6
import UIKit

class ManageModalPresentationController: UIPresentationController {

}

물론 이 클래스는 Cocoa Touch Class에서 만들면 된다.

앵간한게 다있다.

해당글의 순서에 맞게 Modal의 크기와 위치를 잡는 메서드를 만든다.

1. 크기와 위치 설정.

frameOfPresentedViewInContainerView 이녀석을 override 한다.

해당 메서드를 통해 크기와 위치를 설정할 수 있게된다.

여기서도 이전에 ModalVC 구현했던것과 비슷한 매커니즘으로 DimmingView를 사용한다.

DimmingView의 가장 큰 목적은 시각적 효과 + 해당 view를 탭했을때 vc를 dismiss해주는데 있다.

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
private lazy var dimmingView: UIView = {
        let dimmingView = UIView()
        dimmingView.translatesAutoresizingMaskIntoConstraints = false
        dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
        dimmingView.alpha = 0.0
        
        let recognizer = UITapGestureRecognizer(target: self,
                                                action: #selector(handleTap(recognizer:)))
        dimmingView.addGestureRecognizer(recognizer)
        
        return dimmingView
    }()

@objc func handleTap(recognizer: UITapGestureRecognizer) {
           presentingViewController.dismiss(animated: true)
       }

override var frameOfPresentedViewInContainerView: CGRect {
        let screenBounds = UIScreen.main.bounds
            let size = CGSize(width: screenBounds.width,
                              height: screenBounds.height * 0.25)
            let origin = CGPoint(x: .zero, y: screenBounds.height * 0.75)
            
            return CGRect(origin: origin, size: size)
    }

이전에 ModalVC를 해봐서 그런가 크게 헷갈리는 부분은 없다.

2. Modal 시작과 사라질때의 작동할 코드 구현

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
// 시작
override func presentationTransitionWillBegin() {
        super.presentationTransitionWillBegin()

         guard let containerView = containerView else {
            return
        }
        containerView.insertSubview(dimmingView, at: 0)
        
        dimmingView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
        
        guard let coordinator = presentedViewController.transitionCoordinator else {
            dimmingView.alpha = 1.0
            return
        }
        
        coordinator.animate(alongsideTransition: { _ in
            self.dimmingView.alpha = 1.0
        })

    }

// 화면이 사라질때
override func dismissalTransitionWillBegin() {
        super.dismissalTransitionWillBegin()
        
        guard let coordinator = presentedViewController.transitionCoordinator else {
            dimmingView.alpha = 0.0
            return
        }
        
        coordinator.animate(alongsideTransition: { _ in
            self.dimmingView.alpha = 0.0
        })
    }

viewDidload, viewDidDisappear 개념으로 보면될듯.

3. Modal Delegate 상속

1
2
3
4
5
6
extension GameMainPageViewController: UIViewControllerTransitioningDelegate {
    
    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return ManageModalPresentationController(presentedViewController: presented, presenting: presenting)
        }
}

해당 기능을 사용할 VC에서 UIViewControllerTransitioningDelegate를 상속받게 한다.

이때 return하는 VC는 VCtype이 UIPresentationController 이녀석이다.

4. ModalView 생성 & Delegate설정

1
2
3
4
5
6
7
8
9
10
11
12
13
class SelectVocaViewController: UIViewController {
        
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .darkGray
        // Do any additional setup after loading the view.
    }
}

let vc = SelectVocaViewController()
            vc.modalPresentationStyle = .custom
            vc.transitioningDelegate = self
            self.present(vc, animated: true, completion: nil)

이렇게 하면 modalVC가 보이게 된다.

May-20-2024 18-30-06

ModalVC 디자인

simulator_screenshot_84E05B7F-1FFD-4A8B-927F-5A63EE57AB2E

이렇게 해둔상태.

기능 구현.

1. Picker에 category에 대한 정보 입력.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
extension SelectVocaViewController: UIPickerViewDelegate, UIPickerViewDataSource {
    
    func setup () {
        selectBodyView.bookPicker.delegate = self
        selectBodyView.bookPicker.dataSource = self
    }
    
    // picker의 갯수
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    // 휠의 갯수
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return category.count
    }
    
    // 내용.
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return category[row]
    }
    
}

그리고 가장 중요한 게임 시작 버튼이 빠져서 디자인을 했다.

2. 게임 시작 버튼 클릭시 vc 출력

아마 가장 어려운 기능으로 보인다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 lazy var startButton = ButtonFactory().makeButton(title: "게임시작") { [weak self] _ in
        self?.launchGame(complete: { [unowned self] in
            if let currentVC = self?.currentViewController as? GameMainPageViewController {
                let flashVC = FlashCardViewController()
             
                currentVC.present(flashVC,animated: true)
            }
        })
 }
func launchGame (complete: @escaping () -> Void) {
        if let currentVC = currentViewController as? SelectVocaViewController {
            complete()
            currentVC.dismiss(animated: true)
        }
        
    }

우선 이렇게 해두고,

화면 전환만 확인이 되면 끝나는데, 여기가 제일 고비로 보인다.

escaping closure를 통해서 뭔가 가능하지 않을까 라는 막연한 생각을 가지고 있는데.

현재는 되지 않는 상황.

1. 첫번째 시도

dismiss의 complete를 사용 하여 도전

1
2
3
4
5
6
7
8
9
10
11
func launchGame () {
        if let currentVC = currentViewController as? SelectVocaViewController {
            currentVC.dismiss(animated: true) {
                let mainVC = GameMainPageViewController()
                let flashVC = FlashCardViewController()
                
                mainVC.present(flashVC,animated: true)
            }
        }
        
    }

하지만 아래 에러가 뜸

1
Attempt to present <Vocabulary.FlashCardViewController: 0x106f4af10> on <Vocabulary.GameMainPageViewController: 0x10fd05c10> (from <Vocabulary.GameMainPageViewController: 0x10fd05c10>) whose view is not in the window hierarchy.

아마 view계층에 대한 문제로 생각이 된다.

두번째로 GPT로 검색을 한번 했다가, 이건 온전히 나의 능력으로 하고싶어서 방식을 변경하기로 결정

3. 방식 변경

원래 의도한건 게임을 클릭하면 바로 설정이 나오고 그 이후에 dismiss를 하면서 원래 의도한 게임이 실행 되는 것이었는데,

아무런 값이 없을때 해당 modal이 뜨게하는걸로 결정했다.

Delegate를 통해 값을 전달을 해보려 한다.

집중이 되질 않기도하고, view에서 그 view를 포함하는 vc까지는 데이터가 넘어갔지만, GameMainPageViewController 로는 delegate로 안되는걸 확인.

NotificationCenter를 사용해서 전달 해보려 한다.

1
2
3
4
5
extension Notification.Name {
    
    static let sender = Notification.Name("sender")
    
}

그리고 GameMainPageViewController 에서 observer를 만든다.

NotificationCenter.default.addObserver(self, selector: #selector(getSetting), name: .sender, object: nil)

그리고

1
2
3
4
5
 @objc func getSetting (_ notification: Notification) {
        if let data = notification.object as? GenQuizModel {
            receivedData = data
        }
    }

이렇게 해준다.

전달이 되는걸 확인했다.

우선적으로 세팅을 하지않으면 alert를 띄우고 modalvc가 나오게 했다.

1
2
3
4
5
6
7
8
9
10
11
func checkSetting() {
        if receivedData == nil {
            let alert = alertController.makeAlertWithCompletion(title: "설정값이 없습니다.", message: "게임 설정이 필요합니다.\n설정 페이지로 이동합니다.") { _ in
                let vc = SelectVocaViewController()
                vc.modalPresentationStyle = .custom
                vc.transitioningDelegate = self
                self.present(vc, animated: true, completion: nil)
            }
            self.present(alert, animated: true)
        }
    }

이렇게 하면

Simulator Screenshot - iPhone 15 Pro - 2024-05-21 at 04 42 01

alert를 띄우고 확인을 누르면 설정페이지로 간다.

이젠 Coredata에서 가져오는 함수를 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func getSpecificData(query: String, onError: @escaping (Error) -> Void) -> [WordEntity] {
        var array = [WordEntity]()
        let request: NSFetchRequest<WordEntity> = WordEntity.fetchRequest()
        let predicate = NSPredicate(format: "bookCaseName == %@", query)
        request.predicate = predicate
        
        do {
                array = try managedContext!.fetch(request)
        } catch {
           onError(error)
        }
        
        return array
    }

다음과 같이 구현했다.

1
2
3
4
5
6
private func getData () {
        quizArray = CoreDataManager.shared.getSpecificData(query: receivedData!.category, onError: { [unowned self] error in
            let alert = alertController.makeNormalAlert(title: "에러발생", message: "\(error.localizedDescription)가 발생했습니다.")
            self.present(alert, animated: true)
        })
    }

vc에서 받게끔 처리를 하고.

generate(count: receivedData!.quizCount) 여기도 이제 카운트를 설정한 값으로 받게 해준다.

May-21-2024 04-40-23

완료.

이젠 dummydata를 썼던 곳에 모두 적용을 해준다.

flashcard에서는 특이하게

1
2
3
4
5
6
private func getData () {
        quizArray = Array(CoreDataManager.shared.getSpecificData(query: receivedData!.category, onError: { [unowned self] error in
            let alert = alertController.makeNormalAlert(title: "에러발생", message: "\(error.localizedDescription)가 발생했습니다.")
            self.present(alert, animated: true)
        }).shuffled().prefix(receivedData!.quizCount))
    }

값을 가져오고, 셔플을 한 뒤, 슬라이싱을 했다.

1
2
3
4
5
private func checkException () {
        if receivedData!.quizCount > quizArray.count {
            receivedData?.quizCount = quizArray.count
        }
    }

혹시나 현재 배열보다 더 많은 값을 추가를 하는 경우를 대비하여 맥시멈은 count에 맞춰놨다.

1
2
3
4
5
6
7
8
private func getData () {
        quizArray = CoreDataManager.shared.getSpecificData(query: receivedData!.category, onError: { [unowned self] error in
            let alert = alertController.makeNormalAlert(title: "에러발생", message: "\(error.localizedDescription)가 발생했습니다.")
            self.present(alert, animated: true)
        }).shuffled()
        checkException() // added
        quizArray = Array(quizArray.prefix(receivedData!.quizCount))
    }

Hangman예외처리는 내일해야할듯싶다.

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