Skip to content

4.1 SwiftUI Framework Integration Interfacing with UIKit

要在 SwiftUI 中表示 UIKit 视图和视图控制器,您需要创建符合 UIViewRepresentable 和 UIViewControllerRepresentable 协议的类型。

如何交互

1). 需要遵循UIViewControllerRepresentable协议,示例中与UIPageViewController进行举例。

swift
import Foundation
import SwiftUI
import UIKit

// 需要遵循协议 UIViewControllerRepresentable
struct PageViewController<Page: View>: UIViewControllerRepresentable {
    // 页面视图控制器存储了一个 Page 实例数组,该数组是 View 类型。这些页面用于在地标之间滚动。
    var pages: [Page]


    // UIViewControllerRepresentable 协议

    ///  SwiftUI在准备好显示视图时调用该方法一次,然后管理视图控制器的生命周期。
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)

        return pageViewController
    }

    /// 目前,您创建的 UIHostingController 将在每次更新时托管 page SwiftUI 视图。稍后,您将通过在页面视图控制器的生命周期中仅初始化控制器一次来提高效率。
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [UIHostingController(rootView: pages[0])], direction: .forward, animated: true)
    }
}
import Foundation
import SwiftUI
import UIKit

// 需要遵循协议 UIViewControllerRepresentable
struct PageViewController<Page: View>: UIViewControllerRepresentable {
    // 页面视图控制器存储了一个 Page 实例数组,该数组是 View 类型。这些页面用于在地标之间滚动。
    var pages: [Page]


    // UIViewControllerRepresentable 协议

    ///  SwiftUI在准备好显示视图时调用该方法一次,然后管理视图控制器的生命周期。
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)

        return pageViewController
    }

    /// 目前,您创建的 UIHostingController 将在每次更新时托管 page SwiftUI 视图。稍后,您将通过在页面视图控制器的生命周期中仅初始化控制器一次来提高效率。
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [UIHostingController(rootView: pages[0])], direction: .forward, animated: true)
    }
}

2). 导入图片资源
3). 修改数据模型,增加计算属性 featureImage 用于展示feature图片。

swift
struct Landmark: Hashable, Codable, Identifiable {
	...
    var featureImage: Image? {
        isFeatured ? Image(imageName + "_feature") : nil
    }
    ...
}
struct Landmark: Hashable, Codable, Identifiable {
	...
    var featureImage: Image? {
        isFeatured ? Image(imageName + "_feature") : nil
    }
    ...
}

4). 编辑 FeatureCard页面: 创建图片并拥有渐变的背景

swift
import SwiftUI

struct FeatureCard: View {
    var landmark: Landmark

    var body: some View {
        landmark.featureImage?
            .resizable()
            .aspectRatio(3 / 2, contentMode: .fit)
            .overlay {
                            TextOverlay(landmark: landmark)
                        }
    }
}

struct TextOverlay: View {
    var landmark: Landmark

    var gradient: LinearGradient {
        .linearGradient(
            Gradient(colors: [.black, .black.opacity(0)]),
            startPoint: .bottom,
            endPoint: .center)
    }

    var body: some View {
        // bottomLeading 左下对齐
        ZStack(alignment: .bottomLeading) {
            gradient
            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                    .bold()
                Text(landmark.park)
            }
            .padding()
        }
        .foregroundColor(.white)
    }
}

struct FeatureCard_Previews: PreviewProvider {
    static var previews: some View {
        FeatureCard(landmark: ModelData().features[0])
    }
}
import SwiftUI

struct FeatureCard: View {
    var landmark: Landmark

    var body: some View {
        landmark.featureImage?
            .resizable()
            .aspectRatio(3 / 2, contentMode: .fit)
            .overlay {
                            TextOverlay(landmark: landmark)
                        }
    }
}

struct TextOverlay: View {
    var landmark: Landmark

    var gradient: LinearGradient {
        .linearGradient(
            Gradient(colors: [.black, .black.opacity(0)]),
            startPoint: .bottom,
            endPoint: .center)
    }

