본문 바로가기
  • Apple Developer Academy @ POSETECH 1기, 비전공자의 일지
  • WWDC 22 Swift Student Challenge Winner
  • 프라하에서 애플 아카데미 면접을 본 썰...
⌨️ UIKit

[UIKit] UIPageViewController 에서 Swipe 기능 빼기 + 버튼으로 넘기기

by PecanPieiOS 2022. 12. 29.

작업환경: ios 14.0+ / Xcode 14.1

Code Base / No StoryBoards

 

앱에서 보통 화면을 넘긴다고 하면 rootVC 에 navigation 을 넣고 push 나 present 를 많이 쓴다.

또 가끔은 온보딩 화면이나, 이미지를 눌러 넘기는 회면에서 PageViewController 를 쓰기도 한다.

 

나는 이번 앱을 만들면서 조금은 더 부드러운 화면 이동을 추구하려고 노력했다.

사실 UI/UX 적으로는 사용자 입장에서는 큰 차이를 못 느낄 수 있다. 

 

아래의 자료를 보면 ,

왼쪽은 아예 navigation bar 가 생겼다가 사라지면서 이질적인 느낌이 생기기도 한다. 

오른쪽은 navigation bar 와 버튼은 그대로 있고, 중앙에 위치한 컨텐트만 바뀌는 것을 볼 수 있다.

 

좌: 일반 Navigation 으로 화면 넘김   /   우: UIPageViewController 로 화면 넘김

 

일반 화면 이동시에 일반 Navigation 을 통해 하는 것보다 PageController 를 쓰는 것에 대해 Hig 를 찾아봤지만, 화면 이동에 대한 내용을 못 찾았다 (혹시나 있다면 알려주시면 감사하겠습니다!! (꾸벅))

글을 작성하면서 깨달은건데, 메모리 관리에 대한 부정적인 측면은 있는 것 같다.  UIPageViewController 를 쓰게 되면 매 Page 에 UIView 가 아닌 UIViewController 가 들어가게 되는데, UIPageViewContoller 를 가지는 class 내부에는 해당 page 들을 초기화를 해서 가지고 있어야 한다. 그러므로 하나의 VC 에 많은 메모리가 소요가 된다고 생각이 든다. 심지어 그 해당 page 들이 가벼운 컴포넌트들로만 이루어진 화면이 아니라면 더욱 더 무거워질 수 있다고 생각이 든다. 

 

하지만 어떡하리 이미 배포는 되었고, 이렇게 실수를 깨닫고 배우며 다음에는 안 쓰면 되지! 라는 마인드!

 

 

각설하고 본론인 코드로 들어가보자면, 나는 UIPageViewController 를 그저 조금 더 부드러운 화면 이동 하나만을 보고 쓴 것이기 때문에, UIPageViewController 의 가장 큰 존재 이유는 Swipe 를 빼기로 했다. 온보딩 화면에 넣은 것이 아니기 때문이다. 각 page 를 그냥 넘길 수 없게 만들기 위해서였다. (생각하면 할수록 왜 이렇게 해서 고생을 했을까 싶다.)

 

아무튼 코드를 슬슬 살펴보자.

 

[Swipe 기능이 있는 기본적인 구현 방법]

...

private let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
private var pages: [UIViewController] = []
private var currentPage: UIViewController!

...

private func setupPages {
    pageViewController.didMove(toParent: self)
    pageViewController.dataSource = self
    pageViewController.delegate = self
    pageViewController.setViewControllers([pages.first!], direction: .forward, animated: false)
	...
}

...

extension NewViewController: UIPageViewControllerDataSource {
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        return getPreviousViewController(from: viewController)
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        return getNextViewController(from: viewController)
    }
    
    private func getPreviousViewController(from viewController: UIViewController) -> UIViewController? {
        guard let index = pages.firstIndex(of: viewController), index - 1 >= 0 else { return nil }
        currentPage = pages[index - 1]
        return pages[index - 1]
    }
    
    private func getNextViewController(from viewController: UIViewController) -> UIViewController? {
        guard let index = pages.firstIndex(of: viewController), index + 1 < pages.count else { return nil }
        currentPage = pages[index + 1]
        return pages[index + 1]
    }
    
    func presentationCount(for pageViewController: UIPageViewController) -> Int {
        return pages.count
    }

    func presentationIndex(for pageViewController: UIPageViewController) -> Int {
        return pages.firstIndex(of: self.currentPage) ?? 0
    }
}

 

언제나 그렇듯이 UIPageViewControllerDataSource 나 Delegate 을 쓰게 된다. 하지만 이렇게 dataSource 를 받고 pageController 의 Swipe 를 금지시키려고 메서드들을 찾아봤지만 존재하지 않았다. 그래서 구글링을 해보니...

 

PageViewController 의 Swipe 기능은 dataSource 를 받으면 생긴다.

결론: dataSource 를 받지 않으면 Swipe 가 되지 않는다.

 

였다.

 

간단했다. dataSource 를 채택하지 않으면 된다.

 

그런데 이제는 새로운 문제가 생겼다. 그러면 어떻게 페이지를 넘겨서 새로운 VC 를 가져오지?

버튼으로 넘기고 싶은데... 

 

UIPageViewController 의 page 를 넘기는 원리는 pageViewController.setViewControllers 를 통해 내부 페이지를 바꿔주면 끝이다. 이때 좋았던 점은, pageController 이름 그대로 어떠한 넘어가는 애니메이션을 추가하지 않아도 되는 것이었다.

 

즉, 버튼을 누르면 pageViewController.setViewControllers(새로운 페이지) 액션이 작동하게 만들면 알아서 넘어간다.

코드를 보자면...

 

[Swipe 없이 버튼으로 화면을 넘기는 방법]

...

override func viewDidLoad() {
    super.viewDidLoad()
    setupPages()
    nextButtonTap()
}

...

private func setupPages() {
    pages.append(pageTwoRegion)
    pages.append(pageThreeGears)
    pages.append(pageFourTextDescription)
    pages.append(pageFivePortpolio)
    pages.append(pageSixConfirm)

    guard let firstPage = pages.first else { return }
    currentPage = firstPage

    pageViewController.didMove(toParent: self)
    pageViewController.setViewControllers([pages.first!], direction: .forward, animated: false)

    guard let firstPage = pages.first else { return }
    currentPage = firstPage
}

...

extension NewViewController {
    private func nextButtonTap() {
        let buttonTapped = UITapGestureRecognizer(target: self, action: #selector(moveNextTapped))
        nextButtonTapped.addGestureRecognizer(buttonTapped)
    }
    
    @objc func moveNextTapped() {
        guard let page = pages.firstIndex(of: currentPage) else { return }
        pageViewController.setViewControllers([pages[page + 1]], direction: .forward, animated: true)
        currentPage = pages[page + 1]
    }
}

 

이렇게 해당 메인 VC 를 만들었다.

특정 상황에서는 사용하면 좋을지 몰라도, 각 page 가 무겁거나 기능을 가진 VC 라면 그냥 Navigation 을 사용해서 넘기는 것을 추천한다. 

 단지 이 Page UI 를 만드는 것만 복잡한게 아니라, 내부에 들어가는 child VC 를 parent VC 에서 컨트롤을 해야하는 상황이 생길 때, 조금 많이 머리가 아파진다. 버그도 생기고...

 

 

 

틀리거나 오해의 소지가 있는 것에 대해서 가감없이 댓글로 남겨주세요!! 공부에 도움이 됩니다 ㅎㅎ !

댓글