普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月14日首页

Swift 基础语法全景(一):从变量到类型安全

作者 unravel2025
2025年10月14日 07:58

常量与变量:let vs var

  1. 声明语法
// 常量:一次赋值,终身不变
let maximumLoginAttempts = 10        // 最大尝试次数,业务上不允许修改

// 变量:可反复写入
var currentAttempt = 0               // 当前尝试次数,失败+1
  1. 延迟赋值

只要「第一次读取前」完成初始化即可,不必一行写完。

var randomGenerator = SystemRandomNumberGenerator()
let isDevEnvironment = randomGenerator.next() % 3 == 0
let timeout: Int
if isDevEnvironment {
    timeout = 100                   // 开发环境宽松一点
} else {
    timeout = 10                    // 生产环境严格
}
// 编译器会检查所有分支都赋值,否则报错
  1. 一次声明多个
let red = 0xFF0000, green = 0x00FF00, blue = 0x0000FF
var x = 0, y = 0, z = 0

命名规则:Unicode 可用,但别作死

✅ 合法

let π = 3.14159
let 欢迎 = "Hello"
let 🐶🐮 = "dogcow"

❌ 非法

let 3x = 1        // 不能以数字开头
let a-b = 0       // 不能含运算符
let private = 1   // 虽能编译,但与访问控制关键字冲突,别这么干

基本数据类型一览表

类型 说明 字面量示例 备注
Int 平台字长 42, -7 32 位平台 == Int32;64 位 == Int64
UInt 无符号平台字长 42 仅当位运算/内存布局时才用
Int8/16/32/64 指定位宽 127 与 C 交互、网络协议、二进制文件
Double 64 位浮点 3.14159, 1.25e2 默认推断类型
Float 32 位浮点 3.14 内存/带宽敏感场景
Bool 真/假 true, false 不能用 0/1 代替
String UTF-8 文本 "Hello" 值类型,拷贝即复制(写时优化)

整数字面量“花式写法”

let decimal = 17
let binary = 0b10001       // 0b 前缀
let octal = 0o21           // 0o 前缀
let hex = 0x11             // 0x 前缀

// 增加可读性
let oneMillion = 1_000_000
let rgb = 0xFF_FF_FF_00

类型推断与类型注解

  1. 推断
let meaningOfLife = 42        // 推断为 Int
let pi = 3.14159              // 推断为 Double(不是 Float)
  1. 显式注解
var message: String = "Hello" // 显式告诉编译器
// 如果不给初始值,必须写类型
var score: Int
score = 100
  1. 多变量同类型
var a, b, c: Double           // 3 个都是 Double

数值类型转换——“必须显式”

Swift 没有隐式类型转换,防止溢出 Bug。

let age: UInt8 = 25
let weight: UInt16 = 76

// 错误:age + weight          // 类型不一致
let total = UInt16(age) + weight // ✅ 显式构造

浮点与整数互转:

let x = 3
let d = Double(x) + 0.14159     // 3.14159

let fraction: Double = 4.75
let whole = Int(fraction)       // 4,截断(不会四舍五入)

类型别名 typealias——给长名字起小名

typealias Byte = UInt8
typealias AudioSample = UInt16

let maxAmplitude = AudioSample.min   // 0

工程场景:

  • 与 C API 交互时,把 UInt32 起别名叫 CRC32,语义清晰。
  • 以后底层类型换成 UInt64 时,改一行即可,业务层无感知。

Print & 字符串插值

let name = "Swift"
print("Hello, \(name)!")     // Hello, Swift!

// 自定义 terminator
print("Loading...", terminator: "")   // 不换行

注释:可嵌套的多行注释

/* 外层
   /* 内层 1
      /* 内层 2 */
   */
*/

利用嵌套,可以快速“整块注释”掉代码,而不用担心内部已有注释冲突。

分号:可加可不加

let a = 1; let b = 2          // 同一行多条语句才需要

