Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Interfacing with UIKit

Willie edited this page Feb 10, 2020 · 6 revisions

与 UIKit 协作

SwiftUI 可与所有 Apple 平台上的现有 UI 框架无缝协作。例如我们可以在 SwiftUI 视图中放置 UIKit 视图和视图控制器,反之亦然。

本文将展示如何把地标从主屏幕中转换到包装 UIPageViewControllerUIPageControl 的实例中去。我们将使用 UIPageViewController 显示 SwiftUI 视图的轮播,并使用状态变量和绑定来协调整个 UI 中的数据更新。

  • 预计完成时间:25 分钟
  • 项目文件:下载

1. 创建表示 UIPageViewController 的视图

要在 SwiftUI 中表示 UIKit 视图和视图控制器,我们需要创建遵循 UIViewRepresentableUIViewControllerRepresentable 协议的类型。我们的自定义类型创建和配置它们所代表的 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 使用 UIPageViewControllerSwiftUI 视图中显示内容。现在启用滑动交互来从一个页面移动到另一个页面。

一个表示 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 协议。

UIViewRepresentableUIViewControllerRepresentable 类型拥有相同的生命周期,其方法与其基础 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 展示了 UIKitSwiftUI 视图和控制器是如何协同工作的。

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /