-
Notifications
You must be signed in to change notification settings - Fork 317
Interfacing with UIKit

SwiftUI可与所有Apple平台上的现有 UI 框架无缝协作。例如我们可以在SwiftUI视图中放置UIKit视图和视图控制器,反之亦然。本文将展示如何把地标从主屏幕中转换到包装
UIPageViewController和UIPageControl的实例中去。我们将使用UIPageViewController显示SwiftUI视图的轮播,并使用状态变量和绑定来协调整个 UI 中的数据更新。
- 预计完成时间:25 分钟
- 项目文件:下载
要在 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 的预览,所有的操作都将发生在这个视图上。

在几个简短的步骤中,我们已经做了很多工作: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 打开实时预览并测试滑动交互。

要添加自定义的 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
}
}
}
}
现在我们已经准备好给视图添加自定义的包装在 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 视图和控制器是如何协同工作的。

SwiftUI 纲要 - 绘制与动画 - App 设计与布局 - 框架集成