天天看點

界面上下固定_SwiftUI入門八:使用UIKit建構使用者界面01 建立視圖來展現UIPageViewController02 建立視圖控制器的資料源03 在SwiftUI視圖的狀态中追蹤頁面04 添加自定義的頁面控件

SwiftUI可以無縫地與現有的UI架構在所有蘋果平台運作。例如,我們可以将UIKit視圖和視圖控制器嵌套進SwiftUI的視圖,或者是反過來。

我們在這個教程學習如何将首頁面中的特别地标部分嵌套在UIPageViewController和UIPageControl中。我們用UIPageViewController顯示SwiftUI跑馬燈視圖,并使用狀态變量和資料綁定來協調所有界面之間的資料更新。

啟動項目并跟随本教程,或打開完成的項目自己研究代碼。

01 建立視圖來展現UIPageViewController

要在SwiftUI中展現UIKit視圖和視圖控制器,我們需要建立遵循UIViewRepresentable和UIViewControllerRepresentable協定的視圖。在這些自定義的類型中建立并配置用于展示的UIKit視圖,SwiftUI會管理它們的生命周期并在需要的時候更新它們。

界面上下固定_SwiftUI入門八:使用UIKit建構使用者界面01 建立視圖來展現UIPageViewController02 建立視圖控制器的資料源03 在SwiftUI視圖的狀态中追蹤頁面04 添加自定義的頁面控件

第一步

建立一個新的SwiftUI視圖,命名為PageViewController.swift,并聲明遵循UIViewControllerRepresentable協定。

這個頁面視圖控制器會存儲一個UIViewController執行個體數組,這些是用來展示滑動的地标頁面的。

import SwiftUIimport UIKitstruct PageViewController: UIViewControllerRepresentable {    var controllers: [UIViewController]}
           

接下來,添加兩個UIViewControllerRepresentable協定的要求。

第二步

添加makeUIViewController(context:)方法并在其中以需要的配置建立UIPageViewController。

SwiftUI會在準備好顯示視圖的時候調用這個方法一次,然後管理這個視圖控制器的生命周期。

struct PageViewController: UIViewControllerRepresentable {    var controllers: [UIViewController]    func makeUIViewController(context: Context) -> UIPageViewController {        let pageViewController = UIPageViewController(            transitionStyle: .scroll,            navigationOrientation: .horizontal)        return pageViewController    }}
           

第三步

添加updateUIViewController(_:context:)方法并在其中調用setViewControllers(_:direction:animated:)來顯示數組中的第一個視圖控制器。

return pageViewController    }    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {        pageViewController.setViewControllers(            [controllers[0]], direction: .forward, animated: true)    }}
           

建立另外一個SwiftUI視圖來展現我們的UIViewControllerRepresentable視圖。

第四步

建立一個新的SwiftUI視圖檔案,命名為PageView.swift,并更新這個頁面視圖,聲明PageViewController作為它的子視圖。

注意通用的初始化器需要一個視圖數組,并将每一個都嵌套在UIHostingController中。UIHostingController是一個UIViewController的子類用于在UIKit上下文中展現SwiftUI視圖。

