普通视图

发现新文章,点击刷新页面。
今天 — 2025年9月16日iOS

SwiftUI ShareLink – 显示分享表单的使用

作者 Lucklylin
2025年9月15日 23:03

SwiftUI ShareLink – 显示分享表单

SwiftUI 的 ShareLink 简化了应用中添加分享功能的过程,允许用户通过 iOS 分享表单分享文本、URL、图片等。

目录

SwiftUI ShareLink 示例

SwiftUI ShareLink 示例

概述

本文介绍了 SwiftUI 中的 ShareLink,一个方便的工具,用于在应用中启用分享功能。ShareLink 允许用户通过标准的 iOS 分享表单分享文本、URL、图片等内容。这个功能简化了集成分享功能的过程,让用户可以无缝地从你的应用中分享内容给其他人。


代码片段

import SwiftUI

struct ShareLinkExample: View {
    let textToShare = "来看看这个很棒的内容!"
    let urlToShare = URL(string: "https://www.example.com")!

    var body: some View {
        VStack {
            Text("SwiftUI ShareLink 示例")
                .font(.title)
                .padding()

            ShareLink(item: urlToShare) {
                Text("分享 URL")
            }
            .padding()

            ShareLink(item: textToShare) {
                Text("分享文本")
            }
            .padding()
        }
    }
}

代码解释

  • let textToShare = "来看看这个很棒的内容!": 定义一个将要被分享的字符串常量。
  • let urlToShare = URL(string: "https://www.example.com")!: 定义一个将要被分享的 URL 常量。为了简单起见,URL 被强制解包,假设它总是有效的。
  • ShareLink(item: urlToShare) { ... }: 创建一个用于分享 URL 的 ShareLinkitem 参数是需要分享的内容,尾随闭包提供了用于发起分享操作的视图。
  • Text("分享 URL"): 用于分享 URL 的显示视图,这里是一个简单的文本标签。
  • ShareLink(item: textToShare) { ... }: 创建一个用于分享文本的 ShareLinkitem 参数是需要分享的内容,尾随闭包提供了用于发起分享操作的视图。
  • Text("分享文本"): 用于分享文本的显示视图,同样是一个简单的文本标签。

控制分享内容

你可以通过自定义提供给 ShareLinkitem 来控制要分享的内容。例如,你可以根据用户的互动或应用状态来分享不同类型的内容。


代码片段

import SwiftUI

struct ConditionalShareLinkExample: View {
    @State private var shareText = false
    let textToShare = "来看看这个很棒的内容!"
    let urlToShare = URL(string: "https://www.example.com")!

    var body: some View {
        VStack {
            Text("有条件 ShareLink 示例")
                .font(.title)
                .padding()

            Toggle("分享文本", isOn: $shareText)
                .padding()

            if shareText {
                ShareLink(item: textToShare) {
                    Text("分享文本")
                }
                .padding()
            } else {
                ShareLink(item: urlToShare) {
                    Text("分享 URL")
                }
                .padding()
            }
        }
    }
}

代码解释

  • @State private var shareText = false: 声明一个状态变量来跟踪是否应该分享文本。
  • Toggle("分享文本", isOn: $shareText): 一个绑定到 shareText 的开关,允许用户在分享文本和分享 URL 之间切换。
  • if shareText { ... } else { ... }: 一个根据 shareText 的值来决定分享哪种内容的条件语句。
  • ShareLink(item: textToShare) { ... }: 当 shareText 为真时,创建一个用于分享文本的 ShareLink
  • ShareLink(item: urlToShare) { ... }: 当 shareText 为假时,创建一个用于分享 URL 的 ShareLink

自定义分享表单

要自定义分享表单中显示的内容,你可以使用带有自定义数据类型的 ShareLink。下面是一个分享带有附加元数据的自定义消息的示例。


代码片段

import SwiftUI

struct CustomData: Identifiable {
    let id = UUID()
    let title: String
    let description: String
}

struct CustomShareLinkExample: View {
    let customData = CustomData(title: "很棒的内容",
                                description: "这是一个很棒的内容,你应该看看!")

    var body: some View {
        VStack {
            Text("自定义 ShareLink 示例")
                .font(.title)
                .padding()

            ShareLink(item: customData.description) {
                Text("分享自定义数据")
            }
            .padding()
        }
    }
}

代码解释

  • struct CustomData: Identifiable { ... }: 定义一个遵循 Identifiable 协议的自定义数据类型。
  • let customData = CustomData(title: "很棒的内容", description: "这是一个很棒的内容,你应该看看!"): 创建一个带有标题和描述的 CustomData 实例。
  • ShareLink(item: customData.description) { ... }: 创建一个用于分享自定义数据描述的 ShareLinkitem 参数是需要分享的内容,尾随闭包提供了用于发起分享操作的视图。
  • Text("分享自定义数据"): 用于分享自定义数据的显示视图。这是一个简单的文本标签,指示操作。

总结

SwiftUI 中的 ShareLink 组件提供了一种直接的方式,将分享功能集成到你的应用中。通过允许用户通过标准的 iOS 分享表单分享文本、URL、图片和其他内容,ShareLink 增强了用户体验,并使他们能够更轻松地分发应用中的内容。利用 ShareLink 可以显著增加你应用中内容的触达和参与度,并且凭借控制和自定义分享内容的能力,你可以提供量身定制且丰富的分享体验。

本文使用 「Markdown 在线编辑器 | 公众号内容排版工具」 排版

完成 Liquid Glass 的适配了吗?| 肘子的 Swift 周报 #0102

作者 东坡肘子
2025年9月16日 08:02

