因爲 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 是如何協同工做的。