与 UIKit 协作
SwiftUI
可与所有Apple
平台上的现有 UI 框架无缝协作。例如我们可以在SwiftUI
视图中放置UIKit
视图和视图控制器,反之亦然。
本文将展示如何把地标从主屏幕中转换到包装UIPageViewController
和UIPageControl
的实例中去。我们将使用UIPageViewController
显示SwiftUI
视图的轮播,并使用状态变量和绑定来协调整个 UI 中的数据更新。
* 预计完成时间:25 分钟
* 项目文件:下载
1. 创建表示 UIPageViewController 的视图
要在 SwiftUI
中表示 UIKit
视图和视图控制器,我们需要创建遵循 UIViewRepresentable
和 UIViewControllerRepresentable
协议的类型。我们的自定义类型创建和配置它们所代表的 UIKit
类型,而 SwiftUI
管理它们的生命周期并在需要时更新它们。
1.1 创建一个新的 SwiftUI
视图,命名为 PageViewController.swift
,声明遵循 UIViewControllerRepresentable
协议的 PageViewController
类型。
页面的视图控制器存储了 UIViewController
实例的数组。这些是在地标之间滚动的页面。
PageViewController.swift
import SwiftUI
//
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
}
//
接下添加 UIViewControllerRepresentable
协议的两个需求。
1.2 添加一个 makeUIViewController(context:)
方法,创建一个满足需求的 UIPageViewController
。
当 SwiftUI
准备好显示视图时,它会调用此方法一次,然后管理视图控制器的生命周期。
PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
//
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
return pageViewController
}
//
}
1.3 添加一个 updateUIViewController(_:context:)
方法,在其中调用 setViewControllers(_:direction:animated:)
来显示数组中的第一个视图控制器。
PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
return pageViewController
}
//
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[0]], direction: .forward, animated: true)
}
//
}
创建另一个 SwiftUI
视图来显示我们的 UIViewControllerRepresentable
视图。
1.4 创建一个新的 SwiftUI
视图,命名为 PageView.swift
,声明一个 PageViewController
作为子视图。
需要注意的是,泛型初始化方法接收一个视图数组,并将每个视图嵌套在 UIHostingController
中。 UIHostingController
是一个 UIViewController
的子类,表示 UIKit
上下文中的 SwiftUI
view。
PageView.swift
import SwiftUI
//
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
//
var body: some View {
//
PageViewController(controllers: viewControllers)
//
}
}
struct PageView_Preview: PreviewProvider {
static var previews: some View {
PageView()
}
}
1.5 更新 preview provider
,传入必要的视图数组,之后预览就会开始工作。
PageView.swift
import SwiftUI
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
PageViewController(controllers: viewControllers)
}
}
struct PageView_Preview: PreviewProvider {
static var previews: some View {
//
PageView(features.map { FeatureCard(landmark: $0) })
.aspectRatio(3/2, contentMode: .fit)
//
}
}
1.6 在进行下一步之前,在画布中固定 PageView
的预览,所有的操作都将发生在这个视图上。
2. 创建视图控制器的数据源
在几个简短的步骤中,我们已经做了很多工作:PageViewController
使用 UIPageViewController
从 SwiftUI
视图中显示内容。现在启用滑动交互来从一个页面移动到另一个页面。
一个表示 UIKit
视图控制器的 SwiftUI
视图可以定义 SwiftUI
管理的 Coordinator
类型,并将其作为表示视图上下文的一部分提供。
2.1 在 PageViewController
中创建一个嵌套的 Coordinator
类。
SwiftUI
管理我们 UIViewControllerRepresentable
类型的 coordinator
,并在调用上面创建的方法时将其作为上下文的一部分提供。
PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[0]], direction: .forward, animated: true)
}
//
class Coordinator: NSObject {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
}
//
}
给 PageViewController
添加另外一个方法来创建 coordinator
。
SwiftUI
会在调用 makeUIViewController(context:)
方法之前调用 makeCoordinator()
方法,这样配置视图控制器时,我们可以访问 coordinator
对象。
我们可以用这个 coordinator
实现常见的 Cocoa
模式,例如代理、数据源以及通过 target-action
响应用户事件。
PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
//
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(
[controllers[0]], direction: .forward, animated: true)
}
class Coordinator: NSObject {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
}
}
2.3 给 Coordinator
类型遵循 UIPageViewControllerDataSource
协议,并且实现两个必要方法。
这两个方法建立了视图控制器之间的关系,因此我们可以在它们之间来回滑动。
PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
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(
[controllers[0]], direction: .forward, animated: true)
}
//
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]
}
//
}
}
2.4 将 coordinator
作为数据源添加给 UIPageViewController
。
PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
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(
[controllers[0]], direction: .forward, animated: true)
}
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]
}
}
}
2.5 打开实时预览并测试滑动交互。
3. 在 SwiftUI 视图的状态中跟踪页面
要添加自定义的 UIPageControl
,我们需要一种从 PageView
中跟踪当前页面的方法。
为此,我们将在 PageView
中声明一个 @State
属性,并传递一个绑定给此属性,直到 PageViewController
视图。 PageViewController
更新绑定来匹配可见页面。
3.1 给 PageViewController
添加一个 currentPage
的绑定的属性。
除了声明 @Binding
属性外,还要更新对 setViewControllers(_:direction:animated:)
的调用,并传递 currentPage
的绑定的值。
PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
//
@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(
//
[controllers[currentPage]], direction: .forward, animated: true)
//
}
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]
}
}
}
3.2 在 PageView
中声明 @State
变量,并在创建子 PageViewController
时将绑定传递给属性。
请记住使用 $
语法创建用状态来存储值的绑定。
PageView.swift
import SwiftUI
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
//
@State var currentPage = 0
//
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
//
PageViewController(controllers: viewControllers, currentPage: $currentPage)
//
}
}
struct PageView_Preview: PreviewProvider {
static var previews: some View {
PageView(features.map { FeatureCard(landmark: $0) })
.aspectRatio(3/2, contentMode: .fit)
}
}
3.3 通过更改 currentPage
的初始值,测试值是否通过绑定传递给了 PageViewController
。
给 PageView
添加一个按钮,让视图控制器跳转到第二个视图。
PageView.swift
import SwiftUI
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
//
@State var currentPage = 1
//
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
}
}
struct PageView_Preview: PreviewProvider {
static var previews: some View {
PageView(features.map { FeatureCard(landmark: $0) })
.aspectRatio(3/2, contentMode: .fit)
}
}
3.4 添加带有 currentPage
属性的文字视图,以便我们关注 @State
属性的值。
需要注意的是,当从一个页面滑动到另一个页面时,该值不会改变。
PageView.swift
import SwiftUI
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
//
@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)")
}
//
}
}
struct PageView_Preview: PreviewProvider {
static var previews: some View {
PageView(features.map { FeatureCard(landmark: $0) })
}
}
3.5 在 PageViewController.swift
中,让 coordinator
遵循 UIPageViewControllerDelegate
协议,然后添加 pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool)
方法。
只要页面切换动画完成,SwiftUI
就会调用此方法,所以我们可以找到当前视图控制器的索引并更新绑定。
PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
@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(
[controllers[currentPage]], direction: .forward, animated: true)
}
//
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
}
}
//
}
}
3.6 除数据源外,还将 coordinator
指定为 UIPageViewController
的代理。
在两个方向上连接绑定后,文字视图会在每次滑动后更新以显示正确的页码。
PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
@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(
[controllers[currentPage]], direction: .forward, animated: true)
}
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
}
}
}
}
4. 添加自定义的页面控件
现在我们已经准备好给视图添加自定义的包装在 SwiftUI UIViewRepresentable
中的 UIPageControl
了。
4.1 创建一个新的 SwiftUI
视图文件,命名为 PageControl.swift
。让 PageControl
遵循 UIViewRepresentable
协议。
UIViewRepresentable
和 UIViewControllerRepresentable
类型拥有相同的生命周期,其方法与其基础 UIKit
类型相对应。
PageControl.swift
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
}
}
//
4.2 将文字框换成页面控件,把布局从 VStack
换成 ZStack
。
因为我们正在将页面计数和绑定传递给当前页面,所以页面控件已显示正确的值。
PageView.swift
import SwiftUI
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
@State var currentPage = 0
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
//
ZStack(alignment: .bottomTrailing) {
//
PageViewController(controllers: viewControllers, currentPage: $currentPage)
//
PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
.padding(.trailing)
//
}
}
}
struct PageView_Preview: PreviewProvider {
static var previews: some View {
PageView(features.map { FeatureCard(landmark: $0) })
}
}
接下来让页面控件可以交互,以便用户可以点击一侧或另一侧在页面之间移动。
4.3 在 PageControl
中创建嵌套的 Coordinator
类型,然后添加一个 Coordinator()
方法来创建并返回一个新的 coordinator
。
由于 UIPageControl
这样的 UIControl
子类使用 arget-action
模式而不是代理,所以此 Coordinator
实现了 @objc
方法来更新当前页面的绑定。
PageControl.swift
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.4 添加 coordinator
作为 valueChanged
事件的目标,将 updateCurrentPage(sender:)
方法指定为要执行的操作。
PageControl.swift
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
}
}
}
4.5 现在来尝试所有不同的交互, PageView
展示了 UIKit
和 SwiftUI
视图和控制器是如何协同工作的。