issue102.webp

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 [Disco

完成 Liquid Glass 的适配了吗?

明天 iOS 26 就要正式发布了,必然会有大批用户第一时间升级,体验全新的 Liquid Glass 设计语言。相比往年,今年的适配工作量明显更大——尤其对于交互复杂的应用,可能会遇到许多意想不到的问题。

Liquid Glass 不仅仅是视觉风格的改变,它在交互逻辑上也有诸多调整。一些系统组件在新旧版本间差异巨大,加之临近发布时仍存在不少未解决的 Bug,用户很难分辨问题到底源自系统还是应用本身。可以预见,接下来几周,开发者们免不了要在用户支持和问题解释上打一场"硬仗"。

无论你是否喜欢 Liquid Glass 的视觉风格,它无疑会成为新 iPhone 的重要卖点。但如果苹果能在开发者体验上更具前瞻性,尽早提供完善的系统组件和文档支持,应用生态就能更快、更一致地拥抱这一新设计语言,最终让所有用户受益。

你完成 Liquid Glass 的适配了吗?

前一期内容全部周报列表

近期推荐

聊聊 Observation 框架 (We Need to Talk About Observation)

在 Observation 框架诞生三年之际,Jared Sinclair 对比了 Observation 与基于 Combine 的 ObservableObject。他指出,尽管 Observation 能减少 @Published 样板代码、提升 UI 更新效率、支持嵌套组合,并更契合结构化并发与 SwiftUI,但也存在一些问题:@State 替代 @StateObject 会导致对象频繁重建,初始化成本高;withObservationTracking 设计复杂、难以发现和正确使用且缺乏统一取消机制;Observations 虽引入 AsyncSequence,但仍存在取消和生命周期管理不清晰的问题,容易引发内存管理隐患;相较 Combine 的 AnyCancellable,新方案在简洁性和易用性上明显不足。

尽管如此,作者认为 @Observable 依然是苹果的正确方向,尤其在与 SwiftUI 和 SwiftData 结合时前景可期。但 Apple 在非 UI 编程场景的 API 设计上不够“有主见”,开发者仍需在架构层面权衡取舍。


Swift 6.2 的 Main Actor 隔离值得采用吗?(Should you opt-in to Swift 6.2’s Main Actor isolation?)

Swift 6.2 引入了 Default Actor isolation 配置,开发者可以为 Target 设置默认隔离。将其设为 Main Actor 让代码更贴近早期“默认在主线程执行”的直觉。尽管这一设计旨在简化部分并发场景下的开发,但也带来新的疑问:不同 Target 应该如何选择合适的设置?在这篇文章中,Donny Wals 分享了他的思考与策略。

在 Xcode 26 中,App Target 默认启用 Main Actor,而 SPM Package 默认不启用,这种差异可能让初学者或对该特性不熟悉的开发者感到困惑。或许更好的做法,是在项目创建时就提供显式选项,让开发者从一开始就能明确选择。


SwiftUI 日期范围选择器 (Date Range picker in SwiftUI)

在许多应用中,用户需要选择时间范围(周、月、年或自定义区间),而系统的 DatePicker 往往无法满足需求。Kyryl Horbushko 通过“分而治之”的设计思路,将复杂的日期范围选择器拆分为多个独立组件(SegmentPicker、各类型日期选择器等),并巧妙利用 MultiDatePicker 配合状态管理实现了连续日期范围选择,避免了重新构建复杂的日历组件。整个方案充分展示了协议、泛型和 SwiftUI 的强大组合能力。


深入理解 SwiftUI 的 TimelineView

TimelineView 是一个没有自身外观的容器视图,会在预定时间点重新绘制其内容,因而非常适合构建 实时时钟、计时器、动画可视化、实时数据或逐帧动画。在本文中,Kyle Ye 不仅介绍了 TimelineView 的常见用法,还分享了在 OpenSwiftUI 中复刻该功能时的诸多细节。其中对 @Attribute 的应用与解析,也为理解和掌握 OpenAttributeGraph 提供了很好的参考。


我一直想要的 Swift Android 开发环境 (The Swift Android Setup I Always Wanted)

Swift 6 引入了对 Android NDK 的官方支持,解决了长期以来在 Android 平台使用 Swift 的最大障碍。开发者现在可以直接 import Android,无需再手动处理头文件。Mikhail Isaev 在本文中展示了如何借助 JNIKit 在 Android 上优雅地使用 Swift,并详细讲解了通过 VSCode + Dev Containers + Swift Stream IDE 快速搭建 Swift Android 项目,配置完成后可自动拉取 Docker 镜像与工具链并生成 Gradle 工程的完整流程。

JNIKit 将繁琐的 JNI C API 封装为 Swifty 接口,大幅简化了 Swift 与 JVM 的交互,包括类加载、对象缓存和跨线程环境绑定。


SwiftUI 重绘机制深度解析 (SwiftUI Redraw System In Depth: Attributes, Recomputation, Diffing and Observation)

在 SwiftUI 中,优化应用性能的关键之一是减少不必要的视图重绘。然而,由于这涉及状态管理、依赖关系、视图比较等诸多机制,即便 SwiftUI 已经发布多年,依然有许多开发者理解不足。在本文中,Mathis Gaignet 将 SwiftUI 的重绘过程拆解为「属性 → 重算 → diffing → 观察」四个层次,并结合 Self._printChanges()、Instruments 的 Effect Graph、Core Animation 等工具,通过大量实验展示了不同视图声明方式对性能的实际影响。

工具

Swift-Build GitHub Action

为 Swift 项目配置跨平台 CI/CD 向来不是一件轻松的事。Leo G Dion 开源的 swift-build GitHub Action 让这一切变得简单:只需几行配置即可在 macOS 和 Linux 上运行 Swift Package Manager 的构建与测试,同时内置智能缓存策略,大幅提升 CI 效率。想深入了解其工作原理,可以阅读作者的这篇详细介绍

该 Action 已在 GitHub Marketplace 上架,可直接添加到工作流中使用,完全免费。


AppKitUI

许多 macOS 开发者仍在使用 AppKit,但又羡慕 SwiftUI 的声明式语法和数据绑定机制。Darren Ford 开发的 AppKitUI 巧妙地填补了这个空白:它为 AppKit 控件提供了类 SwiftUI 的链式语法、数据绑定和布局系统,让你无需 XIB 文件就能构建复杂的 NSView 界面,甚至支持在 Xcode Preview 中实时预览。该库兼容至 macOS 10.13,特别适合那些需要支持旧版系统但又想享受现代开发体验的项目。

往期内容

THANK YOU

如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

昨天 — 2025年9月15日iOS

零一开源|前沿技术周刊 #16

作者 kymjs张涛
2025年9月15日 17:39

前沿技术周刊 是一份专注于技术生态的周刊,每周更新。本周刊深入挖掘高质量技术内容,为开发者提供持续的知识更新与技术洞察。

订阅渠道:【零一开源】、 【掘金】、 【RSS


大厂在做什么

本文介绍了在 iOS 平台上使用 MNN 框架部署大语言模型(LLM)时,针对聊天应用中文字流式输出卡顿问题的优化实践。通过分析模型输出与 UI 更新不匹配、频繁刷新导致性能瓶颈以及缺乏视觉动画等问题,作者提出了一套包含智能流缓冲、UI 更新节流与批处理、以及打字机动画渲染的三层协同优化方案。最终实现了从技术底层到用户体验的全面提升,让本地 LLM 应用的文字输出更加丝滑流畅,接近主流在线服务的交互体验。

新技术介绍

兄弟们,Kotlin 2.2.20 这波更新我瞅了眼,确实有料!编译速度又快了一截,尤其大项目能感觉到明显差异。标准库加了几个实用函数,比如`takeIfNotNull`,处理空安全更顺手了。Gradle 插件优化了依赖解析,同步项目不卡了。最香的是修复了之前协程调试偶尔崩溃的 bug,终于不用猜谜了。对了,JS 那边支持了最新的 Chrome 调试协议,前端同学也有福。整体稳得一批,建议直接升,没挖坑!

码圈新闻

“氛围编码”留下的烂摊子,终究要让那些被裁掉的人回来收拾。 自生成式人工智能兴起以来,许多人担心它会对人类员工的生计造成损害。如今,CEO 们也开始承认人工智能的影响,裁员人数也开始增加。
现年 81 岁的甲骨文联合创始人兼首席技术官拉里·埃里森(Larry Ellison)在一天之内财富暴涨近 1000 亿美元。这是有史以来最大的一次单日财富增长,而这一切都要归功于他在甲骨文中 41% 的持股。 截至昨日收盘,埃里森的财富为 2930 亿美元;截至周三中午,他的财富已达 4009 亿美元,成为历史上第二位财富突破 4000 亿美元的人。全球首富埃隆·马斯克在去年 12 月率先突破这一关口,彭博社称周三埃里森短暂超越马斯克,首次成为世界首富。
开源鸿蒙 OpenHarmony 6.0 Release 已于昨日发布并上线 Gitee。据介绍,OpenHarmony 6.0 Release 版本进一步增强 ArkUI 组件能力,提供更安全、更灵活的组件布局;进一步增强窗口能力,新增支持窗口处理文本显示的能力;进一步增强分布式数据管理能力,支持管理资产和资产组,支持应用对标准化数据进行展示;进一步增强位置服务、输入法框架的相关能力等等。
智东西9月4日消息,知名苹果爆料人、彭博社记者马克·古尔曼(Mark Gurman)最新爆料,苹果计划于明年春季推出自研AI搜索工具“答案引擎”。

博客推荐

最近看了篇文章,聊老项目里不支持Compose的View咋整,简直说到咱心坎里了。咱们接手的项目哪没几个祖传View?全重构Compose不现实,这篇给的招挺实在。 核心就俩方向:老View想塞进Compose,用AndroidView包一层就行,content里初始化时注意生命周期别瞎搞;要是想在View体系里嵌Compose,直接用ComposeView,addView或者xml里声明都能搞定。重点提了状态同步——别直接调setXxx,用remember存状态,不然容易崩。像地图、自定义播放器这种第三方View,这么搞完全够用。 总之过渡期不用头铁全重构,掌握这俩招,老View和Compose混着用妥妥的,省事儿又稳。
如果我们不满足于普通的在线加载图片,可以使用 ImageRequest 自定义加载。
AGSL 为使用 Jetpack Compose 构建动态、高性能、视觉震撼的 UI 效果打开了全新的大门。如果你曾经对 GLSL、HLSL 或其他着色语言有所耳闻,那么 AGSL 将是你在 Android 原生开发中实现复杂视觉效果的瑞士军刀,不,是屠龙宝刀!
1.图片初始位置相对屏幕Y轴的偏移没有赋值,影响图片放大后拖动的上下边界,导致上边会有黑边,下边有一部分显示不全 2.图片缩放没有限制,图片可以放大很大 3.手势识别区域为图片初始大小,放大之后其他区域没有响应
最近看了篇《KMP on iOS 深度工程化》,聊点实际干活能用上的。KMP跨平台看着香,但iOS工程里落地真得踩不少坑。 文章先说配置:gradle和Xcode的桥接得捋顺,不然编译报错能让你怀疑人生——比如Kotlin代码里调iOS API,参数类型得严格对应,String和NSString别混用,不然Xcode直接红。 然后是依赖管理:Kotlin库和iOS原生库版本得对齐,尤其第三方SDK,建议用ext统一管理版本号,不然编译时库冲突能卡你半天。 编译优化也提了:Kotlin/Native编译慢是老毛病,试试开增量编译,把常用模块打成预编译framework,能省不少时间。调试更得注意,断点打不上别慌,先检查Kotlin/Native的debug开关有没有开,日志用NSLog桥接能直接在Xcode控制台看。 最后说工程规范:建议按业务模块拆Kotlin代码,和iOS工程目录对应上,不然团队协作时找代码能绕晕。 总的来说,KMP在iOS工程化,核心就是把“跨平台”的便利和“iOS原生开发”的严谨捏到一起,坑不少但趟过去是真能提效。
通过分析代码,这个崩溃发生在App应用退出的时候,YYDiskCache会调用_appWillBeTerminated,将YYKVStorage置为nil,接着YYKVStorage会调用dealloc方法,最后调用[YYKVStorage _dbClose],在调用sqlite3_close的时候出现了崩溃。

GitHub 一周推荐


关于我们

零一开源】 是一个 文章开源项目 的分享站,有写博客开源项目的也欢迎来提供投递。 每周会搜集、整理当前的新技术、新文章,欢迎大家订阅。

[奸笑]

完成 Liquid Glass 的适配了吗? - 肘子的 Swift 周报 #102

作者 Fatbobman
2025年9月15日 22:00

明天 iOS 26 就要正式发布了,必然会有大批用户第一时间升级,体验全新的 Liquid Glass 设计语言。相比往年,今年的适配工作量明显更大——尤其对于交互复杂的应用,可能会遇到许多意想不到的问题。

SPM 之 混编(OC、Swift)项目保姆级教程(Swift Package Manager)

2025年9月15日 15:02

一、Swift Package Manager(以下简称SPM)简介

1.1、SPM 核心特性

1. 原生集成

  • Apple 官方支持:从 Xcode 11 开始内置,无需额外安装(对比 CocoaPods 需要 Ruby 环境,Carthage 需要 Homebrew)。
  • 与 Xcode 无缝协作:通过 Package.swift 文件定义依赖,Xcode 可直接解析并下载依赖。

2. 模块化设计

  • 基于 Swift 的模块系统:每个包(Package)是一个独立的模块,支持 target 划分(如库、测试、可执行文件)。
  • 跨平台兼容:同一套代码可编译到不同 Apple 平台或 Linux。

3. 依赖管理

  • 支持远程依赖:通过 Git 仓库(GitHub/GitLab)引入开源库。
  • 支持本地依赖:直接引用本地路径的模块(适合内部开发)。
  • 版本控制:支持语义化版本(SemVer)、分支、Commit Hash 等。

4. 资源管理

  • 原生支持资源文件:如 .xcassetsxibstoryboard,通过 resources 字段声明。
  • 访问方式:使用 Bundle.module 加载资源(无需手动处理路径)。

5. 命令行工具

  • 核心命令

    bash
    swift package init       # 初始化新包
    swift package resolve     # 解析依赖
    swift package update      # 更新依赖
    swift build              # 编译项目
    swift test               # 运行测试
    

1.2、SPM vs CocoaPods vs Carthage

对比项 SPM CocoaPods Carthage
官方支持 ✅ Apple 官方(Xcode 内置) ❌ 第三方(Ruby 编写) ❌ 第三方(Swift/Go 编写)
安装复杂度 ⚪ 无安装(Xcode 自带) ⚫ 需安装 Ruby 和 CocoaPods ⚫ 需安装 Homebrew 或编译
依赖原理 ✅ 直接编译源码 ⚫ 生成 .xcworkspace 和动态库 ✅ 编译二进制框架(静态库)
版本控制 ✅ 支持 SemVer/分支/Commit ✅ 支持 SemVer ✅ 支持 Git Tag
资源支持 ✅ 原生支持(Bundle.module ✅ 通过 resources 插件 ❌ 需手动处理
跨平台 ✅ 支持 macOS/iOS/Linux ❌ 仅 Apple 平台 ✅ 支持 Apple 平台 + Linux
二进制支持 不直接支持.a(包一下), 支持xcframework ✅ 可生成动态库(.framework ✅ 默认生成静态库(.a
学习曲线 ⚪ 中等(需熟悉 Package.swift ⚪ 中等(需了解 Podfile ⚪ 中等(需了解 Cartfile
适用场景 ✅ 新项目、模块化开发 ✅ 遗留项目、需要动态库 ✅ 追求编译速度、静态库

关键差异分析

  1. 依赖原理

    • CocoaPods:通过修改 project.pbxproj 文件,生成 .xcworkspace,依赖动态库(可能增加启动时间)。
    • Carthage:仅编译二进制框架,不修改项目文件,需手动集成到 Xcode。
    • SPM:直接编译源码到目标模块,不生成额外文件,与 Xcode 深度集成。
  2. 资源管理

    • SPM:原生支持资源文件,通过 Bundle.module 访问。
    • CocoaPods:需通过 resources 插件或手动配置 COPY_RESOURCES
    • Carthage:需手动将资源文件拖入项目。
  3. 二进制支持

    • Carthage 默认生成静态库,适合减小应用体积。
    • SPM 和 CocoaPods 默认编译源码,但 CocoaPods 可通过 use_frameworks! 生成动态库。

1.3、SPM 适用场景

1. 推荐使用 SPM 的情况

  • 新项目开发:从零开始的项目,可充分利用 SPM 的模块化设计。
  • 跨平台库:需要同时支持 macOS/iOS/Linux 的 Swift 库。
  • Apple 生态开发:与 Xcode 深度集成,避免第三方工具的兼容性问题。
  • 追求简洁性:不想处理 Ruby 环境或 Homebrew 依赖。

2. 不推荐使用 SPM 的情况

  • 遗留项目迁移:旧项目已使用 CocoaPods,迁移成本较高。
  • 需要二进制分发:如第三方 SDK 仅提供 .framework 文件(需通过 CocoaPods 或手动集成)。
  • 复杂依赖冲突:SPM 的依赖解析逻辑较严格,可能不如 CocoaPods 灵活。

1.4、SPM模块引入方式

image.png 引入方式有两种:

1、直接通过远程链接导入 2、直接依赖本地模块

1. 远程依赖(通过 Git 仓库导入)

适用场景
  • 依赖的模块是开源库(如 AlamofireSwiftLint)。
  • 模块托管在 GitHub、GitLab 或其他 Git 服务器上。
  • 需要自动获取最新版本或指定版本范围。
配置方法
(1) 在 Package.swift 中声明远程依赖
// swift-tools-version:5.7
import PackageDescription
 
let package = Package(
    name: "MyApp",
    dependencies: [
        // 方式1:指定版本范围(如 5.6.0 到 6.0.0)
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.6.0"),
        
        // 方式2:精确指定版本(如 5.6.1)
        .package(url: "https://github.com/SwiftGen/SwiftGen.git", exact: "6.6.0"),
        
        // 方式3:指定分支(如 main 分支,不推荐生产环境使用)
        .package(url: "https://github.com/example/repo.git", branch: "main"),
        
        // 方式4:指定 Commit Hash(用于临时修复)
        .package(url: "https://github.com/example/repo.git", revision: "a1b2c3d4e5f6"),
    ],
    targets: [
        .target(
            name: "App",
            dependencies: [
                .product(name: "Alamofire", package: "Alamofire"),
                .product(name: "SwiftGenKit", package: "SwiftGen"),
            ]
        )
    ]
)
(2) 在 Xcode 中直接添加远程依赖
  1. 打开 Xcode 项目,选择 File > Add Packages
  2. 输入 Git 仓库 URL(如 https://github.com/Alamofire/Alamofire.git)。
  3. 选择版本规则(Up to Next MajorExact Version 或 Branch)。
  4. 点击 Add Package,Xcode 会自动生成 Package.swift 并下载依赖。

常见问题
Q1: 依赖下载失败(网络问题或仓库不存在)
  • 解决方法

    • 检查网络连接,确保能访问 Git 仓库。
    • 如果是私有仓库,需配置 SSH 密钥或 GitHub Personal Access Token。
    • 在 Xcode 中重置包缓存:File > Reset Package Caches
Q2: 版本冲突(如多个依赖要求不同版本的同一库)
  • 解决方法

    • 在 Package.swift 中显式指定版本:

      .package(url: "https://github.com/example/repo.git", from: "1.0.0"),
      
    • 使用 resolution 字段强制解析版本(Xcode 14+ 支持):

      // 在 Package.swift 的顶层添加
      let package = Package(
          // ...
          dependencies: [...],
          // 强制解析版本
          resolutions: [
              .package(url: "https://github.com/example/repo.git", exact: "1.0.0")
          ]
      )
      
Q3: 如何更新依赖到最新版本?
  • 方法

    • 运行 swift package update 命令。
    • 在 Xcode 中:Product > Clean Build Folder,然后重新编译。

2. 本地依赖(直接引用本地模块)

适用场景
  • 依赖的模块是本地开发的(如公司内部库、未开源的模块)。
  • 需要快速迭代本地代码,无需频繁推送 Git。
  • 模块与主工程紧密耦合,不适合远程托管。
配置方法
(1) 在 Package.swift 中声明本地依赖

假设本地模块位于主工程的同级目录 ../LocalModule

// swift-tools-version:5.7
import PackageDescription
 
let package = Package(
    name: "MyApp",
    dependencies: [
        // 本地依赖(相对路径)
        .package(path: "../LocalModule"),
        
        // 也可以混合远程依赖
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.6.0"),
    ],
    targets: [
        .target(
            name: "App",
            dependencies: [
                "LocalModule", // 直接引用本地模块名
                .product(name: "Alamofire", package: "Alamofire"),
            ]
        )
    ]
)
(2) 本地模块的 Package.swift 示例

本地模块也需要是一个有效的 SPM 包,例如:

// swift-tools-version:5.7
import PackageDescription
 
let package = Package(
    name: "LocalModule",
    products: [
        .library(name: "LocalModule", targets: ["LocalModule"]),
    ],
    targets: [
        .target(name: "LocalModule", path: "Sources"),
    ]
)
(3) 在 Xcode 中添加本地依赖
  1. 打开 Xcode 项目,选择 File > Add Packages
  2. 点击 Add Local,选择本地模块的 Package.swift 文件。
  3. Xcode 会自动解析依赖并链接到主工程。

常见问题
Q1: 本地模块路径错误
  • 错误示例

    dependency 'LocalModule' not found at '../LocalModule'
    
  • 解决方法

    • 确保 .package(path: "../LocalModule") 的路径是相对于主工程 Package.swift 的。

    • 如果路径包含空格或特殊字符,用引号包裹路径:

      swift
      .package(path: "/path/with spaces/LocalModule")
      
Q2: 本地模块修改后未生效
  • 原因

    • Xcode 可能缓存了旧版本。
  • 解决方法

    • 清理构建缓存:Product > Clean Build Folder
    • 重新运行 swift package update
    • 如果使用 Xcode 的 SPM 集成,尝试 File > Reset Package Caches
Q3: 本地模块如何引用主工程的代码?
  • 问题

    • 本地模块和主工程可能存在循环依赖。
  • 解决方法

    • 避免循环依赖,将共享代码提取到第三个模块中。
    • 如果必须引用,可以使用 Xcode Workspace 结合 SPM(不推荐,复杂度高)。
Q4: 本地模块包含资源文件(如 .xcassets
  • 解决方法

    • 在本地模块的 Package.swift 中声明资源:

      .target(
          name: "LocalModule",
          dependencies: [],
          resources: [.process("Resources")] // 指定资源目录
      )
      
    • 在代码中通过 Bundle.module 加载资源:

      let bundle = Bundle.module
      let image = UIImage(named: "MyImage", in: bundle, with: nil)
      

3. 远程依赖 vs 本地依赖:如何选择?

对比项 远程依赖 本地依赖
适用场景 开源库、稳定版本 内部开发、快速迭代
版本控制 通过 Git 标签/分支管理 直接修改代码,无需推送
构建速度 较慢(需下载) 快(本地直接引用)
协作性 适合团队共享 适合单人或本地开发
依赖冲突 可能因版本不兼容报错 路径错误或循环依赖更常见

4. 最佳实践

  • 远程依赖:通过 .package(url:from:) 引入,适合开源库和稳定版本。
  • 本地依赖:通过 .package(path:) 引入,适合快速迭代的内部模块。
  • Xcode 集成:支持通过 GUI 添加依赖,但底层仍依赖 Package.swift
  • 常见问题:路径错误、版本冲突、缓存问题,通常通过清理缓存或调整路径解决。
  • 避免混合管理依赖:不要在 Package.swift 和 Podfile/Cartfile 中重复声明同一依赖。
  • 定期更新依赖:运行 swift package update 或使用 Xcode > Product > Clean Build Folder 保持依赖最新。

5. 彻底清理缓存

⚠️注意:当你在尝试配置模块已经依赖时,如果没有完全清空缓存,可能导致你正确的代码无法正常编译。

5.1.当前模块改动

swift package update
swift package clean

5.2.Package依赖缓存清理

Xcode的工具栏:File - Packages - Reset Package Caches image.png

5.3.退出Xcode 工程, 重新打开项目 必要时清理项目缓存 ~/Library/Developer/Xcode/DerivedData/你的工程

二、本地模块实操

实践中通常使用现有项目,此文演示先创建一个空OC项目(Swift也一样, 只不过大部分项目壳工程都是OC,兼容性问题也多,方便演示后面一些问题):

image.png

1、纯Swift文件

项目路径下添加模块

#本地管理路径
➜  SPMDemo git:(main) ✗ mkdir LocalModules && cd LocalModules

#测试模块1
➜  LocalModules git:(main) ✗ mkdir ModulesA && cd ModulesA

#创建项目模块
➜  ModulesA git:(main) ✗ swift package init --type library
Creating library package: ModulesA
Creating Package.swift
Creating .gitignore
Creating Sources/
Creating Sources/ModulesA/ModulesA.swift
Creating Tests/
Creating Tests/ModulesATests/
Creating Tests/ModulesATests/ModulesATests.swift

swift package init --type 参数

--type 参数用于指定初始化包的类型,支持以下选项:

选项 说明
--type library 创建一个 库(Library)  类型的包(默认选项,可省略)。
--type executable 创建一个 可执行文件(Executable)  类型的包(包含 main.swift)。
--type system-module 创建一个 系统模块(System Module)  类型的包(用于 C 语言家族的模块)。
--type manifest 仅创建一个空的 Package.swift 清单文件(不生成其他模板文件)。

生成的library文件结构

ModulesA/
├── Package.swift       # 包描述文件
├── Sources/
│   └── ModulesA/      # 库源码目录,这里可以添加你的swift模块
│       └── ModulesA.swift
└── Tests/
    └── ModulesATests/ # 测试目录
        └── ModulesATests.swift

添加:

image.png

设置目标工程:

image.png

纯swift模块在项目中效果:

image.png

⚠️注意:如果你的库源码目录不在Sources/ModulesA下,而是自定义路径(建议使用规范路径),需要设置path:

image.png

2、纯OC文件

2.1、模块目录结构

MyOCModule/
├── Sources/
│   ├── MyOCModule/          # OC 代码目录
│   │   ├── include/          # 公开头文件目录(可选)
│   │   │   └── MyOCModule.h # 模块主头文件
│   │   │   └── module.modulemap     # 模块映射文件(关键)
│   │   └── src/             # OC 源文件目录
│   │       └── MyOCClass.m  # OC 类实现
│   │       └── MyOCClass.h  # OC 类头文件
├── Package.swift            # SPM 配置文件
└── README.md                # 项目说明(可选)

2.2、关键文件配置

1. module.modulemap(模块映射文件)

定义 OC 模块的接口,指定头文件路径。示例内容:

module MyOCModule {
    header "MyOCModule.h"  // 指向模块主头文件
    export *                       // 导出所有头文件内容
}
  • header:指定模块的主头文件(如 MyOCModule.h),该文件需包含所有对外公开的头文件。
  • export:控制头文件的可见性(* 表示导出所有内容)。
2. Package.swift(SPM 配置文件)

配置 Target 依赖关系及编译路径。示例内容:

// swift-tools-version:5.7
import PackageDescription
 
let package = Package(
    name: "MyOCModule",
    products: [
        .library(
            name: "MyOCModule",
            targets: ["MyOCModule"]
        ),
    ],
    targets: [
        .target(
            name: "MyOCModule",
            path: "Sources/MyOCModule",  // OC 代码路径
            exclude: ["info.plist"], // 被排除的文件,多个用逗号分隔(如果有就行设置,没有的文件不要写,会有警告文件找不到)
            sources: ["src"],  // OC源代码路径:path 路径下
            publicHeadersPath: "include"      // 公开头文件夹
        ),
    ]
)
  • publicHeadersPath:指定公开头文件目录(如 include),需与 module.modulemap 中的路径一致。
  • sources:指定源文件目录(如 src),包含 .m 和 .h 文件。

image.png

⚠️注意:模块集成后,会以库形式在工程中被依赖,假如你要删除此OC模块,需要确保此处同样被移除,否则编译会报错!!!

image.png

3、Swift Package模块 依赖 OC Package 模块

1. 在 Swift 代码中导入 OC 模块

⚠️注意:

这里OC Package 是通过配置 include/module.modulemap 实现被Swift 依赖引用的,另外一种方式后面介绍

1、Package 配置:

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "ModulesA",
    products: [
        .library(
            name: "ModulesA",
            targets: ["ModulesA"]),
    ],
    dependencies: [
        // 依赖地址或者本地资源文件夹地址.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(path: "../MyOCModule")
    ],
    targets: [
        .target(
            name: "ModulesA",
            dependencies: [
                // 依赖OC模块
                .product(name: "MyOCModule", package: "MyOCModule")
            ]
        ),
        .testTarget(
            name: "ModulesATests",
            dependencies: ["ModulesA"]),
    ]
)

⚠️注意:上面依赖一个模块时,一个当前Package索引的依赖路径,也就是和 targets 评级的, 而每个不同的子target根据自己的需要依赖不同的模块,比如此处设置依赖的MyOCModule:

// 依赖OC模块
.product(name: "MyOCModule", package: "MyOCModule")

发现product 的name 和 package name 是一样的,那么就可以简写成如下:

.target(
    name: "ModulesA",
    dependencies: [
        "MyOCModule" // 依赖OC模块
    ]
)
2、在 Swift 文件中,通过 import 语句导入 OC 模块
import MyOCModule  // 导入 OC 模块
 
class SwiftClass {
    func useOCMethod() {
        let ocObject = MyOCClassPublicA()
        ocObject.test() // OC测试方法调用
    }
}

4、同一个 OC Package 模块中多个 OC target(类似于.podspec中 subspec)

1.配置 Target 依赖关系

若模块间需相互调用,需在 Package.swift 中配置 Target 依赖关系:

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "MyOCModule",
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "MyOCModule",
            targets: ["MyOCModule"]),
        .library(
            name: "MyOCModuleB",
            targets: ["MyOCModuleB"]),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .target(
            name: "MyOCModule",
            dependencies: ["MyOCModuleB"], // 依赖 MyOCModuleB
            path: "Sources/MyOCModule",  // OC 代码路径
            exclude: ["info.plist"], // 被排除的文件,多个用逗号分隔(如果有就行设置,没有的文件不要写,会有警告文件找不到)
            sources: ["src"],  // OC源代码路径:path 路径下
            publicHeadersPath: "include"
        ),
        .target(
            name: "MyOCModuleB",
            dependencies: [],
            path: "Sources/MyOCModuleB",  // OC 代码路径
            sources: ["src"],
            publicHeadersPath: "include"
        )
        
    ]
)

2.目录结构和调用:

image.png

⚠️注意:

从上面截图可以看到,在同一个模块内部代码之间一般是直接引用的 #import "MyOCModuleB.h" 那么,如果需要 #import <MyOCModuleB/MyOCModuleB.h> 方式引入呢?在下一节介绍

5、OC Package 依赖另一个 OC Package 模块

前面提到,OC target 通过配置 include/module.modulemap 是可以实现:

1、直接被Swift Package/target 通过import PackageName or targetName 引用

2、或者 OC target 依赖后通过 #import "FileName.h" 引用的

但是 OC Package 之间是需要通过 #import <MyOCModuleB/MyOCModuleB.h> 方式引用头文件的,接下来通过几个步骤介绍:

1、创建一个OC Package:MyOCHello

swift package init --type library MyOCHello


MyOCHello/
├── Sources/
   ├── MyOCHello/          # OC 代码目录
      ├── include/          # 公开头文件目录
         └── MyOCHello.h # 模块主头文件
      └── Core/             # OC 源文件目录
         └── MyOCHelloName.m  # OC 类实现
         └── MyOCHelloName.h  # OC 类头文件
      └── Private/             # OC 源文件目录
          └── MyOCHelloAge.m  # OC 类实现
          └── MyOCHelloAge.h  # OC 类头文件
├── Package.swift            # SPM 配置文件

2、设置公开头文件目录

MyOCHelloName.h 文件作为 public 头文件,前面示例中,我直接将 MyOCClassPublicA.h 文件拖动到 include 文件夹中。

但是在MyOCHello 模块不准备如此,而是创建MyOCHelloName.h 文件替身放到include 文件夹中

原因1:此模块我不打算创建 module.modulemap文件,移走后可能导致 MyOCHelloAge.m 无法使用 #import "MyOCHelloName.h"

原因2:不希望移动现有工程的文件的头文件

3、通过命令 ln -s 创建替身

格式 ln -s xxx文件夹目录/**.h xxx文件夹目录/

ln -s ~/Documents/SPMDemo/LocalModules/MyOCHello/Sources/MyOCHello/Core/MyOCHelloName.h ~/Documents/SPMDemo/LocalModules/MyOCHello/Sources/MyOCHello/include/MyOCHello

⚠️注意: 创建替身实践中存在以下几个问题,导致我项目加载总是失败(以下3种情况创建的替身在我电脑均索引失败😑,可能只是我的电脑问题,列出来希望能少走弯路)

1、右键.h文件创建替身,然后拖动到 include/MyOCHello/ 文件夹下面,替身在Xcode 15中无法编辑,无法索引

2、使用 ln -s 错误命令:ln -s xx/MyOCHelloName.h xx/MyOCHelloName.h

3、文件路径使用的相对路径,拖动替身到其它文件夹中就可能存在无法正常索引情况,请使用绝对路径。

至此,文件夹目录大致是这样的:

image.png

或者在终端通过 tree 命令(安装:brew install tree)生成结构:

MyOCHello git:(main) ✗ tree
.
├── Package.swift
├── Sources
│   └── MyOCHello
│       ├── Core
│       │   ├── MyOCHelloName.h
│       │   └── MyOCHelloName.m
│       ├── Private
│       │   ├── MyOCHelloAge.h
│       │   └── MyOCHelloAge.m
│       └── include
│           └── MyOCHello
│               ├── MyOCHello.h
│               └── MyOCHelloName.h -> xxx/Core/MyOCHelloName.h //会显示真身路径
└── Tests
    └── MyOCHelloTests
        └── MyOCHelloTests.swift

9 directories, 8 files

如果你想展示模块配置信息: 执行命令 swift package describe

MyOCHello git:(main) ✗ swift package describe
Name: MyOCHello
Manifest display name: MyOCHello
Path: /Users/xxx/SPMDemo/LocalModules/MyOCHello
Tools version: 5.10
Dependencies:
Platforms:
Products:
    Name: MyOCHello
    Type:
        Library:
            automatic
    Targets:
        MyOCHello

Targets:
    Name: MyOCHello
    Type: library
    C99name: MyOCHello
    Module type: ClangTarget
    Path: Sources/MyOCHello
    Sources:
        Core/MyOCHelloName.m
        Private/MyOCHelloAge.m
    Product memberships:
        MyOCHello

4、编辑模块头文件:

MyOCHello.h

#ifndef MyOCHello_h
#define MyOCHello_h

// 暴露头文件给外部使用时,使用简括号形式引入,并且对应头文件能在include目录下索引
#import <MyOCHello/MyOCHelloName.h>

#endif /* MyOCHello_h */

MyOCHelloName.h

@interface MyOCHelloName : NSObject

+ (void)helloSomeOne:(NSString *)name;

@end

MyOCHelloName.m

#import "MyOCHelloName.h"
#import "MyOCHelloAge.h"
@implementation MyOCHelloName

+ (void)helloSomeOne:(NSString *)name {
    NSLog(@"MyOCHello, name:%@, age:%@", name, [MyOCHelloName showAge]);
}

@end
5、MyOCModule 依赖 MyOCHello 模块
dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(path: "../MyOCHello"),
    ],
    targets: [
        .target(
            name: "MyOCModule",
            dependencies: [
                "MyOCHello"
            ], // 依赖
            
      等等
6、清理缓存(前面介绍了如何彻底清理缓存)

Xcode的工具栏:File - Packages - Reset Package Caches

image.png

重启工程

7、引用并调用
#import "MyOCClassPublicA.h"
#import <UIKit/UIKit.h>

// 引用其它OC模块
#import <MyOCHello/MyOCHello.h>

@implementation MyOCClassPublicA

/// 调用MyOCHello模块方法
- (void)OCHelloName {
    [MyOCHelloName helloSomeOne:@"Bobo"];
}

@end

6、OC、Swift混编模块

6.1. CocoaPods场景:

CocoaPods中存在一些老项目是OC代码为主,后来新增部分Swift代码, OC 和 Swift直接是可以直接相互引用的。

1、Pods库中代码文件可以 混编

2、Swift可以直接使用当前模块的公开的OC头文件,podspec中设置public路径,最后在-umbrella.h中查看到:

image.png

3、而module中的OC文件可以通过 #import <SHMaasUtils/SHMaasUtils-Swift.h> 依赖模块中 @objc public 的swift文件,而这个-Swift.h头文件是自动生成的,不需要手动创建。

6.2. SPM场景:

1、Package库中混编代码文件 不可以 混装,需要单独设置 target

2、Swift可以通过 dependencies 依赖公开的OC头文件(前面已经介绍了)

3、Package中的OC target文件目前尝试了,无法通过通过 #import <SHMaasUtils/SHMaasUtils-Swift.h> 依赖模块中 @objc public 的swift文件,而这个-Swift.h头文件是不会自动生成的

如果你通过 dependencies 依赖Swift target,那么必然导致依赖循环,可使用拆分公共文件作为独立target,协议解耦, 等方法处理,再使用 @import 引用swift文件即可

下面只介绍最复杂的场景

1、如果存在A.h 文件 和 B.swift 文件相互依赖的情况,那么需要将源码先解耦合 最终:A.h依赖C、 B.swift依赖C

2、将Package拆分: OCTarget、SwiftTarget、AdapterTarget

依赖关系1: OCTarget -> (SwiftTarget -> AdapterTarget)

依赖关系2: SwiftTarget -> (OCTarget -> AdapterTarget)

依赖关系3: SwiftTarget -> AdapterTargetOCTarget -> AdapterTarget

⚠️ 1和2场景的混合模块还是很常见,如果完全做到3的关系可能修改业务比较多,影响过大。

下面就以关系1的场景创建示例模块,并且AdapterTarget为OC代码的场景。

6.3. 创建混合模块

文件目录:

MixModule git:(main) ✗ tree
.
├── MixModuleAdapter
│   ├── PersonAdapter.h
│   └── PersonAdapter.m
├── MixModuleOC
│   ├── PersonHomeView.h
│   ├── PersonHomeView.m
│   └── include
│       └── MixModuleOC
│           ├── MixModuleOC.h
│           └── PersonHomeView.h -> /Users/xxx/SPMDemo/LocalModules/MixModule/MixModuleOC/PersonHomeView.h
├── MixModuleSwift
│   └── PersonInfoView.swift
└── Package.swift

6 directories, 8 files

Package.swift

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "MixModule",
    products: [
        .library(
            name: "MixModuleOC",
            targets: ["MixModuleOC"]),
        .library(
            name: "MixModuleSwift",
            targets: ["MixModuleSwift"]),
        .library(
            name: "MixModuleAdapter",
            targets: ["MixModuleAdapter"]),
    ],
    targets: [
        .target( // OC业务
            name: "MixModuleOC",
            dependencies: ["MixModuleSwift"],
            path:"MixModuleOC",
            sources: [""], // 需要设置
            publicHeadersPath: "include"),
        .target( // swift 业务
            name: "MixModuleSwift",
            dependencies: ["MixModuleAdapter"],
            path:"MixModuleSwift"
        ),
        .target( // 中间、适配
            name: "MixModuleAdapter",
            dependencies: [],
            path:"MixModuleAdapter",
            sources: [""],
            publicHeadersPath: ""
        )
        
    ]
)

1、OC target调用Swift target文件:

MixModuleOC.h (可以对外暴露)

//
//  MixModuleOC.h
//  
//
//  Created by 聂小波 on 2025/9/10.
//

#ifndef MixModuleOC_h
#define MixModuleOC_h

#import <MixModuleOC/PersonHomeView.h>

#endif /* MixModuleOC_h */

PersonHomeView.m (⚠️ 依赖swift target 使用 @import

//
//  PersonHomeView.m
//  
//
//  Created by 聂小波 on 2025/9/10.
//

#import "PersonHomeView.h"

// 引用swift target
@import MixModuleSwift;

@implementation PersonHomeView

- (void)showInfoView {
    NSLog(@"PersonHomeView show");
    PersonInfoView *personInfoView = [[PersonInfoView alloc] init];
    [personInfoView hello];
}

@end
2、Swift target调用OC target文件:
//
//  PersonInfoView.swift
//
//
//  Created by 聂小波 on 2025/9/10.
//

import Foundation
import UIKit

// 依赖OC target
import MixModuleAdapter

// 注意:给OC工程或者模块调用需要 @objc public
@objc
public class PersonInfoView: NSObject {
    @objc public override init() {
        super.init()
    }
    
    @objc public func hello() {
        let adapter = PersonAdapter()
        print("PersonInfoView: 我是", adapter.name())
    }
}

3、其它 Package 依赖 MixModule Package 的 MixModuleOC target
dependencies: [
        // .package(url: /* package url */, from: "1.0.0"),
        .package(path: "../SnapKit"),
        .package(path: "../MixModule") // 本地模块路径
    ],
    targets: [
        .target(
            name: "MyOCModule",
            dependencies: [
                "SHMaasService",
                // 依赖 MixModule Package 的 MixModuleOC target
                .product(name: "MixModuleOC", package: "MixModule")
            ], 
            
     ....

7、依赖 framework 库

如果你的项目必须依赖某个 framework 库

1、如果是 .xcframework 那么不用处理,SPM 直接支持

2、如果是 .framework 需要转 .xcframework

3、如果 .xcframework 放到主工程中,那么可以直接使用

4、如果 .xcframework 在 package 中通过 url 远程加载直接使用

5、如果 .xcframework 在 package 中通过本地文件路径加载,需要先签名

1. MBProgressHUD.framework 转 MBProgressHUD.xcframework

image.png

MBProgressHUD文件结构(不含版本):
MBProgressHUD.framework
.
├── Headers
│   └── MBProgressHUD.h
├── Info.plist
├── MBProgressHUD
└── Modules
    └── module.modulemap

3 directories, 4 files

结构简单,可以直接转xcframework

转换脚本 xcframework.sh:
#!/bin/bash

# 检查是否传入了框架路径参数
if [ $# -eq 0 ]; then
    echo "未输入framework名称"
    exit 1
fi
 

FRAMEWORK_NAME="$1"

# 创建临时目录
mkdir -p ./iOS-Device ./iOS-Simulator

# 提取 arm64(真机)版本
lipo -extract arm64 "${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" -o "./iOS-Device/${FRAMEWORK_NAME}"
cp -R "${FRAMEWORK_NAME}.framework" "./iOS-Device/${FRAMEWORK_NAME}.framework"
mv "./iOS-Device/${FRAMEWORK_NAME}" "./iOS-Device/${FRAMEWORK_NAME}.framework/"

# 提取 x86_64(模拟器)版本
lipo -extract x86_64 "${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" -o "./iOS-Simulator/${FRAMEWORK_NAME}"
cp -R "${FRAMEWORK_NAME}.framework" "./iOS-Simulator/${FRAMEWORK_NAME}.framework"
mv "./iOS-Simulator/${FRAMEWORK_NAME}" "./iOS-Simulator/${FRAMEWORK_NAME}.framework/"

xcodebuild -create-xcframework \
    -framework "./iOS-Device/${FRAMEWORK_NAME}.framework" \
    -framework "./iOS-Simulator/${FRAMEWORK_NAME}.framework" \
    -output "${FRAMEWORK_NAME}.xcframework"


# 如果是本地SPM调试,需要对每个平台(真机/模拟器)的框架进行签名
codesign --force --sign "Apple Development: Dean Nie (XXS7DPMZ77)" \
    --entitlements entitlements.plist \
    --timestamp=none \
    "${FRAMEWORK_NAME}.xcframework/ios-arm64/${FRAMEWORK_NAME}.framework"

codesign --force --sign "Apple Development: Dean Nie (XXS7DPMZ77)" \
    "${FRAMEWORK_NAME}.xcframework/ios-arm64_x86_64-simulator/${FRAMEWORK_NAME}.framework"

 

先授权脚本,再使用: xcframework.sh 和 MBProgressHUD.framework在同一目录下

chmod +x xcframework.sh
./xcframework.sh MBProgressHUD
2. FMDB.framework 转 FMDB.xcframework

image.png

framework文件结构(含版本):
FMDB.framework
.
├── FMDB -> Versions/Current/FMDB
├── Headers -> Versions/Current/Headers
├── Modules
│   └── module.modulemap
└── Versions
    ├── A
    │   ├── FMDB
    │   └── Headers
    │       ├── FMDB.h
    │       ├── FMDatabase.h
    │       ├── FMDatabaseAdditions.h
    │       ├── FMDatabasePool.h
    │       ├── FMDatabaseQueue.h
    │       └── FMResultSet.h
    └── Current -> A

7 directories, 9 files

通过文件结构可以发现,framework 真实二进制文件 可以通过 FMDB.framework/Versions/Current/ 路径查找

⚠️ 注意:在 SPM 中xcframework 文件结构测试下来不支持 版本管理结构。

image.png

可以自己动手修改脚本将文件重新梳理

3. MBProgressHUD.xcframework 放在主工程中使用

修改 linkerSettings 添加依赖即可:

targets: [
        .target(
            name: "MixModuleOC",
            dependencies: ["MixModuleSwift"],
            path:"MixModuleOC",
            sources: [""],
            publicHeadersPath: "include", 
            linkerSettings: [
                .linkedFramework("MBProgressHUD")
            ]
        ),
4. MBProgressHUD.xcframework 放在package中

创建一个包含多个库的Package:FMLibs

目录结构如下:

image.png

FMLibs git:(main) ✗ tree
.
├── Package.swift
└── binaryLibs
    └── MBProgressHUD.xcframework

Package 配置如下:

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "FMLibs",
    products: [
        .library(
            name: "FMLibs",
            targets: [
                "MBProgressHUD",
                // "FMDB"
            ]
        ),
    ],
    targets: [
        .binaryTarget(name: "MBProgressHUD",path: "binaryLibs/MBProgressHUD.xcframework"),
        // .binaryTarget(name: "FMDB",path: "binaryLibs/FMDB.xcframework"),
        
    ]
)
5. MixModule 依赖 FMLibs 并使用

MixModule Package.swift

dependencies: [
        .package(path: "../FMLibs")
    ],
targets: [
    .target(
        name: "MixModuleOC",
        dependencies: [
            "MixModuleSwift",
            "FMLibs"
        ],
        path:"MixModuleOC",
        sources: [""],
        publicHeadersPath: "include", 
        linkerSettings: [
        ]
    ),

显示HUD(直接调用演示)

#import "PersonHomeView.h"
@import MixModuleSwift;

//binaryTarget
#import <MBProgressHUD/MBProgressHUD.h>

@implementation PersonHomeView

/// 测试HUD提示
- (void)showHUD {
    [MBProgressHUD showHUDAddedTo:[UIApplication sharedApplication].keyWindow animated:YES];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [MBProgressHUD hideHUDForView:[UIApplication sharedApplication].keyWindow animated:YES];
    });
}

- (void)showInfoView {
    [self showHUD];
    NSLog(@"PersonHomeView show");
    PersonInfoView *personInfoView = [[PersonInfoView alloc] init];
    [personInfoView hello];
}

@end

运行app:

IMG_0003.PNG

6.包含.bundle 资源的 MJRefresh.framework 库

SPM支持bundle资源文件,直接使用前面脚本转 MJRefresh.xcframework

image.png

FMLibs Package.swift

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "FMLibs",
    products: [
        .library(
            name: "FMLibs",
            targets: [
                "MBProgressHUD",
                "MJRefresh"
            ]
        ),
    ],
    targets: [
        .binaryTarget(name: "MJRefresh",path: "binaryLibs/MJRefresh.xcframework"),
        .binaryTarget(name: "MBProgressHUD",path: "binaryLibs/MBProgressHUD.xcframework")
    ]
)

在ViewController使用:


import MJRefresh


class RefreshViewController: UIViewController {
    private let tableView = UITableView()
    private var dataSource = [String](repeating: "初始数据", count: 10)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
        configureRefreshHeader()
    }
    
    private func setupTableView() {
        tableView.frame = view.bounds
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.dataSource = self
        view.addSubview(tableView)
    }
    
    private func configureRefreshHeader() {
        // 创建自定义刷新头
        let header = MJRefreshNormalHeader { [weak self] in
            self?.loadNewData()
        }
        guard let header else { return }
        // 本地化文本设置
        header.setTitle("下拉刷新", for: .idle)
        header.setTitle("释放立即刷新", for: .pulling)
        header.setTitle("加载中...", for: .refreshing)
        header.lastUpdatedTimeLabel?.isHidden = true
        
        tableView.mj_header = header
    }
    
    private func loadNewData() {
        // 模拟网络请求
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
            self.dataSource.insert("新增数据 \(Date())", at: 0)
            self.tableView.reloadData()
            self.tableView.mj_header?.endRefreshing()
        }
    }
}

extension RefreshViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = dataSource[indexPath.row]
        return cell
    }
}

运行:

image.png

8、集成 .a 静态库

SPM 中无法直接使用 .a 文件,需要包在 xcframework 形式文件夹中访问

接下来演示如何将 libffi.a 转 ffi.xcframework

1、libffi 的源码文件如下:
➜  libffi-source git:(main) ✗ tree
.
├── ffi.h
├── ffi_arm.h
├── ffi_arm64.h
├── ffi_i386.h
├── ffi_x86_64.h
├── ffitarget.h
├── ffitarget_arm.h
├── ffitarget_arm64.h
├── ffitarget_i386.h
├── ffitarget_x86_64.h
└── libffi.a
2、将所以的.h 文件都放入 Headers 文件夹中
3. 检查 .a 库支持的架构
➜  lib git:(main) ✗ lipo -info libffi.a
Architectures in the fat file: libffi.a are: armv7 i386 x86_64 arm64
4. 拆分 .a 库
# 提取 arm64(真机)
➜  lib git:(main) ✗ lipo -extract arm64 libffi.a -o ffi-arm64.a

# 检查提取结果是否正确
➜  lib git:(main) ✗ lipo -info ffi-arm64.a
Architectures in the fat file: ffi-arm64.a are: arm64


# 提取 x86_64(Intel 模拟器)
➜  lib git:(main) ✗ lipo -extract x86_64 libffi.a -o ffi-x86_64.a

# 检查提取结果是否正确
➜  lib git:(main) ✗ lipo -info ffi-x86_64.a
Architectures in the fat file: ffi-x86_64.a are: x86_64
➜  lib git:(main) ✗
5.重新组合成 xcframework 文件结构,我命名为 ffi.xcframework

创建文件夹 ffi.xcframework 并按照以下通用格式移动文件:

image.png

⚠️ 注意:我在前面 生成的 ffi-arm64.affi-x86_64.a 文件最后都更改名称为 ffi (需要删除 .a 后缀)

6.依赖和使用

使用 ffi 调用一个 乘法的 c 函数:


#import "PersonHomeView.h"

//binaryTarget
#import <ffi/ffi.h>

@implementation PersonHomeView

/// 乘法功能的C函数
int cFuncMultiply(int a, int b) {
    return a * b;
}

/// 测试 ffi 调用 c 函数
- (void)libffiTest {
    //1.定义参数类型数组(告诉 libffi 参数的类型)
    ffi_type **argTypes;
    argTypes = malloc(sizeof(ffi_type *) * 2);
    argTypes[0] = &ffi_type_sint;
    argTypes[1] = &ffi_type_sint;
    //2.定义返回值类型(告诉 libffi 返回值的类型)
    ffi_type *retType = &ffi_type_sint; // 返回值是 int
    //3.初始化 CIF(Call Interface,调用接口)
    ffi_cif cif; // CIF 存储函数调用的 ABI、参数类型、返回值类型等信息
    ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, retType, argTypes);
    //4.准备参数值(实际调用时传入的参数)
    void **args = malloc(sizeof(void *) * 2);
    int x = 3, y = 7;
    args[0] = &x; // 第一个参数是 x 的地址
    args[1] = &y; // 第二个参数是 y 的地址
    int ret; // 存储返回值
    //5.动态调用函数
    ffi_call(&cif, (void(*)(void))cFuncMultiply, &ret, args);
    
    // 打印结果
    NSLog(@"libffi:乘法功能的C函数调用结果: %d", ret);
    
    // 释放内存
    free(argTypes);
    free(args);
}

@end

打印结果:

SPMDemo[10519:7206102] libffi:乘法功能的C函数调用结果: 21

9、资源加载

资源文件位置

SPM 要求资源文件放置在包的 Resources 目录下(或通过目录结构隐式标记为资源)。例如:

MyPackage/
├── Sources/
│   └── MyFramework/
│       ├── Resources/  # 显式资源目录
│       │   └── image.png
│       └── MyClass.swift
└── Package.swift

或通过文件扩展名隐式标记(如 .xcassets、.lproj 等),SPM 会自动识别为资源。

Package.swift 配置

在包的清单文件中,需通过 targets 的 resources 字段显式声明资源文件或目录:

swift
targets: [
    .target(
        name: "MyFramework",
        exclude: ["info.plist"], // 排除的文件
        resources: [
            .process("Resources/**"]), // 递归包含 Resources 目录下所有文件
            .copy() //资源按原样复制
        ]  
    )
]
.process(), // 大图等资源会被复制到构建产物中,并可通过 Bundle 访问。
.copy(), // 资源按原样复制,适用于非文本文件(如二进制数据)

10、如何管理工程依赖

1、GUI界面手动管理

通过GUI界面的方式手动的一个一个的进行添加、删除不是一个明智的选择。

你可以通过脚本管理等方式放开你的双手。

2、模块 SPMManager

也可以创建一个空壳模块专门管理依赖

比如,创建一个模块:SPMManager

主工程手动添加 SPMManager 依赖

本地模块示意: image.png

其它的模块都在 SPMManager 中 Package.swift 里配置。

你也可以通过脚本快速修改 Package.swift 文件,比修改工程可是要方便得多。

Swift 结构化并发 6 条铁律 —— 一张图 + 一套模板,让 `async let` / `TaskGroup` / `Task {}` 不再踩坑

作者 unravel2025
2025年9月15日 14:05

思维导图(先保存,再阅读)

Swift Concurrency
├─ Structured(结构化并发,有父有子,有纪律)
│  ├─ async let        → 静态并发
│  └─ TaskGroup        → 动态并发
│  三大规则 EGG
│   ├─ E  Error 传播:出作用域即 取消+等待
│   ├─ G  Group 完成:父必等子
│   └─ G  Group 取消:父取消→子取消
│  特征
│   ├─ 生命周期=作用域
│   ├─ 隐式 await(作用域结束)
│   └─ 继承优先级 & TaskLocal,**不继承 actor**
└─ Unstructured(无父,野生)
   ├─ Task { }           → 继承上下文
   ├─ Task.detached { }  → 啥也不继承
   └─ 无 EGG 规则,一切靠自己

为什么分“结构化”与“非结构化”

维度 Structured Unstructured
能否成为子任务 ❌(只能是根)
能否成为父任务 ✅(再开 structured 子任务)
生命周期 绑定作用域 绑定引用
规则 EGG 自动生效 全无效
典型 API async let / TaskGroup Task {} / Task.detached {}

Structured 任务两大形态

async let —— 静态并发

func fetchData() async {
    async let first  = fetchPart1()   // 立即启动子任务
    async let second = fetchPart2()
    async let third  = fetchPart3()

    let result = await (first, second, third) // 隐式等全部完成
    print(result)
}
// 离开作用域前,**所有子任务必须完成**,否则父任务不会完成

注意:

  • 顺序:await 不影响并发,三个子任务同时飞。
  • 只要作用域还在,就保证等;提前 return/throw 也会自动取消+等待其余子任务。

TaskGroup —— 动态并发

func fetchData(count: Int) async {
    await withTaskGroup(of: String.self) { group in
        for i in 0..<count {
            group.addTask {          // 动态添加子任务
                await fetchPart(i)
            }
        }
        for await result in group {   // 谁跑完谁先处理
            print(result)
        }
    }   // 此处**隐式等待**所有子任务
}

优点:

  • 数量运行时决定
  • fail-fast:任一子任务抛错,其余子任务立即取消并传播第一个错误
  • 支持 group.cancelAll() 手动取消

Unstructured 任务:两座“根”

常规 Task { } —— 继承上下文

enum TaskLocalStorage {
    @TaskLocal static var requestID: String?
}

Task {                                  // 继承
    print(Task.currentPriority)         // 继承调用方优先级
    print(TaskLocalStorage.requestID)   // 继承 TaskLocal
    // 若在外层 @MainActor,也继承 executor,但内部函数自己的 actor 隔离仍生效
}

Task.detached { } —— 啥也不继承

Task.detached {
    // 优先级 = .medium(默认)
    // 无 TaskLocal
    // 一定跑在**全局并发线程池**,不会上主线程
}

使用场景:

  • 真正“后台孤岛”计算
  • 但99 % 场景用常规 Task { } + 函数自身 nonisolated 即可,detached 是最后 resort

三大铁律 EGG(只适用于 Structured)

E - Error 传播规则

定义:作用域因抛错而提前退出时,所有子任务自动取消 + 等待。

func fast() async throws { ... throw TestError() }
func slow() async throws { ... }

func parent() async throws {
    async let f = fast()   // 5 s 后抛错
    async let s = slow()   // 10 s 后完成
    try await (f, s)       // fast 先抛,slow 被**自动取消并等待**
}
// 离开作用域前,**所有子任务必须完成或被取消**,错误才继续向上抛

对比 Unstructured:

let root = Task {
    Task { try await fast() }   // 无父子关系
    Task { try await slow() }   // **不会被自动取消**
}
root.cancel()                   // 两个嵌套 Task 仍跑完

G - Group 完成规则

定义:父任务必须等所有子任务完成后才能完成。

let parent = Task {
    async let a = work()   // 10 s
    async let b = work()   // 10 s
    _ = await (a, b)
}
await parent.value        // **20 s 后才打印**
print("parent completes")

Unstructured 版本:

let root = Task {
    Task { await work() }  // 10 s
}
await root.value          // **立刻打印**,嵌套 Task 继续跑
print("root completes")   // 出现在 work 之前

G - Group 取消规则

定义:父任务被取消 → 所有子任务自动取消(协作式)。

let parent = Task {
    async let a = longWork()   // 10 s
    async let b = longWork()   // 10 s
    _ = await (a, b)
}
parent.cancel()              // a & b 收到 `CancellationError`
await parent.value

Unstructured 再次失效:

let root = Task {
    Task { await longWork() } // **收不到取消**
}
root.cancel()

上下文继承差异速查

继承项 Structured 子任务 Task {} Task.detached { }
优先级 ❌(默认 .medium)
TaskLocal
Actor 隔离 ❌(永远不继承,跑全局并发池) ✅(继承调用方 executor) ❌(全局池)

易错点:

async let/group.addTask 不会把代码钉在 @MainActor,即使外层是主线程;

内部函数自己的isolate决定最终 executor。

什么时候用哪种任务?一张表搞定

需求 选型
静态并发(固定数量) async let
动态并发 & fail-fast TaskGroup
同步→异步 Task { }
后台“孤岛”计算,不继承任何东西 Task.detached { }(最后 resort)
Fire-and-forget(不 await) Task { } 但记得手动管理生命周期

实战模板:网络层“结构化”封装

/// 并发下载多张图片,任一失败立即取消其余,返回首张
@MainActor
func fetchFirstImage(urls: [URL]) async throws -> UIImage {
    try await withThrowingTaskGroup(of: UIImage.self) { group in
        for url in urls {
            group.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                guard let image = UIImage(data: data) else {
                    throw URLError(.badURL)
                }
                return image
            }
        }
        // 只要第一张成功,其余自动取消
        for try await image in group {
            return image              // 提前返回 → 其余任务被取消
        }
        throw URLError(.badServerResponse)
    }
}

