Skip to content

Interfacing with UIKit

Willie edited this page Feb 10, 2020 · 6 revisions

与 UIKit 协作

SwiftUI 可与所有 Apple 平台上的现有 UI 框架无缝协作。例如我们可以在 SwiftUI 视图中放置 UIKit 视图和视图控制器,反之亦然。

本文将展示如何把地标从主屏幕中转换到包装 UIPageViewControllerUIPageControl 的实例中去。我们将使用 UIPageViewController 显示 SwiftUI 视图的轮播,并使用状态变量和绑定来协调整个 UI 中的数据更新。

  • 预计完成时间:25 分钟
  • 项目文件:下载

1. 创建表示 UIPageViewController 的视图

要在 SwiftUI 中表示 UIKit 视图和视图控制器,我们需要创建遵循 UIViewRepresentableUIViewControllerRepresentable 协议的类型。我们的自定义类型创建和配置它们所代表的 UIKit 类型,而 SwiftUI 管理它们的生命周期并在需要时更新它们。

1.1 创建一个新的 SwiftUI 视图,命名为 PageViewController.swift ,声明遵循 UIViewControllerRepresentable 协议的 PageViewController 类型。

页面的视图控制器存储了 UIViewController 实例的数组。这些是在地标之间滚动的页面。

PageViewController.swift

import SwiftUI
//
import UIKit

struct PageViewController: UIViewControllerRepresentable {
    var controllers: [UIViewController]
}
//

接下添加 UIViewControllerRepresentable 协议的两个需求。

1.2 添加一个 makeUIViewController(context:) 方法,创建一个满足需求的 UIPageViewController

SwiftUI 准备好显示视图时,它会调用此方法一次,然后管理视图控制器的生命周期。

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:) 来显示数组中的第一个视图控制器。

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 视图来显示我们的 UIViewControllerRepresentable 视图。

1.4 创建一个新的 SwiftUI 视图,命名为 PageView.swift,声明一个 PageViewController 作为子视图。

需要注意的是,泛型初始化方法接收一个视图数组,并将每个视图嵌套在 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 ,传入必要的视图数组,之后预览就会开始工作。

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 在进行下一步之前,在画布中固定 PageView 的预览,所有的操作都将发生在这个视图上。

2. 创建视图控制器的数据源

在几个简短的步骤中,我们已经做了很多工作:PageViewController 使用 UIPageViewControllerSwiftUI 视图中显示内容。现在启用滑动交互来从一个页面移动到另一个页面。

一个表示 UIKit视图控制器的 SwiftUI 视图可以定义 SwiftUI 管理的 Coordinator 类型,并将其作为表示视图上下文的一部分提供。

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() 方法,这样配置视图控制器时,我们可以访问 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 协议,并且实现两个必要方法。

这两个方法建立了视图控制器之间的关系,因此我们可以在它们之间来回滑动。

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 视图的状态中跟踪页面

要添加自定义的 UIPageControl ,我们需要一种从 PageView 中跟踪当前页面的方法。

为此,我们将在 PageView 中声明一个 @State 属性,并传递一个绑定给此属性,直到 PageViewController 视图。 PageViewController 更新绑定来匹配可见页面。

3.1 给 PageViewController 添加一个 currentPage 的绑定的属性。

除了声明 @Binding 属性外,还要更新对 setViewControllers(_:direction:animated:) 的调用,并传递 currentPage 的绑定的值。

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 时将绑定传递给属性。

请记住使用 $ 语法创建用状态来存储值的绑定。

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 的初始值,测试值是否通过绑定传递给了 PageViewController

PageView 添加一个按钮,让视图控制器跳转到第二个视图。

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 属性的文字视图,以便我们关注 @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 就会调用此方法,所以我们可以找到当前视图控制器的索引并更新绑定。

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 的代理。

在两个方向上连接绑定后,文字视图会在每次滑动后更新以显示正确的页码。

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. 添加自定义的页面控件

现在我们已经准备好给视图添加自定义的包装在 SwiftUI UIViewRepresentable 中的 UIPageControl 了。

4.1 创建一个新的 SwiftUI 视图文件,命名为 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 将文字框换成页面控件,把布局从 VStack 换成 ZStack

因为我们正在将页面计数和绑定传递给当前页面,所以页面控件已显示正确的值。

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

接下来让页面控件可以交互,以便用户可以点击一侧或另一侧在页面之间移动。

4.3 在 PageControl 中创建嵌套的 Coordinator 类型,然后添加一个 Coordinator() 方法来创建并返回一个新的 coordinator

由于 UIPageControl 这样的 UIControl 子类使用 arget-action 模式而不是代理,所以此 Coordinator 实现了 @objc 方法来更新当前页面的绑定。

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 视图和控制器是如何协同工作的。