小结 & 工程化思考

  1. 默认用 Int、Double,除非有明确位宽需求。
  2. 常量优先(let),减少可变状态。
  3. 命名用英文/中文均可,但团队要统一;CI 可加 --strict-conventions 检查。
  4. 类型转换显式写,让 Code Review 一眼看出截断/溢出风险。
  5. typealias 不仅为了“少打字”,更是语义抽象,跨平台迁移利器。

高通收购 Arduino:历史的轮回 | 肘子的 Swift 周报 #0106

作者 东坡肘子
2025年10月14日 07:54

issue106.webp

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

高通收购 Arduino:历史的轮回

上周,高通宣布收购知名开源硬件平台 Arduino,并同步发布首款搭载自家芯片的 Arduino UNO Q。与经典版本不同,UNO Q 采用了“双脑”架构——由运行 Linux 的 Qualcomm Dragonwing 处理器负责高性能计算,同时保留 STM32 微控制器以执行实时控制任务。这种设计无疑强大,却也悄然偏离了 Arduino 一直以来“简单、低成本、易上手”的初心。

尽管高通承诺保持 Arduino 的品牌独立与开源特性,但考虑到其在专利授权领域一贯的强势作风,以及深植于商业化的企业基因,社区的担忧并非杞人忧天。44 美元的定价,也让这款产品距离 Arduino 最初面向教育与创客的定位愈发遥远。

有趣的是,“Arduino” 这个名字本身就带着宿命的意味。2005 年项目诞生时,创始人们常在意大利伊夫雷亚的 Bar di Re Arduino (阿尔杜因国王酒吧)聚会,遂以此命名。而那位意大利国王 Arduin of Ivrea,曾代表本土势力反抗神圣罗马帝国的统治,坚守 12 年后终告退位。自此,意大利北部并入帝国版图,失去独立地位近 850 年。

千年之后,以反抗者命名的 Arduino,在独立运营 20 年后,同样被美国科技“帝国”收编。这种历史的轮回,令人唏嘘。或许在命名的那一刻,命运的伏笔已悄然埋下。

然而,正如 Arduin 国王虽败犹荣,其反抗精神流传千年。愿 Arduino 所代表的开源理想与创客精神,也能超越公司所有权的变迁,继续在世界各地延续与发芽。

这场收购映照出开源世界的恒久困境:如何在坚持理想主义的同时,实现商业的可持续?

也许,实体终将归于凡尘,而唯有精神才能长久流传。

前一期内容全部周报列表

近期推荐

静默执行后台任务 (Do Job Silently)

在 iOS 中,应用进入后台后,系统会严格限制其资源使用。若开发者希望执行数据刷新或周期性计算等任务,可以借助后台任务机制,让系统在合适的时机自动触发相应逻辑。Kyryl Horbushko 在本文中详细介绍了后台任务的两种实现方式:传统的 BGTaskScheduler 与更现代的 .backgroundTask 修饰符。文章的亮点在于提供了完整的配置清单、调试技巧与常见陷阱的规避方案——包括使用 LLDB 命令模拟任务触发、通过本地通知获得可视化反馈等实用方法。作者认为,对于新的 SwiftUI 项目,.backgroundTask 更契合声明式编程范式,是更自然的首选方案。


OpenSwiftUI 集成指南 (How to Integrate OpenSwiftUI into Your Project)

OpenSwiftUI 是一个面向研究与教育的 SwiftUI 开源实现。随着框架的不断完善,越来越多的开发者开始关注并尝试使用它。但在实际集成时,你会发现它并非“即插即用”——需要手动处理私有框架依赖。为此,项目主要开发者 Kyle Ye 撰写了本文,详细介绍了通过 Swift Package Manager 集成 OpenSwiftUI 的完整步骤,包括处理 DarwinPrivateFrameworks 等关键环节。

OpenSwiftUI 是少数能让开发者深入理解 SwiftUI 内部渲染机制的实践项目。现阶段,它更适合作为探索工具,而非生产方案。


Foundation Models 框架实操问答 (iOS 26: Foundation Model Framework - Code-Along Q&A)

Apple 在今年推出了全新的开发者教育形式——Code-Along,这是一种结合实时编码演示与即时答疑的在线教学活动。在 9 月举行的首场 Code-Along 中,Apple 工程师用两个小时详细演示了如何将 iOS 26 的 Foundation Models 框架集成到应用中,从基础 API 调用到性能优化技巧,同时实时回答了众多的开发者问题。

Anton Gubarenko 对本次活动问答记录进行了详尽的整理,内容涵盖了模型的 4K token 上下文限制、1.2GB 内存占用、结构化输出(Generable)、流式响应、并发处理等关键技术细节,以及开发者最关心的隐私保护、App Store 审核等实践问题。


在 SwiftUI 中使用 SwiftData 实现搜索 (Performing Search with SwiftData in a SwiftUI app)

由于 SwiftData 的 @Query 不支持在视图内动态更新谓词,因此在使用 searchable 构建搜索功能时,需要进行一些额外处理。本文中,Letizia Granata 提出了一个巧妙的解决方案:通过分离视图结构——主视图负责管理搜索状态,子视图负责处理动态查询,从而优雅地解决了这一限制。她还建议在谓词中使用 localizedStandardContains 进行比较,以忽略大小写并提升本地化搜索体验。


为 Toggle 添加动态图标覆盖层 (SwiftUI Toggle with Dynamic Image Overlay)

虽然开发者可以通过自定义 ToggleStyle 完全控制 Toggle 的外观,但这往往意味着需要重新实现所有系统原生行为——包括尺寸适配、色调处理和动画效果。Artem Mirzabekian 在本文中展示了一种更务实的方案:保留原生 Toggle,通过 GeometryReader 获取尺寸、DragGesture 捕获触摸位置,以 overlay 的方式添加能够响应用户交互的动态图标。这种扩展而非重写的思路,既保持了系统一致性,又实现了视觉增强。


Apple 平台的 macOS DNA (Apple Platforms Runs on macOS DNA)

为什么 Swift 开发中会遇到 NS 前缀?为什么在 iOS 设备上,TARGET_OS_MAC 也会返回 true?这些看似奇怪的设计其实都有其历史渊源。Uwais Alqadri 在本文中探讨了 Apple 平台架构的三个关键节点:NeXTSTEP 合并带来的 NS 前缀与 Objective-C 体系、Darwin 作为共享的 Unix 基础,以及各平台实际上都“运行在” macOS 技术栈之上的分层架构。

了解这段历史,不仅能解释那些“反直觉”的设计决策,也能帮助我们正确使用平台条件编译,并理解这些特性为何至今仍然存在、并且难以改变。

工具

Swift Profile Recorder:无需系统权限的性能分析利器

Swift Profile Recorder 是 Apple 开源的进程内采样分析器,专为 Swift 服务端应用设计,已在 Apple 内部大规模使用多年。与传统性能分析工具(如 eBPF、DTrace)需要系统特权不同,它以 Swift Package 形式直接运行在应用进程内部,无需额外权限即可进行 On-CPU 和 Off-CPU 分析。这使其能够在 Kubernetes、Docker 容器等受限环境中正常工作,只需通过 curl 命令即可采集性能样本,并支持 Speedscope、Firefox Profiler 等主流工具可视化。

该项目的“零权限、易集成、跨平台”特性,让生产环境的性能分析不再是特权环境的专属。想深入了解其背景和 Apple 的实践经验,推荐阅读 Johannes WeissMitchell Allison 撰写的 Introducing Swift Profile Recorder: Identifying Performance Bottlenecks in Production


RichText:让文本与视图自由混排的 SwiftUI 组件

SwiftUI 的 Text 无法自由嵌入可交互视图,文本选择体验也不够好。由 LiYanan 开发的 RichText 通过声明式语法实现文本与视图混排,基于 TextKit 2 精确排版,嵌入的视图(例如 Button)完全保留交互能力,同时支持流畅的文本选择和复制。

TextView {
      Text("Hi, This is **RichText**.")   // Markdown 会被解析
      " Hello "                           // 普通字符串
      Button("Tap Me") {                  // 完全可交互的按钮
          print("Button Clicked")
      }
      .id("button")                       // 建议所有视图都加 id
      Text(.now, style: .timer)           // 动态文本
          .id("timer")                    // 通过 id 保持为视图以维持动态更新
}

Foundation Models Playgrounds

Ivan Campos 构建并维护的 Playgrounds 集合,展示如何调用 Apple Foundation Models 框架完成对话、摘要、创作、工具调用等场景。依主题划分示例:聊天对话、摘要解释、内容生成、代码与数据、图文多模态、安全评测、垂直工具、智能体模式等,每个 Playground 都聚焦一个能力点。

image-20251011081702134

往期内容

THANK YOU

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

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

昨天 — 2025年10月13日首页

高通收购 Arduino:历史的轮回 - 肘子的 Swift 周报 #106

作者 Fatbobman
2025年10月13日 22:00

上周,高通宣布收购知名开源硬件平台 Arduino,并同步发布首款搭载自家芯片的 Arduino UNO Q。与经典版本不同,UNO Q 采用了“双脑”架构——由运行 Linux 的 Qualcomm Dragonwing 处理器负责高性能计算,同时保留 STM32 微控制器以执行实时控制任务。这种设计无疑强大,却也悄然偏离了 Arduino 一直以来“简单、低成本、易上手”的初心。

昨天以前首页

浪浪山 iOS 奇遇记:给 APP 裹上 Liquid Glass “琉璃罩”(上集)

2025年10月7日 13:05

在这里插入图片描述

引子

浪浪山的朝阳刚爬上山头,小妖怪阿强就抱着他那台快被摸包浆的 MacBook,跟阿花蹲在老桃树下唉声叹气。

在这里插入图片描述

山大王昨天拍着青石桌下令,三天内必须给 “浪浪山访客登记 APP” 换上 iOS 26 新出的Liquid Glass设计,要是搞不定,俩人这个月的桃干俸禄就得全扣光。

在本篇奇遇记中,您将学到如下内容:

  • 引子
  • 🔍 先搞明白:Liquid Glass 这 “妖法” 该啥时候用?
  • 🛠️ 动手试试:给 UI 元素裹上 Liquid Glass “琉璃罩”
  • 🎨 给 “琉璃罩” 上色:加 tint 调背景
  • ✨ 让按钮 “活” 起来:加 interactive “互动咒”
  • 上集尾声:山大王催进度,容器 “秘招” 待解锁

可这Liquid Glass到底是啥 “妖法”?俩小妖连门儿都没摸着,只能对着屏幕抓耳挠腮,连树上掉下来的桃毛都没心思拍。

在这里插入图片描述


🔍 先搞明白:Liquid Glass 这 “妖法” 该啥时候用?

阿花翻遍了 Apple 给的 “仙册”(其实就是开发者文档),终于指着一行字喊:“阿强你看!Liquid Glass是 iOS 26 的新设计语言,说白了就是给 APP 盖一层‘琉璃罩’,但这罩子可不能乱盖!”

原来这 “琉璃罩” 的核心规矩是:只盖在 “浮” 于主界面上的元素,不能裹住整个 APP 的内容。阿强不信邪,偷偷给 APP 里的访客列表每一行都加了 “琉璃罩”,结果运行起来一看 —— 界面乱得像妖精打架,文字和背景糊在一块儿,比山大王喝醉后画的地图还难认。

“你这是犯了‘本末倒置’的错!” 阿花戳了戳屏幕,“主内容是访客信息,得清清楚楚;‘琉璃罩’该用在工具栏、标签栏、浮标按钮这些‘外挂’元素上,就像浪浪山入口的岗亭,得盖层罩子挡雨,但不能把山路都罩起来啊!”