特点:

  • 结构化保证“失败即取消”
  • 离开作用域自动等待,不会泄漏任务

常见踩坑清单

踩坑 正确做法
async let 忘了 await 作用域结束自动等,但返回值别丢
Task { } 里开 async let 就当子任务 ❌ 无父子关系,EGG 规则全失效
想取消单个 async let 必须取消整个父任务;用 TaskGroup + cancelAll() 更细
Task.detached 当成“性能加速器” 99 % 用常规 Task { } + 函数自己的 nonisolated 即可

一句话总结

Structured = 有爹管:生命周期、取消、错误、完成,作用域帮你兜底。

Unstructured = 野生根:一切自己 await、自己 cancel、自己管理。

记住 EGG 只给“有爹”的任务吃,野生任务饿了自己煮。

深入理解 Swift Concurrency:从 async/await 到 Actor 与线程池的完整运行机制

作者 unravel2025
2025年9月15日 14:05

一、async 函数的本质:可挂起的函数

✅ 什么是 async 函数?

func asyncWork() async {
    // 这是一个异步函数
}
  • async 函数是可以被挂起的函数。
  • 挂起 ≠ 阻塞线程,而是让出线程去执行其他任务。
  • 挂起只发生在 await 处,称为潜在挂起点。

✅ sync 函数可以当作 async 函数使用,反之不行

