1. Create a view to represent a UIPageViewController
UIViewControllerRepresentable 프로토콜을 준수하는 구조체를 만든다.
UIViewControllerRepresentable이 요구하는 메서드를 구현한다.SwiftUI 는 뷰를 보여줄 준비가 되었을 때 makeUIViewController(context:)를 호출하고
updateUIViewController는 해당 뷰 컨트롤러가 업데이트되어야 할 때마다 호출한다.
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)
}
}
튜토리얼에서 제공하는 이미지 파일(파일 이름이 "_feature"로 끝남)을 에셋에 추가한 후
데이터 모델이 되는 Landmark에 featureImage 라는 computed property를 추가한다.
var featureImage: Image? {
isFeatured ? Image(imageName + "_feature") : nil
}
FeatureCard view를 구현
Landmark에 방금 추가한 프로퍼티를 통해 Image 를 선언하고 overLay modifier로 텍스트와 그라디언트를 덮는다.
struct FeatureCard: View {
var landmark: Landmark
var body: some View {
landmark.featureImage?
.resizable()
.overlay {
TextOverlay(landmark: landmark)
}
}
}
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()
}
.foregroundStyle(.white)
}
}
#Preview {
FeatureCard(landmark: ModelData().features[0])
.aspectRatio(3 / 2, contentMode: .fit)
}
PageView 에서 PageViewController를 자식 view로 선언
import SwiftUI
struct PageView<Page: View>: View {
var pages: [Page]
var body: some View {
PageViewController(pages: pages)
.aspectRatio(3 / 2, contentMode: .fit)
}
}
#Preview {
PageView(pages: ModelData().features.map { FeatureCard(landmark: $0) })
}
2. Create the view controller's data source
PageViewController에 Coordinator 라는 중첩 클래스를 선언한다.
class Coordinator: NSObject {
var parent: PageViewController
var controllers = [UIViewController]()
init(_ pageViewController: PageViewController) {
parent = pageViewController
controllers = parent.pages.map { UIHostingController(rootView: $0) }
}
}
makeCoordinator() 메서드에서 Coonrdinator(self)를 반환.
SwiftUI는 makeUIViewController(context:)를 호출하기 전에 makeCoordinator를 호출한다.
이후 viewController를 조정할 때 Coordinator에 접근할 수 있게 된다.
updateUIViewController에서 context.coordinator.controllers 라는 변수로 접근한다.
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)
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[0]], direction: .forward, animated: true)
}
class Coordinator: NSObject {
var parent: PageViewController
var controllers = [UIViewController]()
init(_ pageViewController: PageViewController) {
parent = pageViewController
controllers = parent.pages.map { UIHostingController(rootView: $0) }
}
}
}
Coordinator 에 UIPageViewControllerdataSource 프로토콜을 추가. 프로토콜이 요구하는 메서드를 구현한다.
페이지의 스크롤 시 첫번째 페이지의 전 페이지, 마지막 페이지의 전 페이지를 설정하는 로직이다.
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]
}
}
마지막으로 PageViewController를 보이기 전에 makeUIViewController(context:) 메서드 내부에서
pageViewController.Source 에 context.coordinator를 넘겨준다.
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
return pageViewController
}
3. Track the page in a SwiftUI view's state
다음으로 PageView에서 PageViewController의 currentPage를 추적하도록 구현한다.
PageView 에서 @State로 선언한 변수를 PageViewController에 전달하여 바인딩
struct PageView<Page: View>: View {
var pages: [Page]
@State private var currentPage = 0
var body: some View {
PageViewController(pages: pages, currentPage: $currentPage)
.aspectRatio(3 / 2, contentMode: .fit)
}
}
PageView의 currentPage가 업데이트되면 UIViewController도 이에 반응하도록 수정
struct PageViewController<Page: View>: UIViewControllerRepresentable {
var pages: [Page]
@Binding var currentPage: Int
...
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
}
반대로 PageViewController의 스크롤로 페이지가 바뀔 경우 currentPage도 바뀌도록 수정
UIPageViewControllerDelegate 프로토콜을 추가하여 페이지 전환이 완료되면 바인딩된 currentPage에 값이 변하도록 구현
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
...
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
}
}
}
makeUIViewController(context:) 메서드에서 pageViewController.delegate에 Coordinator 전달
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
4. Add a custom page control
UIViewRepresentable 프로토콜을 준수하는 PageControl
currentPage를 바인딩
struct PageControl: UIViewRepresentable {
var numberOfPages: Int
@Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIPageControl {
let control = UIPageControl()
control.numberOfPages = numberOfPages
control.addTarget(
context.coordinator,
action: #selector(Coordinator.updateCurrentPage(sender:)),
for: .valueChanged)
return control
}
func updateUIView(_ uiView: UIPageControl, context: Context) {
uiView.currentPage = currentPage
}
class Coordinator: NSObject {
var control: PageControl
init(_ control: PageControl) {
self.control = control
}
@objc
func updateCurrentPage(sender: UIPageControl) {
control.currentPage = sender.currentPage
}
}
}
이에 PageControl과 PageView를 CategoryHome에 붙이자.
struct PageView<Page: View>: View {
var pages: [Page]
@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)
}
.aspectRatio(3 / 2, contentMode: .fit)
}
}
struct CategoryHome: View {
@Environment(ModelData.self) var modelData
var body: some View {
NavigationSplitView {
List {
PageView(pages: modelData.features.map { FeatureCard(landmark: $0) })
.listRowInsets(EdgeInsets())
ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: modelData.categories[key]!)
}
.listRowInsets(EdgeInsets())
}
.navigationTitle("Featured")
} detail: {
Text("Select a Landmark")
}
}
}
#Preview {
CategoryHome()
.environment(ModelData())
}
챕터 끝나고 나니 이전 챕터를 건너뛰고 했다는걸 깨달았다...
'iOS > SwiftUI Tutorial' 카테고리의 다른 글
SwiftUI Tutorial 7 - Composing complex interfaces (0) | 2024.07.09 |
---|---|
SwiftUI Tutorial 6 - Animating views and transitions (1) | 2024.07.08 |
SwiftUI Tutorial 5- Drawing paths and shapes (0) | 2024.07.07 |
SwiftUI Tutorial 4 - Handling user input (0) | 2024.07.06 |
SwiftUI Tutorial 3 - Building lists and navigation (0) | 2024.07.05 |