在这里插入图片描述

说着阿花打开参考 APP “Maxine”(据说是山外神仙做的健身 APP),指着屏幕底部:“你看这默认的标签栏,就是Liquid Glass做的‘琉璃罩’,盖在列表上面不挡内容;还有那个浮标加号,也裹了层薄罩,就是背景太亮看不太清 —— 这才是正确用法!”

在这里插入图片描述

阿强摸了摸后脑勺:“原来如此!那要是实在不想用这‘琉璃罩’咋办?” 阿花又翻了翻 “仙册”:“Apple 留了个‘逃生舱’,下一个大版本前都能用,但山大王要新效果,咱躲不过咯!”

在这里插入图片描述


想要进一步了解如何在 iOS 26 中让 App 界面不适配液体玻璃效果的方法,请小伙伴们移步如下链接观赏精彩的内容:


🛠️ 动手试试:给 UI 元素裹上 Liquid Glass “琉璃罩”

既然躲不过,俩小妖决定先从一个小功能下手 —— 复刻山外早已失传的 “Path APP” 按钮,给它裹上Liquid Glass

在这里插入图片描述

阿强从 GitHub 上扒来了起始代码(据说那是山外神仙留下的 “秘籍”),代码长这样,阿花还贴心加了中文注释:

struct ContentView: View {
    // 控制按钮展开/收起的状态,就像控制桃树结果子的开关
    @State private var isExpanded = false
    
    var body: some View {
        // ZStack:把背景图和按钮叠放,类似先铺桃叶再放果子
        ZStack(alignment: .bottomTrailing) {
            Color
                .clear
                .overlay(
                    // 背景图:浪浪山的风景图,铺满整个屏幕
                    Image("bg_img")
                        .resizable()
                        .scaledToFill()
                        .edgesIgnoringSafeArea(.all)
                )

            // 四个功能按钮:首页、写字、聊天、邮件
            button(type: .home)
            button(type: .write)
            button(type: .chat)
            button(type: .email)

            // 主按钮:点一下展开/收起其他按钮,像打开果篮的开关
            Button {
                // 加动画:让按钮动起来不生硬,类似果子落地的缓冲
                withAnimation {
                    isExpanded.toggle()
                }
            } label: {
                Label("Home", systemImage: "list.bullet")
                    .labelStyle(.iconOnly) // 只显示图标,不显示文字
                    .frame(width: 50, height: 50) // 按钮大小:像个小桃儿
                    .background(Circle().fill(.purple)) // 紫色圆形背景
                    .foregroundColor(.white) // 图标白色
            }.padding(32) // 离屏幕边缘留点空,不然像贴在悬崖边
        }
    }

    // 自定义按钮方法:根据类型返回不同按钮(首页、写字等)
    private func button(type: ButtonType) -> some View {
        return Button {} label: {
            Label(type.label, systemImage: type.systemImage)
                .labelStyle(.iconOnly)
                .frame(width: 50, height:50)
                .background(Circle().fill(.white)) // 白色背景
        }
        .padding(32)
        .offset(type.offset(expanded: isExpanded)) // 按钮展开时的位置偏移
        .animation(.spring(duration: type.duration, bounce: 0.2)) // 弹簧动画,有点弹性
    }
}

“这按钮现在就是普通的‘硬疙瘩’,咱给它裹上Liquid Glass试试!” 阿强说着,在主按钮后面加了个glassEffect() 修饰符,代码变成这样:

Button {
    withAnimation {
        isExpanded.toggle()
    }
} label: {
    Label("Home", systemImage: "list.bullet")
        .labelStyle(.iconOnly)
        .frame(width: 50, height: 50)
        .background(Circle().fill(.purple))
        .foregroundColor(.white)
}
// 加上Liquid Glass“琉璃罩”!
.glassEffect()
.padding(32)

结果运行一看 —— 啥 “琉璃罩” 都没有!按钮还是原来的紫色硬疙瘩。

在这里插入图片描述

在这里插入图片描述

阿花凑过来一看,突然笑出声:“你这是把‘隐身符贴在铠甲外面’啊!按钮有个紫色背景,把glassEffect全挡住了,得把背景去掉才行哦!”

🎨 给 “琉璃罩” 上色:加 tint 调背景

阿强赶紧删掉.background(Circle().fill(.purple)),再运行 —— 按钮是透明了,但图标淡得像蒙了层雾,差点看不清。俩人对着屏幕嘀咕:“这是 beta 版的‘妖气’干扰,还是本来就这样啊?”

阿花又翻了翻 “仙册”:“还有个buttonStyle(.glass) 能试,不过这风格太‘死板’,像山大王给的统一制服,想绣个小桃花都不行。” 试了之后果然如此,自定义空间少得可怜。

“有了!” 阿花忽然拍了下手,“给glassEffect加个 tint(色调),就能给‘琉璃罩’上色,还能调透明度!” 说着就改了代码:

Button {
    withAnimation {
        isExpanded.toggle()
    }
} label: {
    Label("Home", systemImage: "list.bullet")
        .labelStyle(.iconOnly)
        .frame(width: 50, height: 50)
        .foregroundColor(.white)
}
// 给琉璃罩加紫色调,像后山的紫藤花汁
.glassEffect(.regular.tint(.purple))
.padding(32)

运行后一看 —— 嘿!按钮变成了带紫色的透明 “琉璃盏”,还自动是圆形的,不用再画背景了!

在这里插入图片描述

“Apple 这‘妖法’还挺贴心,知道圆形好看!” 阿强忍不住夸了一句。

在这里插入图片描述

但阿花觉得还不够通透:“再加点透明度,像晨雾里的琉璃盏才好看!” 于是又把颜色改成.purple.opacity(0.8),这下效果刚好 —— 既清楚又有 “玻璃感”,比山大王的琉璃酒杯还精致。

在这里插入图片描述

✨ 让按钮 “活” 起来:加 interactive “互动咒”

按钮好看了,但点下去没反应,像块死木头。

阿花又找到了 “仙册” 里的秘诀:给glassEffect加个interactive(),就能让按钮点的时候 “亮一下” 还稍微变大,像 “一碰就发光的仙果”!

改完的代码是这样:

Button {
    withAnimation {
        isExpanded.toggle()
    }
} label: {
    Label("Home", systemImage: "list.bullet")
        .labelStyle(.iconOnly)
        .frame(width: 50, height: 50)
        .foregroundColor(.white)
}
// 加了interactive,点的时候会有 shimmer 效果还会变大
.glassEffect(.regular.tint(.purple.opacity(0.8)).interactive())
.padding(32)

在这里插入图片描述

俩人轮番点了点,都笑了:“这下有那味儿了!像摸了会发光的萤火虫,比山大王的夜明珠还灵!”

在这里插入图片描述

可高兴没多久,阿强又皱起眉:“虽然互动有了,但这些按钮还是各自独立的,像散落在石桌上的果子,没‘液态’那感觉 ——Apple 说的‘液体玻璃’,得像流水似的融在一起才对呀!”

上集尾声:山大王催进度,容器 “秘招” 待解锁

阿花盯着屏幕忽然眼睛一亮:“‘仙册’里提了个GlassEffectContainer!把所有按钮放进这个‘容器’里,它们靠近时就会像融在一起的糖浆,动起来也会像流水似的!”

在这里插入图片描述

俩人刚要动手写代码,就听见山大王的大嗓门从远处传来:“俩小妖!APP 改得咋样了?再磨蹭午饭的肉干也没了!”

阿强赶紧把 MacBook 合上,阿花攥着写满笔记的桃叶小声说:“别急,下晌咱们就试这个GlassEffectContainer,肯定能让这些按钮像浪浪河的水似的,流着动起来!”

在这里插入图片描述

到底这 “容器” 咋用?按钮能不能真的 “液态” 起来?山大王的肉干能不能保住?咱们下集接着唠!

Sora 2:好模型,但未必是好生意 - 肘子的 Swift 周报 #105

作者 Fatbobman
2025年10月6日 22:00

一周前,OpenAI 发布了 Sora 2 模型,并同步推出了带有社交平台属性的 Sora 应用。目前,用户仅能通过 iOS 应用使用该模型生成视频。无论在视觉细节、人物形象、环境纹理,还是声画同步方面,Sora 2 相较早期版本都有显著提升。

SwiftUI Preferences 完全指南:从“向上传值”到 Swift 6 并发安全

作者 unravel2025
2025年9月30日 08:30

为什么需要 Preferences?

在 SwiftUI 里,向下传值有 @State@Binding@Environment,但向上传值一直是个空白。

典型痛点:

  1. 深层嵌套子视图想告诉祖先“我有多高”、“我是否出错”
  2. 如果逐层传递 Binding,会出现 Prop-Drilling(属性钻井)噩梦
  3. 祖先并不关心中间层,只想聚合子孙的信息

SwiftUI 给出的答案就是 Preferences:“子视图把数据塞进信封,祖先统一签收。”

官方已默默用它实现 navigationTitlepreferredColorSchemetabItem 等我们每天都在用的 API。

核心概念速览

名词 作用 类比
PreferenceKey 定义“信封”格式 字典的 key
.preference(key:value:) 子视图写信 塞进信封
.onPreferenceChange(_:perform:) 祖先收信 读信封
reduce 多封信合并策略 合并冲突规则

自定义 Preference

目标:做一个表单错误统计,子字段把错误信息抛给外层,外层统一显示“共 3 处错误”。

定义信封

struct ValidationMessagesKey: PreferenceKey {
    static let defaultValue: [String] = []          // ① Swift 6 必须用 let
    static func reduce(
        value: inout [String],                      // ② 累加容器
        nextValue: () -> [String]                   // ③ 子节点新值
    ) {
        value.append(contentsOf: nextValue())       // ④ 合并策略:追加
    }
}

子视图写信

struct ValidatedField: View {
    @Binding var text: String
    let rules: [ValidationRule]
    
    private var error: String? {
        rules.lazy.compactMap { $0.validate(text) }.first
    }
    
    var body: some View {
        VStack(alignment: .leading) {
            TextField("Input", text: $text)
                .textFieldStyle(.roundedBorder)
            if let error {
                Text(error)
                    .font(.caption)
                    .foregroundColor(.red)
            }
        }
        // ⑤ 把局部错误抛上去
        .preference(key: ValidationMessagesKey.self,
                    value: error.map { [$0] } ?? [])
    }
}

祖先收信

struct SignupForm: View {
    @State private var username = ""
    @State private var password = ""
    @State private var errors: [String] = []
    
    var body: some View {
        VStack(spacing: 16) {
            if !errors.isEmpty {
                Label("\(errors.count) 处错误", systemImage: "exclamationmark.triangle")
                    .foregroundColor(.red)
            }
            
            ValidatedField(text: $username, rules: [.minLength(3)])
            ValidatedField(text: $password, rules: [.minLength(8), .complex])
        }
        // ⑥ 收信并刷新 UI
        .onPreferenceChange(ValidationMessagesKey.self) { new in
            errors = new
        }
    }
}

运行效果:

用户在第一个字段输入“ab”→ 立即显示“1 处错误”;再输入密码“123”→ 显示“2 处错误”。

全程零 Binding 钻井,子视图完全解耦。

Swift 6 并发模式下的坑与官方解法

从 Swift 6 开始,编译器把所有全局可变状态视为潜在数据竞争。

Preferences 刚好踩中两条红线:

报错信息 原因 官方修复
Static property 'defaultValue' is not concurrency-safe static var被视为共享可变状态 把 var改成 let
Main actor-isolated property 'errors' can not be mutated... onPreferenceChange的闭包默认 @Sendable未限定 @MainActor ① Task { @MainActor in ... } ② 捕获 @Binding直接改 wrappedValue

两种写法对比:

// 写法 1:显式切回主线程
.onPreferenceChange(ValidationMessagesKey.self) { messages in
    Task { @MainActor in
        errors = messages
    }
}

// 写法 2:利用 Binding 的 Sendable 能力
.onPreferenceChange(ValidationMessagesKey.self)
{ [binding = $errors] messages in
    binding.wrappedValue = messages
}

苹果把后者标记为 @preconcurrency,意味着“以后会再review”,但目前两种写法均官方合法。

进阶技巧合集

  1. 层级覆盖策略

reduce 决定“谁说了算”。

例如 preferredColorScheme 采用“后者覆盖前者”策略:

static func reduce(value: inout Value, nextValue: () -> Value) {
    value = nextValue()   // 直接覆盖
}

想实现“最深层优先”就把新值放后面;想“最祖先优先”就保留旧值。

  1. 一次性收集几何信息
struct SizeKey: PreferenceKey {
    static let defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()   // 只保留最后一个
    }
}

// 子视图
.background(
    GeometryReader { proxy in
        Color.clear
            .preference(key: SizeKey.self, value: proxy.size)
    }
)

祖先即可拿到“实际渲染尺寸”,常用于瀑布流、瀑布布局。

  1. anchorPreference 联动拿到 CGRect
.anchorPreference(key: BoundsKey.self, value: .bounds) { $0 }

再用 .overlayPreferenceValue 即可在祖先层绘制箭头指示器、气泡引导等效果。

官方用 Preferences 实现的 API 速查

修饰符 对应 PreferenceKey 备注
.navigationTitle(_:) _NavigationTitleKey 私有,但可观测
.navigationBarTitleDisplayMode(_:) _NavigationTitleDisplayModeKey 私有
.preferredColorScheme(_:) PreferredColorSchemeKey 公有,可手动 .preference(key:value:)
.tabItem(_:) _TabItemKey 私有
.badge(_:) _TabItemBadgeKey 私有

公有 key 可直接使用,私有 key 可通过 Mirror 偷窥,但不建议,随时被苹果改。

性能 & 调试小贴士

  1. Preference 传播是懒加载

    只有当祖先订阅了 onPreferenceChange 才会触发 reduce,所以不用担心子视图过多导致瞬间爆炸。

  2. 调试打印

    reduce 里加 print 即可观察传播顺序:

   static func reduce(value: inout [String], nextValue: () -> [String]) {
       let new = nextValue()
       print(#function, "current:", value, "next:", new)
       value.append(contentsOf: new)
   }
  1. 巨量数据请用 Equatable 优化

    只要 Value 遵守 Equatable,SwiftUI 会自动 diff,避免重复刷新。

总结 & checklist

Preferences 是 SwiftUI 唯一原生、官方、线程安全的“向上传值”机制。

当你遇到以下场景,请第一时间想到它:

  • 子视图想告诉祖先“我有多高 / 我出错 / 我要标题”
  • 祖先需要聚合多个子孙的信息
  • 不想引入 ObservableObjectBinding 钻井
  • Swift 6 并发模式,需要编译器帮你排雷

口诀:“子写信,父收信,reduce 管合并;Swift 6 改 let,闭包加 @MainActor。”

苹果正在为系统级支持 MCP 做准备 - 肘子的 Swift 周报 #104

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

根据 9TO5Mac 的报道,苹果正在为其生态系统添加 MCP(Model Context Protocol)支持,以实现智能体 AI 功能。其实现路径与我们在周报 #077 中的设想十分吻合:通过开发者熟悉的 App Intents 框架进行系统级集成,既保持了苹果一贯追求的“可控、安全、完整”用户体验,又巧妙规避了让普通用户直接面对复杂 MCP 配置的门槛。

❌
❌