protocol SomeProtocol {
    func work() async
}

struct SomeStruct: SomeProtocol {
    func work() {
        // ✅ 合法:sync 实现 async 协议方法
    }
}
protocol SomeProtocol {
    func work()
}

struct SomeStruct: SomeProtocol {
    func work() async {
        // ❌ 非法:async 实现 sync 协议方法
    }
}

二、await:异步等待,非阻塞线程

✅ 示例:顺序执行异步代码

func asyncWork() async -> Int {
    return 42
}

let result = await asyncWork() // 不会阻塞线程
  • await 是潜在挂起点,只有当调用者和被调用者执行上下文不同时,才会真正挂起。
  • 挂起时,系统会保存当前状态(称为 continuation),并释放线程。

三、Continuation:挂起点的“快照”

  • 包含:返回地址、参数、局部变量等。
  • 存储在堆中,允许跨线程恢复。
  • 由运行时管理,开发者无需直接操作。

四、Task:异步执行的单位

✅ 创建 Task 进入异步上下文

Task {
    await callAsyncFunc()
}
  • 每个 async 函数都在某个 Task 中运行。
  • Task 是异步函数的“容器”,类似线程之于 sync 函数。
  • Task 本身不具备并发能力,一个 Task 一次只执行一个函数。

✅ Task 的三种状态

  • 🔴 suspended:等待调度或外部事件
  • 🟡 running:正在线程上运行
  • 🟢 completed:执行完成