import SwiftUIstruct PageView: View {    var viewControllers: [UIHostingController]    init(_ views: [Page]) {        self.viewControllers = views.map { UIHostingController(rootView: $0) }    }    var body: some View {        PageViewController(controllers: viewControllers)
           

第五步

更新PageView的預覽提供器傳入需要的視圖數組,預覽就可以運作了。

struct PageView_Previews: PreviewProvider {    static var previews: some View {        PageView(features.map { FeatureCard(landmark: $0) })            .aspectRatio(3/2, contentMode: .fit)    }}
           

第六步

在我們繼續之前将PageView的預覽固定在畫布中-我們之後的動作都會在這個視圖中展現。

界面上下固定_SwiftUI入門八:使用UIKit建構使用者界面01 建立視圖來展現UIPageViewController02 建立視圖控制器的資料源03 在SwiftUI視圖的狀态中追蹤頁面04 添加自定義的頁面控件

02 建立視圖控制器的資料源

在短短幾步中,我們已經完成了很多-使用UIPageViewController從SwiftUI視圖中展示内容的PageViewController。現在是時候啟用滑動互動從一個頁面切換到另外一個頁面了。

界面上下固定_SwiftUI入門八:使用UIKit建構使用者界面01 建立視圖來展現UIPageViewController02 建立視圖控制器的資料源03 在SwiftUI視圖的狀态中追蹤頁面04 添加自定義的頁面控件

一個展現UIKit視圖控制器的SwiftUI視圖可以定義一個由SwiftUI管理的Coordinator類型作為可展現的視圖上下文的一部分。

第一步

在PageViewController中聲明一個嵌套的Coordinator類。

SwiftUI會管理UIViewControllerRepresentable類型中的coordinator,并在調用上面建立的那些方法時将其作為上下文的一部分。

[controllers[0]], direction: .forward, animated: true)    }    class Coordinator: NSObject {        var parent: PageViewController        init(_ pageViewController: PageViewController) {            self.parent = pageViewController        }    }}
           

第二步

在PageViewController中添加另外一個方法makeCoordinator()。

SwiftUI會在調用makeUIViewController(context:)方法前先調用makeCoordinator()方法,是以在我們配置視圖控制器的時候可以通路coordinator對象了。

提示 我們可以使用這個coordinator對象來實作常見的Cocoa模式,比如代理、資料源以及通過target-action方式來響應使用者事件。
var controllers: [UIViewController]    func makeCoordinator() -> Coordinator {        Coordinator(self)    }    func makeUIViewController(context: Context) -> UIPageViewController {        let pageViewController = UIPageViewController(
           

第三步

在Coordinator類上添加協定UIPageViewControllerDataSource,并實作兩個必要的方法。

這兩個方法會在視圖控制器之間建立聯系,是以我們可以在它們之間前後滑動。

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]        }
           

第四步

将coordinator作為UIPageViewController的資料源。

transitionStyle: .scroll,            navigationOrientation: .horizontal)        pageViewController.dataSource = context.coordinator        return pageViewController
           

第五步

打開實時預覽模式并測試滑動互動操作。

界面上下固定_SwiftUI入門八:使用UIKit建構使用者界面01 建立視圖來展現UIPageViewController02 建立視圖控制器的資料源03 在SwiftUI視圖的狀态中追蹤頁面04 添加自定義的頁面控件

03 在SwiftUI視圖的狀态中追蹤頁面

為了添加自定義的UIPageControl,我們需要一個辦法來追蹤PageView中的目前頁面。

要實作這個功能,我們需要在PageView中聲明一個@State屬性,并将這個屬性的資料綁定傳遞到PageViewController視圖中。PageViewController會更新綁定資料以比對目前可見頁面。

界面上下固定_SwiftUI入門八:使用UIKit建構使用者界面01 建立視圖來展現UIPageViewController02 建立視圖控制器的資料源03 在SwiftUI視圖的狀态中追蹤頁面04 添加自定義的頁面控件

第一步

首先在PageViewController中添加一個currentPage的資料綁定作為屬性。

另外,在聲明了@Binding屬性後,我們還需要更新調用的setViewControllers(:_direction:animated:)方法,傳入currentPage綁定的值。

