SwiftUI八与UIKIT交互

news2024/11/26 22:44:17

代码下载

SwiftUI可以在苹果全平台上无缝兼容现有的UI框架。例如,可以在SwiftUI视图中嵌入UIKit视图或UIKit视图控制器,反过来在UIKit视图或UIKit视图控制器中也可以嵌入SwiftUI视图。

本文展示如何把landmark应用的主页混合使用UIPageViewController和UIPageControl。使用UIPageViewController来展示由SwiftUI视图构成的轮播图,使用状态变量和绑定来操作用户界面数据的更新。

下载起步项目并跟着本篇教程一步步实践,或者查看本篇完成状态时的工程代码去学习,项目文件。

创建一个用来展示UIPageViewController的SwiftUI视图

为了在SwiftUI视图中展示UIKit视图和UIKit视图控制器,需要创建遵循 UIViewRepresentable 和 UIViewControllerRepresentable 协议的类型。创建的自定义视图类型,用来创建和配置所要展示的UIKit类型,SwiftUI框架来管理UIKIt类型的生命周期并在适当的时机更新它们。
请添加图片描述

1、创建一个新的 SwiftUI 视图文件,命名为 PageViewController.swift,并且声明 PageViewController 类型遵循 UIViewControllerRepresentable 协议。这个页面视图控制器存放一个 UIViewController 实例数组,数组中的每一个元素代表在地标滚动过程中的一页视图。

import SwiftUI
import UIKit


struct PageViewController<Page: View>: UIViewControllerRepresentable {
    var pages: [Page]


}

下一步添加UIViewControllerRepresentable协议的两个实现, 目前因为协议方法没有完成实现,会有报错提示。

2、添加一个 makeUIViewController(context:) 方法,方法内部以指定的配置创建一个 UIPageViewController。SwiftUI 会在准备显示视图时调用一次 makeUIViewController(context:) 方法创建 UIViewController 实例,并管理它的生命周期。

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)


        return pageViewController
    }

3、添加 updateUIViewController(_:context:) 方法,这个方法里调用 setViewControllers(_:direction:animated:) 方法展示数组中的第一个视图控制器。

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

现在,创建了 UIHostingController,它在每次更新时托管页面SwiftUI视图。稍后将通过在页面视图控制器的生命周期中只初始化一次控制器来提高效率。

4、将下载的项目文件 Resources 目录中的图片拖到应用程序的 Asset 目录中。地标的特征图像,如果存在的话,其维度与常规图像不同。

5、将计算属性添加到返回特征图像(如果存在)的 Landmark 结构中。

   var featureImage: Image? {
        isFeatured ? Image(imageName + "_feature") : nil
    }

6、添加一个新的 SwiftUI 视图文件,名为 FeatureCard.swift,用于显示地标的特征图像。

import SwiftUI

struct FeatureCard: View {
    var landmark: Landmark

    var body: some View {
        landmark.featureImage?
            .resizable()
    }
}

#Preview {
    FeatureCard(landmark: ModelData().features[0])
        .aspectRatio(3 / 2, contentMode: .fit)
}

包括长宽比修改器,这样它就可以模仿视图的长宽比,而 FeatureCard 稍后将预览该视图最终的样子。

7、在图像上叠加有关地标的文本信息。

struct FeatureCard: View {
    var landmark: Landmark


    var body: some View {
        landmark.featureImage?
            .resizable()
            .overlay {
                TextOverlay(landmark: landmark)
            }
    }
}


struct TextOverlay: View {
    var landmark: Landmark


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


    var body: some View {
        ZStack(alignment: .bottomLeading) {
            gradient
            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                    .bold()
                Text(landmark.park)
            }
            .padding()
        }
        .foregroundStyle(.white)
    }
}

接下来,创建另一个SwiftUI视图展示遵循UIViewControllerRepresentable协议的视图。

8、创建一个名为PageView.swift的视图,声明一个PageViewController作为子视图。初始化时使用一个视图数组来初始化,并把每一个视图都嵌入在一个UIHostingController中。UIHostingController是一个UIViewController的子类,用来在UIKit环境中表示一个SwiftUI视图。

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


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


#Preview {
    PageView()
}

预览失败是因为Xcode无法推断Page的类型。

9、添加宽高比修改器,更新预览视图,并传入视图数组,预览视图就会开始工作了。

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


    var body: some View {
        PageViewController(pages: pages)
            .aspectRatio(3 / 2, contentMode: .fit)
    }
}


#Preview {
    PageView(pages: ModelData().features.map { FeatureCard(landmark: $0) })
}

创建视图控制器的数据源

短短几个步骤就做了很多事,PageViewController 使用 UIPageViewController 去展示来自 SwiftUI 内容。现在是时候添加扫动手势进行页面之间的滚动了。

请添加图片描述

