안녕하세요 오늘은 Tutorial의 마지막인 SwiftUI에서 UIKit을 사용하는 방법에 대해 알아보겠습니다.
예전에 iOS 컨퍼런스에서 한 개발자분이 SwiftUI로만 개발을 진행하기에는 한계가 있다고 말을 할 적이 있습니다. 이유는 실제 개발에서는 target version을 낮게 가져가는데 SwiftUI는 최신 기술이다 보니 version이 낮으면 구현하기 어려운 화면이 존재하기 때문이었습니다.
결과부터 확인해 보겠습니다.
왼쪽이 SwiftUI의 TabView를 이용해 구현한 화면이고 오른쪽이 UIPageViewController와 UIPageControl를 이용해 구현한 화면입니다.
두 화면은 비슷하지만 한 가지 차이점이 존재합니다. 바로 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가지 있습니다. makeUIViewControlle
와 updateUIViewController
입니다. 이름 그대로 만들고 업데이트하는 역할을 하고 있습니다.
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