SwiftUI可以無縫地與現有的UI架構在所有蘋果平台運作。例如,我們可以将UIKit視圖和視圖控制器嵌套進SwiftUI的視圖,或者是反過來。
我們在這個教程學習如何将首頁面中的特别地标部分嵌套在UIPageViewController和UIPageControl中。我們用UIPageViewController顯示SwiftUI跑馬燈視圖,并使用狀态變量和資料綁定來協調所有界面之間的資料更新。
啟動項目并跟随本教程,或打開完成的項目自己研究代碼。
01 建立視圖來展現UIPageViewController
要在SwiftUI中展現UIKit視圖和視圖控制器,我們需要建立遵循UIViewRepresentable和UIViewControllerRepresentable協定的視圖。在這些自定義的類型中建立并配置用于展示的UIKit視圖,SwiftUI會管理它們的生命周期并在需要的時候更新它們。
第一步
建立一個新的SwiftUI視圖,命名為PageViewController.swift,并聲明遵循UIViewControllerRepresentable協定。
這個頁面視圖控制器會存儲一個UIViewController執行個體數組,這些是用來展示滑動的地标頁面的。
import SwiftUIimport UIKitstruct PageViewController: UIViewControllerRepresentable { var controllers: [UIViewController]}
接下來,添加兩個UIViewControllerRepresentable協定的要求。
第二步
添加makeUIViewController(context:)方法并在其中以需要的配置建立UIPageViewController。
SwiftUI會在準備好顯示視圖的時候調用這個方法一次,然後管理這個視圖控制器的生命周期。
struct PageViewController: UIViewControllerRepresentable { var controllers: [UIViewController] func makeUIViewController(context: Context) -> UIPageViewController { let pageViewController = UIPageViewController( transitionStyle: .scroll, navigationOrientation: .horizontal) return pageViewController }}
第三步
添加updateUIViewController(_:context:)方法并在其中調用setViewControllers(_:direction:animated:)來顯示數組中的第一個視圖控制器。
return pageViewController } func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) { pageViewController.setViewControllers( [controllers[0]], direction: .forward, animated: true) }}
建立另外一個SwiftUI視圖來展現我們的UIViewControllerRepresentable視圖。
第四步
建立一個新的SwiftUI視圖檔案,命名為PageView.swift,并更新這個頁面視圖,聲明PageViewController作為它的子視圖。
注意通用的初始化器需要一個視圖數組,并将每一個都嵌套在UIHostingController中。UIHostingController是一個UIViewController的子類用于在UIKit上下文中展現SwiftUI視圖。
import SwiftUIstruct PageView: View { var viewControllers: [UIHostingController] init(_ views: [Page]) { self.viewControllers = views.map { UIHostingController(rootView: $0) } } var body: some View { PageViewController(controllers: viewControllers)
第五步
更新PageView的預覽提供器傳入需要的視圖數組,預覽就可以運作了。
struct PageView_Previews: PreviewProvider { static var previews: some View { PageView(features.map { FeatureCard(landmark: $0) }) .aspectRatio(3/2, contentMode: .fit) }}
第六步
在我們繼續之前将PageView的預覽固定在畫布中-我們之後的動作都會在這個視圖中展現。
02 建立視圖控制器的資料源
在短短幾步中,我們已經完成了很多-使用UIPageViewController從SwiftUI視圖中展示内容的PageViewController。現在是時候啟用滑動互動從一個頁面切換到另外一個頁面了。
一個展現UIKit視圖控制器的SwiftUI視圖可以定義一個由SwiftUI管理的Coordinator類型作為可展現的視圖上下文的一部分。
第一步
在PageViewController中聲明一個嵌套的Coordinator類。
SwiftUI會管理UIViewControllerRepresentable類型中的coordinator,并在調用上面建立的那些方法時将其作為上下文的一部分。
[controllers[0]], direction: .forward, animated: true) } class Coordinator: NSObject { var parent: PageViewController init(_ pageViewController: PageViewController) { self.parent = pageViewController } }}
第二步
在PageViewController中添加另外一個方法makeCoordinator()。
SwiftUI會在調用makeUIViewController(context:)方法前先調用makeCoordinator()方法,是以在我們配置視圖控制器的時候可以通路coordinator對象了。
提示 我們可以使用這個coordinator對象來實作常見的Cocoa模式,比如代理、資料源以及通過target-action方式來響應使用者事件。
var controllers: [UIViewController] func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIViewController(context: Context) -> UIPageViewController { let pageViewController = UIPageViewController(
第三步
在Coordinator類上添加協定UIPageViewControllerDataSource,并實作兩個必要的方法。
這兩個方法會在視圖控制器之間建立聯系,是以我們可以在它們之間前後滑動。
class Coordinator: NSObject, UIPageViewControllerDataSource { var parent: PageViewController init(_ pageViewController: PageViewController) { self.parent = pageViewController } func pageViewController( _ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index == 0 { return parent.controllers.last } return parent.controllers[index - 1] } func pageViewController( _ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index + 1 == parent.controllers.count { return parent.controllers.first } return parent.controllers[index + 1] }
第四步
将coordinator作為UIPageViewController的資料源。
transitionStyle: .scroll, navigationOrientation: .horizontal) pageViewController.dataSource = context.coordinator return pageViewController
第五步
打開實時預覽模式并測試滑動互動操作。
03 在SwiftUI視圖的狀态中追蹤頁面
為了添加自定義的UIPageControl,我們需要一個辦法來追蹤PageView中的目前頁面。
要實作這個功能,我們需要在PageView中聲明一個@State屬性,并将這個屬性的資料綁定傳遞到PageViewController視圖中。PageViewController會更新綁定資料以比對目前可見頁面。
第一步
首先在PageViewController中添加一個currentPage的資料綁定作為屬性。
另外,在聲明了@Binding屬性後,我們還需要更新調用的setViewControllers(:_direction:animated:)方法,傳入currentPage綁定的值。
struct PageViewController: UIViewControllerRepresentable { var controllers: [UIViewController] @Binding var currentPage: Int func makeCoordinator() -> Coordinator {// 省略部分代碼 func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) { pageViewController.setViewControllers( [controllers[currentPage]], direction: .forward, animated: true) }
第二步
在PageView中聲明@State變量,并在建立子視圖PageViewController時傳入這個資料的綁定。
重要
記住使用$符号建立對于作為狀态存儲的資料的綁定。
struct PageView: View { var viewControllers: [UIHostingController] @State var currentPage = 0 init(_ views: [Page]) { self.viewControllers = views.map { UIHostingController(rootView: $0) } } var body: some View { PageViewController(controllers: viewControllers, currentPage: $currentPage) }}
第三步
通過改變初始值測試通過綁定傳入PageViewController的值。
體驗
可以通過在PageView中添加一個按鈕讓PageViewController跳轉到第二個視圖。
第四步
使用currentPage屬性建立一個文本視圖,這樣我們可以看到@State屬性的值。
可以觀察到當我們從一個頁面滑動到另外一個時,這個值沒有變化。
struct PageView: View { var viewControllers: [UIHostingController] @State var currentPage = 0 init(_ views: [Page]) { self.viewControllers = views.map { UIHostingController(rootView: $0) } } var body: some View { VStack { PageViewController(controllers: viewControllers, currentPage: $currentPage) Text("Current Page: (currentPage)") } }}
第五步
在PageViewController.swift檔案中,讓Coordinator類遵循UIPageViewControllerDelegate協定,并添加pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool)方法。
由于SwiftUI會在頁面切換動畫結束後調用這個方法,我們可以在這個方法中查詢目前視圖控制器的索引并更新綁定資料。
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate { var parent: PageViewController init(_ pageViewController: PageViewController) { self.parent = pageViewController } func pageViewController( _ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index == 0 { return parent.controllers.last } return parent.controllers[index - 1] } func pageViewController( _ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index + 1 == parent.controllers.count { return parent.controllers.first } return parent.controllers[index + 1] } func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { if completed, let visibleViewController = pageViewController.viewControllers?.first, let index = parent.controllers.firstIndex(of: visibleViewController) { parent.currentPage = index } } }
第六步
在作為資料源之後,再将coordinator作為UIPageViewController的代理。
在将兩方的綁定連接配接之後,文本視圖會在每次滑動後顯示正确的頁數。
navigationOrientation: .horizontal) pageViewController.dataSource = context.coordinator pageViewController.delegate = context.coordinator return pageViewController
04 添加自定義的頁面控件
現在我們已經準備好在視圖中添加一個自定義的UIPageControl了,這個控件會嵌套在SwiftUI的UIViewRepresentable視圖中。
第一步
建立一個新的SwiftUI視圖檔案,命名為PageControl.swift。更新PageControl類讓它遵循UIViewRepresentable協定。
UIViewRepresentable和UIViewControllerRepresentable協定擁有相同的生命周期,方法也和它們在UIKit架構中的類型相對應。
import SwiftUIimport UIKitstruct PageControl: UIViewRepresentable { var numberOfPages: Int @Binding var currentPage: Int func makeUIView(context: Context) -> UIPageControl { let control = UIPageControl() control.numberOfPages = numberOfPages return control } func updateUIView(_ uiView: UIPageControl, context: Context) { uiView.currentPage = currentPage }}
第二步
将PageView.swift中的文本替換成新建立的頁面控件,并将VStack布局換成ZStack布局。
由于我們傳入了頁面總數和目前頁面索引的資料綁定,頁面控件已經可以顯示正确的值了。
var body: some View { ZStack(alignment: .bottomTrailing) { PageViewController(controllers: viewControllers, currentPage: $currentPage) PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
接下來,我們讓頁面控件可以互動,使用者點選的時候可以在頁面間切換。
第三步
在PageControl中建立一個嵌套的Coordinator類,并添加makeCoordinator()方法建立并傳回新的coordinator對象。
由于UIControl的子類如UIPageControl使用的是target-action模式而不是代理模式,這個Coordinator類會實作一個@objc方法來更新目前頁面索引資料的綁定。
@Binding var currentPage: Int func makeCoordinator() -> Coordinator { Coordinator(self) }// 省略部分代碼 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 } }
第四步
将coordinator作為valueChanged事件的目标,指定updateCurrentPage(sender:)方法作為執行的行動。
func makeUIView(context: Context) -> UIPageControl { let control = UIPageControl() control.numberOfPages = numberOfPages control.addTarget( context.coordinator, action: #selector(Coordinator.updateCurrentPage(sender:)), for: .valueChanged) return control }
第五步
現在我們可以嘗試所有不同的互動方式了-PageView展示了如何将UIKit和SwiftUI視圖和控件組合在一起工作。