一个展示 UIKit 视图控制器的 SwiftUI 视图可以定义一个 Coordinator 类型,这个 Coordinator 类型由SwitUI管理,用来作为视图展示的上下文。

1、在 PageViewControlelr 中定义一个嵌套类型 Coordiantor。SwiftUI管理 UIViewControllerRepresentable 类型的 coordinator,并在调用方法时把它作为上下文的一部分。

    class Coordinator: NSObject {
        var parent: PageViewController


        init(_ pageViewController: PageViewController) {
            parent = pageViewController
        }
    }

2、在 PageViewController 中添加另一个方法,创建coordinator。SwiftUI在调用 makeUIViewController(context:) 前会先调用 makeCoordinator() 方法,因此在配置视图控制器时是可以访问到coordiantor对象的。

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

可以使用coordinator为实现通用的Cocoa模式,例如:代理模式、数据源以及目标-动作。

3、在 Coordinator 中使用 pages 的视图数组初始化控制器数组。

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


    class Coordinator: NSObject {
        var parent: PageViewController
        var controllers = [UIViewController]()


        init(_ pageViewController: PageViewController) {
            parent = pageViewController
            controllers = parent.pages.map { UIHostingController(rootView: $0) }
        }
    }

Coordinator 是存储这些控制器的好地方,因为系统只初始化它们一次,并且在需要它们更新视图控制器之前。

4、让 Coordinator 类型遵循 UIPageViewControllerDataSource 协议,并且实现两个必要方法。这两个必要方法会建立起视图控制器之间的联系,因此可以实现页面之前的前后切换。

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

5、把coordiantor作为UIPageViewController的数据源。

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator


        return pageViewController
    }

打开实时预览,并测试一下 PageView 前后页面切换的功能是否正常。

在SwiftUI视图的状态下跟踪页面

如果要添加一个自定义的 UIPageControl 控件,就需要一种方式能够在 PageView 中跟踪当前展示的页面。

这就需要在 PageView 中声明一个 @State 属性,并传递一个针对该属性的绑定关系给 PageViewController 视图,在 PageViewController 中通过绑定关系更新状态属性,来反映当前展示的页面。

1、在 PageViewController 中添加一个绑定属性 currentPage。除了使用关键字 @Binding 声明属性为绑定属性外,还需要更新一下函数 setViewControllers(_:direction:animated:),给它传入 currentPage 绑定属性。

    @Binding var currentPage: Int

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

2、在PageView中声明 @State变量,并在创建 PageViewController 时把绑定属性传入。注意使用 $ 语法创建一个针对状态变量的绑定关系。

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


    var body: some View {
        PageViewController(pages: pages, currentPage: $currentPage)
            .aspectRatio(3 / 2, contentMode: .fit)
    }
}

3、通过改变 PageView 视图中的 currentPage 初始值来测试绑定关系是否正常生效。也可以做一个测试按钮,点击按钮时让第二个页面展示出来。

    @State private var currentPage = 1

4、添加一个TextView控件来展示状态变量currentPage的值,拖动页面切换时观察TextView上的值,目前不会发生变化。因为PageViewController内部没有在切换页面的过程中更新currentPage的值。

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)")
        }
        .aspectRatio(3 / 2, contentMode: .fit)
    }
}

5、在 PageViewController.swift 中让 coordinator 作为 UIPageViewController 的代理,并添加p ageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool) 方法。因为 SwiftUI 在页面切换动画完成时会调用这个方法,这样就可以这个方法内部获取当前正在展示的页面的下标,并同时更新绑定属性currentPage的值。

class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        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
            }
        }
        ...

6、coordinator 除了是 UIPageViewController 数据源外,再把它赋值为UIPageViewController的代理。由于绑定关系是双向的,所以当页面切换时,PageView视图上的Text就会实时展示当前的页码。

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator


        return pageViewController
    }

添加一个自定义PageControl

准备为包裹在 UIViewRepresentable 视图中的子视图上添加了一个自定义 UIPageControl。

请添加图片描述

1、创建一个新的 SwiftUI 视图,命名为 PageControl.swift,并使 PageControl 类型遵循 UIViewRepresentable 协议。UIViewRepresentable 和 UIViewControllerRepresentable 类型有相同的生命周期,在 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
    }
}

2、在 PageView 中用 PageControl 替换 Text ,并把 VStack 换成 ZStack 。因为总页数和当前页面都已经传入 PageControl,所以 PageControl 已经可以正确的显示。

    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)
        }
        .aspectRatio(3 / 2, contentMode: .fit)
    }

下一步要处理PageControl与用户的交互,让它可以被用户点击任意一边进行页面间的切换。

