因为 API 变更,此文章部份内容已失效,最新完整中文教程及代码请查看 github.com/WillieWangW…git
SwiftUI
表明将来构建 App 的方向,欢迎加群一块儿交流技术,解决问题。github
SwiftUI
可与全部Apple
平台上的现有 UI 框架无缝协做。例如咱们能够在SwiftUI
view 中放置UIKit
view 和 view controllers,反之亦然。canvas本文将展现如何把地标从
home screen
中转换到包装UIPageViewController
和UIPageControl
的实例。咱们将使用UIPageViewController
显示SwiftUI
view 的轮播,并使用状态变量和绑定来协调整个 UI 中的数据更新。swift
- 预计完成时间:25 分钟
- 项目文件:下载
要在 SwiftUI
中表示 UIKit
view 和 view controllers,咱们须要建立遵循 UIViewRepresentable
和 UIViewControllerRepresentable
协议的类型。咱们的自定义类型建立和配置它们所表明的 UIKit
类型,而 SwiftUI
管理它们的生命周期并在须要时更新它们。数组
1.1 建立一个新的 SwiftUI
view,命名为 PageViewController.swift
,声明遵循 UIViewControllerRepresentable
协议的 PageViewController
类型。bash
页面的 view controller 存储了 UIViewController
实例的数组。这些是在地标之间滚动的页面。微信
PageViewController.swiftapp
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
}
复制代码
接下添加 UIViewControllerRepresentable
协议的两个需求。框架
1.2 添加一个 makeUIViewController(context:)
方法,建立一个知足需求的 UIPageViewController
。ide
当 SwiftUI
准备好显示 view 时,它会调用此方法一次,而后管理 view controller 的生命周期。
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:)
来显示数组中的第一个 view controller。
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
view 来显示咱们的 UIViewControllerRepresentable
view。
1.4 建立一个新的 SwiftUI
view,命名为 PageView.swift
,声明一个 PageViewController
做为子 view。
须要注意的是,泛型初始化方法接收一个 view 数组,并将每一个 view 嵌套在 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
,传入必要的 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(features.map { FeatureCard(landmark: $0) })
.aspectRatio(3/2, contentMode: .fit)
}
}
复制代码
1.6 在进行下一步以前,在 canvas
中固定 PageView
的预览,全部的操做都将发生在这个 view 上。
在几个简短的步骤中,咱们已经作了不少工做:PageViewController
使用 UIPageViewController
从 SwiftUI
view 中显示内容。如今启用滑动交互来从一个页面移动到另外一个页面。
一个表示 UIKit
view controller 的 SwiftUI
view 能够定义 SwiftUI
管理的 Coordinator
类型,并将其做为表示 view 上下文的一部分提供。
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()
方法,这样配置 view controller 时,咱们能够访问 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
协议,而且实现两个必要方法。
这两个方法创建了 view controllers 之间的关系,所以咱们能够在它们之间来回滑动。
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
属性,并传递一个 binding
给此属性,直到 PageViewController
view。 PageViewController
更新 binding
来匹配可见页面。
3.1 给 PageViewController
添加一个 currentPage
的 binding
的属性。
除了声明 @Binding
属性外,还要更新对 setViewControllers(_:direction:animated:)
的调用,并传递 currentPage
的 binding
的值。
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
时将 binding
传递给属性。
请记住使用 $
语法建立用状态来存储值的 binding
。
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
的初始值,测试值是否经过 binding
传递给了 PageViewController
。
给 PageView
添加一个按钮,让页面 view controller 跳转到第二个 view。
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
属性的 text view,以便咱们关注 @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
就会调用此方法,因此咱们能够找到当前 view controller 的索引并更新 binding
。
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
的代理。
在两个方向上链接 binding
后,text view 会在每次滑动后更新以显示正确的页码。
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
}
}
}
}
复制代码
如今咱们已经准备好给 view 添加自定义的包装在 SwiftUI UIViewRepresentable
中的 UIPageControl
了。
4.1 建立一个新的 SwiftUI
view 文件,命名为 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 将 text box 换成 page control,把布局从 VStack
换成 ZStack
。
由于咱们正在将页面计数和 binding
传递给当前页面,因此 page control 已显示正确的值。
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) })
}
}
复制代码
接下来让 page control 能够交互,以便用户能够点击一侧或另外一侧在页面之间移动。
4.3 在 PageControl
中建立嵌套的 Coordinator
类型,而后添加一个 Coordinator()
方法来建立并返回一个新的 coordinator
。
因为 UIPageControl
这样的 UIControl
子类使用 arget-action
模式而不是代理,因此此 Coordinator
实现了 @objc
方法来更新当前页面的 binding
。
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
view 和 controllers 是如何协同工做的。