    var body: some View {
        // bottomLeading 左下对齐
        ZStack(alignment: .bottomLeading) {
            gradient
            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                    .bold()
                Text(landmark.park)
            }
            .padding()
        }
        .foregroundColor(.white)
    }
}

struct FeatureCard_Previews: PreviewProvider {
    static var previews: some View {
        FeatureCard(landmark: ModelData().features[0])
    }
}

|300

  1. 创建 PageView页面用于调用uikit
swift
import SwiftUI

struct PageView<Page: View>: View {
    var pages: [Page]


    var body: some View {
        PageViewController(pages: pages)
    }
}


struct PageView_Previews: PreviewProvider {
    static var previews: some View {
        PageView(pages: ModelData().features.map {
            // 这里使用了 FeatureCard
            FeatureCard(landmark: $0) })
                    .aspectRatio(3 / 2, contentMode: .fit)
    }
}
import SwiftUI

struct PageView<Page: View>: View {
    var pages: [Page]


    var body: some View {
        PageViewController(pages: pages)
    }
}


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

使用 Coordinator

swift
// 需要遵循协议 UIViewControllerRepresentable
struct PageViewController<Page: View>: UIViewControllerRepresentable {
    // 页面视图控制器存储了一个 Page 实例数组,该数组是 View 类型。这些页面用于在地标之间滚动。
    var pages: [Page]

    // 在 PageViewController 中添加另一种方法,使其成为协调器。
    // SwiftUI 在 makeUIViewController(context:) 之前调用此 makeCoordinator() 方法,以便您在配置视图控制器时访问协调器对象。
    // 您可以使用该协调器来实现常见的Cocoa模式,如委托、数据源以及通过target-action响应用户事件。
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource {
        var parent: PageViewController
        var controllers = [UIViewController]()
        init(_ pageViewController: PageViewController) {
            parent = pageViewController
            controllers = parent.pages.map { UIHostingController(rootView: $0) }
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index == 0 {
                return controllers.last
            }
            return controllers[index - 1]
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index + 1 == controllers.count {
                return controllers.first
            }
            return controllers[index + 1]
        }
    }

    // UIViewControllerRepresentable 协议

    ///  SwiftUI在准备好显示视图时调用该方法一次,然后管理视图控制器的生命周期。
    /// - Parameter context:
    /// - Returns:
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        return pageViewController
    }

    /// 目前,您创建的 UIHostingController 将在每次更新时托管 page SwiftUI 视图。稍后,您将通过在页面视图控制器的生命周期中仅初始化控制器一次来提高效率。
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
//        pageViewController.setViewControllers(
//            [UIHostingController(rootView: pages[0])], direction: .forward, animated: true)

        pageViewController.setViewControllers(
            [context.coordinator.controllers[0]], direction: .forward, animated: true)
    }
}
// 需要遵循协议 UIViewControllerRepresentable
struct PageViewController<Page: View>: UIViewControllerRepresentable {
    // 页面视图控制器存储了一个 Page 实例数组,该数组是 View 类型。这些页面用于在地标之间滚动。
    var pages: [Page]

    // 在 PageViewController 中添加另一种方法,使其成为协调器。
    // SwiftUI 在 makeUIViewController(context:) 之前调用此 makeCoordinator() 方法,以便您在配置视图控制器时访问协调器对象。
    // 您可以使用该协调器来实现常见的Cocoa模式,如委托、数据源以及通过target-action响应用户事件。
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource {
        var parent: PageViewController
        var controllers = [UIViewController]()
        init(_ pageViewController: PageViewController) {
            parent = pageViewController
            controllers = parent.pages.map { UIHostingController(rootView: $0) }
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index == 0 {
                return controllers.last
            }
            return controllers[index - 1]
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index + 1 == controllers.count {
                return controllers.first
            }
            return controllers[index + 1]
        }
    }

    // UIViewControllerRepresentable 协议

    ///  SwiftUI在准备好显示视图时调用该方法一次,然后管理视图控制器的生命周期。
    /// - Parameter context:
    /// - Returns:
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        return pageViewController
    }

    /// 目前,您创建的 UIHostingController 将在每次更新时托管 page SwiftUI 视图。稍后,您将通过在页面视图控制器的生命周期中仅初始化控制器一次来提高效率。
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
//        pageViewController.setViewControllers(
//            [UIHostingController(rootView: pages[0])], direction: .forward, animated: true)

        pageViewController.setViewControllers(
            [context.coordinator.controllers[0]], direction: .forward, animated: true)
    }
}