五、Job:Task 的“同步片段”

Task {
    beforeWork()
    await asyncWork()
    afterWork()
}
  • 每个 await 将 Task 拆分为多个 Job。
  • Job 是同步执行的最小单位,不包含 await
  • Job 按顺序执行,不能并发。

六、Actor:线程安全的并发模型

✅ 示例:Actor 隔离状态

actor SomeActor {
    let immutableState = 1
    var mutableState = 2

    func updateState(_ newValue: Int) {
        mutableState = newValue
    }
}

let actor = SomeActor()

print(actor.immutableState) // ✅ 无需 await

Task.detached {
    await print(actor.mutableState) // ✅ 需 await
    // actor.mutableState = 3 // ❌ 编译错误
    await actor.updateState(3) // ✅ 合法
}
  • Actor 保证同一时间只有一个任务访问其可变状态。
  • 不可变状态可同步访问,无需 await
  • 可变状态必须通过 await 调用 actor 方法访问。

七、Executor:Job 的执行器

  • Executor 负责将 Job 调度到线程执行。
  • 类型:
    • Default concurrent executor:非 actor 隔离任务
    • Serial executor:每个 actor 一个,顺序执行
    • Main executor:主线程,处理 @MainActor 任务

八、Cooperative Thread Pool(CTP):线程池

  • Swift Concurrency 使用固定数量线程(= CPU 核心数),避免线程爆炸。
  • 所有 executor(除主线程)都从 CTP 借线程。
  • 主线程独立,不参与 CTP。

九、线程与 executor 的映射关系

Task.detached {
    // 默认并发 executor,CTP 线程
    await someAsyncFunction()
    // 仍为默认并发 executor,可能换线程
}
actor SomeActor {
    func someMethod() async {
        // 当前 actor 的 serial executor
        await someAsyncFunction()
        // 仍为同一 actor executor,可能换线程
    }
}
Task { @MainActor in
    // 主线程
    await someAsyncFunction()
    // 仍在主线程
}

十、完整运行机制图解(文字版)

CPU 核心
   ↓
Cooperative Thread Pool(固定线程)
   ↓
Executor(调度 Job)
   ↓
Job(同步片段)
   ↓
Task(异步函数容器)
   ↓
async/await(挂起点与 continuation)
   ↓
Actor(隔离状态,防止数据竞争)

十一、总结与个人见解

✅ Swift Concurrency 的优势

  • 结构化并发:Task、async/await、actor 构成完整模型。
  • 线程安全:Actor 提供编译期保证,避免数据竞争。
  • 性能优化:CTP 限制线程数,避免线程爆炸。
  • 可读性强:异步代码像同步代码一样线性书写。

⚠️ 学习曲线与挑战

  • 概念多,机制复杂,需理解底层模型。
  • 调试困难,尤其是线程切换与挂起恢复。
  • 与旧代码(GCD、delegate)集成需谨慎。

十二、扩展使用场景与实践建议

✅ 场景 1:网络请求 + 数据解析

func fetchData() async throws -> Data {
    let url = URL(string: "https://api.example.com")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

✅ 场景 2:并发请求多个接口

async let user = fetchUser()
async let posts = fetchPosts()
let result = await (user, posts)

✅ 场景 3:Actor 管理缓存

actor ImageCache {
    private var cache: [String: UIImage] = [:]

    func image(for key: String) -> UIImage? {
        return cache[key]
    }

    func save(image: UIImage, for key: String) {
        cache[key] = image
    }
}

【Swift Concurrency】深入理解 `async let` 与 `TaskGroup`:并发任务的生命周期与错误传播机制

作者 unravel2025
2025年9月15日 14:04

一、什么是 async let

async let 是 Swift 提供的一种结构化并发语法糖,用于并发地启动多个子任务,并延迟等待其结果。

✅ 基本用法

func fetchData() async {
    async let first = fetchPart1()
    async let second = fetchPart2()
    async let third = fetchPart3()

    let result = await (first, second, third)
    print(result)
}
  • 每个 async let 会创建一个子任务(child task)。
  • 子任务立即开始执行,不会阻塞当前任务。
  • 使用 await 获取结果时,按顺序等待(从左到右)。

✅ 支持任意表达式

async let number = 123
async let str = "Hello"

实际上,Swift 会将这些表达式包装为 ChildTask,并在后台并发执行。

二、什么是 TaskGroup?

TaskGroup 是 Swift 提供的动态并发任务创建机制,适合任务数量在运行时决定的场景。

✅ 示例:动态创建任务

func fetchData(count: Int) async {
    var results = [String]()

    await withTaskGroup(of: String.self) { group in
        for index in 0..<count {
            group.addTask {
                await self.fetchPart(index)
            }
        }

        for await result in group {
            results.append(result)
        }
    }

    print(results)
}
  • addTask 动态添加子任务。
  • for await 按完成顺序处理结果(谁先完成谁先处理)。
  • 也可以使用 group.next() 手动迭代。

三、生命周期机制对比

特性 async let TaskGroup
生命周期作用域 当前局部作用域(函数、do块等) withTaskGroup 闭包
正常退出时 自动取消并等待所有子任务 自动等待所有子任务(不取消)
异常退出时 自动取消并等待所有子任务 自动取消并等待所有子任务
是否支持动态任务数 ❌ 不支持 ✅ 支持
是否按完成顺序处理结果 ❌ 按声明顺序 await ✅ 按完成顺序

四、错误传播机制详解

async let 的错误传播

async let f = fast()
async let s = slow()

do {
    try await (f, s)
} catch {
    print("Caught error: \(error)")
}
  • 错误传播顺序取决于 await 的顺序。
  • 如果 fast() 先抛出错误,slow() 会被隐式取消并等待。
  • 如果 slow() 先完成但 fast() 先抛出错误,slow() 的错误不会被捕获。

✅ TaskGroup 的错误传播

try await withThrowingTaskGroup(of: Void.self) { group in
    group.addTask { try await fast() }
    group.addTask { try await slow() }

    for try await _ in group {
        // 处理结果
    }
}
  • 使用 for try await 时,第一个抛出的错误会立即传播。
  • 其余任务会被取消并等待。
  • 错误传播顺序与任务完成顺序无关,谁先抛谁传播。

五、实战建议:如何选择?

场景 推荐方式 原因
任务数量固定 async let 简洁、语法糖优雅
任务数量动态 TaskGroup 支持运行时添加任务
需要“fail fast” TaskGroup 错误传播更及时、可控
需要按顺序处理结果 async let 可控制 await 顺序
需要按完成顺序处理结果 TaskGroup AsyncSequence 自动支持

六、常见误区与注意事项

⚠️ 1. async let 不支持逃逸闭包

async let result = someAsyncFunc()
DispatchQueue.main.async {
    // ❌ 不能使用 result,生命周期已结束
}

⚠️ 2. 不 await 也会等待完成

async let f = slowTask()
print("End") // 仍会等待 f 完成后再退出作用域

⚠️ 3. 不支持“真正的 fire-and-forget”

  • 结构化任务总是会被等待。
  • 若需真正“放飞自我”,请使用非结构化 Task:
Task.detached {
    await someAsyncFunc()
}

七、总结:一句话记住差异

async let 是固定并发任务的语法糖,生命周期绑定作用域,错误传播顺序依赖 await 顺序;

TaskGroup 是动态并发任务的管理器,生命周期绑定闭包,错误传播顺序更及时、可控。

Flutter 三种方式实现页面切换后保持原页面状态

作者 ALLIN
2025年9月15日 13:19

前言:

在 Flutter 应用中,导航栏切换页面后默认情况下会丢失原页面状态,即每次进入页面时都会重新初始化状态,如果在initState中打印日志,会发现每次进入时都会输出,显然这样增加了额外的开销,并且带来了不好的用户体验。

在正文之前,先看一些常见的 App 导航,以喜马拉雅 FM 为例:

动图封面

它拥有一个固定的底部导航以及首页的顶部导航,可以看到不管是点击底部导航切换页面还是在首页左右侧滑切换页面,之前的页面状态都是始终维持的,下面就具体介绍下如何在 flutter 中实现类似喜马拉雅的导航效果

第一步:实现固定的底部导航

在通过flutter create生成的项目模板中,我们先简化一下代码,将MyHomePage提取到一个单独的home.dart文件,并在Scaffold脚手架中添加bottomNavigationBar底部导航,在body中展示当前选中的子页面。

/// home.dart
import 'package:flutter/material.dart';

import './pages/first_page.dart';
import './pages/second_page.dart';
import './pages/third_page.dart';

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final items = [
    BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('首页')),
    BottomNavigationBarItem(icon: Icon(Icons.music_video), title: Text('听')),
    BottomNavigationBarItem(icon: Icon(Icons.message), title: Text('消息'))
  ];

  final bodyList = [FirstPage(), SecondPage(), ThirdPage()];

  int currentIndex = 0;

  void onTap(int index) {
    setState(() {
      currentIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('demo'),
        ),
        bottomNavigationBar: BottomNavigationBar(
            items: items,
            currentIndex: currentIndex,
            onTap: onTap
        ),
        body: bodyList[currentIndex]
    );
  }
}

其中的三个子页面结构相同,均显示一个计数器和一个加号按钮,以first_page.dart为例:

/// first_page.dart
import 'package:flutter/material.dart';

class FirstPage extends StatefulWidget {
  @override
  _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  int count = 0;

  void add() {
    setState(() {
      count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: Text('First: $count', style: TextStyle(fontSize: 30))
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: add,
          child: Icon(Icons.add),
        )
    );
  }
}

当前效果如下:

动图封面

可以看到,从第二页切换回第一页时,第一页的状态已经丢失

第二步:实现底部导航切换时保持原页面状态

可能有些小伙伴在搜索后会开始直接使用官方推荐的AutomaticKeepAliveClientMixin,通过在子页面的 State 类重写wantKeepAlivetrue 。 然而,如果你的代码和我上面的类似,body 中并没有使用PageViewTabBarView,很不幸的告诉你,踩到坑了,这样是无效的,原因后面再详述。现在我们先来介绍另外两种方式:

① 使用IndexedStack实现

IndexedStack继承自Stack,它的作用是显示第indexchild,其它child在页面上是不可见的,但所有child的状态都被保持,所以这个Widget可以实现我们的需求,我们只需要将现在的bodyIndexedStack包裹一层即可

/// home.dart
class _MyHomePageState extends State<MyHomePage> {
  ...
  ...
  ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('demo'),
        ),
        bottomNavigationBar: BottomNavigationBar(
            items: items, currentIndex: currentIndex, onTap: onTap),
        // body: bodyList[currentIndex]
        body: IndexedStack(
          index: currentIndex,
          children: bodyList,
        ));
  }

保存后再次测试一下

动图封面

② 使用Offstage实现

Offstage的作用十分简单,通过一个参数来控制child是否显示,所以我们同样可以组合使用Offstage来实现该需求,其实现原理与IndexedStack类似

/// home.dart
class _MyHomePageState extends State<MyHomePage> {
  ...
  ...
  ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('demo'),
        ),
        bottomNavigationBar: BottomNavigationBar(
            items: items, currentIndex: currentIndex, onTap: onTap),
        // body: bodyList[currentIndex],
        body: Stack(
          children: [
            Offstage(
              offstage: currentIndex != 0,
              child: bodyList[0],
            ),
            Offstage(
              offstage: currentIndex != 1,
              child: bodyList[1],
            ),
            Offstage(
              offstage: currentIndex != 2,
              child: bodyList[2],
            ),
          ],
        ));
  }
}

在上面的两种方式中都可以实现保持原页面状态的需求,但这里有一些开销上的问题,有经验的小伙伴应该能发现当应用第一次加载的时候,所有子页状态都被实例化了(>这里的细节并不是因为我直接把子页实例化放在bodyList里...<),如果在子页StateinitState中打印日志,可以在终端看到一次性输出了所有子页的日志。下面就介绍另一种通过继承AutomaticKeepAliveClientMixin的方式来更好的实现保持状态。

第三步:实现首页的顶部导航

首先我们通过配合使用TabBar+TabBarView+AutomaticKeepAliveClientMixin来实现顶部导航(注意:TabBarTabBarView需要提供controller,如果自己没有定义,则必须使用DefaultTabController包裹)。此处也可以选择使用PageView,后面会介绍。

我们先在home.dart文件移除Scaffold脚手架中的appBar顶部工具栏,然后开始重写首页first_page.dart:

/// first_page.dart
import 'package:flutter/material.dart';

import './recommend_page.dart';
import './vip_page.dart';
import './novel_page.dart';
import './live_page.dart';

class _TabData {
  final Widget tab;
  final Widget body;
  _TabData({this.tab, this.body});
}

final _tabDataList = <_TabData>[
  _TabData(tab: Text('推荐'), body: RecommendPage()),
  _TabData(tab: Text('VIP'), body: VipPage()),
  _TabData(tab: Text('小说'), body: NovelPage()),
  _TabData(tab: Text('直播'), body: LivePage())
];

class FirstPage extends StatefulWidget {
  @override
  _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  final tabBarList = _tabDataList.map((item) => item.tab).toList();
  final tabBarViewList = _tabDataList.map((item) => item.body).toList();

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
        length: tabBarList.length,
        child: Column(
          children: <Widget>[
            Container(
              width: double.infinity,
              height: 80,
              padding: EdgeInsets.fromLTRB(20, 24, 0, 0),
              alignment: Alignment.centerLeft,
              color: Colors.black,
              child: TabBar(
                  isScrollable: true,
                  indicatorColor: Colors.red,
                  indicatorSize: TabBarIndicatorSize.label,
                  unselectedLabelColor: Colors.white,
                  unselectedLabelStyle: TextStyle(fontSize: 18),
                  labelColor: Colors.red,
                  labelStyle: TextStyle(fontSize: 20),
                  tabs: tabBarList),
            ),
            Expanded(
                child: TabBarView(
              children: tabBarViewList,
              // physics: NeverScrollableScrollPhysics(), // 禁止滑动
            ))
          ],
        ));
  }
}

其中推荐页、VIP 页、小说页、直播页的结构仍和之前的首页结构相同,仅显示一个计数器和一个加号按钮,以推荐页recommend_page.dart为例:

/// recommend_page.dart
import 'package:flutter/material.dart';

class RecommendPage extends StatefulWidget {
  @override
  _RecommendPageState createState() => _RecommendPageState();
}

class _RecommendPageState extends State<RecommendPage> {
  int count = 0;

  void add() {
    setState(() {
      count++;
    });
  }

  @override
  void initState() {
    super.initState();
    print('recommend initState');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body:Center(
          child: Text('首页推荐: $count', style: TextStyle(fontSize: 30))
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: add,
          child: Icon(Icons.add),
        ));
  }
}

保存后测试,

动图封面

可以看到,现在添加了首页顶部导航,且默认支持左右侧滑,接下来再进一步的完善状态保持

第四步:实现首页顶部导航切换时保持原页面状态

③ 使用AutomaticKeepAliveClientMixin实现

写到这里已经很简单了,我们只需要在首页导航内需要保持页面状态的子页State中,继承AutomaticKeepAliveClientMixin并重写wantKeepAlivetrue即可。