3、在 PageControl 中创建一个嵌套类型 Coordiantor,添加一个 makeCoordinator() 方法创建并返回一个 coordinator 实例。因为 UIControl 子类(包括 UIPageControl)使用 Target-Action 模式,Coordinator 实现一个 @objc 方法来更新 currentPage 绑定属性的值。

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    

    class Coordinator: NSObject {
        var control: PageControl


        init(_ control: PageControl) {
            self.control = control
        }


        @objc
        func updateCurrentPage(sender: UIPageControl) {
            control.currentPage = sender.currentPage
        }
    }

4、把 coordinator 作为 PageControl 值改变事件的目标处理器,并指定 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
    }

5、最后,在 CategoryHome 中,用新的页面视图替换占位符特征图像。

struct CategoryHome: View {
    @Environment(ModelData.self) var modelData
    @State private var showingProfile = false


    var body: some View {
        NavigationSplitView {
            List {
                PageView(pages: modelData.features.map { FeatureCard(landmark: $0) })
                    .listRowInsets(EdgeInsets())


                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    CategoryRow(categoryName: key, items: modelData.categories[key]!)
                }
                .listRowInsets(EdgeInsets())
            }
            .listStyle(.inset)
            .navigationTitle("Featured")
            .toolbar {
                Button {
                    showingProfile.toggle()
                } label: {
                    Label("User Profile", systemImage: "person.crop.circle")
                }
            }
            .sheet(isPresented: $showingProfile) {
                ProfileHost()
                    .environment(modelData)
            }
        } detail: {
            Text("Select a Landmark")
        }
    }
}

6、现在就可以尝试 PageControl 的各种交互来切换页面,PageView展示了SwiftUI和UIKit视图如何混合使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1875099.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CXL:拯救NVMe SSD缓存不足设计难题-2

LMB提出了基于CXL协议的内存扩展框架和内核模块。该方案利用CXL内存扩展器作为物理DRAM源&#xff0c;旨在提供一个统一的内存分配接口&#xff0c;使PCIe和CXL设备都能方便地访问扩展的内存资源。通过这个接口&#xff0c;NVMe驱动和CUDA的统一内存内核驱动可以直接高效地访问…

telegram mini APP或游戏开发之bot设置

无意中发现telegram上居然也能发布小程序和游戏了,感觉发现了新大陆一样,自己好玩试了下。 参考教程 更详细的免费文章,请查看博客:https://lengmo714.top/6e79860b.html 要在telegram上开发一个mini App或者game,创建一个专属于你的机器人就必不可少了。 创建bot机器人 …

IoTDB Committer+Ratis PMC Member:“两全其美”的秘诀是?

IoTDB & Ratis 双向深耕&#xff01; 还记得一年前我们采访过拥有 IoTDB 核心研发 Ratis Committer “双重身份”的社区成员宋子阳吗&#xff1f;&#xff08;点此阅读&#xff09; 我们高兴地发现&#xff0c;一年后&#xff0c;他在两个项目都更进一步&#xff0c;已成为…

MapReduce学习

目录 7.3 MapReduce工作流程 7.3.1 工作流程概述 7.3.2 MapReduce各个执行阶段 7.3.3 Shuffle过程详解 1. Shuffle过程简介&#xff08;过程分为Map端的操作和Reduce端的操作&#xff09; 2、Map端的Shuffle过程&#xff1a; 3、在Reduce端的Shuffle过程 7.4 实例分析&am…

Drag Select Compose:实现多平台图片多选功能的利器

Drag Select Compose:实现多平台图片多选功能的利器 在现代移动应用开发中,图片多选功能是一个常见且实用的需求。而实现这种功能可能涉及到复杂的手势处理和状态管理。今天,我将介绍一款强大的Compose多平台库——Drag Select Compose,它能够轻松实现类似于Google Photos…

[推荐]有安全一点的网贷大数据信用查询网站吗?

在互联网金融日益发展的今天&#xff0c;网贷大数据查询网站成为了许多人申贷前的必备工具。随着使用这些网站的人群越来越多&#xff0c;安全问题也逐渐浮出水面。最近&#xff0c;就有许多用户反馈自己的个人信息在网贷大数据查询网站上被泄露。为了解决这一问题&#xff0c;…

日立EX-PROII+系列全新升级,智慧随心控畅享新生活

随着科技的进步&#xff0c;各种智能家电也开始走入人们的生活&#xff0c;而在这个领域&#xff0c;日立用技术创新生活&#xff0c;不断为新时代注入活力&#xff0c;推出日立 EX-PROII系列家用净化中央空调&#xff0c;贯彻“小身材&#xff0c;大能量”核心设计理念&#x…

STM32第十一课:ADC采集光照

文章目录 需求一、ADC概要二、实现流程1.开时钟&#xff0c;分频&#xff0c;配IO2.配置ADC工作模式3.配置通道4.复位校准5.数值的获取 三、需求的实现总结 需求 通过ADC转换实现光照亮度的数字化测量&#xff0c;最后将实时测量的结果打印在串口上。 一、ADC概要 ADC全称是A…

