SwiftUI : UIKit 과의 통합

버들비·2020년 8월 24일
0

SwiftUI

목록 보기
4/8

애플의 SwiftUI 튜토리얼
https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit


목표: UIPageViewController와 UIPageControl을 SwiftUI에 통합시키기

위 URL 로 가서 프로젝트 파일을 다운받자. 튜토리얼은 SwiftUI와 UIKit 사이의 통합에 집중하였기에, 그외 data asset 이나 뷰 요소들은 설명이 없고 프로젝트 파일을 다운받아야 한다.


Section 1

UIPageViewController 를 표현하는 뷰(View) 만들기

UIViewRepresentable 또는 UIViewControllerRepresentable 프로토콜을 준수하는 타입을 만듦으로써 UIKit 요소를 SwiftUI에 추가한다.


SwiftUI는 UIKit 요소의 라이프 사이클을 관리하고 적절한 때에 업데이트 해준다.

Step 1

PageViewControoler.swift 파일을 만들고 UIViewControllerRepresentable 프로토콜을 준수하는 구조체를 하나 만들자.
페이지 뷰 컨트롤러는 UIViewController 를 담은 배열을 변수로 가진다. 배열의 각 요소는 스크롤링할때 나타나는 각 페이지가 된다.

PageViewController.swift

import SwiftUI
import UIKit

struct PageViewController: UIViewControllerRepresentable {
    var controllers: [UIViewController]
}

Step 2

UIPageViewController 를 형성하는 makeUIViewController(context:) 메소드를 추가한다.
SwiftUI는 뷰를 표시할 때가 됐을때 makeUIViewController 메소드를 한번 호출한다.

PageViewController.swift

struct PageViewController: UIViewControllerRepresentable {
    var controllers: [UIViewController]

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)

        return pageViewController
    }
}

Step 3

PageViewController 구조체에 updateUIViewController(_:context:) 메소드를 추가한다. setViewControllers 를 호출해, view controller 들이 담긴 배열의 첫번째 요소를 디스플레이에 표시하기 위해서이다.

func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [controllers[0]], direction: .forward, animated: true)
    }

Step 4

step 1~3에서 만든 PageViewController 구조체를 SwiftUI View 에 추가한다. 새로 PageView.swift 파일을 만들자.

PageView.swift

import SwiftUI

struct PageView<Page: View>: View {
    var viewControllers: [UIHostingController<Page>]
    
    init(_ views: [Page]) {
        self.viewControllers = views.map { element in
            UIHostingController(rootView: element) }
    }
    
    var body: some View {
        PageViewController(controllers: viewControllers)
    }
}

UIHostingController 는 UIViewController 의 서브클래스이다.


Section 2

뷰 컨트롤러의 데이터 소스(Data Source) 만들기

Step 1

PageViewController 구조체 안에 NSObject 클래스를 상속하는 Coorniator 클래스를 만들자.
SwiftUI는 makeUIViewController 메소드와 updateUIViewController 메소드를 호출할때마다 coordinator를 두 함수의 context로 전달해 준다.

PageViewController.swift

	class Coordinator: NSObject {
        var parent: PageViewController

        init(_ pageViewController: PageViewController) {
            self.parent = pageViewController
        }
    }

Step 2

coordinator 를 생성하는 메소드 makeCoordinator()를 PageViewController 구조체 안에 만든다.
SwiftUI는 makeUIViewController 메소드가 호출되기 전에 앞서 makeCoordinator() 메소드를 호출한다.
SwiftUI에서 기존 코코아 패턴(델리게이트, 데이터 소스, 타겟-액션 이벤트)는 coordinator 를 사용해서 구현한다.

PageViewController.swift

	func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

Step 3

step 1 에서 만든 Coordinator 클래스가 UIPageViewControllerDataSource 프로토콜을 준수한다고 선언한다.

Step 4

makeUIViewController 메소드 내부에 pageViewController.dataSource = context.coordinator 를 추가한다.
coordinator 로 만들어진 컨텍스트가 makeUIViewController 로 전달되고, context 의 coordinator 는 makeUIViewController 가 만들어낼 뷰의 데이터 소스가 된다.


Section 3

SwiftUI 뷰의 상태를 트래킹 하기

PageView 내부에 @State 프로퍼티를 추가하고, 해당 프로퍼티를 PageViewController 뷰와 바인딩 한다.

Step 1

PageViewController 내부에 @Binding var currentPage: Int 를 추가하고, 뷰를 업데이트 해주는 updateViewController 내부에 currentPage 변수를 추가한다.

PageViewController.swift

struct PageViewController: UIViewControllerRepresentable {
    var controllers: [UIViewController]
    @Binding var currentPage: Int
    ...    
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [controllers[currentPage]], direction: .forward, animated: true)
    }
	...
 
}

Step 2

PageView 내부에는 @State var currentPage = 0 를 추가한뒤, PageViewController 에 인자로 currentPage 를 전달한다. @State 가 붙은 프로퍼티는 변수명 앞에 $ 심볼을 붙여줘야 바인딩이 이루어진다.

PageView.swift

struct PageView<Page: View>: View {
    var viewControllers: [UIHostingController<Page>]
    @State var currentPage = 0

    init(_ views: [Page]) {
        self.viewControllers = views.map { UIHostingController(rootView: $0) }
    }

    var body: some View {
        PageViewController(controllers: viewControllers, currentPage: $currentPage)
    }
}

Step 3 & Step 4

@State var currentPage 변수의 초기값을 변경하면서 바인딩이 잘 이루어졌는지 체크

Step 5

PageViewController 내부 Coordinator 클래스가 UIPageViewControllerDelegate 프로토콜을 준수하도록 추가한다. Coordinator 클래스 안에 pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool) 메소드를 추가한다.
페이지 변경 애니메이션이 끝나면 SwiftUI는 pageViewController 메소드를 호출하고, 지금의 뷰컨트롤러의 인덱스를 찾고 바인딩된 변수를 업데이트 한다.

class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PageViewController

        init(_ pageViewController: PageViewController) {
            self.parent = pageViewController
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController) -> UIViewController?
        {
            guard let index = parent.controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index == 0 {
                return parent.controllers.last
            }
            return parent.controllers[index - 1]
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController) -> UIViewController?
        {
            guard let index = parent.controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index + 1 == parent.controllers.count {
                return parent.controllers.first
            }
            return parent.controllers[index + 1]
        }

        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            if completed,
                let visibleViewController = pageViewController.viewControllers?.first,
                let index = parent.controllers.firstIndex(of: visibleViewController)
            {
                parent.currentPage = index
            }
        }
    }

커스텀 페이지 컨트롤을 더하는것은 최상단 링크를 참조.

0개의 댓글