[새싹 iOS] 6주차_PageViewController

임승섭·2023년 8월 25일
0

새싹 iOS

목록 보기
16/45

1. A: UIPageViewController

  • UIPageViewController 를 상속받는 클래스로 화면을 구성했다
  • 각 화면은 UIViewController를 상속받는 6개의 클래스로 생성했다
  • 마지막 화면(6번)에서 로그인 버튼을 누르면 메인화면으로 루트뷰를 바꿔주었다
  • 앱 로그인 전 온보딩 화면 느낌으로 구상

화면

코드

OnboardingViewController.swift

class OnboardingViewController: UIPageViewController {

    override init(transitionStyle style: UIPageViewController.TransitionStyle, navigationOrientation: UIPageViewController.NavigationOrientation, options: [UIPageViewController.OptionsKey : Any]? = nil) {
        // 페이지뷰의 스타일
        super.init(transitionStyle: .scroll, navigationOrientation: .horizontal)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 페이지로 활용할 배열 - 초기는 빈 배열
    var list: [UIViewController] = [];
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemGray2
        
        list = [ViewController1(), ViewController2(), ViewController3(), ViewController4(), ViewController5(), ViewController6()]
        
        // 프로토콜 연결
        connectProtocol()
        
        // 첫 화면 설정
        guard let first = list.first else { return }
        setViewControllers([first], direction: .forward, animated: true)
        
    }
}



extension OnboardingViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource {
    
    func connectProtocol() {
        delegate = self
        dataSource = self
    }
    
    // 비포
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard let currentIndex = list.firstIndex(of: viewController) else { return nil }
        return currentIndex <= 0 ? nil : list[currentIndex - 1]
    }
    
    // 애프터
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard let currentIndex = list.firstIndex(of: viewController) else { return nil }
        return currentIndex >= list.count - 1 ? nil : list[currentIndex + 1]
    }
    
    
    // 아래 똥글뱅이
    func presentationCount(for pageViewController: UIPageViewController) -> Int {
        return list.count
    }
    
    func presentationIndex(for pageViewController: UIPageViewController) -> Int {
        guard let first = viewControllers?.first, let index = list.firstIndex(of: first) else { return 0}
        return index
    }
}

이슈

뷰 생명주기

  • PageViewController에서 화면이 넘어갈 때,
    각 뷰의 생명 함수가 어떻게 실행되는지 궁금해서 프린트로 찍었따
  • "화면 번호" "함수 이름" 으로 찍힌다
  • 맨 처음 화면

    
    1 viewDidLoad
    1 viewWillAppear
    1 viewDidAppear
    • 맨 처음 뷰에 관한 함수만 출력되고, 앞뒤 뷰에 관한 함수는 출력되지 않는다

  • 오른쪽으로 살짝 스크롤 (당기는 중)

    
    2 viewDidLoad
    2 viewWillAppear
    1 viewWillDisappear
    • 새로운 뷰가 화면에 나올 준비하고,
      이전 뷰는 없어질 준비한다

  • 오른쪽 도착

    
    2 viewDidAppear
    1 viewDidDisappear
    3 viewDidLoad
    • 새로운 뷰가 화면에 나오고, 이전 뷰가 없어졌다
    • 그리고 그 다음 뷰의 viewDidLoad가 실행된다
      • 하지만 매번 호출되는 건 아니었다
      • 다음 화면으로 스크롤을 해주어야 viewDidLoad가 실행되는 경우도 있었따

self vs. ViewController6.self

  • 6번 화면에서 메인 화면으로 넘어갈 때, 자꾸 에러가 발생했다
    • PageView로 띄운 view에서는 화면 전환이 되지 않나.. 라고도 생각했다..
  • 버튼에 addTarget을 연결해주는 부분에서 실수가 있었다
// 기존 코드
button.addTarget(ViewController6.self, action: #selector(loginSuccess), for: .touchUpInside)

// 제대로 된 코드
button.addTarget(self, action: #selector(loginSuccess), for: .touchUpInside)
  • self는 ViewController6 클래스의 생성된 인스턴스를 의미하고,
    ViewController6.self는 ViewController6 자체의 타입을 의미한다고
    그렇게 배웠는데도, 착각하고 잘못 쓰고 앉아있었다
  • 더 헷갈렸던 이유는, 에러 메세지에 selector가 문제있다고 쓰여있어서,
    전달해주는 함수가 뭔가 잘못되었다고 생각했다

  • 생각하면서.. 코딩합시다...

2. let A = UIPageViewController()

  • 위 화면을 구성하면서 생각한 점은,
    온보딩 화면을 스킵하는 기능이 필요하다고 생각했다
    (보기싫은 유저가 있을 수 있으니까)
  • 그래서 1 ~ 5번 화면에서 스킵 버튼을 누르면
    바로 6번 화면으로 넘어가는 기능을 구현하고자 했다
  • 문제는 지금 페이지 뷰에 각 뷰가 띄워져 있고,
    화면 자체가 PageViewController로 만들었기 때문에
    버튼을 올려둘 곳이 없었다.
  • 구글링 결과, 화면 자체는 ViewController로 만들고,
    그 안에 PageViewController 객체를 생성하는 방법을 찾았다

화면

코드

SkipPagePracticeViewController.swift

class SkipPagePracticeViewController: UIViewController {
    
    // 하나의 객체로 선언한다
    let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
    
    let skipButton = {
        let button = UIButton()
        
        button.setTitle("skip", for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 20)
        button.backgroundColor = .white
        button.setTitleColor(UIColor.black, for: .normal)
        
        return button
    }()
    
    // 1에서는 빈 배열 생성 후, viewDidLoad에서 요소를 넣어주었는데,
    // 여기서는 클로저 이용해서 바로 값을 넣어주었다
    // 근데 왜 이렇게... 했지..? 그냥 초기화 할 때 넣어주면 안되냐..??
    let viewList = {
        let list = [ViewController1(), ViewController2(), ViewController3(), ViewController4(), ViewController5(), ViewController6()]

        return list
    }()
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        
        pageViewController.dataSource = self
        pageViewController.delegate = self
        
        guard let first = viewList.first else { return }
        pageViewController.setViewControllers([first], direction: .forward, animated: true)
        

        view.addSubview(pageViewController.view)
        pageViewController.view.snp.makeConstraints { make in
            make.edges.equalTo(view)
        }
        
        view.addSubview(skipButton)
        skipButton.snp.makeConstraints { make in
            make.centerX.equalTo(view)
            make.width.equalTo(200)
            make.bottom.equalTo(view).inset(80)
        }
        
        skipButton.addTarget(self, action: #selector(skipButtonClicked), for: .touchUpInside)
    }
    
    
    // 바로 6번 화면으로 보내버린다
    // 맨 처음 화면 로드하는 코드를 참고했다
    @objc
    func skipButtonClicked() {
        guard let last = viewList.last else { return }
        pageViewController.setViewControllers([last], direction: .forward, animated: true)
    }
}

// extension 부분은 1과 거의 동일하다

이슈

pageViewController.view

  • pageView 인스턴스 생성하고, 각 화면에 들어갈 viewController도 다 만들었는데
    생각해보니까 pageViewController를 어떻게 addSubView 안에 넣어야 하나
    고민에 푹 빠졌다
  • 아무거나 적어보기라도 할 걸 그랬다
  • UIPageViewControllerUIViewController를 상속받고,
    UIViewController 클래스에는 맨날 쓰는 open var view: UIView!가 있다
  • 결국, 뒤에 .view 붙여주면 된다
view.addSubview(pageViewController.view)
pageViewController.view.snp.makeConstraints { make in
	~~~
}

3. Delegate Pattern

  • 2에서는 메인 화면에 PageViewController을 올리고,
    그 위에 skip 버튼을 올리는 방식으로 디자인했다
  • 그러다보니 상대적으로 아래에 있는 페이지뷰의 뷰가 넘어갈 때,
    위에 있는 skip 버튼은 가운데에 그대로 위치해있다

  • 보통 화면이 넘어갈 때는 모든 요소가 다 넘어가야 할 것 같아서
    그 점이 좀 불편했다

  • 그리고 또, 6번 화면에도 굳이 필요없는 skip 버튼을 띄워두고 있어야 한다
  • 이걸 해결하려면, 화면을 구성하는 view에 각각 버튼을 추가하고,
    버튼을 누르면 6번 화면으로 넘어가는 기능을 구현해야 한다
  • 문제는, 버튼은 View1Controller에서 선언하고
    화면 넘어가는건 SkipPagePracticeVewController에서 동작하는데
    얘네를 어떻게 연결할 거냐
  • 또 고민에 푹 빠져있었는데,
    예전에 테이블 뷰 셀에 버튼 기능 구현하는 방법이 떠올랐다
  • 총 3개의 방법이 있었는데, 첫 번째와 두 번째 방법만 이용해보고
    나머지 하나는 해야겠다 생각만 하고 하지 않았다.
    1. tag
    2. 클로저 콜백함수
    3. Delegate Pattern
  • Delegate 패턴을 이용해서 서로 다른 파일이 연결되게(?) 하였다

화면

코드

SkipDelegate.swift

// 프로토콜 선언
protocol SkipToEndDelegate: class {
	func skipToEnd()
}

ExtraViewController.swift

// 필요한 부분만 적고, 대부분 생략했다

// skip 버튼 생성 및 프로토콜 변수 생성
class ViewController1: UIViewController {
	
    weak var delegate: SkipToEndDelegate?
    
    let skipButton = {
    	let button = UIButton()
        ~~
        return button
    }
    
    override func viewDidLoad() {
    	view.addSubview(skipButton)
        skipButton.snp.makeConstraints { make in
        	~~
        }
        skipButton.addTarget(self, action: #selector(skipButtonClicked), for: .touchUpInside)
    }
    
    // 버튼을 누르면 -> 프로토콜 변수의 함수가 실행되게 한다
    @objc
    func skipButtonClicked() {
    	delegate?.skipToEnd()
    }
}

SkipPagePracticeViewController.swift

// 각 뷰 클래스의 delegate를 현재 인스턴스로 잡아준다
class SkipPagePracticeViewController: UIViewController {
	
    override func viewDidLoad() {
    	super.viewDidLoad()
        
        // 업캐스팅 후 연결해준다
        (viewList[0] as! ViewController1).delegate = self
        (viewList[1] as! ViewController2).delegate = self
        (viewList[2] as! ViewController3).delegate = self
        (viewList[3] as! ViewController4).delegate = self
        (viewList[4] as! ViewController5).delegate = self
	}
}


// 프로토콜 채택 후 필수 메서드 정의
// 6번 화면으로 바로 넘어가는 코드 그대로 적어주었다 (기존 skipButton)
extension SkipPagePracticeViewController: SkipToEndDelegate {
	func skipToEnd() {
    	guard let last = viewList.last else { return }
        pageViewController.setViewControllers([last], direction: .forward, animated: true)
    }
}

이슈

타입 캐스팅

  • 각 뷰에 선언한 delegate의 값을 self로 연결해줄 때,
    타입이 맞지 않아서 시간이 좀 걸렸다
  • viewList 안에 여러 ViewController를 담았더니
    viewList의 타입이 [UIViewController]가 되었고,
    그러다 보니 인덱스로 배열에 접근한 viewList[0] 의 타입은
    UIViewController가 되었다
  • 문제는, delegate를 선언해 둔 부분은
    UIViewController를 상속받은 ViewController1 클래스이기 때문에
    UIViewController 클래스에서는 delegate를 찾을 수 없다
  • 그래서 단순히 반복문 돌면서 배열의 원소에 접근해서는
    delegate를 연결해줄 수 없었다
  • 그래서 각 원소의 타입(UIViewController)을 업캐스팅해서
    해당 원소의 원래 타입(UIViewController1)으로 바꾸어 주었고,
    그래서 delegate 변수에 접근할 수 있었다
  • 사실상 배열의 모든 요소에 직접 접근하고,
    각 요소의 원래 타입을 모두 알아야 구현할 수 있는 코드이기 때문에
    그닥 좋아보이는 코드는 아니다

0개의 댓글