小柴冲刺软考中级嵌入式系统设计师系列一、计算机系统基础知识(1)嵌入式计算机系统概述

flechazohttps://www.zhihu.com/people/jiu_sheng 小柴冲刺嵌入式系统设计师系列总目录https://blog.csdn.net/qianshang52013/article/details/139975720?spm1001.2014.3001.5501 根据IEEE&#xff08;国际电气电子工程师协会&#xff09;的定义&#xff0c;嵌入式系统是&q…

lvs+上一章的内容

书接上回这次加了个keepalived 一、集群与分布式 1.1 集群介绍 **集群&#xff08;Cluster&#xff09;**是将多台计算机组合成一个系统&#xff0c;以解决特定问题的计算机集合。集群系统可以分为以下三种类型&#xff1a; **LB&#xff08;Load Balancing&#xff0c;负载…

rtthread stm32h743的使用(十一)spi设备使用

我们要在rtthread studio 开发环境中建立stm32h743xih6芯片的工程。我们使用一块stm32h743及fpga的核心板完成相关实验&#xff0c;核心板如图&#xff1a; 1.建立新工程&#xff0c;选择相应的芯片型号及debug引脚及调试器 2.编译下载&#xff0c;可以看到串口打印正常 3.…

SOAP vs REST介绍

SOAP&#xff08;简单对象访问协议&#xff09; 定义&#xff1a;SOAP是一种基于XML的通信协议&#xff0c;用于在网络中交换结构化信息&#xff0c;特别是在分布式环境和需要中介&#xff08;如网关或防火墙&#xff09;的环境中。它通过HTTP、SMTP等多种传输协议传输信息&…

APP项目测试 之 APP功能测试

1. APP测试流程 需求评审——计划编写——用例设计——用例执行——缺陷管理——测试报告 2. APP测试内容 功能测试 专项测试 性能测试 3.注册测试点扩充 4.登录测试点扩充 5.购物车测试点扩充 6.搜索测试点扩充 7.支付测试点扩充 8.评论测试点扩充 未完待续。…

Java校园跑腿小程序校园代买帮忙外卖源码社区外卖源码

&#x1f525;校园跑腿与外卖源码揭秘&#x1f525; &#x1f680; 引言&#xff1a;为何需要校园跑腿与外卖源码&#xff1f; 在快节奏的校园生活里&#xff0c;学生们对于便捷、高效的服务需求日益增长。校园跑腿和外卖服务成为了解决这一需求的热门选择。然而&#xff0c;…

新版周易测算系统源码 去授权完美运行

已经去掉授权可以完美运行 更新了三个模板市面上都是几千几千的卖 更新了三套首页新ui 自己后台切换就行 源码大小&#xff1a;338M 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/89447857 更多资源下载&#xff1a;关注我.

深入解析Android的Sound Trigger中间件架构

深入解析Android的Sound Trigger中间件架构 在现代移动设备中,应用程序对声音事件的检测需求越来越多,如热词检测和背景音乐识别等。为了实现低功耗和隐私敏感的声音事件检测,Android引入了Sound Trigger中间件。本文将详细介绍Sound Trigger的架构、工作原理以及其在不同层…

JavaWeb——MySQL:事务的简单学习

前面学习完了数据库增删查改的SQL语言&#xff0c;约束&#xff0c;数据库设计&#xff0c;以及多表查询&#xff0c;再学完事务就达到初级工程师的水平了。 6. 事务 6.1 概念 事务类似于编程语言的方法&#xff0c;包含一组SQL指令。 事务是不可分割的&#xff1b; 该指令步…

十四、Maven(2)

&#x1f33b;&#x1f33b;目录 一、maven项目的目录结构&#xff1f;---约定二、在创建maven项目的时候三、依赖四、依赖的传递性五、依赖的排除性六、继承---项目结构的设计七、聚合---项目的设计方式 一、maven项目的目录结构&#xff1f;—约定 Maven是自动化构建工具&am…

java用pdf.js在线预览pdf文件(jeecg框架)

最近在jeecg框架的后台要做一个pdf在线预览的页面功能&#xff0c;可是每次点预览都是下载&#xff0c;所以就要解决这个问题&#xff0c;现在解决了&#xff0c;记录一下&#xff0c;防止后面踩坑。 先放代码&#xff1a; 下面是点“预览”按钮的点击事件&#xff0c;代码放…

算法基础-----【动态规划】

动态规划(待完善) 动规五部曲分别为&#xff1a; 确定dp数组&#xff08;dp table&#xff09;以及下标的含义确定递推公式&#xff08;状态转移公式&#xff09;dp数组如何初始化确定遍历顺序举例推导dp数组、 动态规划的核心就是递归剪枝&#xff08;存储键值&#xff0c;…