struct PageViewController: UIViewControllerRepresentable {    var controllers: [UIViewController]    @Binding var currentPage: Int    func makeCoordinator() -> Coordinator {// 省略部分代碼    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {        pageViewController.setViewControllers(            [controllers[currentPage]], direction: .forward, animated: true)    }
           

第二步

在PageView中聲明@State變量,并在建立子視圖PageViewController時傳入這個資料的綁定。

重要

記住使用$符号建立對于作為狀态存儲的資料的綁定。

struct PageView: View {    var viewControllers: [UIHostingController]    @State var currentPage = 0    init(_ views: [Page]) {        self.viewControllers = views.map { UIHostingController(rootView: $0) }    }    var body: some View {        PageViewController(controllers: viewControllers, currentPage: $currentPage)    }}
           

第三步

通過改變初始值測試通過綁定傳入PageViewController的值。

體驗

可以通過在PageView中添加一個按鈕讓PageViewController跳轉到第二個視圖。

第四步

使用currentPage屬性建立一個文本視圖,這樣我們可以看到@State屬性的值。

可以觀察到當我們從一個頁面滑動到另外一個時,這個值沒有變化。

struct PageView: View {    var viewControllers: [UIHostingController]    @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)")        }    }}
           

第五步

在PageViewController.swift檔案中,讓Coordinator類遵循UIPageViewControllerDelegate協定,并添加pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool)方法。

由于SwiftUI會在頁面切換動畫結束後調用這個方法,我們可以在這個方法中查詢目前視圖控制器的索引并更新綁定資料。

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            }        }    }
           

第六步

在作為資料源之後,再将coordinator作為UIPageViewController的代理。

在将兩方的綁定連接配接之後,文本視圖會在每次滑動後顯示正确的頁數。

navigationOrientation: .horizontal)        pageViewController.dataSource = context.coordinator        pageViewController.delegate = context.coordinator        return pageViewController
           

04 添加自定義的頁面控件

現在我們已經準備好在視圖中添加一個自定義的UIPageControl了,這個控件會嵌套在SwiftUI的UIViewRepresentable視圖中。

界面上下固定_SwiftUI入門八:使用UIKit建構使用者界面01 建立視圖來展現UIPageViewController02 建立視圖控制器的資料源03 在SwiftUI視圖的狀态中追蹤頁面04 添加自定義的頁面控件

第一步

建立一個新的SwiftUI視圖檔案,命名為PageControl.swift。更新PageControl類讓它遵循UIViewRepresentable協定。

UIViewRepresentable和UIViewControllerRepresentable協定擁有相同的生命周期,方法也和它們在UIKit架構中的類型相對應。

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    }}
           

第二步

将PageView.swift中的文本替換成新建立的頁面控件,并将VStack布局換成ZStack布局。

由于我們傳入了頁面總數和目前頁面索引的資料綁定,頁面控件已經可以顯示正确的值了。

var body: some View {        ZStack(alignment: .bottomTrailing) {            PageViewController(controllers: viewControllers, currentPage: $currentPage)            PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
           

接下來,我們讓頁面控件可以互動,使用者點選的時候可以在頁面間切換。

第三步

在PageControl中建立一個嵌套的Coordinator類,并添加makeCoordinator()方法建立并傳回新的coordinator對象。

由于UIControl的子類如UIPageControl使用的是target-action模式而不是代理模式,這個Coordinator類會實作一個@objc方法來更新目前頁面索引資料的綁定。

@Binding var currentPage: Int    func makeCoordinator() -> Coordinator {        Coordinator(self)    }// 省略部分代碼    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        }    }
           

第四步

将coordinator作為valueChanged事件的目标,指定updateCurrentPage(sender:)方法作為執行的行動。

func makeUIView(context: Context) -> UIPageControl {        let control = UIPageControl()        control.numberOfPages = numberOfPages        control.addTarget(            context.coordinator,            action: #selector(Coordinator.updateCurrentPage(sender:)),            for: .valueChanged)        return control    }
           

第五步

現在我們可以嘗試所有不同的互動方式了-PageView展示了如何将UIKit和SwiftUI視圖和控件組合在一起工作。

界面上下固定_SwiftUI入門八:使用UIKit建構使用者界面01 建立視圖來展現UIPageViewController02 建立視圖控制器的資料源03 在SwiftUI視圖的狀态中追蹤頁面04 添加自定義的頁面控件