notes:Subclasses must implement wantKeepAlive, and their build methods must call super.build (the return value will always return null, and should be ignored)

以首页推荐recommend_page.dart为例:

/// recommend_page.dart
import 'package:flutter/material.dart';

class RecommendPage extends StatefulWidget {
  @override
  _RecommendPageState createState() => _RecommendPageState();
}

class _RecommendPageState extends State<RecommendPage>
    with AutomaticKeepAliveClientMixin {
  int count = 0;

  void add() {
    setState(() {
      count++;
    });
  }

  @override
  bool get wantKeepAlive => true;

  @override
  void initState() {
    super.initState();
    print('recommend initState');
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(
        body:Center(
          child: Text('首页推荐: $count', style: TextStyle(fontSize: 30))
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: add,
          child: Icon(Icons.add),
        ));
  }
}

再次保存测试,

动图封面

现在已经可以看到,不管是切换底部导航还是切换首页顶部导航,所有的页面状态都可以被保持,并且在应用第一次加载时,终端只看到recommend initState的日志,第一次切换首页顶部导航至 vip 页面时,终端输出vip initState,当再次返回推荐页时,不再输出recommend initState

所以,使用TabBarView+AutomaticKeepAliveClientMixin这种方式既实现了页面状态的保持,又具有类似惰性求值的功能,对于未使用的页面状态不会进行实例化,减小了应用初始化时的开销。

更新

前面在底部导航介绍了使用IndexedStackOffstage两种方式实现保持页面状态,但它们的缺点在于第一次加载时便实例化了所有的子页面State。为了进一步优化,下面我们使用PageView+AutomaticKeepAliveClientMixin重写之前的底部导航,其中PageViewTabBarView的实现原理类似,具体选择哪一个并没有强制要求。更新后的home.dart文件如下:

/// home.dart
import 'package:flutter/material.dart';

import './pages/first_page.dart';
import './pages/second_page.dart';
import './pages/third_page.dart';

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final items = [
    BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('首页')),
    BottomNavigationBarItem(icon: Icon(Icons.music_video), title: Text('听')),
    BottomNavigationBarItem(icon: Icon(Icons.message), title: Text('消息'))
  ];

  final bodyList = [FirstPage(), SecondPage(), ThirdPage()];

  final pageController = PageController();

  int currentIndex = 0;

  void onTap(int index) {
    pageController.jumpToPage(index);
  }

  void onPageChanged(int index) {
    setState(() {
      currentIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        bottomNavigationBar: BottomNavigationBar(
            items: items, currentIndex: currentIndex, onTap: onTap),
        // body: bodyList[currentIndex],
        body: PageView(
          controller: pageController,
          onPageChanged: onPageChanged,
          children: bodyList,
          physics: NeverScrollableScrollPhysics(), // 禁止滑动
        ));
  }
}

然后在bodyList的子页State中继承AutomaticKeepAliveClientMixin并重写wantKeepAlive,以second_page.dart为例:

/// second_page.dart
import 'package:flutter/material.dart';

class SecondPage extends StatefulWidget {
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage>
    with AutomaticKeepAliveClientMixin {
  int count = 0;

  void add() {
    setState(() {
      count++;
    });
  }

  @override
  bool get wantKeepAlive => true;

  @override
  void initState() {
    super.initState();
    print('second initState');
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(
        body: Center(
          child: Text('Second: $count', style: TextStyle(fontSize: 30))
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: add,
          child: Icon(Icons.add),
        ));
  }
}

Ok,更新后保存运行,应用第一次加载时不会输出second initState,仅当第一次点击底部导航切换至该页时,该子页的State被实例化。

至此,如何实现一个类似的 底部 + 首页顶部导航 完结 ~

昨天以前iOS

老司机 iOS 周报 #351 | 2025-09-15

作者 ChengzhiHuang
2025年9月14日 19:20

ios-weekly
老司机 iOS 周报,只为你呈现有价值的信息。

你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。

新闻

Memory Integrity Enforcement: A complete vision for memory safety in Apple devices

@Damien:本文章是 Apple 推出的 Memory Integrity Enforcement(MIE)技术,结合自研芯片与系统级安全机制,为 iPhone 17 系列提供业界首个全时开启的内存安全保护,无需用户感知即可防御缓冲区溢出、释放后使用等漏洞。该功能基于增强版 Arm 内存标记扩展(EMTE),配合安全内存分配器与标签保密技术,显著增加间谍软件等高级攻击的开发成本,Apple 称之为“消费级操作系统内存安全史上最大升级”。

文章

🐎 SwiftUI WebView

@DylanYang:作者向我们介绍了 Swift UI 中新引入的 WebView 组件。它有非常简单的初始化方式,同时也兼具了很多我们在使用 WKWebview 时经常会用到的功能,如页面加载各个时机的回调、自定义的 scheme handler、js 注入等。作者提供了一个完善的 demo 来展示这些能力。感兴趣的读者可以阅读下本文。

🐎 Debugging Swift Concurrency: “ Am I on the Main Actor? ”

@JonyFang: Swift Concurrency 调试推荐做思维上的转变:从线程思维转向 Actor 思维,关注"我在哪个 Actor 上"而非"我在哪个线程上"。文中描述了 MainActor.assertIsolated()MainActor.preconditionIsolated() 在 Debug 和 Release 模式下的差异用法。整体在推荐拥抱 Swift 6 的并发安全模型,让 Actor 成为开发过程中思考并发的基础单元。

🐎 Building a design system at Genius Scan

@david-clang:文章详细介绍了如何利用 SwiftUI 的组合思想和环境机制,为应用构建灵活的设计系统。核心是通过创建可复用的基础组件(如 Row),并利用 ViewBuilder 和 Environment 实现样式配置与定制,从而高效解决 UI 一致性与代码复用问题。

🐕 Four Corners: the first game in Catalog written in Swift

@极速男孩:Playdate 游戏《Four Corners》被开发者 Steven Chipman 用 Swift 重写,成为 Catalog 中首款 Swift 游戏。此举旨在探索 Swift 在 Playdate 上的性能潜力。Chipman 熟悉 Swift,选择重写此游戏是因为它相对简单且存在性能瓶颈。尽管遇到了缺少部分 API 和 Foundation 库的挑战,以及调试困难,但 PlaydateKit 库极大地简化了开发。Chipman 认为 Swift 的优势(如 C 级性能和易用性)胜过这些不便,并计划未来继续使用 Swift 开发 Playdate 游戏。他还因此重启了《Four Corners》的 iOS 版本。

🐎 Implementing a Refractive Glass Shader in Metal

@BluesJiang:这篇文章使用 Metal Shader 来自己尝试实现类似苹果 Liquid Glass 的效果,介绍了玻璃效果的各个组成部分,已经相关的算法。同时也演示了算法中的各个参数是如何影响最终的效果的。推荐有自定义液态玻璃效果的需求或者是想了解相关的实现方式的开发者阅读。

内推

重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考

具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)

关注我们

我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参

同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom

说明

🚧 表示需某工具,🌟 表示编辑推荐

预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)

iOS 26 仅需几行代码让 SwiftUI 7 液态玻璃界面焕发新春

2025年9月14日 14:31

在这里插入图片描述

概述

在今年的 WWDC 25 中,苹果为全平台推出了崭新的液态玻璃(Liquid Glass)皮肤。不仅如此,Apple 在此基础之上还打造了一整套超凡脱俗的动画和布局体系让 SwiftUI 7 界面焕发新机。

在这里插入图片描述

现在,我们只需寥寥几行代码就能将原本平淡无奇、乏善可陈的 SwiftUI 布局变成上面这般鲜活灵动。

在这里插入图片描述

想知道如何实现吗?看这篇就对啦!

在本篇博文中,您将学到如下内容:

  1. “一条大河窄又长”
  2. SwiftUI 7 全新玻璃特效来袭
  3. 融入,鲜活!

那还等什么呢?让我们马上迈入液态玻璃奇妙的特效世界吧? Let‘s go!!!;)


1. “一条大河窄又长”

在如今 App 现代化布局中,秃头小码农们非常喜爱像下面这般简洁、小巧、紧凑的全局菜单系统:

在这里插入图片描述

它就像一条长长的河流,伸缩自如,温柔又调皮的流入用户的心坎里。

不幸的是,目前它仿佛少了一点灵动的气息,而且感觉和 WWDC 25 中全新的液态玻璃也不太般配。

struct BadgesView: View {
    @Environment(ModelData.self) private var modelData
    @State private var isExpanded: Bool = false
    
    var body: some View {
        VStack(alignment: .center, spacing: Constants.badgeButtonTopSpacing) {
                if isExpanded {
                    VStack(spacing: Constants.badgeSpacing) {
                        ForEach(modelData.earnedBadges) {
                            BadgeLabel(badge: $0)
                        }
                    }
                }

                Button {
                    withAnimation {
                        isExpanded.toggle()
                    }
                } label: {
                    ToggleBadgesLabel(isExpanded: isExpanded)
                        .frame(width: Constants.badgeShowHideButtonWidth,
                               height: Constants.badgeShowHideButtonHeight)
                     
                }
                #if os(macOS)
                .tint(.clear)
                #endif
            }
            .frame(width: Constants.badgeFrameWidth)
    }
}

诚然,我们可以利用 SwiftUI 优秀的动画底蕴重新包装上面 BadgesView 视图的动画和转场效果,但这需要秃头小码农们宝贵的时间和头发,而且效果往往强差人意。

在这里插入图片描述

不过别担心,从 SwiftUI 7(iOS 26 / iPadOS 26 / macOS 26)开始,我们有了全新的选择,简单的不要不要的!

2. SwiftUI 7 全新玻璃特效来袭

从 WWDC 25 开始,全面支持 Liquid Glass 的 SwiftUI 7 推出了玻璃特效容器 GlassEffectContainer ,让我们真的可以对玻璃“为所欲为”:

在这里插入图片描述

GlassEffectContainer 能把多个带 glassEffect(_:in:) 的视图合并成一张“可变形的联合玻璃”,既省性能又能让玻璃形状彼此融合、 变形(morph)。

核心要点:

  • 用法:给子视图添加 .glassEffect(.liquid, in: container) 修改器,系统会把它们自动收集到同一个 GlassEffectContainer 中;
  • 效果:子视图的玻璃形状不再各自独立,而是当成一个整体渲染,可互相吸引、拼接、渐变和 morph;
  • 控制融合:通过容器的 spacing 值调节——值越大,子视图相距越远时就开始“粘”在一起;
  • 并发:@MainActor 隔离,线程安全。

总而言之,GlassEffectContainer 让多块“液态玻璃”合成一块可 morph 的超级玻璃,性能更高、动画更连贯。

在这里插入图片描述

同时,SwiftUI 7 还新增了两个配套方法 glassEffect(_:in:)glassEffectID(_:in:) : 在这里插入图片描述

在这里插入图片描述

我们可以利用它们结合 Namespace 来完成液态玻璃世界中的视图动画效果。

另外 SwiftUI 7 还专门为 Button 视图添加了 glass 按钮样式,真可谓超级“银杏化”:

在这里插入图片描述

有了这些 SwiftUI 中的宝贝,小伙伴们可以开始来打造我们的梦幻玻璃天堂啦!

3. 融入,鲜活!

将之前的 BadgesView 视图重装升级为如下实现:

struct BadgesView: View {
    @Environment(ModelData.self) private var modelData
    @State private var isExpanded: Bool = false
    @Namespace private var namespace
    
    var body: some View {
        GlassEffectContainer(spacing: Constants.badgeGlassSpacing) {
            VStack(alignment: .center, spacing: Constants.badgeButtonTopSpacing) {
                if isExpanded {
                    VStack(spacing: Constants.badgeSpacing) {
                        ForEach(modelData.earnedBadges) {
                            BadgeLabel(badge: $0)
                                .glassEffect(.regular, in: .rect(cornerRadius: Constants.badgeCornerRadius))
                                .glassEffectID($0.id, in: namespace)
                        }
                    }
                }

                Button {
                    withAnimation {
                        isExpanded.toggle()
                    }
                } label: {
                    
                    ToggleBadgesLabel(isExpanded: isExpanded)
                        .frame(width: Constants.badgeShowHideButtonWidth,
                               height: Constants.badgeShowHideButtonHeight)
                     
                }
                .buttonStyle(.glass)
                #if os(macOS)
                .tint(.clear)
                #endif
                .glassEffectID("togglebutton", in: namespace)
            }
            .frame(width: Constants.badgeFrameWidth)
        }
    }
}

上面这段新代码把“ earned 徽章列表”与底部的“展开/收起”按钮一起放进同一个 GlassEffectContainer 容器中,从而让它们全部参与 iOS 26 的「液态玻璃」合并渲染。

下面按“玻璃特性”逐句拆解:

  1. GlassEffectContainer(spacing: …)
  • 建立一块「联合玻璃画布」。
  • spacing 决定徽章彼此、徽章与按钮之间多早开始“粘”成一体:值越大,离得越远就开始融合。
  1. 展开时才出现的 VStack + ForEach
  • 每个 BadgeLabel 同时挂两个修饰符:
    • .glassEffect(.regular, in: .rect(cornerRadius: …))
      声明“我是 regular 风格玻璃,形状是圆角矩形”。
    • .glassEffectID(badge.id, in: namespace)
      给玻璃发身份证;同一 namespace 里身份证不同,SwiftUI 就能在增减徽章时做“液态 morph”——旧玻璃流走、新玻璃流进来,而不是生硬闪现。
  1. 底部 Button
  • .buttonStyle(.glass) 让按钮本身也是玻璃,但风格、圆角与徽章不同。
  • 同样用 .glassEffectID("togglebutton", in: namespace) 注册身份证,于是按钮的玻璃和上面徽章的玻璃被当成“同一张可变形大图”处理。
  • 展开/收起时,按钮玻璃会与刚出现(或消失)的徽章玻璃在边缘处“拉丝”融合,形成液态过渡。
  1. withAnimation { isExpanded.toggle() }
  • 状态变化被包进动画块,GlassEffectContainer 会同步驱动所有玻璃路径的 morph 动画:
    • 徽章从 0 高度“流”出来,边缘先与按钮玻璃粘连,再各自分离成独立圆角矩形。
    • 收起时反向流回,最终只剩按钮玻璃。
  1. 整体效果
    用户看到的不是“一行行控件出现”,而是一块完整的「可变玻璃」:
    • 展开 → 玻璃区域向下延伸,新徽章像水泡一样从主体里分裂长出;
    • 收起 → 多余部分被“吸”回按钮,边缘圆润地收缩消失。
      全程保持同一高光、折射、模糊背景,性能也优于多图层叠加。

在这里插入图片描述

简单来说,上面的实现用 GlassEffectContainer 把徽章与按钮收进同一块「液态玻璃」,凭借 glassEffectIDnamespace 让它们在展开/收起时像流体一样自然融合、morph,呈现出 iOS 26 独有的“整块玻璃可生长可收缩”的视觉魔法。

在这里插入图片描述

要注意哦,上面动图中按钮组背后的阴影是由于 gif 图片显示局限导致的,模拟器和真机实际测试的阴影效果可是美美哒的呢!

我们把 BadgesView 视图嵌入到主视图中,宝子们再来一起欣赏一下叹为观止的液态玻璃动画效果吧: 在这里插入图片描述

大功告成,打完收工,棒棒哒!💯

在这里插入图片描述

总结

在本篇文章中,我们讨论了在 iOS 26/iPadOS 26 里如何使用 SwiftUI 7 最新的液体玻璃系统来装饰小伙伴们的 App 界面。

在这里插入图片描述

感谢观赏,再会吧!8-)

Redux 中›ABC三个页面是如何通信的?

作者 littleplayer
2025年9月14日 12:31

你的这种感觉非常正常!这也是很多初学者对 Redux 最大的误解。如果 A、B、C 三个页面分别有自己的 Store,那你就完全违背了 Redux 最核心的“单一数据源”原则,自然会陷入无法通讯的困境。

Redux 的核心理念是:整个应用有且只有一个全局 Store。A、B、C 三个页面共享这个唯一的 Store,而不是各自拥有一个。

让我用正确的 Redux 思维来为你重构这个问题,你会发现通讯变得非常简单和清晰。


正确的 Redux 结构:单一数据源

flowchart TD
    AppState["全局 AppState<br>包含三个页面的数据"]
    
    subgraph A [页面A]
        A_State[StateA]
        A_Action[ActionA]
    end

    subgraph B [页面B]
        B_State[StateB]
        B_Action[ActionB]
    end

    subgraph C [页面C]
        C_State[StateC]
        C_Action[ActionC]
    end

    AppState --> A_State
    AppState --> B_State
    AppState --> C_State

    A_Action -- dispatch --> Store
    B_Action -- dispatch --> Store
    C_Action -- dispatch --> Store

    Store -- 更新 --> AppState