完善:绑定当前页码

1). pageView页面调整

swift
import Foundation
import SwiftUI
import UIKit

// 需要遵循协议 UIViewControllerRepresentable
struct PageViewController<Page: View>: UIViewControllerRepresentable {
    // 页面视图控制器存储了一个 Page 实例数组,该数组是 View 类型。这些页面用于在地标之间滚动。
    var pages: [Page]
    @Binding var currentPage: Int

    // 在 PageViewController 中添加另一种方法,使其成为协调器。
    // SwiftUI 在 makeUIViewController(context:) 之前调用此 makeCoordinator() 方法,以便您在配置视图控制器时访问协调器对象。
    // 您可以使用该协调器来实现常见的Cocoa模式,如委托、数据源以及通过target-action响应用户事件。
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PageViewController
        var controllers = [UIViewController]()
        init(_ pageViewController: PageViewController) {
            parent = pageViewController
            controllers = parent.pages.map { UIHostingController(rootView: $0) }
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index == 0 {
                return controllers.last
            }
            return controllers[index - 1]
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index + 1 == controllers.count {
                return controllers.first
            }
            return controllers[index + 1]
        }

        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            if completed, let visibleViewController = pageViewController.viewControllers?.first,
               let index = controllers.firstIndex(of: visibleViewController)
            {
                parent.currentPage = index
            }
        }
    }

    // UIViewControllerRepresentable 协议
    ///  SwiftUI在准备好显示视图时调用该方法一次,然后管理视图控制器的生命周期。
    /// - Parameter context:
    /// - Returns:
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator
        return pageViewController
    }

    /// 目前,您创建的 UIHostingController 将在每次更新时托管 page SwiftUI 视图。稍后,您将通过在页面视图控制器的生命周期中仅初始化控制器一次来提高效率。
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
//        pageViewController.setViewControllers(
//            [UIHostingController(rootView: pages[0])], direction: .forward, animated: true)
        pageViewController.setViewControllers(
            [context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
    }
}
import Foundation
import SwiftUI
import UIKit

// 需要遵循协议 UIViewControllerRepresentable
struct PageViewController<Page: View>: UIViewControllerRepresentable {
    // 页面视图控制器存储了一个 Page 实例数组,该数组是 View 类型。这些页面用于在地标之间滚动。
    var pages: [Page]
    @Binding var currentPage: Int

    // 在 PageViewController 中添加另一种方法,使其成为协调器。
    // SwiftUI 在 makeUIViewController(context:) 之前调用此 makeCoordinator() 方法,以便您在配置视图控制器时访问协调器对象。
    // 您可以使用该协调器来实现常见的Cocoa模式,如委托、数据源以及通过target-action响应用户事件。
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PageViewController
        var controllers = [UIViewController]()
        init(_ pageViewController: PageViewController) {
            parent = pageViewController
            controllers = parent.pages.map { UIHostingController(rootView: $0) }
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index == 0 {
                return controllers.last
            }
            return controllers[index - 1]
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index + 1 == controllers.count {
                return controllers.first
            }
            return controllers[index + 1]
        }

        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            if completed, let visibleViewController = pageViewController.viewControllers?.first,
               let index = controllers.firstIndex(of: visibleViewController)
            {
                parent.currentPage = index
            }
        }
    }

    // UIViewControllerRepresentable 协议
    ///  SwiftUI在准备好显示视图时调用该方法一次,然后管理视图控制器的生命周期。
    /// - Parameter context:
    /// - Returns:
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator
        return pageViewController
    }

    /// 目前,您创建的 UIHostingController 将在每次更新时托管 page SwiftUI 视图。稍后,您将通过在页面视图控制器的生命周期中仅初始化控制器一次来提高效率。
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
//        pageViewController.setViewControllers(
//            [UIHostingController(rootView: pages[0])], direction: .forward, animated: true)
        pageViewController.setViewControllers(
            [context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
    }
}

