[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이나 뷰 갱신을 자기가 필요할 때에 맞춰 관리할 수 있다는 데 어떻게 활용되는지 확인해보자.

  1. PageViewController<Page: View> 구조체를 만든다.
    • UIViewControllerRepresentable을 채택!
  2. 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 동안 한 번만 초기화해서 효율성을 높인다고 한다 (튜토리얼 페이지 번역).

  1. pageViewController에서 사용할 이미지를 튜토리얼 페이지의 프로젝트를 다운받아 옮겨 넣는다.
    • _feature로 명명된 이미지를 Assets에 추가한다.
  2. Landmark 구조체에 featureImage 프로퍼티를 추가한다.
    • ImageName_feature로 된 이미지가 있는 경우 return, 없는 경우 nil을 반환한다.
var featureImage: Image? {
        isFeatured ? Image(imageName + "_feature") : nil
}
  1. FeatureCard.swift 생성
    • featureImage를 노출하는 FeatureCard를 생성한다.
struct FeatureCard: View {
    var landmark: Landmark

    var body: some View {
        landmark.featureImage?
            .resizable()
            .aspectRatio(3 / 2, contentMode: .fit)
    }
}
  1. 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)
     }
    }
    
  2. PageView 생성
    • 앞서 만들어뒀던 PageViewController를 추가해 사용할 PageView(SwiftUI) 파일을 생성한다.

Section 1을 쭉 따라하고 나니 PageViewController라는 UIViewControllerRepresentable 프로토콜을 준수한 구조체를 생성해 SwiftUI 화면에 사진 한 장을 띄울 수 있게 됐다. 다음 Section에서는 Data Source를 추가해 페이징을 할 수 있도록 구현한다.

Section 2. Create the View Controller’s Data Source

  1. PageViewController 구조체 내부에 Coordinator 클래스 생성
    • SwiftUI는 UIViewControllerRepresentable type의 coordinator를 관리하고 위에서 만든 메서드를 호출할 때 컨텍스트의 일부로 제공한다. (makeUIViewController나 updateUIViewController의 context를 말하는 것 같다.)
   class Coordinator: NSObject {
        var parent: PageViewController

        init(_ pageViewController: PageViewController) {
            parent = pageViewController
        }
    }
  1. makeCoordinator() 메서드 추가
    • SwiftUI는 makeUIViewController 메서드가 호출되기 전에 makeCoordinator 메서드를 호출한다.

coordinator에 delegate나 data source 같은 Cocoa patterns를 구현할 수 있고 target-action을 통해 사용자 이벤트에 응답할 수도 있다.

  1. Coordinator에서 UIHostingController로 초기화 된 controllers를 관리하도록 수정
    • 기존에는 updateUIViewController에서 setViewControllers를 호출할때 바로 생성하도록 했지만 Coordinator에서 controllers 프로퍼티를 선언해 관리하도록 한다.
    • 이렇게 되면 coordinator 코드가 viewcontroller가 update하기 전 단 한번 초기화 되기 때문에 controller들을 여러번 초기화 할 필요가 없어 좋다.
  2. 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