实现步骤

第 1 步:定义全局的 State、Action 和 Reducer

State.swift - 单一数据源

// 整个应用只有一个根状态
struct AppState {
    // 页面A的状态,只是这个根状态的一个属性
    var pageAState: PageAState
    // 页面B的状态
    var pageBState: PageBState
    // 页面C的状态
    var pageCState: PageCState
    // 还可以有跨页面的共享状态
    var userIsLoggedIn: Bool
}

// 每个页面的状态仍然是独立的结构体,但被整合到AppState中
struct PageAState {
    var dataForA: String = ""
    var valueFromB: String? = nil // 用于接收来自B的数据
}

struct PageBState {
    var dataForB: Int = 0
    var valueFromC: String? = nil // 用于接收来自C的数据
}

struct PageCState {
    var dataForC: [String] = []
}

Action.swift - 统一的行为定义

// 所有页面的Action都集中在一个枚举中
enum AppAction {
    // 页面A的Action
    case pageA(PageAAction)
    case pageB(PageBAction)
    case pageC(PageCAction)
    // 全局的Action,如登录、登出
    case global(GlobalAction)
}

// 每个页面自己的Action枚举
enum PageAAction {
    case buttonTapped
    case dataLoaded(String)
    case receivedDataFromB(String) // 专门用于接收B的消息
}

enum PageBAction {
    case sliderValueChanged(Int)
    case sendDataToA(String) // 专门用于向A发送数据
}

enum PageCAction {
    case itemSelected(Int)
}

Reducer.swift - 统一的 reducer

// 根Reducer,负责组合所有页面的reducer
func appReducer(state: inout AppState, action: AppAction) -> Void {
    switch action {
    
    // 分解处理页面A的Action
    case .pageA(let pageAAction):
        pageAReducer(state: &state.pageAState, action: pageAAction)
        
    // 分解处理页面B的Action
    case .pageB(let pageBAction):
        pageBReducer(state: &state.pageBState, action: pageBAction)
        // B的Action可能会影响到其他页面!
        // 例如:当B发送数据时,需要更新A的状态
        if case .sendDataToA(let data) = pageBAction {
            state.pageAState.valueFromB = data // 直接修改A的状态
        }
        
    // 分解处理页面C的Action
    case .pageC(let pageCAction):
        pageCReducer(state: &state.pageCState, action: pageCAction)
        
    // 处理全局Action
    case .global(let globalAction):
        globalReducer(state: &state, action: globalAction)
    }
}

// 每个页面自己的reducer(纯函数)
func pageAReducer(state: inout PageAState, action: PageAAction) {
    switch action {
    case .buttonTapped:
        print("A的按钮被点击")
    case .dataLoaded(let data):
        state.dataForA = data
    case .receivedDataFromB(let dataFromB):
        state.valueFromB = dataFromB // 更新来自B的数据
    }
}

func pageBReducer(state: inout PageBState, action: PageBAction) {
    switch action {
    case .sliderValueChanged(let value):
        state.dataForB = value
    case .sendDataToA(let data):
        // 注意:这个Action的主要处理逻辑在根Reducer中
        // 这里可以处理B自身相关的状态更新
        print("B准备发送数据给A: \(data)")
    }
}

第 2 步:创建唯一的全局 Store

Store.swift

class Store: ObservableObject {
    @Published private(set) var state: AppState
    private let reducer: (inout AppState, AppAction) -> Void
    
    init(initialState: AppState, reducer: @escaping (inout AppState, AppAction) -> Void) {
        self.state = initialState
        self.reducer = reducer
    }
    
    func dispatch(_ action: AppAction) {
        reducer(&state, action)
    }
}

// 在应用入口创建唯一Store
let globalStore = Store(initialState: AppState(
    pageAState: PageAState(),
    pageBState: PageBState(), 
    pageCState: PageCState(),
    userIsLoggedIn: false
), reducer: appReducer)

第 3 步:在页面中使用全局 Store

PageAView.swift

struct PageAView: View {
    @EnvironmentObject var store: Store // 注入的是全局唯一的Store
    
    // 从全局State中取出页面A需要的部分状态
    private var pageAState: PageAState { store.state.pageAState }
    
    var body: some View {
        VStack {
            Text("页面A的数据: \(pageAState.dataForA)")
            // 显示从页面B传来的数据
            if let dataFromB = pageAState.valueFromB {
                Text("来自B的消息: \(dataFromB)")
            }
            Button("通知B") {
                // 派发Action,而不是直接调用B的方法
                store.dispatch(.pageB(.sendDataToA("你好,我是A!")))
            }
        }
    }
}

PageBView.swift

struct PageBView: View {
    @EnvironmentObject var store: Store // 同一个全局Store
    
    private var pageBState: PageBState { store.state.pageBState }
    
    var body: some View {
        VStack {
            Text("B的数值: \(pageBState.dataForB)")
            Button("发送数据到A") {
                // 通过全局Store派发Action
                store.dispatch(.pageB(.sendDataToA("Hello from B!")))
            }
            NavigationLink("去C") {
                PageCView()
            }
        }
    }
}

通信场景实现

现在,让我们看看如何实现具体的通信:

  1. A -> B 通信

    • A 中:store.dispatch(.pageB(.sendDataToA("你好,我是A!")))
    • 根Reducer 接收到 AppAction.pageB(.sendDataToA),它会: a. 调用 pageBReducer 处理 B 自身的状态(如果需要) b. 直接修改 state.pageAState.valueFromB
    • 由于 PageAView 依赖于 store.state.pageAState,SwiftUI 会自动重绘页面A,新的数据就显示出来了!
  2. B -> A 通信:(同上,方向相反)

  3. 任何页面 -> 全局状态

    • 任何页面都可以派发全局 Action:store.dispatch(.global(.loginSuccess))
    • 这会在根Reducer中处理,更新 state.userIsLoggedIn
    • 所有依赖 userIsLoggedIn 的页面都会自动更新!

总结

  • 只有一个 Store:这是 Redux 架构的绝对核心。
  • State 是组合的:每个页面的 State 是全局 AppState 的一个属性。
  • Action 是统一的:所有页面的 Action 都通过一个统一的枚举管理。
  • Reducer 是分形的:有一个根 Reducer,它负责将 Action 分发给各个页面的 Reducer 处理。
  • 通信方式:页面间通信就是派发一个目标为其他页面的 Action。这个 Action 会在根 Reducer 中被处理,并直接修改目标页面的 State

这种方式虽然初期需要更多样板代码,但带来的好处是巨大的:极其清晰的数据流、可预测的状态变化、易于调试和测试。所有页面间的耦合都被解除了,它们都只依赖于全局的 Store,而不是彼此。

Redux在iOS中的使用

作者 littleplayer
2025年9月14日 09:47

好的,我们来详细探讨一下 Redux 在 iOS 开发中的应用。Redux 是一个源自 Web 前端(通常与 React 搭配)的架构模式,它因其单一数据源、状态不可变和纯函数Reducer 等特性,在 iOS 开发中也获得了大量关注和实践。

Redux 核心概念回顾

理解 Redux 在 iOS 的实现,首先要理解其三个基本原则:

  1. 单一数据源 (Single Source of Truth): 整个应用的状态(State)被存储在一个单一的、中心化的 Store 对象中。这消除了状态分散在不同组件所带来的复杂性,使得状态的追踪和调试变得非常容易。

  2. 状态是只读的 (State is Read-Only): 唯一改变状态的方法就是派发一个 Action。Action 是一个简单的、描述“发生了什么”的对象(通常是结构体或枚举)。你不能直接修改状态,这保证了状态更新的可预测性。

  3. 使用纯函数进行更改 (Changes are Made with Pure Functions): 为了指定状态如何被 Action 转换,你需要编写 Reducers。Reducer 是一个纯函数,它接收当前的 State 和一个 Action,并返回一个新的、更新后的 State(而不是修改旧的 State)。


在 iOS 中的核心组件映射

Redux 概念 iOS 中的实现 说明
State 一个结构体 (struct) 或类 包含整个应用当前所有数据的模型。必须是值类型struct)以确保不可变性。
Action 一个枚举 (enum) 描述所有可能改变状态的事件。每个 case 可以关联一些数据。
Reducer 一个函数 (function) (State, Action) -> State。根据 Action 生成新 State 的纯函数。
Store 一个单例或通过依赖注入的类 (class) 持有当前 State;接收并派发 Action;运行 Reducer 来更新 State;通知观察者。
View UIViewControllerSwiftUI.View 观察 State 的变化并重新渲染 UI;向 Store 派发用户交互产生的 Action。

一个简单的计数器示例 (SwiftUI + Combine)

让我们用一个经典的计数器例子来演示如何在 iOS (SwiftUI) 中实现 Redux。

第 1 步:定义 State

// 应用的状态。必须是结构体,以保证不可变性。
struct AppState {
    var count: Int = 0
}

第 2 步:定义 Action

// 所有能改变状态的动作。
enum Action {
    case increment
    case decrement
    case incrementBy(Int) // 关联值
}

第 3 步:定义 Reducer

// 这是一个纯函数:相同的输入,永远得到相同的输出,且无副作用。
func appReducer(state: AppState, action: Action) -> AppState {
    var newState = state // 复制当前状态(因为 state 是 struct,是值类型)
    
    switch action {
    case .increment:
        newState.count += 1
    case .decrement:
        newState.count -= 1
    case .incrementBy(let amount):
        newState.count += amount
    }
    // 返回一个全新的状态对象
    return newState
}

第 4 步:创建 Store

这是最关键的一步。Store 是大脑,它协调所有操作。

import Combine

// Store 是一个 ObservableObject,这样 SwiftUI 视图才能观察它的变化。
class Store: ObservableObject {
    // 发布者:State 的变化会驱动 UI 更新
    @Published private(set) var state: AppState
    
    // Reducer 函数
    private let reducer: (AppState, Action) -> AppState
    
    init(initialState: AppState, reducer: @escaping (AppState, Action) -> AppState) {
        self.state = initialState
        self.reducer = reducer
    }
    
    // 唯一能改变状态的方法:派发 Action
    func dispatch(_ action: Action) {
        // 在主线程上同步更新状态,保证线程安全
        DispatchQueue.main.async {
            // 调用 reducer 生成新状态,并替换旧状态
            self.state = self.reducer(self.state, action)
            // 由于 @Published 属性发生变化,objectWillChange 会自动发出信号,
            // 通知所有观察的 View 更新。
        }
    }
}

第 5 步:在 SwiftUI View 中使用

struct CounterView: View {
    // 从环境中获取或直接注入 Store
    @EnvironmentObject var store: Store
    
    var body: some View {
        VStack {
            Text("Count: \(store.state.count)") // 从 Store 中读取状态
                .font(.largeTitle)
            
            HStack {
                // 向 Store 派发 Action
                Button("-") { store.dispatch(.decrement) }
                Button("+") { store.dispatch(.increment) }
                Button("+10") { store.dispatch(.incrementBy(10)) }
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

第 6 步:在入口点设置 Store

@main
struct MyApp: App {
    // 创建全局唯一的 Store,并注入到环境中
    let store = Store(initialState: AppState(), reducer: appReducer)
    
    var body: some Scene {
        WindowGroup {
            CounterView()
                .environmentObject(store) // 注入 Store
        }
    }
}

处理副作用 (Side Effects)

上面的 Reducer 是纯的,但真实应用需要副作用(如网络请求、读写磁盘等)。纯函数不能处理这些。解决方案是使用 “Effect” 模式(这正是 The Composable Architecture (TCA) 等库的核心)。

  1. 让 Reducer 返回一个 Effect:Reducer 除了返回新 State,还返回一个描述副作用的 Effect 对象。
  2. Store 执行 Effect:Store 在运行 Reducer 后,会执行返回的 Effect(比如发起网络请求)。
  3. Effect 完成后派发新 Action:当网络请求完成时,Effect 会自动派发一个新的 Action(如 .dataLoaded(Result)),这个 Action 会再次通过 Reducer 来更新状态。

简化版的 Effect 示例:

// 1. 扩展 Action 来包含副作用结果
enum Action {
    case increment
    case fetchButtonTapped
    case dataLoaded(Result<Data, Error>)
}

// 2. Reducer 可以返回一个额外的 Effect
func appReducer(state: AppState, action: Action) -> (AppState, Effect<Action>?) {
    var newState = state
    var effect: Effect<Action>? = nil
    
    switch action {
    case .fetchButtonTapped:
        effect = Effect { // 返回一个发起网络请求的 Effect
            // 模拟网络请求
            let result = Result { try await fetchDataFromNetwork() }
            return Action.dataLoaded(result)
        }
    case .dataLoaded(.success(let data)):
        newState.data = data
    case .dataLoaded(.failure(let error)):
        newState.error = error
    ...
    }
    return (newState, effect)
}

// 3. Store 的 dispatch 方法需要处理返回的 Effect 并执行它。

在 UIKit 中的使用

在 UIKit 中,概念完全相同,但需要手动实现状态观察。

  1. Store 仍然是一个中心化的类。
  2. ViewControllers 需要订阅 Store 的状态变化(例如,使用 Combine 的 $state.sink {...})。
  3. 在订阅的闭包中,根据新的 State 来手动更新 UI(设置 label 的 text、刷新 table view 等)。
  4. 在 IBAction 或代理方法中,调用 store.dispatch(...)

优缺点分析

优点:

  • 可预测性:状态变化非常清晰,总是 Action -> Reducer -> New State
  • 可调试性:可以轻松记录和重放每一个 Action 和状态快照。
  • 可测试性:Reducer 是纯函数,极易测试。只需给定输入,断言输出。
  • 单一数据源:避免了状态在不同组件间同步的难题。

缺点:

  • 样板代码 (Boilerplate):需要为每个功能定义 State, Action, Reducer,略显繁琐。
  • 学习曲线:对于新手来说,概念相对复杂。
  • 性能:对于非常庞大的状态树,频繁复制整个 state 可能带来性能开销(但通常不是问题)。

总结与建议

  • 对于简单应用:直接使用 @PublishedObservableObject 可能更轻量。
  • 对于中大型复杂应用:Redux 架构能极大地提升代码的可维护性和可预测性。
  • 推荐使用库:手动实现完整的 Redux 和副作用处理比较复杂。强烈推荐使用 The Composable Architecture (TCA),它是一个非常成熟、强大的 Swift 库,完美实现了 Redux 模式,并提供了出色的工具和测试支持。它大大减少了样板代码,是 iOS 上实践 Redux 的最佳选择。

在同步代码里调用 async/await:Task 就是你的“任意门”

作者 unravel2025
2025年9月12日 19:51

场景:同步上下文想调异步函数

func fetchData() async -> String { /* 网络请求 */ }

struct ContentView: View {
    var body: some View {
        Button("Load") {
            await fetchData()   // ❌ 编译错误:同步闭包里不能用 await
        }
    }
}

错误提示:

Cannot pass function of type '() async -> Void' to parameter expecting synchronous function type

官方逃生舱:包一层 Task {}

Button("Load") {
    Task {                      // ✅ 立即启动异步任务
        let data = await fetchData()
        print(data)
    }
}
  • Task 会立刻在新协程里执行闭包,不要求外部上下文支持并发。
  • 无需手动持有 Task 实例,除非你想取消或等待它完成。

Task 的 3 种常见“同步→异步”桥梁模式

模式 代码片段 用途
Fire-and-forget Task { await work() } 按钮点击、日志上报
取消友好 Task { [weak self] in … } ViewController/View 生命周期
Delegate/App 生命周期 Task { await requestPermissions() } application(_:didFinishLaunchingWithOptions:)

实战 1:带取消的 SwiftUI 任务

struct ContentView: View {
    @State private var task: Task<Void, Never>?   // 1️⃣ 持有任务
    
    var body: some View {
        VStack {
            Button("Start") {
                task = Task {                     // 2️⃣ 创建并保存
                    let data = await fetchData()
                    print(data)
                }
            }
            Button("Cancel") {
                task?.cancel()                    // 3️⃣ 外部取消
                task = nil
            }
        }
        .onDisappear {
            task?.cancel()                        // 4️⃣ 生命周期清理
        }
    }
}

记住:视图消失时必须取消,否则后台任务可能访问已销毁的 @State

实战 2:AppDelegate 里请求推送权限

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        
        // 同步方法内直接启动异步任务
        Task {
            do {
                try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound])
                DispatchQueue.main.async {
                    application.registerForRemoteNotifications()
                }
            } catch {
                print("权限请求失败: \(error)")
            }
        }
        
        return true
    }
}
  • Task 让“老派”的同步生命周期钩子也能享受结构化并发。
  • 不需要 awaitapplication(_:didFinishLaunchingWithOptions:) 返回值里,启动即忘即可。

闭包捕获注意事项

Button("Load") {
    Task { [weak self] in          // ✅ 防止循环引用
        guard let self else { return }
        self.model.data = await self.fetchData()
    }
}

在 View/ViewController/ViewModel 里使用 Task 时,养成 [weak self] 习惯,避免闭包持有整个视图层级。

一句话总结

“同步上下文想 await?包一层 Task {} 就行。”

它是 Swift 结构化并发的任意门:轻量、无样板、可取消。

只要记得生命周期对齐 + 弱引用捕获,就能在按钮、Delegate、App 生命周期里放心使用 async/await。

参考资料

  1. How to use async/await in synchronous Swift code with tasks

Swift 三目运算符指南:写法、场景与避坑

作者 unravel2025
2025年9月12日 12:32

什么是三目运算符?

三目运算符(ternary operator)是 if/else 的单行表达式版,语法模板:

<条件> ? <条件为真结果> : <条件为假结果>

必须同时给出真/假两个分支,否则编译器直接报错。

最小可运行示例

struct DemoView: View {
    @State private var username = ""

    var body: some View {
        Button("Submit") {}
            .tint(username.isEmpty ? .gray : .red)   // ← 三目
    }
}

username 为空时按钮呈灰色,否则红色。

一行代码完成“判断 + 赋值”,是 SwiftUI 里高频用法。

适用场景 checklist

✅ 推荐

  • 仅两个分支
  • 每个分支单个表达式
  • 表达式短且无副效应
  • 作为参数/赋值使用

❌ 不推荐

  • 嵌套三目 → 可读性断崖
  • 分支里再调函数/打印/网络请求
  • 一行超长 120+ 字符

if 表达式对比(Swift 5.9+)

Swift 5.9 起,if 也能当表达式用:

let color: Color = if username.isEmpty {
    .gray
} else {
    .red
}
维度 三目 if表达式
行数 1 行 多行
可读性 简洁但易过长 清晰
分支数 仅 2 可 else if
使用位置 任意表达式上下文 只能用于赋值/返回
嵌套 容易失控 结构清晰

结论:

  • 简单二选一 → 三目
  • 需要 else if / 长表达式 → if 表达式
  • 副作用或复杂逻辑 → 普通 if 语句

实战:SwiftUI 里常用的一行代码

Image(systemName: isOn ? "checkmark.circle.fill" : "circle")
    .foregroundColor(isOn ? .green : .gray)

Text("Remain: \(secondsLeft > 0 ? "\(secondsLeft)s" : "Done")")
    .fontWeight(secondsLeft > 0 ? .regular : .bold)

Circle()
    .fill(status == .loading ? AnyShapeStyle(.orange) : AnyShapeStyle(.blue))

踩坑指南

  1. 类型必须一致
   let x = flag ? 1 : 1.0        // ❌ 编译错误:Int vs Double
   let x = flag ? Double(1) : 1.0 // ✅
  1. 优先级陷阱
   print(false ? "A" : "B" + "!")   // 输出 B!,+ 优先级高于三目

推荐加括号:

   print((false ? "A" : "B") + "!")
  1. 嵌套地狱
   let color = a ? (b ? .red : .green) : (c ? .blue : .gray)

超过一层嵌套,立即改成 if 表达式或工厂方法。

小结:一句话口诀

“二选一、短表达式、只取值,用三目;否则换 if。”

把三目当作“单行表达式糖”,而非“万能 if”,就能在简洁与可读之间找到最佳平衡点。祝你写出既短又稳的 Swift 代码!

一篇读懂 Swift 不透明类型:让带 associatedtype 的协议也能当返回值

作者 unravel2025
2025年9月12日 10:04

参考原文:Understanding opaque types and protocols with associatedtype in Swift

环境:Swift 6.2 + Xcode 26

why:带 associatedtype 的协议为何不能当返回值?

protocol Store {
    associatedtype Item
    func persist(item: Item)
}

// ❌ 编译失败:Protocol 'Store' can only be used as a generic constraint
func makeStore() -> Store { ... }
  • associatedtype 未被确定 → 编译期无法决定具体内存布局。
  • Swift 拒绝“协议当作类型”使用,除非用泛型或 opaque 类型。

传统 workaround:泛型约束

func makeStore<T: Store>() -> T { ... }   // ✅ 可行,但调用端要写类型

痛点:

  • 调用处仍需显式指定类型
  • 代码膨胀(每种 T 一份实现)
  • 无法隐藏实现细节(返回类型泄露)

Swift 5.1+ 解法:opaque 类型 (some)

func makeStore() -> some Store { 
    return UserDefaultsStore()   // 具体类型被隐藏,调用端只认 Store 协议
}
  • 返回类型由编译器推断,调用者无需知道 UserDefaultsStore
  • 内存布局确定(编译期知道真实类型大小)。
  • 语法糖:等价于“泛型参数由编译器自动填充”。

opaque vs 泛型 vs 存在容器(any)速查表

特性 具体类型 内存布局 性能 隐藏实现 调用端写法 适用场景
opaque (some) 编译期已知 静态派发,无额外开销 最优 最简洁 返回值/参数想隐藏具体类型
泛型 <T: Store> 调用者指定 静态 最优 需显式类型 需要多类型复用实现
存在容器 (any Store) 运行时动态 存在容器(1 ptr + metadata) 动态派发,略慢 同 opaque 需要运行时异构集合

实战:同一函数三种写法对比

// 1. 泛型 — 调用者决定类型
func makeStore<T: Store>() -> T { T() }

// 2. Opaque — 实现者决定类型,调用者无感
func makeStore() -> some Store { UserDefaultsStore() }

// 3. 存在容器 — 运行时多态
func makeStore() -> any Store { UserDefaultsStore() }

调用侧:

let s1: some Store = makeStore()   // 编译期知道真实类型
let s2: any Store  = makeStore()   // 运行时才知道

什么时候选 opaque?

  1. 只想隐藏返回类型,不关心具体实现
  2. 性能敏感(避免存在容器额外间接层)
  3. API 向前兼容——日后可无缝换成别的具体类型,不破坏二进制接口

一句话总结

带 associatedtype 的协议不能当返回值?

some Protocol 就行!

它 = “编译期泛型” + “实现细节隐藏” + “零成本抽象”,

让协议真正像“类型”一样使用,而无需把泛型复杂性抛给调用者。

`@dynamicCallable`:把 Swift 对象当函数喊

作者 unravel2025
2025年9月12日 10:02

一、为什么需要“假装函数”?

有时我们想让一个值看起来就是函数,从而写出更自然的 DSL:

logger("App launched")           // 像 print
let person = creator(name: "A")  // 像工厂

@dynamicCallable 就是 Swift 给的“变身器”: “让实例像函数一样被 call,背后转到你定义的方法。”

二、核心机制:两条魔法方法

方法 对应调用语法 参数类型
dynamicallyCall(withArguments:) instance(a, b, c) [T]
dynamicallyCall(withKeywordArguments:) instance(name: x, age: y) KeyValuePairs<String, T>

只需实现任意一个或两个,即可开启 callable 语法。

三、最小可运行示例:Hello Greeter

  1. 传统写法
struct Greeter {
    func sayHello(to name: String) -> String {
        "Hello, \(name)!"
    }
}
let g = Greeter()
g.sayHello(to: "Alice")
  1. @dynamicCallable 变身
@dynamicCallable
struct Greeter {
    func dynamicallyCall(withArguments names: [String]) -> String {
        guard let first = names.first else { return "Hello, World!" }
        return "Hello, \(first)!"
    }
}

let g = Greeter()
g("Alice")        // "Hello, Alice!"
g()               // "Hello, World!"

变化:

g.sayHello(to:) → 直接 g(...),更像函数。

四、带标签参数:KeyValuePairs 实战

@dynamicCallable
struct PersonCreator {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) -> String {
        args.map { "\($0) is \($1)" }.joined(separator: ", ")
    }
}

let creator = PersonCreator()
creator(name: "John")                    // "name is John"
creator(name: "Alice", age: "25", city: "NYC") // "name is Alice, age is 25, city is NYC"

KeyValuePairs 保持标签顺序,比 Dictionary 更适合 DSL。

五、真实场景:可调用 Logger

@dynamicCallable
struct Logger {
    func dynamicallyCall(withArguments msgs: [String]) {
        print("[\(Date())] \(msgs.joined(separator: " "))")
    }
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) {
        let pairs = args.map { "\($0): \($1)" }.joined(separator: ", ")
        print("[\(Date())] \(pairs)")
    }
}

let log = Logger()
log("App", "started")                       // 简写
log(event: "login", user: "john", status: "ok")  // 结构化

输出:

[2025-09-05 14:22:10 +0000] App started
[2025-09-05 14:22:10 +0000] event: login, user: john, status: ok

六、与 Swift 6 并发兼容

@dynamicCallable 方法默认继承调用者的隔离域:

@MainActor
class ViewModel {
    @dynamicCallable
    struct Logger {
        func dynamicallyCall(withArguments msgs: [String]) {
            print("[Main] \(msgs.joined())")
        }
    }
    
    func tap() {
        let log = Logger()
        log("Button tapped")   // 主线程执行,安全
    }
}

→ 无需额外标注,自动遵循隔离规则。

七、什么时候用 / 不用

✅ 适合

  • 构建DSL(日志、配置、SQL、Shell)
  • 希望 API 像函数一样自然
  • 参数数量或标签不固定

❌ 不适合

  • 普通业务逻辑——直接方法更清晰
  • 需要强类型检查(编译期无法看到具体标签)
  • 团队对“魔法”语法接受度低

八、常见编译错误对照

错误原因修复 Member dynamicallyCall has unsupported type方法签名不对改为官方模板 [T]KeyValuePairs<String, T> Call arguments don't match any overload参数类型/数量不符检查实参类型与 withArguments/withKeywordArguments 是否一致 Cannot call value of non-function type忘记加 @dynamicCallable补上属性


九、小结:一句话背下来

@dynamicCallable = “把实例当函数喊”,背后转到你写的 dynamicallyCall

它让 API 更自然、让 DSL 更优雅,但也别滥用——清晰比酷炫更重要。

记住口诀:

“要 callable,加 @dynamicCallable; positional 用数组,labeled 用 KeyValuePairs。”

下次写配置、日志、DSL 时,不妨让它“像个函数”——一声 call,就搞定。

仓颉语言Option 的“问号”全景图—— 一个 `?` 与 `.` `()` `[]` `{}` 的组合写法

作者 unravel2025
2025年9月12日 09:59

一句话先给结论

在仓颉里,? 是 Option 的“安全导航符”;它能无缝插进任何“取值/调用/下标/代码块”场景,遇到 None 就立即短路返回 None,否则继续往后走。

下面带你一次看全所有花式用法。

基础规则速查表

符号 意义 是否可接 ? 短路行为
. 成员访问 None 时跳过成员访问
() 函数/方法调用 None 时跳过调用
[] 下标取值 None 时跳过取下标
{} lambda/闭包 None 时跳过闭包执行

场景示例

方法返回Option类型

func readFile(): ?String {
    None<String>
}
let cache: Option<String> = readFile()

?. 访问成员变量

import std.random.Random

let rand = Random()

struct User {
    User(let name: String) {}
}

func getUser(): ?User {
    if (rand.nextBool()) {
        User("unravel")
    } else {
        None
    }
}

let u: Option<User> = getUser() // 可能 None
let len = u?.name.size // Option<Int>

?() 调用函数

func twice(x: Int64): Int64 {
    x * 2
}

let f: Option<(Int64) -> Int64> = Some(twice)
let r = f?(10) // Some(20)

?[] 访问下标

// 安全下标
let arr: Option<Array<Int64>> = Some([7, 8, 9])
// arr存在时才可以返回值,不能保证下标越界的崩溃
let second = arr?[1] // Some(8)

?{} 传入尾随闭包

type IntCallFunc = ((Int64) -> Int64) -> Int64

func opFunc(action: (Int64) -> Int64): Int64 {
    action(5)
}

let op: ?IntCallFunc = opFunc
let doubled = op? {
    x => x * 2
} // Some(10)

链式混写——一次写完所有符号

type IntCallFunc = ((Int64) -> Int64) -> Int64

func opFunc(action: (Int64) -> Int64): Int64 {
    action(5)
}
class ContainerItem {
    func compute(): ?IntCallFunc {
        return opFunc
    }
}

class Container {
    Container(public let items: Array<?ContainerItem>) {}
}

// 链式混写——一次写完所有符号
let deep: Option<Container> = Container([ContainerItem()])
// 安全导航:取值→下标→调用→闭包
let result = deep?.items[0]?.compute()? {x => x + 1}

与 match 组合:把最终 None 转成默认值

当然,更建议使用coalescing操作符。coalescing操作符和下面的实现等价

let final0 = result ?? -1

let final = match (result) {
    case Some(v) => v
    case _ => -1
}

配合标准库ifSome、ifNone使用

ifSome(cache) {
    c => println("拿到缓存 ${c}")
}

ifNone(cache) {
    println("没有拿到缓存")
}

多层嵌套 struct 一路点下去

struct A {
    A(let b: Option<B>) {}
}

struct B {
    B(let c: Option<C>) {}
}

struct C {
    C(let value: Int64) {}
}

let a: Option<A> = A(B(C(64)))
let v = a?.b?.c?.value // Option<Int64>

数组元素本身是 Option

let opts: Array<Option<Int64>> = [Some(1), None, Some(3)]
let heads = opts[1] // 先取下标 → 得到 None
ifSome(heads) {
    v => println("heads的值是${v}")
}

高阶函数指针

type Fn = (Int64) -> Option<Int64>

let maybeFn: Option<Fn> = Some({x => Some(x * 3)})
let out = maybeFn?(7) // Some(21)

自定义下标运算符

extend<T> Array<T> {
    public operator func [](idx: Int64, action: (T) -> T): T {
        if (idx >= 0 && idx < size) {
            let v = this[idx]
            action(v)
        } else {
            throw Exception("下标越界")
        }
    }
} // Some(30)

let data = Some([10, 20])
let x = data?[1, {
        v => v + 10
    }]

一张图记住所有写法

Option<T> 变量 ──→ ? ──→ .member     → Option<U>
                 │     │()
                 │     │[]
                 │     │{ ... }
                 │     │
                 └─→ 任意一环 None 就整体返回 None

iOS26适配指南之UISlider

作者 YungFan
2025年9月12日 09:00

介绍

在 iOS 26 中,UISlider 迎来了两项重要更新:

  • 增加了类型为UISlider.Style的属性sliderStyle,用于设置拖拽时的样式。
  • 增加了类型为UISlider.TrackConfiguration?的属性trackConfiguration,用于添加刻度,并且支持自定义刻度。

这两个属性结合使用,可以让 UISlider 从传统的“连续滑块”进化为带刻度的选择器,常见于音量调节、亮度调节、进度选择、配置项选择等场景。

使用

  • 代码。
import UIKit

class ViewController: UIViewController {
    lazy var basicTickSlider: UISlider = {
        let slider = UISlider()
        slider.value = 0.5
        slider.addTarget(self, action: #selector(valueChanged), for: .valueChanged)
        // iOS26新增
        slider.sliderStyle = .default
        // iOS26新增,刻度数量
        var config = UISlider.TrackConfiguration(numberOfTicks: 10)
        config.allowsTickValuesOnly = true
        slider.trackConfiguration = config
        slider.translatesAutoresizingMaskIntoConstraints = false
        return slider
    }()
    lazy var customTickSlider: UISlider = {
        let slider = UISlider()
        slider.value = 0.5
        slider.addTarget(self, action: #selector(valueChanged), for: .valueChanged)
        slider.sliderStyle = .thumbless
        // iOS26新增,自定义刻度
        let customTicks = [
            UISlider.TrackConfiguration.Tick(position: 0),
            UISlider.TrackConfiguration.Tick(position: 0.1),
            UISlider.TrackConfiguration.Tick(position: 0.3),
            UISlider.TrackConfiguration.Tick(position: 0.6),
            UISlider.TrackConfiguration.Tick(position: 1.0)
        ]
        let config = UISlider.TrackConfiguration(allowsTickValuesOnly: true, ticks: customTicks)
        slider.trackConfiguration = config
        slider.translatesAutoresizingMaskIntoConstraints = false
        return slider
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(basicTickSlider)
        view.addSubview(customTickSlider)

        NSLayoutConstraint.activate([
            basicTickSlider.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
            basicTickSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            basicTickSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            basicTickSlider.heightAnchor.constraint(equalToConstant: 44),
            customTickSlider.topAnchor.constraint(equalTo: basicTickSlider.bottomAnchor, constant: 40),
            customTickSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            customTickSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
        ])
    }

    // MARK: 滑块内容变化事件
    @objc func valueChanged(_ sender: Any) {
        print(#function)
    }
}
  • 效果。

效果.gif

❌
❌