SwiftUI 与所有 Apple 平台上的现有 UI 框架无缝集成。例如,您可以在 SwiftUI 视图中放置 UIKit 视图和视图控制器,反之亦然。
本教程将向您展示如何将首页中的特色地标转换为包裹 UIPageViewController 和 UIPageControl 的实例。您将使用 UIPageViewController 来显示 SwiftUI 视图的轮播,并使用状态变量和绑定来协调用户界面中的数据更新。
请按照步骤构建此项目,或下载已完成的项目以自行探索。
第 1 节
创建一个表示 UIPageViewController 的视图 要在 SwiftUI 中表示 UIKit 视图和视图控制器,您需要创建符合 UIViewRepresentable 和 UIViewControllerRepresentable 协议的类型。您的自定义类型创建并配置它们所表示的 UIKit 类型,而 SwiftUI 管理它们的生命周期并在需要时更新它们。
步骤 1
在项目的 Views 文件夹中创建一个 PageView 组,并添加一个名为 PageViewController.swift 的新 Swift 文件;声明 PageViewController 类型符合 UIViewControllerRepresentable.
页面控制器存储了一个 Page 实例数组,这些实例必须是 View 的一种。这些是您用来在地标之间滚动的页面。
接下来,为 UIViewControllerRepresentable 协议添加两个要求。
第 2 步
添加一个 makeUIViewController(context:) 方法,用于创建具有所需配置的 UIPageViewController 。
SwiftUI 在准备显示视图时会调用此方法一次,并且会管理视图控制器的生命周期。
第 3 步
添加一个 updateUIViewController(:context:) 方法来调用 setViewControllers(:direction:animated:) 以提供一个视图控制器用于显示。
目前,你将在每次更新时创建一个 UIHostingController 来托管 page 的 SwiftUI 视图。稍后,你将通过在页面视图控制器的生命周期内仅初始化一次控制器来使其更高效。
在继续之前,请准备一个功能卡片以用于页面。
第 4 步
将下载项目文件中 Resources 目录中的图片拖放到你的应用的资源库中。
如果存在地标特色图片,其尺寸与普通图片不同。
第 5 步
在 Landmark 结构中添加一个计算属性,如果存在的话,返回特色图片。
第 6 步
添加一个新的 SwiftUI 视图文件,命名为 FeatureCard.swift ,用于显示地标特色图片。
包含 aspect ratio 修饰符,使其模拟视图中 FeatureCard 最终预览的宽高比。
第 7 步
在图像上叠加关于地标的信息。
接下来,您将创建一个自定义视图来呈现您的 UIViewControllerRepresentable 视图。
第 8 步
创建一个新的 SwiftUI 视图文件,命名为 PageView.swift ,并更新 PageView 类型以声明 PageViewController 作为子视图。
预览失败,因为 Xcode 无法推断 Page 的类型。
第 9 步
添加宽高比修改器并更新预览以传递所需的视图数组,预览即可开始工作。
第 2 节
创建视图控制器的数据源 在几步之内,你已经做了很多事情—— PageViewController 使用 UIPageViewController 来显示来自 SwiftUI 视图的内容。现在是时候启用滑动交互以在页面之间切换了。
一个表示 UIKit 视图控制器的 SwiftUI 视图可以定义一个由 SwiftUI 管理并在表示视图的上下文中提供的 Coordinator 类型。
步骤 1
在 Coordinator 内声明一个嵌套的 PageViewController 类。
SwiftUI 管理你的 UIViewControllerRepresentable 类型的协调器,并在调用你上面创建的方法时将其作为上下文的一部分提供。
第 2 步
在 PageViewController 中添加另一个方法来创建协调器。
SwiftUI 在 makeCoordinator() 方法调用之前调用这个方法,这样你就可以在配置视图控制器时访问协调器对象。
提示 您可以使用此协调器来实现常见的 Cocoa 模式,例如代理、数据源以及通过目标-动作响应用户事件。
第 3 步
在协调器中使用 pages 视图数组初始化控制器数组。
协调者是存储这些控制器的好地方,因为系统只会初始化它们一次,并且在您需要更新视图控制器之前。
第 4 步
将 UIPageViewControllerDataSource 符合 Coordinator 类型,并实现两个必需的方法。
这两种方法建立了视图控制器之间的关系,因此您可以在这两者之间来回滑动。
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)
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]
}
}
}
第 5 步
将协调者添加为 UIPageViewController 的数据源。
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]
}
}
}
第 6 步
返回 PageView ,确保处于实时模式,并测试滑动交互。
第三部分
在 SwiftUI 视图的状态中跟踪页面 为了添加自定义 UIPageControl ,你需要一种方法从 PageView 内部跟踪当前页面。
为此,你将在 PageView 中声明一个 @State 属性,并将此属性的绑定传递给 PageViewController 视图。 PageViewController 会更新绑定以匹配可见页面。
步骤 1
首先,添加一个 currentPage 绑定作为 PageViewController 的属性。
除了声明 @Binding 属性,您还需要更新 setViewControllers(_:direction:animated:) 的调用,传递 currentPage 绑定的值。
import SwiftUI
import UIKit
struct PageViewController<Page: View>: UIViewControllerRepresentable {
var pages: [Page]
@Binding var currentPage: Int
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[currentPage]], 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]
}
}
第 2 步
在 PageView 中声明 @State 变量,并在创建子 PageViewController 时传递一个绑定到属性。
重要 请记住使用 $ 语法创建一个绑定到存储为状态的值。
第 3 步
通过更改初始值来测试值是否通过绑定传递到 PageViewController 。
实验 在 PageView 中添加一个按钮,使页面控制器跳转到第二个视图。
第 4 步
添加一个文本视图,并设置 currentPage 属性,这样可以监控 @State 属性的值。
注意当你从一个页面滑动到另一个页面时,值不会改变。
第 5 步
在 PageViewController.swift 中,将协调者设置为 UIPageViewControllerDelegate ,并添加 pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool) 方法。
因为 SwiftUI 会在页面切换动画完成后调用此方法,所以你可以找到当前视图控制器的索引并更新绑定。
import SwiftUI
import UIKit
struct PageViewController<Page: View>: UIViewControllerRepresentable {
var pages: [Page]
@Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
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]
}
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
}
}
}
}
第 6 步
将协调者设置为 UIPageViewController 的代理,同时还要设置为数据源。
在两个方向都连接了绑定之后,文本视图会在每次滑动后更新以显示正确的页码。
第 4 节
添加自定义页面控件 你可以为你的视图添加一个自定义的 UIPageControl ,并用 SwiftUI 的 UIViewRepresentable 视图包裹起来。
步骤 1
创建一个新的 SwiftUI 视图文件,命名为 PageControl.swift 。更新 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
}
}
第 2 步
将文本框替换为页面控件,从 VStack 切换到 ZStack 进行布局。
因为您传递了页数和装订信息给当前页面,所以页面控制已经显示了正确的值。
import SwiftUI
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)
}
}
#Preview {
PageView(pages: ModelData().features.map { FeatureCard(landmark: $0) })
}
接下来,使页面控件可交互,以便用户可以轻触一侧或另一侧以在页面之间切换。
第 3 步
在 PageControl 中创建嵌套的 Coordinator 类型,并添加一个 makeCoordinator() 方法以创建并返回一个新的协调者。
由于 UIControl 的子类如 UIPageControl 使用目标-操作模式而不是委托,因此此 Coordinator 实现了 @objc 方法以更新当前页面绑定。
import SwiftUI
import UIKit
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
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
}
}
}
第 4 步
将协调者作为目标添加到 valueChanged 事件中,并指定 updateCurrentPage(sender:) 方法作为要执行的操作。
import SwiftUI
import UIKit
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
}
}
}
第 5 步
最后,在 CategoryHome 中,将占位图文替换为新的页面视图。
import SwiftUI
struct CategoryHome: View {
@Environment(ModelData.self) var modelData
@State private var showingProfile = false
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())
}
.listStyle(.inset)
.navigationTitle("Featured")
.toolbar {
Button {
showingProfile.toggle()
} label: {
Label("User Profile", systemImage: "person.crop.circle")
}
}
.sheet(isPresented: $showingProfile) {
ProfileHost()
.environment(modelData)
}
} detail: {
Text("Select a Landmark")
}
}
}
#Preview {
CategoryHome()
.environment(ModelData())
}
第 6 步
现在尝试所有不同的交互方式 — PageView 展示了 UIKit 和 SwiftUI 视图及控制器如何协同工作。