[譯] SwiftUI 官方教程 (九)

因爲 API 變更,此文章部份內容已失效,最新完整中文教程及代碼請查看 github.com/WillieWangW…git

微信技術羣

SwiftUI 表明將來構建 App 的方向,歡迎加羣一塊兒交流技術,解決問題。github

UIKit 接口

SwiftUI 可與全部 Apple 平臺上的現有 UI 框架無縫協做。例如咱們能夠在 SwiftUI view 中放置 UIKit view 和 view controllers,反之亦然。canvas

本文將展現如何把地標從 home screen 中轉換到包裝 UIPageViewControllerUIPageControl 的實例。咱們將使用 UIPageViewController 顯示 SwiftUI view 的輪播,並使用狀態變量和綁定來協調整個 UI 中的數據更新。swift

  • 預計完成時間:25 分鐘
  • 項目文件:下載

1. 建立表示 UIPageViewController 的 View

要在 SwiftUI 中表示 UIKit view 和 view controllers,咱們須要建立遵循 UIViewRepresentableUIViewControllerRepresentable 協議的類型。咱們的自定義類型建立和配置它們所表明的 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:) 方法,建立一個知足需求的 UIPageViewControlleride

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 上。

2. 建立 View Controller 的數據源

在幾個簡短的步驟中,咱們已經作了不少工做:PageViewController 使用 UIPageViewControllerSwiftUI 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 打開實時預覽並測試滑動交互。

3. 在 SwiftUI View 的狀態中跟蹤頁面

要添加自定義的 UIPageControl ,咱們須要一種從 PageView 中跟蹤當前頁面的方法。

爲此,咱們將在 PageView 中聲明一個 @State 屬性,並傳遞一個 binding 給此屬性,直到 PageViewController view。 PageViewController 更新 binding 來匹配可見頁面。

3.1 給 PageViewController 添加一個 currentPagebinding 的屬性。

除了聲明 @Binding 屬性外,還要更新對 setViewControllers(_:direction:animated:) 的調用,並傳遞 currentPagebinding 的值。

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
            }
        }
    }
}
複製代碼

4. 添加自定義的 Page Control

如今咱們已經準備好給 view 添加自定義的包裝在 SwiftUI UIViewRepresentable 中的 UIPageControl 了。

4.1 建立一個新的 SwiftUI view 文件,命名爲 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 將 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 展現了 UIKitSwiftUI view 和 controllers 是如何協同工做的。

相關文章
相關標籤/搜索