[SwiftUI] Tutorial 4. Interfacing with UIKit

DongHeon·2023년 11월 2일
0

SwiftUI

목록 보기
6/6
post-thumbnail

안녕하세요 오늘은 Tutorial의 마지막인 SwiftUI에서 UIKit을 사용하는 방법에 대해 알아보겠습니다.

예전에 iOS 컨퍼런스에서 한 개발자분이 SwiftUI로만 개발을 진행하기에는 한계가 있다고 말을 할 적이 있습니다. 이유는 실제 개발에서는 target version을 낮게 가져가는데 SwiftUI는 최신 기술이다 보니 version이 낮으면 구현하기 어려운 화면이 존재하기 때문이었습니다.

결과부터 확인해 보겠습니다.

왼쪽이 SwiftUI의 TabView를 이용해 구현한 화면이고 오른쪽이 UIPageViewControllerUIPageControl를 이용해 구현한 화면입니다.

두 화면은 비슷하지만 한 가지 차이점이 존재합니다. 바로 PageControl의 위치인데요 SwiftUI로 구현한 화면에서 PageControl은 가운데에 위치하지만 오른쪽 화면은 trailing 쪽에 위치하는 걸 확인할 수 있습니다.

TabView의 PageControl 위치를 변경하는 방법을 찾기 힘들었습니다. 오히려 좋은 방법은 UIPageContol을 만들어 사용하는 것을 추천하는 글이 많았습니다. 위에서 얘기했듯이 SwiftUI로만 모든 화면을 구현하는 것은 힘들 수 있습니다.

그럼 이제 UIKit과 SwiftUI를 연동하는 방법에 대해서 알아보겠습니다. 코드는 Tutorial에 있는 코드와 똑같습니다.

그중에서 UIPageViewController를 만들어 사용하는 코드를 가져와보겠습니다.

import SwiftUI
import UIKit

struct PageViewController<Page: View>: UIViewControllerRepresentable {
    var pages: [Page]
    @Binding var currentPage: Int
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal
        )
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator
        
        return pageViewController
    }
    
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
    }
    
    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PageViewController
        var controllers = [UIViewController]()
        
        init(_ pageViewController: PageViewController) {
            parent = pageViewController
            controllers = parent.pages.map { UIHostingController(rootView: $0) }
        }
        
        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index == 0 {
                return controllers.last
            }
            return controllers[index - 1]
        }
        
        
        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index + 1 == controllers.count {
                return controllers.first
            }
            return controllers[index + 1]
        }
        
        func pageViewController(
            _ pageViewController: UIPageViewController,
            didFinishAnimating finished: Bool,
            previousViewControllers: [UIViewController],
            transitionCompleted completed: Bool) {
            if completed,
                let visibleViewController = pageViewController.viewControllers?.first,
                let index = controllers.firstIndex(of: visibleViewController) {
                parent.currentPage = index
            }
        }
    }
}

코드가 복잡하지만 우리가 알아야 하는 키워드는 UIViewControllerRepresentable입니다.

왜냐하면 해당 프로토콜을 채택한 객체를 이용해 View를 그리기 때문입니다.

UIViewControllerRepesentable를 채택했다면 반드시 구현해야 하는 함수가 2가지 있습니다. makeUIViewControlleupdateUIViewController입니다. 이름 그대로 만들고 업데이트하는 역할을 하고 있습니다.

makeUIViewController를 이용해 원하는 Controller를 만들었다면 updateUIViewController를 이용해 SwiftUI 데이터로 Controller의 상태를 업데이트해야 합니다.

위 코드도 PageViewController를 만들고 SwiftUI의 View를 이용해 Controller를 업데이트하고 있습니다.

그럼 이제 PageViewController를 사용하는 SwiftUI 코드를 보겠습니다.

struct PageView<Page: View>: View {
    var pages: [Page] // 배너 이미지 View
    @State private var currentPage = 0
    
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            PageViewController(pages: pages, currentPage: $currentPage)
            PageControl(numberOfPages: pages.count, currentPage: $currentPage)
                .frame(width: CGFloat(pages.count * 18))
                .padding(.trailing)
        }
    }
}

우리가 미리 구현한 View를 Controller에 주입시켜 주면 해당 View를 이용해 Controller를 만들어 화면을 구현할 수 있습니다.

Coordinator상호작용이 필요한 경우 구현해 사용하는 객체입니다. 튜토리얼에서는 스크롤이라는 상호작용이 발생하기 때문에 구현했습니다.

모든 화면을 SwiftUI만을 사용해 구현하는 것은 아직 한계가 있습니다. 따라서 UIKit을 적절히 사용한다면 더 좋은 UI를 만들 수 있을 것 같습니다.

참고 자료
Tutorial 4. Interfacing with UIKit
공식 문서 - UIViewControllerRepresentable

0개의 댓글