안녕하세요 오늘은 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