调整 PageView 页面,将页码绑定

swift
import SwiftUI

struct PageView<Page: View>: View {
    var pages: [Page]
    @State private var currentPage = 0

    var body: some View {
        VStack {
            PageViewController(pages: pages, currentPage: $currentPage)
            Text("Current Page: \(currentPage)")
        }
    }
}

struct PageView_Previews: PreviewProvider {
    static var previews: some View {
        PageView(pages: ModelData().features.map {
            // 这里使用了 FeatureCard
            FeatureCard(landmark: $0)
        })
        .aspectRatio(3 / 2, contentMode: .fit)
    }
}
import SwiftUI

struct PageView<Page: View>: View {
    var pages: [Page]
    @State private var currentPage = 0

    var body: some View {
        VStack {
            PageViewController(pages: pages, currentPage: $currentPage)
            Text("Current Page: \(currentPage)")
        }
    }
}

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

自定义页面组件,使用 UIPageControl

swift
import SwiftUI

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

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

组合pageControl到PageView页面:

swift
import SwiftUI

struct PageView<Page: View>: View {
    var pages: [Page]
    @State private var currentPage = 0

    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            PageViewController(pages: pages, currentPage: $currentPage)
            PageControl(numberOfPages: pages.count, currentPage: $currentPage)
                            .frame(width: CGFloat(pages.count * 18))
                            .padding(.trailing)
        }
    }
}

struct PageView_Previews: PreviewProvider {
    static var previews: some View {
        PageView(pages: ModelData().features.map {
            // 这里使用了 FeatureCard
            FeatureCard(landmark: $0)
        })
        .aspectRatio(3 / 2, contentMode: .fit)
    }
}
import SwiftUI

struct PageView<Page: View>: View {
    var pages: [Page]
    @State private var currentPage = 0

    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            PageViewController(pages: pages, currentPage: $currentPage)
            PageControl(numberOfPages: pages.count, currentPage: $currentPage)
                            .frame(width: CGFloat(pages.count * 18))
                            .padding(.trailing)
        }
    }
}

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

将pageView页面集成到首页:

swift
struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData
    @State private var showingProfile = false

    var body: some View {
        NavigationView {
            List(content: {
//                modelData.features[0].image
//                    .resizable()
//                    .scaledToFill()
//                    .frame(height: 200)
//                    .clipped()
                PageView(pages: modelData.features.map { FeatureCard(landmark: $0) })
                    .aspectRatio(3 / 2, contentMode: .fit)
                    .listRowInsets(EdgeInsets())

                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    CategoryRow(categoryName: key, items: modelData.categories[key]!)
                }
            })
            // 调整样式
            .listStyle(.inset)
            .navigationTitle("Featured")
            // 增加顶部按钮 (类似navgationItem)
            .toolbar {
                Button {
                    showingProfile.toggle()
                } label: {
                    Label("User Profile", systemImage: "person.crop.circle")
                }
            }
            // 弹出模态视图
            .sheet(isPresented: $showingProfile) {
                ProfileHost()
                    .environmentObject(modelData)
            }
        }
    }
}
struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData
    @State private var showingProfile = false

    var body: some View {
        NavigationView {
            List(content: {
//                modelData.features[0].image
//                    .resizable()
//                    .scaledToFill()
//                    .frame(height: 200)
//                    .clipped()
                PageView(pages: modelData.features.map { FeatureCard(landmark: $0) })
                    .aspectRatio(3 / 2, contentMode: .fit)
                    .listRowInsets(EdgeInsets())

                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    CategoryRow(categoryName: key, items: modelData.categories[key]!)
                }
            })
            // 调整样式
            .listStyle(.inset)
            .navigationTitle("Featured")
            // 增加顶部按钮 (类似navgationItem)
            .toolbar {
                Button {
                    showingProfile.toggle()
                } label: {
                    Label("User Profile", systemImage: "person.crop.circle")
                }
            }
            // 弹出模态视图
            .sheet(isPresented: $showingProfile) {
                ProfileHost()
                    .environmentObject(modelData)
            }
        }
    }
}

显示效果如图: