[译] SwiftUI 官方教程 (九)(完结)
完整中文教程及代码请查看 WillieWangWei/SwiftUI-Tutorials
UIKit 接口
SwiftUI
可与所有Apple
平台上的现有 UI 框架无缝协作。例如我们可以在SwiftUI
view 中放置UIKit
view 和 view controllers,反之亦然。本文将展现如何把地标从
home screen
中转换到包装UIPageViewController
和UIPageControl
的实例。我们将使用UIPageViewController
显示SwiftUI
view 的轮播,并使用状态变量和绑定来协调整个 UI 中的数据升级。
- 估计完成时间:25 分钟
- 项目文件:下载
1. 创立表示 UIPageViewController 的 View
要在 SwiftUI
中表示 UIKit
view 和 view controllers,我们需要创立遵循 UIViewRepresentable
和 UIViewControllerRepresentable
协议的类型。我们的自己设置类型创立和配置它们所代表的 UIKit
类型,而 SwiftUI
管理它们的生命周期并在需要时升级它们。
1.1 创立一个新的 SwiftUI
view,命名为 PageViewController.swift
,公告遵循 UIViewControllerRepresentable
协议的 PageViewController
类型。
页面的 view controller 存储了 UIViewController
实例的数组。这些是在地标之间滚动的页面。
PageViewController.swift
import SwiftUIimport UIKitstruct PageViewController: UIViewControllerRepresentable { var controllers: [UIViewController]}
接下增加 UIViewControllerRepresentable
协议的两个需求。
1.2 增加一个 makeUIViewController(context:)
方法,创立一个满足需求的 UIPageViewController
。
当 SwiftUI
准备好显示 view 时,它会调用此方法一次,而后管理 view controller 的生命周期。
PageViewController.swift
import SwiftUIimport UIKitstruct 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:)
来显示数组中的第一个 view controller。
PageViewController.swift
import SwiftUIimport UIKitstruct 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
view 来显示我们的 UIViewControllerRepresentable
view。
1.4 创立一个新的 SwiftUI
view,命名为 PageView.swift
,公告一个 PageViewController
作为子 view。
需要注意的是,泛型初始化方法接收一个 view 数组,并将每个 view 嵌套在 UIHostingController
中。 UIHostingController
是一个 UIViewController
的子类,表示 UIKit
上下文中的 SwiftUI
view。
PageView.swift
import SwiftUIstruct 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
,传入必要的 view 数组,之后预览就会开始工作。
PageView.swift
import SwiftUIstruct 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 在进行下一步之前,在 canvas
中固定 PageView
的预览,所有的操作都将发生在这个 view 上。
2. 创立 View Controller 的数据源
在几个简短的步骤中,我们已经做了很多工作:PageViewController
使用 UIPageViewController
从 SwiftUI
view 中显示内容。现在启用滑动交互来从一个页面移动到另一个页面。
一个表示 UIKit
view controller 的 SwiftUI
view 可以定义 SwiftUI
管理的 Coordinator
类型,并将其作为表示 view 上下文的一部分提供。
2.1 在 PageViewController
中创立一个嵌套的 Coordinator
类。
SwiftUI
管理我们 UIViewControllerRepresentable
类型的 coordinator
,并在调用上面创立的方法时将其作为上下文的一部分提供。
PageViewController.swift
import SwiftUIimport UIKitstruct 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()
方法,这样配置 view controller 时,我们可以访问 coordinator
对象。
我们可以用这个 coordinator
实现常见的 Cocoa
模式,例如代理商、数据源以及通过 target-action
响应客户事件。
PageViewController.swift
import SwiftUIimport UIKitstruct 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 } }}
[图片上传失败…(image-d8c3c5-1560084475115)]
2.3 给 Coordinator
类型遵循 UIPageViewControllerDataSource
协议,并且实现两个必要方法。
这两个方法建立了 view controllers 之间的关系,因而我们可以在它们之间来回滑动。
PageViewController.swift
import SwiftUIimport UIKitstruct 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 SwiftUIimport UIKitstruct 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 View 的状态中跟踪页面
要增加自己设置的 UIPageControl
,我们需要一种从 PageView
中跟踪当前页面的方法。
为此,我们将在 PageView
中公告一个 @State
属性,并传递一个 binding
给此属性,直到 PageViewController
view。 PageViewController
升级 binding
来匹配可见页面。
3.1 给 PageViewController
增加一个 currentPage
的 binding
的属性。
除了公告 @Binding
属性外,还要升级对 setViewControllers(_:direction:animated:)
的调用,并传递 currentPage
的 binding
的值。
PageViewController.swift
import SwiftUIimport UIKitstruct 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
时将 binding
传递给属性。
请记住使用 $
语法创立用状态来存储值的 binding
。
PageView.swift
import SwiftUIstruct 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
的初始值,测试值能否通过 binding
传递给了 PageViewController
。
给 PageView
增加一个按钮,让页面 view controller 跳转到第二个 view。
PageView.swift
import SwiftUIstruct 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
属性的 text view,以便我们关注 @State
属性的值。
需要注意的是,当从一个页面滑动到另一个页面时,该值不会改变。
PageView.swift
import SwiftUIstruct 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
就会调用此方法,所以我们可以找到当前 view controller 的索引并升级 binding
。
PageViewController.swift
import SwiftUIimport UIKitstruct 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
的代理商。
在两个方向上连接 binding
后,text view 会在每次滑动后升级以显示正确的页码。
PageViewController.swift
import SwiftUIimport UIKitstruct 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. 增加自己设置的 Page Control
现在我们已经准备好给 view 增加自己设置的包装在 SwiftUI UIViewRepresentable
中的 UIPageControl
了。
4.1 创立一个新的 SwiftUI
view 文件,命名为 PageControl.swift
。让 PageControl
遵循 UIViewRepresentable
协议。
UIViewRepresentable
和 UIViewControllerRepresentable
类型拥有相同的生命周期,其方法与其基础 UIKit
类型相对应。
PageControl.swift
import SwiftUIimport UIKitstruct 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 将 text box 换成 page control,把布局从 VStack
换成 ZStack
。
由于我们正在将页面计数和 binding
传递给当前页面,所以 page control 已显示正确的值。
PageView.swift
import SwiftUIstruct 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) }) }}
接下来让 page control 可以交互,以便客户可以点击一侧或者另一侧在页面之间移动。
4.3 在 PageControl
中创立嵌套的 Coordinator
类型,而后增加一个 Coordinator()
方法来创立并返回一个新的 coordinator
。
因为 UIPageControl
这样的 UIControl
子类使用 arget-action
模式而不是代理商,所以此 Coordinator
实现了 @objc
方法来升级当前页面的 binding
。
PageControl.swift
import SwiftUIimport UIKitstruct 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 SwiftUIimport UIKitstruct 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
view 和 controllers 是如何协同工作的。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » [译] SwiftUI 官方教程 (九)(完结)