[SwiftUI] Interfacing with UIKit 1
원문: 튜토리얼 페이지
오랜만에 돌아온 튜토리얼 따라하기!
SwiftUI는 기존 UI Framework와도 함께 사용할 수 있다. UIKit의 View나 ViewController를 SwiftUI 내부에서 사용 가능하고 반대로 SwiftUI의 요소들도 UIKit에서 사용할 수 있다.
요번 튜토리얼에서는 UIPageViewController와 UIPageControl을 사용하는 화면을 구성해본다.
Section 1. Create a View to Represent a UIPageViewController
UIKit에서 제공하는 View나 ViewController를 SwiftUI에서 사용하기 위해서는 UIViewRepresentable 이나 UIViewControllerRepresentable 프로토콜을 준수해야 한다. SwiftUI가 lift cycle이나 뷰 갱신을 자기가 필요할 때에 맞춰 관리할 수 있다는 데 어떻게 활용되는지 확인해보자.
- PageViewController<Page: View> 구조체를 만든다.
- UIViewControllerRepresentable을 채택!
- UIViewControllerRepresentable 프로토콜을 준수하기 위해서는 두 가지 메서드를 구현해야 한다.
makeUIViewController(context:)
/updateUIViewController(_:context:)
import SwiftUI
import UIKit
struct PageViewController<Page: View>: UIViewControllerRepresentable {
var pages: [Page]
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(transitionStyle: .scroll,
navigationOrientation: .horizontal)
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[UIHostingController(rootView: pages[0])], direction: .forward, animated: true)
}
}
makeUIViewController(context:)
에서는 우리가 사용하게 될 UIPageViewController를 생성해서 반환한다. SwiftUI에서는 이 메서드를 view를 화면에 표시할 준비가 되었을 때 한 번 호출하고 vc의 라이프 사이클을 관리한다.
updateUIViewController(_:context:)
에서는 pageViewController의 setViewControllers를 호출해 PageViewController를 구성한다. setViewControllers에서 UIHostingController
가 사용되는데, 이는 pageviewcontroller의 life cycle 동안 한 번만 초기화해서 효율성을 높인다고 한다 (튜토리얼 페이지 번역).
- pageViewController에서 사용할 이미지를 튜토리얼 페이지의 프로젝트를 다운받아 옮겨 넣는다.
- _feature로 명명된 이미지를 Assets에 추가한다.
- Landmark 구조체에 featureImage 프로퍼티를 추가한다.
- ImageName_feature로 된 이미지가 있는 경우 return, 없는 경우 nil을 반환한다.
var featureImage: Image? {
isFeatured ? Image(imageName + "_feature") : nil
}
- FeatureCard.swift 생성
- featureImage를 노출하는 FeatureCard를 생성한다.
struct FeatureCard: View {
var landmark: Landmark
var body: some View {
landmark.featureImage?
.resizable()
.aspectRatio(3 / 2, contentMode: .fit)
}
}
- FeatureCard에 들어가는 이미지 위에 랜드마크 이름과 위치를 추가하기 위해 TextOverlay를 추가한다.
struct TextOverlay: View { var landmark: Landmark var gradient: LinearGradient { .linearGradient( Gradient(colors: [.black.opacity(0.6), .black.opacity(0)]), startPoint: .bottom, endPoint: .center) } var body: some View { ZStack(alignment: .bottomLeading) { gradient VStack(alignment: .leading) { Text(landmark.name) .font(.title) .bold() Text(landmark.park) } .padding() } .foregroundColor(.white) } }
- PageView 생성
- 앞서 만들어뒀던 PageViewController를 추가해 사용할 PageView(SwiftUI) 파일을 생성한다.
Section 1을 쭉 따라하고 나니 PageViewController라는 UIViewControllerRepresentable 프로토콜을 준수한 구조체를 생성해 SwiftUI 화면에 사진 한 장을 띄울 수 있게 됐다. 다음 Section에서는 Data Source를 추가해 페이징을 할 수 있도록 구현한다.
Section 2. Create the View Controller’s Data Source
- PageViewController 구조체 내부에
Coordinator
클래스 생성- SwiftUI는 UIViewControllerRepresentable type의 coordinator를 관리하고 위에서 만든 메서드를 호출할 때 컨텍스트의 일부로 제공한다. (makeUIViewController나 updateUIViewController의 context를 말하는 것 같다.)
class Coordinator: NSObject {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
parent = pageViewController
}
}
makeCoordinator()
메서드 추가- SwiftUI는 makeUIViewController 메서드가 호출되기 전에 makeCoordinator 메서드를 호출한다.
coordinator에 delegate나 data source 같은 Cocoa patterns를 구현할 수 있고 target-action을 통해 사용자 이벤트에 응답할 수도 있다.
- Coordinator에서 UIHostingController로 초기화 된 controllers를 관리하도록 수정
- 기존에는 updateUIViewController에서 setViewControllers를 호출할때 바로 생성하도록 했지만 Coordinator에서 controllers 프로퍼티를 선언해 관리하도록 한다.
- 이렇게 되면 coordinator 코드가 viewcontroller가 update하기 전 단 한번 초기화 되기 때문에 controller들을 여러번 초기화 할 필요가 없어 좋다.
- UIPageViewControllerDataSource 구현
- DataSource는 Coordinator 클래스에서 관리하도록 한다.
- makeUIViewController에서 pageViewController의 dataSource를 coordinator로 지정한다.
** Section 2 에서 수정한 PageViewController.swift 코드**
import SwiftUI
import UIKit
struct PageViewController<Page: View>: UIViewControllerRepresentable {
var pages: [Page]
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[0]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource {
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]
}
}
}
화면
마무리
일단 1과 2 섹션에서는 UIViewControllerRepresentable을 준수하는 PageViewController를 생성해 SwiftUI 코드에서 사용하도록 했다. 약간 분량이 길어지는 느낌이 들어 Section3과 4는 다른 게시물로 올릴 예정..! 튜토리얼도 벌써 끝이 보인다. (요거 하나 남았는데 이걸 안했다니..)
Leave a comment