SwiftUI 官方教程:SwiftUI Tutorials 仅是几个体现 SwiftUI 简单使用的小 demo 而已,简单易学,循序渐进,先看完可以对 SwiftUI 有一个大概的认知。
八:Interfacing with UIKit
Framework Integration - Interfacing with UIKit 与 UIKit 交互。
SwiftUI 与所有 Apple 平台上的现有 UI 框架无缝协作。例如,你可以将 UIKit views 和 view controllers 放置在 SwiftUI views 中,反之亦然。
本教程介绍如何将特色 landmark 从 home screen 转换为 UIPageViewController 和 UIPageControl 的包装实例。你将使用 UIPageViewController 来显示 SwiftUI views 的轮播,并使用状态变量和绑定在整个用户界面中协调数据更新。
按照步骤生成此项目,或下载完成的项目自行探索。
Create a View to Represent a UIPageViewController
要在 SwiftUI 中表示 UIKit 视图和视图控制器,请创建符合 UIViewRepresentable 和 UIViewControllerRepresentable 协议的类型。你的自定义类型创建和配置它们所代表的 UIKit 类型,而 SwiftUI 管理其生命周期并在需要时更新它们。
在项目的 Views 文件夹中创建一个 PageView 文件夹,并添加一个名为 PageViewController.swift 的新 Swift 文件;将 PageViewController 类型声明为符合 UIViewControllerRepresentable。page view controller 存储 Page 实例数组,这些实例必须是一种 View 类型。这些是你用于在地标之间滚动的页面。
接下来,添加 UIViewControllerRerepresentationable 协议的两个要求。
添加一个 makeUIViewController(context:) 方法,以创建具有所需配置的 UIPageViewController 的方法。SwiftUI 在准备好显示视图时调用此方法一次,然后管理视图控制器的生命周期。
添加一个 updateUIViewController(_:context:) 方法,内部调用 setViewControllers(_:direction:animated:) 方法时以提供用于显示的 view controller(pageViewController)。现在,你将创建 UIHostingController,该控制器在每次更新时托管页面 SwiftUI 视图。稍后,通过在 page view controller 的生命周期内仅初始化一次控制器,可以提高效率。
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)
}
}
在继续之前,请准备要用作 page 的 card。
将下载的项目文件的 Resources 目录中的图像拖到应用的 Asset catalog 中。landmark 的 feature image(如果存在)的尺寸与常规图像不同。
将返回 feature image 的 Landmark 结构体(如果存在)添加计算属性。
...
var featureImage: Image? {
isFeatured ? Image(imageName + "_feature") : nil
}
...
添加一个名为 FeatureCard 的新 SwiftUI View 文件,该文件用于显示 landmark’s feature image。
在图像上叠加有关 landmark 的文本信息。
import SwiftUI
struct FeatureCard: View {
var landmark: Landmark
var body: some View {
landmark.featureImage?
.resizable()
.aspectRatio(3 / 2, contentMode: .fit)
.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()
}
.foregroundColor(.white)
}
}
struct FeatureCard_Previews: PreviewProvider {
static var previews: some View {
FeatureCard(landmark: ModelData().features[0])
}
}
接下来,你将创建一个自定义视图来展示你的 UIViewControllerRepresentable 视图。
创建一个名为 PageView 的新 SwiftUI 视图文件,并更新 PageView 类型以将 PageViewController 声明为子视图。预览失败,因为 Xcode 无法推断出 Page 的类型。更新 preview provide 以传递所需的视图数组,此时预览便可开始正常工作。
import SwiftUI
struct PageView<Page: View>: View {
var pages: [Page]
var body: some View {
PageViewController(pages: pages)
}
}
struct PageView_Previews: PreviewProvider {
static var previews: some View {
PageView(pages: ModelData().features.map({ FeatureCard(landmark: $0) }))
.aspectRatio(3 / 2, contentMode: .fit)
}
}
Create the View Controller’s Data Source
在短短的几个步骤中,你已经做了很多事情 —— PageViewController 使用 UIPageViewController 来显示来自 SwiftUI 视图的内容。现在是时候启用滑动交互以从一个 page 移动到另一个 page 了。
表示 UIKit 视图控制器的 SwiftUI 视图可以定义 SwiftUI 管理的 Coordinator 类型,并将其作为 representable 视图上下文的一部分提供。
在 PageViewController 中声明一个嵌套的 Coordinator 类。SwiftUI 管理你的 UIViewControllerRepresentable 类型的 coordinator,并在调用上面创建的方法时将其作为上下文的一部分提供。
将另一个方法添加到 PageViewController 以创建 coordinator。
SwiftUI 在 makeUIViewController(context:) 之前调用此 makeCoordinator() 方法,以便你在配置视图控制器时可以访问 coordinator 对象。
你可以使用此 coordinator 实现常见的 Cocoa 模式,例如委托(delegates)、数据源(data sources)以及通过 target-action 响应用户事件。
使用视图的 pages 数组初始化 coordinator 中的控制器数组。
将 UIPageViewControllerDataSource conformance 添加到 Coordinator 类型,并实现两个必需的方法。这两种方法建立视图控制器之间的关系,以便你可以在它们之间来回轻扫。
添加 coordinator 作为 UIPageViewController 的数据源。
返回 PageView,打开实时预览,并测试滑动交互。
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]
}
}
}
Track the Page in a SwiftUI View’s State
要准备添加自定义 UIPageControl,你需要一种从 PageView 中跟踪当前 Page 的方法。为此,你需要在 PageView 中声明一个 @State 属性,并将此属性的绑定向下传递到 PageViewController 视图。PageViewController 更新绑定以匹配可见 page。
首先添加一个 currentPage 绑定作为 PageViewController 的属性。除了声明 @Binding 属性之外,还可以更新对 setViewControllers(_:direction:animated:) 的调用,传递 currentPage 绑定的值。
...
@Binding var currentPage: Int
...
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
}
在 PageView 中声明 @State 变量,并在创建子 PageViewController 时将绑定传递给该属性。请记住使用 $ 语法创建与存储为 state 的值的绑定。
...
@State private var currentPage = 0
...
var body: some View {
PageViewController(pages: pages, currentPage: $currentPage)
}
通过更改其初始值来测试该值是否通过绑定流向 PageViewController。向 PageView 添加一个按钮,使 page view controller 跳转到第二个视图。
添加带有 currentPage 属性的文本视图,以便你可以密切关注 @State 属性的值。请注意,当你从一页滑动到另一页时,该值不会改变。
...
var body: some View {
VStack {
PageViewController(pages: pages, currentPage: $currentPage)
Text("Current Page: \(currentPage)")
}
}
...
在 PageViewController.swift 中,使 Coordinator 遵循 UIPageViewControllerDelegate 协议,并添加 pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool) 方法。
...
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
}
}
}
...
除了 dataSource 之外,还将 coordinator 分配为 UIPageViewController 的委托。在两个方向上绑定链接后,文本视图将在每次轻扫后更新以显示正确的页码。
pageViewController.delegate = context.coordinator
Add a Custom Page Control
你已准备好将自定义 UIPageControl 添加到你的视图中,包装在 SwiftUI UIViewRepresentable 视图中。
创建一个名为 PageControl 的新 SwiftUI 视图文件。更新 PageControl 类型以符合 UIViewRepresentable 协议。
UIViewRepresentable 和 UIViewControllerRepresentable 类型具有相同的生命周期,其方法对应于它们的底层 UIKit 类型。
import SwiftUI
import UIKit
struct 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
}
}
将 text box 替换为 page control,从 VStack 切换到 ZStack 进行布局。因为你将页数和绑定传递给当前页面,所以页面控件已经显示了正确的值。
...
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)
}
}
...
接下来,使 page control 具有交互性,以便用户可以点击一侧或另一侧在页面之间移动。
在 PageControl 中创建一个嵌套的 Coordinator 类型,并添加一个 makeCoordinator() 方法来创建和返回一个新的 coordinator。
由于 UIControl 子类(如 UIPageControl)使用 target-action 模式而不是 delegation,因此此 Coordinator 实现 @objc 方法来更新 currentPage 绑定。
...
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
...
class Coordinator: NSObject {
var control: PageControl
init(_ control: PageControl) {
self.control = control
}
@objc
func updateCurrentPage(sender: UIPageControl) {
control.currentPage = sender.currentPage
}
}
添加 coordinator 作为 valueChanged 事件的 target,指定 updateCurrentPage(sender:) 方法作为要执行的 action。
...
control.addTarget(
context.coordinator,
action: #selector(Coordinator.updateCurrentPage(sender:)),
for: .valueChanged)
...
最后,在 CategoryHome 中,将 placeholder feature image 替换为新的 page View。
...
PageView(pages: modelData.features.map { FeatureCard(landmark: $0) })
.aspectRatio(3 / 2, contentMode: .fit)
.listRowInsets(EdgeInsets())
...
现在尝试所有不同的交互 —— PageView 展示了 UIKit 和 SwiftUI 视图和控制器如何协同工作。
参考链接
参考链接:🔗