普通视图

发现新文章,点击刷新页面。
昨天以前戴铭的博客 - 星光社

Ioser 铭(iOS开发2015-2025)

作者 戴铭
2026年2月23日 11:18

春节期间我在整理笔记本的一些资料,感叹这些年真是一转眼就溜过去了。如果不记录点什么,就会感觉有点空。于是一口气整理了下这十年来的一些零散记录,主要是些看过、学过和研究过的东西,就是记录些经历。如果有些类似的体验,希望能有些共鸣。想看看擦肩过啥,也可以读读。

关于标题的 Ioser 实际是谐形梗,是一个 iOS 开发者自嘲的老梗了。铭既有铭文体的意思也有代表我(戴铭)的意思,一语双关。我现在在京东端基础技术组,有兴趣的,对端技术热爱的朋友可以联系我,目前业务、架构、性能和跨端动态化都有需要的。

小学的时候,父亲从学校借回一台 Apple II。那时候我对它没什么兴趣,只觉得这个米色的大盒子挺占地方。谁能想到,二十多年后我会因为乔布斯说的那段

成长过程中,总有人会告诉你,这个世界是不会改变的,而你呢,只要学会去适应这个世界,尽量少把自己撞的头破血流。但生活完全可以更广阔的,只要你认清一个简单的事实,那就是,你身边被称之为生活的这一切,都是由人创造的,但这些人并不比你更聪明。而你可以改变这一切,影响这一切,甚至亲手创造出能造福他人的东西。

而走上 iOS 开发这条路。

这些年开发中我发现苹果技术在性能方面有很多很棒的优化手段。例如,在 64 位架构下,指针其实用不了 64 位(通常只用 30-40 位),Apple 将剩下的位用来直接存储数据,比如一些小对象如 NSNumber @42 不在堆上分配内存,直接把数据编码在指针值里。iOS 14 将 ARM64 的 Tag 位从高位移到低位,利用 ARM64 的 Top Byte Ignore (TBI) 特性。使用 ldp 指令一次加载两个寄存器,减少内存访问次数,使用 br 而非 blr,直接跳转而非函数调用,保留所有参数寄存器。dyld3 将符号解析、依赖分析等工作从运行时移到编译时。iOS 14 Runtime 内存重构,只有 约 10% 的类会在运行时动态修改(Category、Method Swizzling 等),90% 的类只需要精简版的 class_rw_t,扩展数据 class_rw_ext_t 按需分配。iOS 16 时为了用户体验不惜修改 c 的 ABI,使用自定义的 call convention 根据指针位置,适时使用正确变量而不用移动它,从而摆脱了调用里的多余 retain/release 函数调用。在内存紧张时通过高效的 WKDM 算法压缩,而不是使用磁盘,这个做法也是相当聪明了。等等吧,实在很多,这些不拘一格,敢于突破的优化手段和思路给我了很大的启发。

学习 iOS 开发以来我认识的朋友比以前多上好多倍。特别是近两年,除了欧美日本外,在首尔还认识了一些韩国朋友,聊聊 App 开发和 kpop 等我感兴趣的话题。

下面就让我们一起拉开这段历史的帷幕吧。

站在巨人肩膀上

这十年正是中国移动互联网的黄金时代。那时候孙源(sunnyxx)在微博和博客分享底层技术,郭耀源(ibireme)YYKit横空出世,唐巧的《iOS开发进阶》成为必读书目,喵神王巍(onevcat)的OneV’s Den的博客是每周必看,Casa Taloyum(casatwy)的组件化架构方案影响深远,Bang的 JSPatch 改变了热修复格局,雷纯锋分享的 MVVM 和 RAC 实践,limboy的技术思考,小虾等前辈在技术会议上分享实践经验。他们的文章和开源项目,影响了一代 iOS 开发者。

如今巧哥、喵神、Bang 和 limboy 还在输出着,虽然内容和 iOS 不相关了,但也是与时俱进的跨入了新时代,新时代也出现了很多 iOS 技术极客,比如 SwiftUI 和 Core Data 专家东坡肘子、weak self 主理人13pofat和硬核独立开发者Lakr233

当然还有国际上的 NSHipster 创始人 Mattt、objc.io 创始人 Chris Eidhof、Swift 社区重要贡献者 John Sundell 和 Hacking with Swift 教程创始人 Paul Hudson

这些前辈和同行,用他们的代码、文章和开源项目,照亮了无数 iOS 开发者的成长之路。

这篇文章不仅记录技术成长,也记录这个行业的变迁——那些激动人心的 WWDC,那些改变行业的框架,以及我们一起经历的技术浪潮。接下来,正片开始:

2015年:移动互联网的黄金起点

2015 年,我反复刷了电影《夏洛特烦恼》,胡歌主演的《琅琊榜》成为现象级高分剧;《王者荣耀》在 11 月上线后迅速崛起。4 月 Apple Watch 开售,苹果终于把穿戴设备这块拼图补上。应用侧也很精彩:音乐流媒体 Apple Music 把“订阅听歌”带进更多人的日常;照片管理上 Google Photos 以“无限量(当时)+ 智能整理”迅速出圈;社交/内容消费里 Periscope 把直播推向大众;效率工具 Workflow 用“拖拽拼积木”的方式让普通人也能做自动化(后来成为系统的 Shortcuts/快捷指令);邮件客户端 Spark 以“智能收件箱”俘获了不少重度用户;国内健身应用 Keep 也在这一年走红。Mac 端设计工具方面,Sketch 逐渐成为 UI 设计师标配;而像 Paste 这样的剪贴板管理工具,也开始进入“装机必备”的清单。

技术大背景同样剧烈变动。ES6(ES2015) 标准正式发布,箭头函数、Promise、let/const 等现代特性让 JavaScript 进入“可工程化”阶段。Visual Studio Code 凭借“轻量 + 插件生态”迅速出圈,TensorFlow 开源则把深度学习框架带入更广泛的工程实践——后来 AI 基础设施有不少都能追溯到这波浪潮。中国还解除了长达 14 年的游戏主机禁令,PS4、Xbox One 终于可以光明正大地买了。

盘古团队 10 月发布面向 iOS 9 的越狱工具(代号“伏羲琴”)。

苹果官方层面,9 月 iPhone 6s 与 iOS 9 发布,带来 3D Touch;iPad 多任务(Split View / Slide Over)让适配复杂度陡增——尺寸、旋转、约束与交互策略都要重新梳理。WWDC 2015 的工程关键词是:App Thinning(应用瘦身)、Bitcode 以及 UIStackViewwatchOS 2 开放了更多原生能力,让 Apple Watch 从“通知屏幕”走向真正的应用平台——我也是在这一年买了第一块 Apple Watch。

WWDC15 里有几场 Session 很“提效率”。性能调优方面,《Profiling in Depth》(Session 412,slides)把 Time Profiler 热点定位流程讲得更体系化;并发与响应性方面,《Building Responsive and Efficient Apps with GCD》(Session 718,slides)把 QoS、优先级反转以及队列/RunLoop 边界讲透。图形与计算这条线更底层:iOS 9 引入 Metal Performance Shaders(MPS),把卷积、模糊、直方图等常用图像算法下沉到 GPU(《What’s New in Metal, Part 2》Session 607,slides)。

工具链方面,Xcode 7 引入 UI Testing 并支持 Swift 2.0;更底层的改进是把 Address Sanitizer 带进日常调试,让内存越界、Use-After-Free 等问题能在开发期更早暴露。Swift 2.0 带来的错误处理(do/try/catch)与协议扩展(protocol extensions),让我第一次明确感受到“语言特性正在推动工程化”。12 月 3 日,Apple 正式开源 Swift 并发布 Swift.org Blog 首篇文章,Swift 项目从此进入社区协作节奏。其实我入门 iOS 用的是 Swift 1.x,甚至用它写过一个完整 App;但进入业务开发后,因团队技术栈与交付节奏,我又回到了更“有年头”的 Objective-C。

对我个人而言,2015 是第一次把所学 iOS 技术真正用于业务的一年。美团和大众点评在 10 月合并,滴滴快的在 2 月合并,我在这一年加入滴滴做顺风车业务。刚入职时还是新手,为补齐基础,我重刷了斯坦福 CS107 公开课,周末带娃去游乐场时也在手机上看视频。那时顺风车正处于高速增长期,需求排得满满的,但我还是抽时间把 Auto Layout 和 Masonry 吃透了——毕竟适配工作量实在太大。

围绕 Auto Layout,我做了一份内部分享,后来整理成《深入剖析 Auto Layout 的幻灯片》放到博客上,算是对那段“被约束支配”日子的总结(关键词:Auto Layout/Cassowary(约束求解)、约束优先级、intrinsicContentSize、Content Hugging / Compression Resistance、layout pass(updateConstraints/layoutSubviews)、systemLayoutSizeFittingSize)。为了更深入理解自动布局,我围绕“给谁做约束、如何设置约束、设置完后如何处理”三个问题,对比了 SnapKit(Swift)与 Masonry(Objective-C)两套 DSL 的设计取舍,阅读了源码并整理成笔记:读 SnapKit 和 Masonry 自动布局框架源码(关键词:DSL/Builder、ConstraintMaker(SnapKit)/MASConstraintMaker(Masonry)、NSLayoutConstraint.activateConstraints、更新/重建约束策略、可读性与性能取舍)。

再看当年的工程生态。大厂“标配技术栈”大致如下:网络层 AFNetworking 仍是首选(基于 NSURLSession 封装,支持请求/响应序列化、Reachability 网络状态监测、SSL Pinning 证书校验);Auto Layout 逐渐普及,Masonry / SnapKit 成为写约束的主流(链式 DSL 让约束代码更可读,底层封装 NSLayoutConstraint);数据存储常见 FMDB(轻量 SQLite 封装,FMDatabaseQueue 保证线程安全,支持事务与批量操作)或 Realm(零拷贝设计、对象-数据库直接映射、跨平台支持、实时数据同步);图片加载几乎离不开 SDWebImage(异步下载、内存/磁盘双缓存、渐进式解码、图片处理 pipeline)。ReactiveCocoa 把函数响应式编程(FRP)带入 iOS(Signal/SignalProducer、冷热信号、操作符组合),与 MVVM、路由、组件化等关键词一起高频出现,很多团队把“可测试性/解耦/模块边界”写进工程规范。依赖管理上 CocoaPods 仍是绝对主流,Carthage 也开始被认真讨论(去中心化设计、直接构建 framework、Cartfile 版本锁定);构建、签名、发布自动化方面,fastlane 在不少团队里开始萌芽(gym 打包、match 证书同步、deliver 上架、pilot TestFlight 分发)。

开源社区这年非常活跃。UIStackView 仅支持 iOS 9+,老项目没法直接用;当时社区里很实用的“兼容层”之一是 FDStackView,把类似的布局能力带到更低版本(API 与原生 UIStackView 几乎一致,支持 iOS 6+,通过 Auto Layout 约束实现 axis/distribution/alignment/spacing 等核心属性)。除此之外,FDFullscreenPopGesture 解决了”全屏跟手返回”(一行代码集成,通过 method swizzling 扩展 UINavigationController,支持自定义手势识别区域与交互式 pop 动画),UITableView-FDTemplateLayoutCell 则把“动态算高”这件事变得简单可控(内部维护模板 Cell,通过 systemLayoutSizeFittingSize 自动计算并基于 IndexPath 缓存高度,hook 刷新 API 自动失效)——是那种一用就回不去的痛点级工具。同一时期,Facebook Paper 团队开源的 AsyncDisplayKit(介绍文:Introducing AsyncDisplayKit for smooth and responsive apps on iOS)把异步布局/异步渲染做成框架能力:引入 Node 线程安全抽象层(对应 UIView/CALayer),将图片解码、布局计算(绕开 Auto Layout 的 Cassowary 算法开销)、文本渲染等耗时操作移到后台线程并行执行,主线程只做轻量 setFrame,成为当时讨论“列表流畅度”的代表性方案。那几年,微博/博客上也有大量高质量技术输出,比如 sunnyxx 关于 Runtime、Block 内部实现、Category 原理的文章就非常深入浅出,是当时学习底层技术的必读材料。

同一时期,唐巧的《iOS 开发进阶》出版,几乎成了圈内必读(关键词:Instruments/Time Profiler、RunLoop/GCD、内存管理、性能与工程实践)。ibireme 的 YYKit 系列也在那几年逐步成型,后来成为性能优化与工程实践的标杆(关键词:YYModel、YYCache、YYImage、YYText、异步绘制、图片解码与缓存)。SwiftGG 翻译组也在这一阶段逐渐形成,通过系统翻译 Swift 官方文档,显著降低了国内开发者学习 Swift 的门槛(关键词:Swift Language Guide、Standard Library、do/try/catchguard/defer、protocol extensions)。国际上,NSHipster 持续更新 Swift 2.0 新特性解读,围绕 Protocol Extensions、Error Handling 等话题给了很多“能直接用”的实践建议(关键词:Foundation、NSOperation/GCD、NSURLSession、KVC/KVO、Runtime 小技巧)。objc.io 的文章依旧高密度高质量,涵盖函数式编程、Core Data 等主题(关键词:Functional Programming、Value Semantics、Core Data 并发(MOC/PSC)、架构/测试)。Ray Wenderlich(现 Kodeco) 团队也发布了《iOS 9 by Tutorials》,项目驱动的学习方式很受欢迎(关键词:UIStackView、UI Testing/XCTest、3D Touch、iPad 多任务(Split View/Slide Over)、App Thinning/Bitcode)。中文圈里,InfoQ 中文站的徐川总结了 WWDC 2015 十大看点,喵神王巍也发表了 《WWDC15 召开,细数新 SDK 带来的全新变化》,帮助大家快速抓住 iOS 9 与 Swift 2.0 的核心变化(关键词:iOS 9 SDK、UIStackView、3D Touch、iPad 多任务、do/try/catch、protocol extensions)。

如果说“工具与框架”解决的是当下效率,那些“原理与架构文章”则在塑造接下来几年的共同语言。ibireme 的 深入理解 RunLoop 从 CFRunLoop 源码出发,把 Mode/Source0/Source1/Observer/Timer 五大核心类、mach_msg() 休眠唤醒机制、线程与 RunLoop 一对一关系,以及事件分发、AutoreleasePool、界面刷新这些机制串在一起;后来很多“卡顿监控/常驻线程/异步任务调度”的方案都会引用它。还有唐巧的 被误解的 MVC 和被神化的 MVVM:把架构讨论从“站队/玄学”拉回到职责边界、可测试性与可维护性——指出 Controller 臃肿的根源是复用性问题,建议通过抽取网络请求层/ViewModel层/Service层/Storage层改进 MVC,同时澄清用 ReactiveCocoa 不等于 MVVM 的误区,既给当年的 MVVM 热降了温,也推动了更理性的工程化讨论。Casa 的 iOS 应用架构谈:组件化方案:在“业务爆炸/多人协作”的背景下,系统对比了 URL 注册方案与 Target-Action/Mediator 中间件方案,提出通过 OC Runtime 动态调用实现无需注册的服务发现,把组件拆分、路由、依赖治理等问题放到同一张图里讲清楚,影响了后来很多大厂的工程组织方式。bang 的 JSPatch - 动态更新 iOS APP:基于 JavaScriptCore 与 OC Runtime 反射能力,通过 class_replaceMethod 实现方法替换,用正则转换 JS 调用为消息桥接,把“线上热修复”从概念变成可落地的工程实践,显著拓宽了团队对“动态化/发布效率”的想象边界。

同年 3 月,React Native 也通过官方博客 React Native: Bringing modern web techniques to mobile 正式亮相,成为 2015 年跨端/动态化讨论的另一个引爆点,把“热更新/工程效率/多端复用”拉进了移动端主流视野(关键词:JavaScriptCore、Bridge、Native Modules、JS bundle、Flexbox 布局;国内后来也迅速跟进)。

回看 2015,这年更像是把“基础补齐并能稳定交付”:适配、交付、工程化萌芽一起发生;很多后来会变成“标配”的理念与工具,也是在这一年开始进入视野。当业务规模和协作复杂度继续上升时,光靠堆需求一定会失控——你必须开始做“拆职责、治依赖、控边界”的系统性工作,而这也正是 2016 会加速的方向。

2016年:组件化与技术深耕

回到 2016,随着顺风车业务增长与协作规模扩大,“把需求做出来”不再是唯一目标,更重要的是把工程体系搭稳:边界要清晰、依赖要可控、性能要可量化。

我这一年玩得最多的游戏是皇室战争(Clash Royale),算是高压项目之余的碎片化放松。

2016 年的大时代背景是英国脱欧、美国大选、里约奥运。科技圈同样风起云涌:AlphaGo 击败李世石让 AI 破圈,Pokémon GO 把 LBS 与 AR 游戏做成全球现象级爆款。移动支付继续渗透,网约车新政落地,Apple Pay 也在中国上线

技术趋势层面,前端框架 Vue 2 / Angular 2 发布,TypeScript 2.0 让类型安全成为主流选择;Kotlin 1.0 正式发布,为 Android 开发埋下伏笔。
与此同时,Serverless 架构开始被讨论——“工程化、平台化、跨端化”逐渐成为长期命题。跨端方面,React Native 在国内快速扩散,阿里开源的 Weex 也频繁出现在技术分享中。

2016 年是我在滴滴技术成长最快的一年。如果说 2015 更像“把基础补齐并能稳定交付”,那 2016 则是在规模化协作里把工程体系搭起来:滴滴顺风车业务高速发展,订单量翻了好几倍,我也从写业务代码逐步参与到架构讨论中。

这一年我深入学习了 Runtime 的消息转发机制,研究了 RunLoop 在卡顿监控中的应用,也第一次真正理解了“组件化不是拆代码,而是拆职责”这句话。而苹果生态的几次大版本更新,也成为这一年技术讨论的另一条主线。

硬件方面,9 月 iPhone 7 发布,取消 3.5mm 耳机孔;12 月 AirPods 正式开售,“真无线”从此成为主流体验。新款 MacBook Pro 引入触控条(Touch Bar),开发者一度期待很高,但市场反响平平。

WWDC 与 iOS 10。WWDC 2016 带来了 iOS 10,系统开放程度大幅提升:通知体系重构,UserNotifications 成为统一入口;SiriKit 首次向第三方开放;CallKit 让 VoIP 应用能以原生来电界面呈现;Speech 框架提供语音识别能力;iMessage 也支持了扩展(Messages)。性能与调试层面,UITableView/UICollectionView 引入数据预取(UITableViewDataSourcePrefetching),可在 cell 上屏前预加载数据;网络侧新增 URLSessionTaskMetrics,DNS/TCP/TLS/请求响应各阶段耗时终于有了结构化数据。

Swift 3.0 与 Xcode 8。9 月 13 日 Swift 3.0(swift-3.0-RELEASE)正式发布,带来全新的 API 命名规范与大规模重命名(如去除 NS 前缀),几乎所有 Swift 项目都需要迁移。Xcode 8 随之发布,提供迁移工具与新的签名流程。虽然升级成本不低,但 Swift 的采用热情依然高涨。Xcode 8 还把内存排查前移到日常调试:Memory Graph Debugger 能一键抓取对象引用图,配合 Runtime Issues Navigator 直接定位 retain cycle 与泄漏点,详见 Visual Debugging with Xcode(WWDC16 410)

配合 iOS 10 / Swift 3 的更新,大厂的“标配技术栈”在 2015 年基础上继续演进:网络层 AFNetworking 仍是主力,Alamofire(Swift 原生网络库,链式请求构建、请求/响应拦截器、参数编码、Combine/async-await 支持)在 Swift 项目中逐渐普及;图片加载 SDWebImage 依然坚挺,Kingfisher(Swift 图片下载与缓存库,Processor 图片处理、Prefetcher 预取、SwiftUI 原生支持)开始被 Swift 项目采用;数据存储 FMDB/Realm 继续使用,Core Data 因 iOS 10 的改进重新进入讨论;异步编程 PromiseKit(链式异步、then/catch/finally、bridge 回调转 Promise)与 RxSwift(Observable 序列、Operator 操作符、Scheduler 调度器、MVVM 绑定)把 Promise/FRP 推到风口。

列表性能优化成为这一年的热词。AsyncDisplayKit(后更名为 Texture)与 IGListKit(数据驱动的 UICollectionView 框架,ListDiffable 协议、O(n) 的 diff 算法、SectionController 架构、增量更新)把”异步渲染 + diff 更新”从大厂内部实践带到了可复用的开源形态。Facebook 开源了 iOS 内存检测工具集 Automatic memory leak detection on iOS(含 FBRetainCycleDetector,通过运行时遍历对象引用图、检测强引用环),把 retain cycle 检测做成可直接集成的库;LinkedIn 开源 LayoutKit(声明式布局 API、后台线程异步计算、布局结果缓存),用异步计算与缓存将布局从 Auto Layout 性能开销中解耦(Open-Sourcing LayoutKit);年底 Facebook 又以 Yoga 的形式开源跨平台 Flexbox 布局引擎(C 实现、高性能布局计算、支持 iOS/Android/Web、flexDirection/justifyContent/alignItems 等属性),至今仍是 React Native 的布局底座(Yoga: A cross-platform layout engine)。这些基础设施的演进,让“组件化的工程落地”更顺滑了。

2016 年是组件化真正“从概念走向标配”的一年。6 月极客邦在北京举办了首届 GMTC 全球移动技术大会,滴滴的李贤辉在会上分享了大规模团队协作与组件化建设。5 月 CocoaPods 1.0 发布,依赖管理工具趋于成熟,“依赖可控”终于成为可落地的工程实践。

组件化架构讨论在这一年达到高潮。limboy 基于蘑菇街实践发表了 蘑菇街 App 的组件化之路,系统阐述了 URL 路由式组件化(MGJRouter)的收益与落地经验:通过 openURL: 实现页面跳转解耦,通过「协议-类绑定」(Protocol <-> Class)解决组件间同步数据调用,借助 ModuleManager 统一管理组件生命周期与 AppDelegate 回调,并配套壳工程、CocoaPods 私有库与 CI 持续集成来保障多团队协作效率,后来成为许多团队的参考范本。与此同时,Casa 在落地层面给出了 Target-Action / 中间件(CTMediator)的另一条路径。两种思路的碰撞,让组件化讨论从“要不要拆”走向“怎么拆、怎么治依赖、怎么控边界”。

在组件化成势的同时,热修复也持续火热,JSPatch 这类动态化方案把 JavaScriptCore 与 Objective-C Runtime 拼到一起:通过消息转发与方法替换(method swizzling / class_replaceMethod)把线上崩溃与逻辑 bug 的修复下沉为脚本发布,配合灰度与开关把迭代节奏从“等发版”变成“分钟级止血”;但也因此更容易踩到签名不匹配、类型桥接、线程与生命周期边界等坑,风险与合规压力在暗处累积。

国内社区产出了多篇高质量技术文章。YY 大神的 不再安全的 OSSpinLock,从一个真实 bug 出发,系统讲解了自旋锁在 iOS 新版 QoS 线程调度下的优先级反转(Priority Inversion)问题——高优先级线程忙等导致低优先级线程无法释放锁,并对比了 dispatch_semaphorepthread_mutexos_unfair_lock 等替代方案的性能基准,是当年传播最广的 iOS 底层文章之一。巧哥的 猿题库从 Objective-C 到 Swift 的迁移,分享了 15 万行 OC 代码渐进迁移的真实经验:新功能用 Swift、旧功能按需重写,重点探讨了泛型编程与面向协议编程(POP)如何替代 ObjC Runtime 的动态能力,以及混编接口设计的注意事项,适合“准备迁移但担心风险”的团队参考。喵神的 Swift 性能探索和优化分析,从测量方法(CACurrentMediaTime/mach_absolute_time()、Time Profiler、XCTest measureBlock)到优化路径(GCD 多线程、NSCache 缓存、Accelerate.framework 并发计算、-whole-module-optimization 编译优化、避免 Swift/NSObject 隐式转换开销、@autoclosure 延迟求值),把 Swift 早期性能问题与“先定位再优化”的方法论讲得清晰实用。

国际社区同样活跃:NSHipster 围绕 Swift 3 的 API Design Guidelines 讨论了不少“写给调用方看”的细节——参数标签/默认参数如何让调用点读起来像英文、值语义与可变性(mutating)的约定、以及 Swift 与 Foundation(URL/Data/Date 等)的桥接在迁移期最常见的坑;objc.io 推出了 Swift Talk 视频系列,用小案例拆解泛型、protocol extensions、不可变数据与函数式组合(map/filter/reduce),把 Swift 的抽象能力与可测试写法讲得很“能抄”;Ray Wenderlich 团队的《iOS 10 by Tutorials》则以项目驱动串起 UserNotifications、SiriKit/CallKit、iMessage App Extension 以及 Xcode 8 调试工作流,适合在业务里按需对照落地。

2016 年的应用生态同样精彩。前文提到的 Pokémon GO 让“AR + 地图”首次真正破圈;AI 滤镜应用 Prisma 把神经风格迁移带入大众视野;国内短视频赛道开始升温,抖音 在这一年上线;知识付费领域 得到 让“订阅专栏/音频课”成为新的内容形态。

Mac 端上,Readdle 把 Spark 带到 macOS,把邮箱做成“可管理的任务流”:Smart Inbox/规则分流、Snooze、Send Later、模板与团队协作(Shared Inbox)让邮件更像待办而不是噪音;Setapp 以订阅制挑战传统买断模式,也在某种程度上推动了“工具链按需组合”的使用习惯;Docker Desktop for Mac 则让容器化开发更顺手:基于 HyperKit/Hypervisor.framework 的轻量虚拟化跑 Docker Engine,配合 docker-compose 一键拉起 Redis/MySQL/ELK 等依赖,让“本地=线上”的环境一致性更易维护;写作工具方面,Typora 的 Live Preview 把 Markdown 的语法与排版合到一个视图里,配合导出(HTML/PDF)让“写-发”链路更顺滑。

2017年:热修落幕与 AR/ML,底座升级

2017 年我花时间最多刷的剧是 《三生三世十里桃花》,后来入职阿里时的花名“夜桦”也来自“夜华”的谐音。

17年共享单车 成了街头风景,摩拜ofo 的颜色大战让城市多了几分戏剧性——我们在滴滴大厦楼下吃完饭还会围观摩拜单车,讨论它怎么联网、怎么供电。

说回 iOS 开发本身,2017 年最直接的政策冲击是动态热修被全面收紧:App Store Review Guidelines 明确禁止下载、安装或执行会改变 App 行为的代码(常被引用的是 2.5.2 条款)。JSPatch、Rollout 等方案逐渐降温,热修复的黄金时代基本落幕。

工程侧的应对也随之“前移”。这一变化直接推动了组件化、模块化与发布流程治理——既然线上“修不了”,就把质量关口前移。CI/CD、灰度发布、功能开关逐渐变成大型 App 的标配。

WWDC 2017 与 iOS 11。WWDC 2017 堪称“未来技术预演”。台前,ARKitCore ML 同时亮相,加上 Vision 框架,苹果为移动端 AI 与 AR 铺好了路——ARKit 通过 Visual Inertial Odometry(VIO)融合相机与 CoreMotion 数据实现世界追踪(world tracking)、平面检测(plane detection)与环境光估算(lighting estimation),让虚拟物体能稳定”贴”在真实表面上;Core ML 把训练好的 .mlmodel 模型带到端侧推理,自动在 CPU/GPU/Neural Engine 间调度以平衡性能与功耗,配合 Xcode 自动生成 Swift/OC 包装类让集成只需两行代码;Vision 则用 VNImageRequestHandler 统一图片分析入口,内置人脸检测(VNDetectFaceRectanglesRequest)、条码识别、矩形检测与文字识别(OCR)等请求类型。当时我们还觉得 AR 是噱头,没想到几年后 Vision Pro 真的来了。系统层面,iOS 11 带来了全新的文件管理器、改进的 iPad 多任务处理、可自定义的控制中心,以及 Drag and Drop 交互。硬件方面,苹果发布了 HomePodiMac Pro,在智能家居和专业市场同时发力。

底座升级与工具链。台下同样有大动作。3 月 iOS 10.3 把默认文件系统切换到 APFS,Copy-on-write、快照、加密模型为存储底座全面升级。Xcode 9 新增的 Main Thread Checker 让“UIKit 在后台线程被调用”这类玄学 bug 变成可复现的 Runtime Issue(详见 WWDC17 Session 406)。图形层面,Metal 2 把更多工作交给 GPU;iOS 11 则全面拥抱 HEVC/HEIF,大幅降低媒体体积(相关 session:503 讲为什么切换到这套标准、511 讲 API 层面如何落地——HEVC 相机采集比 H.264 压缩率提升约 2 倍、默认采集已启用、513 则从容器结构到辅助图像(alpha/depth/Live Photo)逐一拆解)。这些底层变化看似无感,却决定了后续几年性能治理能走到哪里。

硬件拐点:iPhone X。9 月,iPhone X 发布,“刘海屏”与 Face ID 让全行业跟进,也让 iOS 开发者迎来一轮 Safe Area 适配的噩梦。我在第一时间入手了一台,Face ID 解锁确实方便不少。

同期 Swift 4.0 发布,带来了更强大的字符串处理(String 重新成为 Collection)、Codable 协议(JSON 序列化终于有了原生方案),以及改进的泛型和编译速度。ABI 稳定性路线继续推进,开源社区开始明显地往 Swift 迁移。

这一年社区涌现了大量 Swift 原生库:Hero(声明式转场动画,heroID 匹配、自定义动画曲线、交互式手势)、Moya(Alamofire 上层抽象,TargetType 协议、Plugin 拦截器、RxSwift/Combine 扩展)、R.swift(资源类型安全,编译时生成强类型引用、避免字符串硬编码、支持图片/字体/Storyboard/本地化)、SwiftLint(代码规范检查,基于 SourceKit、可配置规则集、Xcode 集成)、Lottie(After Effects 动画跑在移动端,JSON 动画描述、矢量渲染、动态修改属性)。SnapKit 成了 Swift Auto Layout 首选,RxSwift 响应式编程也更加普及。不过这些库在国内大厂落地并不快——Objective-C 依然是主流。

同年,Pinterest 宣布 AsyncDisplayKit 更名为 Texture,异步渲染从“经验”变成“可复用的框架”;腾讯开源 WCDB(高性能 SQLite ORM,WINQ 类型安全查询语言、全文搜索 FTS、数据库损坏修复、加密支持),把高并发下的 SQLite 工程化做成基础设施(报道:InfoQ);滴滴也在 GMTC 分享了 iOS 端瘦身实践。这些内容后来都成了大厂性能专项的“常见母题”。

跨端与外部技术的风向同样清晰。跨平台方面,1 月 微信小程序 正式上线,让“一次开发多端运行”的想法更加深入人心。React Native(JavaScript Bridge 架构、原生组件渲染、Flexbox 布局、热更新支持)和 Weex(阿里开源、Vue 语法、JS Engine + Native 渲染)成了各大公司争相接入的方案。桌面端则是 Electron 应用爆发式增长,VS Code、Slack、Discord 都基于它构建——虽然被吐槽“内存杀手”,但确实改变了桌面应用开发格局。

移动端之外,Google 宣布 Kotlin 成为 Android 官方语言,iOS 开发者隔岸观望,感叹 Swift 和 Kotlin 的相似度之高。深度学习框架 TensorFlow 1.0PyTorch 相继发布,两大框架的竞争就此拉开。前端圈 Vue.js 持续走红,9 月 React 16 引入 Fiber 架构,后续几年前端渲染话题几乎都绑定在它身上。

这一年 iOS 和 macOS 上涌现了不少优秀应用。Bear 凭借优雅的 Markdown 写作体验获得 Apple Design Award 2017Things 3 以全新设计亮相,把 GTD 做到极致优雅;Affinity PhotoAffinity Designer 以买断制挑战 Adobe。

效率工具方面,iStat Menus 6 让状态栏变成性能仪表盘,Bartender 3 解决菜单栏图标过多的烦恼,Magnet 几乎成了“装完 Mac 必装”的清单项。开发者工具中,Paw 是当时最漂亮的 API 调试工具,Dash 是查阅文档的必备神器。Notion 开始在硅谷圈子里流行,虽然国内用户还不多,但已有先行者在尝试这个“All-in-One”的新物种。

喵神王巍在 objccn.io 上推出《Swift 进阶》,把 Swift 高级特性讲得深入浅出,成为进阶必读。SwiftGG 持续输出高质量翻译。国际上,John Sundell 创办 Swift by Sundell 博客,以高产、高质迅速成为社区明星;Ray Wenderlich(现 Kodeco)发布《iOS 11 by Tutorials》;NSHipsterobjc.io 也持续更新 Swift 4、ARKit、Core ML 等主题的深度解析。

我自己在这一年写了三篇「深入剖析」系列,把注意力放到更底层的三块:

  • 深入剖析 iOS 编译 Clang / LLVM(3 月):从词法分析(Lexer)生成 Token、语法分析(Parser)构建 AST 抽象语法树,到 IR 中间代码与 LLVM Pass 优化,再到 Mach-O 可执行文件格式、dyld 动态链接;还覆盖了 Swift 的 SIL 中间语言、Bitcode 以及 Clang Static Analyzer 与 CFG 控制流图等静态分析机制——串起 iOS 编译工具链的全链路。
  • 深入剖析 iOS 性能优化(6 月):从集合操作的时间复杂度(O(1)/O(n)/O(log n))、GCD 的 QoS 服务质量与 dispatch_semaphore 并发控制,到 I/O 批量写入、NSCache 自动清理策略与线程爆炸防范——尝试把性能问题量化到方法调用链路与频次。
  • 深入剖析 WebKit(10 月):沿着 HTML Tokenize → DOM Tree、CSS 解析 → CSS Rule Tree/CSSOM → Render Object → Layout 的主链路,深入 WebCore 排版引擎与 JavaScriptCore 脚本引擎的协作机制,把 WebKit 架构拆开来看。

当年我反复翻的几篇资料也值得一提:

  • SwiftGG 的 Swift 4.0 Released!:发布说明的中文精读版:除了 Codable(面向 JSON/plist 的类型安全序列化)之外,还把 String/Substring 体系重做(多行字符串、Index overhaul、性能与 Unicode 正确性)、Dictionary/Set 增强,以及 Swift 3.2/4.0 兼容模式与 Swift Package Manager 新 API 等迁移要点串到一起。
  • 一篇文章 get 微信开源移动端数据库组件 WCDB 的一切!:系统梳理 WCDB 基于 SQLCipher 的加密机制、WINQ 语法抽象层与 ORM 映射、数据库损坏修复三板斧、全文搜索分词器以及内建反注入保护等核心能力。
  • Build More Intelligent Apps with Core ML:Apple 官方 news 的“入门引子”,强调用 Core ML 这套端侧机器学习基础能力把智能特性带进 App:人脸跟踪(face tracking)、文字检测(text detection)、语言识别(language identification)等,并直接引导继续阅读 Core ML 文档完成从模型到功能的落地。
  • Hacking with Swift 的 New App Store Review guidelines:把审核条款变化翻译成开发者 checklist——Face ID 必须通过 LocalAuthentication 而非 ARKit 实现、ARKit 面部数据禁止用于用户画像挖掘、AR 应用须提供完整沉浸体验等。
  • Michael Tsai 的 Swift 4.0 Released:引用式聚合:一边记录“迁移很顺滑,但 Xcode/文档/跳转定义等工具链仍粗糙”的一线反馈,一边把 Swift 4 的关键议题(Codable/Encoder、Key-Value Observation、错误处理实现、as 转型的 bridging peephole、Equatable/Hashable 自动合成、String 大改、Dictionary Keys/Values 集合等)集中串起来,适合按图索骥深挖。

回头看 2017,这一年是“热修落幕、AR/ML 起航、底座升级”的分水岭:苹果封堵了动态化的“野路子”,同时把 AR/ML 推向台前;而 APFS、Main Thread Checker、Metal 2 与 HEIF/HEVC 则在台下把性能与系统底座悄悄换了一遍。

对我个人而言,这也是在滴滴的最后一年:共享单车的疯狂、热修复的终结、AR 的萌芽、WannaCry 的惊恐,都成了记忆里无法抹去的标签。下一年 9 月我就会入职高德,成为阿里人,也和很多如雷贯耳的 iOS 前辈成为了同事。

2018年:内功修炼与性能

3 月 斯蒂芬·霍金 去世,10 月 金庸 先生辞世,“飞雪连天射白鹿,笑书神侠倚碧鸳”成了朋友圈的主旋律。

游戏 《旅行青蛙》 意外走红,佛系养蛙刷屏了一阵。这一年滴滴顺风车安全事件也让整个行业反思。

技术与开源同样热闹。前端圈 React 16.3 带来新的 Context API(createContext)与一批为 async rendering 铺路的生命周期替换(如 getDerivedStateFromProps/getSnapshotBeforeUpdate,配合 StrictMode 做迁移提醒),Vue CLI 3.0 则把脚手架做成基于 webpack 的插件化体系(preset/mode、多页面、vue ui 可视化配置),Prettier 通过“基于 AST 的再打印”把格式化变成确定性规则,配合 husky/lint-staged 很快成了团队协作的默认选项。GitHub 被微软以 75 亿美元收购,开源社区一片哗然,后来证明担心是多余的。

APM(应用性能监控)系统在各大厂普及,自动化测试、持续集成趋于成熟,组件二进制化被用来提升编译速度,性能优化开始成为团队级议题。

海外生态里,Google 在 I/O 2018 宣布 Firebase Performance Monitoring 从 beta 走向 GA,把启动/自定义 trace、网络请求等指标采集做成 SDK 能力;Facebook 则在同年开源跨 iOS/Android 的调试平台 Flipper(仓库:facebook/flipper),用插件体系把 Layout、Network、Logs、Performance markers 等可视化调试能力收口到一个桌面端里——性能问题的定位开始从“会用 Instruments 的少数人”向“团队共享的工具平台”迁移。各大厂技术博客也开始活跃起来——美团技术团队滴滴技术和字节跳动的文章内容系统且深入。跨端方向也迎来新节点:12 月 4 日 Flutter 1.0 正式发布(GA),以 Dart 语言、Skia 自绘渲染、Widget 声明式 UI、热重载等特性在国内引发了新一轮的技术讨论。

在苹果这一侧,2018 的主线也很明确:把“性能”写进系统升级的主题。9 月 iPhone XS / XS Max / XR 发布,XS Max 成为史上最大屏幕的 iPhone;同期发布的 Apple Watch Series 4 首次支持 ECG(心电图)功能和跌倒检测,健康监测进入新阶段。10 月的发布会也很有记忆点:新 iPad Pro 全面屏 + USB‑C + Apple Pencil 2 让 iPad 更像生产力设备;MacBook Air 换上 Retina + Touch ID;还有“多年不更新”的 Mac mini 终于回归,开发者们一边感动一边开始算预算。

WWDC 2018 的主题是“让老设备也能跑得飞快”:iOS 12 主打性能优化,iPhone 6 Plus 上 App 启动速度提升 40%,键盘响应速度提升 50%,相机打开速度提升 70%——苹果终于对老设备友好了一把。新特性方面也有几项“立刻能用”的系统能力:

  • ARKit 2:把多人共享 AR 体验真正落到“共享世界坐标”上(如 ARWorldMap/anchor 同步),并加入 3D Object Detection(Reference Objects)让现实物体能被识别并作为锚点。
  • Create ML:把端侧模型训练塞进 Xcode 工作流里,用 MLDataTable 等抽象支持表格数据/图像/文本分类等常见任务,一键导出为 Core ML 模型用于 App 侧推理。
  • Siri Shortcuts(快捷指令):通过 NSUserActivity/Intents 的 donation(INInteraction)让系统学会“用户常做的事”,再把它变成可被 Siri/Spotlight/Shortcuts 调用的入口——语音只是表象,本质是把关键动作做成可组合的 Intent。
  • Screen Time:以按 App/分类的使用统计为基础,配套 App Limits 与 Downtime 等“可执行的限制”,把数字健康从口号变成系统级的可用功能。

macOS Mojave 则带来了系统级 Dark Mode(深色模式),开发者们纷纷适配,那段时间打开 App Store 全是“支持深色模式”的更新日志。

在更偏“系统底层”的 API 上,WWDC 2018 还发布了 Introducing Network.framework: A modern alternative to Sockets(715):把连接(NWConnection)、监听(NWListener)、服务发现(NWBrowser)和网络路径监测(NWPathMonitor)统一到一套状态机里,很多团队后来用它替换 Reachability + Socket 的旧组合,把“网络切换/弱网/省流量”这类问题尽量往系统能力上靠。

语言与工具链也在同步打底。Swift 4.2 在 9 月 17 日正式发布,带来了条件遵循(Conditional Conformance)、统一的随机数 API、改进的 Hashable 协议,以及显著提升的编译速度;Xcode 10 引入全新的构建系统(New Build System),支持并行化编译,大项目的构建时间明显缩短。ABI 稳定性继续推进,为 Swift 5 的到来铺路。

国内团队的“标配技术栈”也进一步成熟:

  • 数据存储:WCDB(基于 SQLite/SQLCipher 的加密存储,配合 WINQ/ORM 抽象,并提供损坏修复等工程能力)开始被更多团队采用。
  • 研发效率工具:8 月 14 日滴滴在 GitHub 建仓并开源了 DoKit(DoraemonKit),一款面向移动端的研发效率工具,用“悬浮窗入口 + 插件化面板”把数据 Mock、网络/性能监控、UI 检查等能力收口到一个工具箱里,后来成为滴滴首个 GitHub 破万 Stars 的开源项目。

性能治理的核心命题是:如何把“救火式优化”升级成可复用、可度量、可协作的工程能力?苹果在 WWDC 2018 通过 Measuring Performance Using LoggingPractical Approaches to Great App Performance 把方法论讲成流程:先统一日志,再用 signpost 对关键路径做埋点,最后让问题能被 Instruments 的时间线直接“读出来”。中文圈里也很快出现了对这套体系的落地实践,比如 os_signpost API Introduction 详细演示了 os_log_create(用反向 DNS 格式创建 Category)、os_signpost_id_generate(生成唯一 ID 匹配 begin/end)以及 os_signpost_interval_begin/end(标记耗时区间)这套完整流程,把 os_signpost 从”看过视频”推进到”能在 Instruments 的 Points of Interest 里可视化调试”;而像 《深入浅出-iOS程序性能优化(转载)》 这种按 CPU/内存/渲染/启动拆维度的文章,则更像团队治理时随手可用的“检查表”。

当团队把关键路径用 signpost 统一标记后,下一步就是把“方法”做成“工具”:

当“量化”成为共识后,讨论自然会收敛到最影响体验的那几条链路上:启动、链接与渲染。

  • 启动治理:比如美团的 《美团外卖 iOS App 冷启动治理》 将冷启动拆为 T1(main 前的 dyld 加载与链接)、T2(main 到 didFinishLaunching)、T3(首页渲染完成)三阶段,通过 Kylin 组件实现启动项自注册(利用 Clang 的 __attribute__((section())) 将函数指针写入 __DATA 段,运行时按阶段触发),并结合 Time Profiler 与火焰图可视化定位”平顶山”式瓶颈,同时采用闪屏页并行首页构建、缓存定位 + 首页预请求等策略把收益落到数据上。
  • 链接与交付:Static linking vs dyld3 从 dyld3/链接策略切入,实测将 26 个动态库改为静态链接后 iPhone 5c 启动时间缩短约 2 秒,并通过 MACH_O_TYPE=staticlib 构建设置与 framework 转 resource bundle 的两步方案渐进落地,把”静态链接到底能省多少启动时间、边界在哪里”讲清楚。更底层一点看,dyld3 代表苹果把大量动态链接工作尽量前移到安装/更新阶段(最初主要用于系统 App):用 launch closure 把 Mach-O 解析、依赖解析与绑定信息预计算出来,让启动阶段更多做校验与映射(dyld 开源代码:apple-oss-distributions/dyld;概念来源:WWDC 2017 App Startup Time: Past, Present, and Future(413))。与此同时,围绕“如何交付与复用”的工程现实也被拿到台面上讨论:在 Swift ABI 尚未稳定的时代,Binary Frameworks in Swift 直面二进制分发的痛点与取舍——这也与后来业内热议的二进制化/构建速度治理形成了前奏。
  • 渲染机制:渲染这条线同样在补课中变得体系化,比如 《深入理解 iOS Rendering Process》 从 OpenGL ES/Metal 渲染管线角度剖析 UIKit/CoreAnimation 的离屏渲染触发条件(如 cornerRadius + masksToBounds、shadow、group opacity)与 GPU 瓶颈成因,帮助把”为什么卡”从经验判断升级成对 Core Animation 提交事务、图层树遍历与 GPU Tile-Based Rendering 机制的理解。

性能一旦成为主线,很多原本“看起来像基础设施”的能力也会被重新定义:日志不再是简单的 print,而要高性能、可加密、可可靠上传,于是有了美团对 Logan 的系统化总结与开源历程(《美团移动端基础日志库——Logan》 / 《Logan:美团开源移动端基础日志库》);存储要更快、更稳,就有了 MMKV 的工程化实现思路(《微信自用高性能通用key-value组件MMKV已开源!》),用 mmap 内存映射避免传统文件 I/O 的系统调用开销,结合 protobuf 二进制序列化压缩存储体积,并通过 CRC 校验 + 增量写入实现写入失败后的数据恢复,把”系统能力榨性能”变成可复用的组件化方案。

上述这些实践——无论是 signpost、APM 还是高性能日志与存储——都指向同一个方向:把性能治理做成可沉淀的工程能力。而要让这些能力长期发挥作用,代码本身的可维护性就成了另一条暗线。Swift by Sundell 系列里,导航/解耦的 Navigation in Swift(介绍 Coordinator pattern 和 Navigator protocol + associated type 的实现,以及 URL Scheme 深度链接的落地方式)、接口协作的 Delegation in Swift(详解 weak delegate 防循环引用、protocol 可选方法的 @objc optional 与 extension 默认实现两种方案)、复杂页面组织的 Custom container view controllers in Swift,以及质量保障的 Unit testing asynchronous Swift code(介绍 XCTestExpectation、async/await 测试模式及 mock 注入策略),本质都在回答“团队如何在变更中不失控”。语言与工具层面也在同步补齐这套工程表达:在 Result 还没进入标准库前,The power of Result types in Swift 用类型系统提前“演练”了错误处理的可组合性;围绕集合与函数式写法的性能细节(Performance, functional programming and collections in SwiftcompactMap vs flatMap: The differences explained)提醒我们,性能往往就藏在这些日常习惯里。

应用生态方面,Apple Design Award 获奖者中 FlorenceAgendaAlto’s Odyssey 都很有代表性。效率工具领域 Notion 开始流行,开发工具方面 TowerProxymanTablePlus 被更多开发者采用;游戏方面 FortnitePUBG Mobile 霸榜,手机吃鸡成为现象级。

2018 “性能元年”对我来说最直接压力来自一个很具体的目标:App 包体积瘦身。它是一个自上而下的目标,于是大家都在想办法“抠体积”——资源侧能做的无非是去无用/去重;再往下,就只能动二进制本身:调编译参数、尝试缩短符号/命名长度……但现实是,收益非常有限,而且这些“挤出来的一点点”很快会随着版本迭代又涨回去。

为了把目标真正做实,只能回到最朴素也最有效的路径:清理无用代码。但当时现成工具无法保证“删了就一定不会在运行时被调用”,我只能靠人工逐个排查与验证,效率极低。于是我干脆把这套手动检查与验证流程,在周末写成了工具——让“无用函数/无用类”的识别尽可能自动化,同时把误删风险降到可控范围。

同事看到效果后问我原理,我随口解释了实现思路,他一句“这不就是编译原理吗?”把我点醒了:很多所谓的“工程治理”,底层其实就是编译器视角下的依赖与可达性分析。后来我专门去补了 Xcode 背后的 LLVM/Clang 工具链资料;也差不多在同一时期,滴滴杭州团队用 Clang 插件技术做了类似的瘦身工具,并在大会上分享了实践经验(GMTC 上分享滴滴出行 iOS 端瘦身实践的 Slides)。

静态手段做到极致,大概也就到“基于编译与链接信息,尽量准确地找出不可达代码”这个程度;要更进一步,就必须引入运行时证据:线上到底有没有用到它。

粗粒度上,可以通过类里记录初始化信息的字段判断“这个类在运行时是否真的被触发”;细粒度则是函数级别——我后来更倾向用函数级覆盖率来做闭环:线下跑主流程拿到覆盖数据,再只在非主流程上做小范围灰度,以最小的性能损失去识别真正的无用函数/代码路径。

相关的静态检查工具我做了两个开源项目:SMCheckProjectsmck;而关于“用 LLVM 做函数级覆盖率/利用覆盖率找无用代码”的更系统总结,我也写在了这篇文章里:使用 LLVM

2018 年是我从滴滴到阿里的过渡期。9 月我正式入职高德,成了一名阿里人。因为开始做 JS 解析与处理相关工作,我把 JavaScriptCore 从 Lexer/Parser/AST、字节码,到 LLInt/分层 JIT(Baseline/DFG/FTL/B3),再到类型分析、指令集架构等主链路梳理了一遍,并写了篇《深入剖析 JavaScriptCore》为后面做动态化方案打底。

第三届 @swift 大会分享“用 Swift 写解释器”的思路,开源了一个支持少量 OC 语法的解释器 demo(HTN),以及一个扩展 CTMediator 并引入 AOP/状态管理的架构 demo(ArchitectureDemo);幻灯片则尝试用“漫画分镜”的方式来表达技术内容,见 这次 Swift 大会分享准备的幻灯片和 demo

回头看 2018,这一年是“修炼内功”的一年:苹果把性能优化做成了 iOS 12 的主打卖点,各大厂把 APM、启动优化、包体积治理当成工程标配。对我而言,包体积瘦身几乎贯穿全年——从资源去无用/去重,到把无用代码清理工具化,再到补编译原理与 LLVM/Clang 工具链,甚至思考如何用覆盖率与灰度在运行时做最终验证:这些都是后来很多“性能治理”工作的底层思维训练。

2019年:SwiftUI 与 Swift ABI 稳定

3 月 996.ICU 项目在 GitHub 横空出世,短短几天 Stars 破 10 万,“工作 996,生病 ICU”成了程序员的年度自嘲。与此同时,李子柒 的田园视频在 YouTube 爆火,年底订阅数突破 700 万,成为中国文化输出的现象级案例。

3 月 25 日 Swift 5.0 发布,最重要的里程碑是 ABI 稳定:Swift 运行时库被嵌入操作系统,App 不再需要打包 Swift 标准库,包体积显著减小,跨版本兼容性大幅提升。语言与生态正式从“快速演进”迈向“可长期维护”。Swift 5.0 还带来了 UTF-8 编码的 String 重构、Result 类型、原始字符串字面量、改进的字符串插值等实用特性。

9 月随 iOS 13 发布的 Swift 5.1 则为 SwiftUI 铺路:some 关键字(Opaque Result Types)、Property WrappersFunction Builders / Result Builders 等新特性,让声明式 UI 的 API 设计成为可能。John Sundell 在 The Swift 5.1 features that power SwiftUI’s API 里把这条因果关系解释得非常清楚:some View 如何替代繁琐的类型擦除(AnyView)、@functionBuilder/@ViewBuilder 如何让多视图组合获得 DSL 般的声明式体验、以及 @propertyWrapper 如何把 @State/@Binding 这类状态管理代码简化到一行注解。

WWDC 2019 是近年来最令人兴奋的一届:SwiftUICombine 的发布让整个社区沸腾。SwiftUI 采用声明式语法,配合实时预览(Live Preview),让 UI 开发体验焕然一新;Combine 则为 iOS 带来了官方的响应式编程框架,虽然很多团队已经在用 RxSwift,但官方方案的到来意味着响应式编程正式成为苹果推荐的范式。

同一波发布里,iOS 13 带来了系统级 Dark Mode(深色模式),适配深色模式成了那几个月开发者的“必修课”;Sign in with Apple 为用户隐私提供了新选择,也给使用第三方登录的 App 带来了新的审核要求。ARKit 3 引入了人体遮挡(People Occlusion,让虚拟物体能正确地被真人遮挡或遮挡真人)和动作捕捉(Motion Capture,实时追踪人体关节位置),同时带来多人脸追踪与协作式 AR 会话,配套的 RealityKit 框架把光照渲染、物理模拟与动画打包成更高层抽象;Core ML 3 支持设备端模型训练——通过 MLUpdateTask + MLBatchProvider 在不重新编译的情况下做个性化微调,新增 70+ 神经网络层与 MLModelConfiguration 让 CPU/GPU/Neural Engine 切换更可控,苹果在 AR 和 ML 上的布局越来越清晰。iPadOS 首次从 iOS 中独立出来,iPad 终于有了自己的身份,多任务、文件管理、鼠标支持都有了显著改进。Xcode 11Swift Package Manager 集成进来,开发者终于可以在 IDE 里直接管理 Swift 包依赖,不再必须依赖 CocoaPods 或 Carthage。

同时 iOS 13 也给 UIKit 带来了几项“立刻能用”的工程级新能力:比如 Diffable Data SourceUITableViewDiffableDataSource / UICollectionViewDiffableDataSource)让列表更新从“手写 insert/delete/move”变成“应用快照”;UICollectionViewCompositionalLayout 让复杂布局更易组合;而 UIScene / UIWindowSceneDelegate(我们口语里常叫 SceneDelegate)则带来了新的生命周期适配工作。Mac Catalyst 也在这一年开始被更多人认真尝试。

9 月 iPhone 11 / iPhone 11 Pro 系列发布,“浴霸”三摄成为新的视觉标志,夜景模式让手机摄影再上台阶;同场发布的 Apple Watch Series 5 以 Always‑On 屏幕成了那年最“顺眼”的升级之一。10 月 AirPods Pro 发布,主动降噪让真无线耳机体验飞跃。

10 月 macOS Catalina 上线,彻底告别 32 位应用,让不少老软件“一夜变灰”,也让很多人升级前第一次认真做了兼容性清单。11 月 16 英寸 MacBook Pro 发布,剪刀脚键盘回归,把不少人从 2016-2018 款的阴影里拉出来;11 月 1 日 Apple TV+ 流媒体服务上线,苹果正式进军内容领域;WWDC 2019 上亮相的“奶酪刨丝器”造型 Mac Pro(2019) 在年底开售,专业用户终于迎来性能怪兽;同期 Apple Arcade 游戏订阅服务也正式上线,为移动游戏带来新的商业模式。

2019 年 iOS 圈涌现了大量高质量的技术文章。

第一条主线是语言层面“终于定型”。Swift 团队用 ABI Stability and More 把 Swift 5 的关键意义讲透,随后 Swift 5 Released! 宣告 ABI 稳定真正落地:语言与生态从“快迭代”迈向“可长期维护”。也正因为底座稳定了,Swift 5.1 那批为声明式 API 铺路的特性(some View@State 等)才显得更“像一套设计”。

第二条主线是框架层面的“范式迁移”:从 SwiftUI 的声明式 UI 与预览驱动开发,到 Combine 这套官方响应式框架把异步与数据流“写进类型系统”。宏观上,Mattt 在 NSHipster 的 WWDC 2019 不只把 SwiftUI/Catalyst 放进“平台长期可行性”的叙事里,还给了更偏落地的选择题:存量 App 先把 iOS 13 的 Dark Mode、Sign in with Apple、SF Symbols 等必修项做成 checklist;iPad App 则评估通过 Catalyst 迁移到 macOS Catalina。微观上,中文圈几乎是同一时间把“上手路线”补齐:喵神的《SwiftUI 的一些初步探索》系列( / )让人能快速把核心概念跑通,而淘宝技术的《系列文章深度解读|SwiftUI 背后那些事儿》则更偏机制与实现视角,适合想弄懂“为什么这样设计”的读者。

当 SwiftUI/Combine 真的进入真实工程,大家关心的就不再是“会不会写”,而是“怎么和存量共存、边界在哪里”。例如 objc.io 的 SwiftUI: Loading Data Asynchronously 用 “Endpoint(URL + parse)+ 泛型 Resource” 的抽象把按需请求、解析与加载态(loading/loaded)到驱动 UI 更新的链路串了起来(当时还要靠 BindableObjectdidChange 来触发刷新),同时也直面 function builder 早期不支持 if let 等写法的限制;而 Swift by Sundell 的 Functional networking 则代表另一条常青线:把网络层收敛为可注入的函数/闭包(而不是巨型 manager),让“请求 + 解析”更可组合、更可测试、更易 mock——哪怕 UI 框架在变,这类“把依赖变成参数”的思路依然能长期复用。中文圈里也有很多“拿项目说话”的复盘,比如 InfoQ 的《历时五天用 SwiftUI 做了一款 APP,如何做到?》,把 SwiftUI 在原型/小工具上的效率红利与现实边界讲得很具体。

第三条主线更“工程化”。如前面 iOS 13 的变化所示,真正让团队都“动起来”的,往往不是某个新 API,而是一组必须落地成 checklist 的工程关键词:比如 Dark Mode 对外观与资源体系的重塑、Sign in with Apple 把登录方案和隐私合规绑定、以及 UIScene / SceneDelegate 带来的新生命周期与多窗口语义(适配工作量也随之上升)。这类问题最需要“可执行的检查表”,比如 AvanderLee 的 Dark Mode: Adding support to your app in Swift 就很适合用来对照扫坑:从 UIUserInterfaceStyleInfo.plist 里一键 opt-out,到按 view/view controller/window 覆写 userInterfaceStyle,再到语义/自适应颜色与 UIColor(dynamicProvider:) 的兼容策略、traitCollectionDidChange 修复 CALayer 边框色不动态更新,以及在 Asset Catalog 里为图片配置 light/dark 变体或用 template+tint 控制资源膨胀;而 NSHipster 的 iOS 13 则更像“你这一版不该漏掉什么”的总览清单。

更有意思的是,UIKit 并没有被新范式抛下,反而在列表与布局上给了更现代的工程解法:快照驱动的 Diffable Data Source(可配合 Modern table views with diffable data sources 快速上手——文章详解了 NSDiffableDataSourceSnapshotappendSections/appendItems/apply 更新流程,以及如何用 Hashable 标识符让 diff 算法正确识别变更),以及用 UICollectionViewCompositionalLayout 描述复杂布局(参考 Using compositional collection view layouts in iOS 13)。工具链与生态也在同步补齐:Xcode 11 把 Swift Package Manager 集成进 IDE,很多团队会像 AvanderLee 这篇 Using Xcode Previews with existing UIKit views without using SwiftUI 那样,用 canImport(SwiftUI) + #if DEBUG 把 SwiftUI 只留在调试构建里,再用 UIViewRepresentable/UIViewControllerRepresentable + PreviewProvider 给 UIKit 视图“套壳”,甚至开发期临时把 deployment target 调到 iOS 13 来解锁 Canvas 预览,从而在不真正迁移 SwiftUI 的前提下先吃到实时预览的效率红利;而想把 Combine 用进 UIKit 的存量体系,也可以参考 Creating a custom Combine Publisher to extend UIKit 这类桥接实践——文章演示了如何从 Subscription 处理 demand/cancel,到实现自定义 Publisher,再通过 Subscriber.receiveUIControl 事件转化为响应式数据流。

但 2019 还有一条更“底层”的暗线:性能治理开始从“靠经验抓一把”走向“可观测 + 可回归”。WWDC 2019 的 Improving Battery Life and Performance(417) 把这条链路讲得很完整:开发阶段用 XCTest 的性能度量把关键路径写成可自动回归的测试,上线后再用 iOS 13 新引入的 MetricKit 收集真实用户设备上的能耗与性能指标(CPU/内存/磁盘写入/启动等),最后在 Xcode Organizer 的 Metrics 面板里看趋势与回归。

而当视角再往下,Optimizing App Launch(423) 把启动成本拆到 dyld 动态链接、ObjC runtime 类注册与 +load、静态初始化、主线程阻塞这些“看不见的开销”,几乎成了后来做启动专项的官方教科书;后台侧 iOS 13 的 BackgroundTasksAdvances in App Background Execution(707))把后台刷新/处理从“碰运气”变成更符合系统调度与省电策略的 API:以 BGTaskScheduler 注册/调度任务,用 BGAppRefreshTaskRequest/BGProcessingTaskRequest 声明是否需要网络/外接电源等约束,并在过期回调里做好取消与收尾。配合 os_signpost + Instruments(Getting Started with Instruments(411))把关键路径打点,这一年官方工具链对“性能与底层”的表达其实前所未有地清晰。

这条线在大厂输出上也很明显:微信团队的 Matrix 把 APM 做成插件体系,WCCrashBlockMonitorPlugin(基于 KSCrash)把 crash 与卡顿的堆栈抓取打通,配合 RunLoop 状态判断卡死;WCFPSMonitorPlugin 在滑动等场景采集主线程 call stack;WCMemoryStatPlugin 记录内存分配与调用栈,为 OOM 归因补齐证据链。更轻量的泄漏排查上,腾讯的 MLeaksFinder 几乎是“装上就生效”:默认盯 UIView/UIViewControllerdealloc,泄漏时直接弹窗给出 View-ViewController 栈,还支持通过 willDealloc 排除单例/常驻对象并扩展到更大的对象图。美图的 MTHawkeye 走的是“in-app profiling”路线:UITimeProfiler(VC Life Trace + ObjC CallTrace)、ANRTrace(卡死时采样主线程栈)、NetworkMonitor(带 waterfall/重复请求检测)等插件直接挂在浮窗面板里;滴滴的 DoraemonKit/DoKit 则把接口 Mock(网络拦截)、沙盒浏览/文件导出、Mock GPS、Crash/卡顿堆栈、CPU/内存曲线、UI 层级检查、+load 耗时分析等调试能力统一收口到一个入口。

国际圈里,Facebook 的 Flipper(配套的工程博客:Introducing Flipper)把插件化调试平台做成标准形态:桌面端 client + 移动端 SDK,通过双向 socket 通道把数据送到 React 驱动的 UI;Layout Inspector、Network(甚至 GraphQL 请求流)、日志等能力都可以按插件组合扩展。它们共同指向一个趋势:性能优化开始从“某个专家会用 Instruments”迁移到“团队日常工程流程可复用”,而当你要继续把启动/加载成本压到 dyld/Mach-O 这一层时,Apple 的开源代码也能作为很好的旁证(例如 dyld)。

信息获取层面,周报能很好地把“当周必读”过滤出来(比如《老司机 iOS 周报 #73 | 2019-07-01》这种把资讯、文章与开源资源打包投喂的 weekly);而在“框架之外”的工程实践上,像 InfoQ 的《0.3 秒完成渲染!信息流内容页“闪开”优化总结和思考》也很典型:通过浏览器内核、客户端外壳、服务端、前端的多端协作,结合离线缓存策略突破 PWA/SSR 的局限,在日均亿级 PV 的场景下实现 100% 消除白屏——新框架再热,最终还是要落到渲染、链路与体验这些硬指标上。年末回看趋势时,再读一遍 The Decade of Swift 这种复盘文章(从 Swift 3 的标准库 API 重设计、Swift 4 的 Codable/key paths,到 2019 的 Combine/SwiftUI,以及编译器诊断/并发愿景),也能把“热点”重新拉回到“长期方向”。

2019 年 Apple Design Award 的获奖者很有代表性:Flow by Moleskine 是一款精致的数字笔记应用,与 Apple Pencil 配合得天衣无缝;Pixelmator Photo(现 Photomator) 把机器学习驱动的图片编辑带到 iPad,简单易用却功能强大;The Gardens Between 是一款关于时间、记忆与友谊的解谜游戏,画面和叙事都很治愈;Asphalt 9: Legends 把主机级画质带到了移动端;Butterfly iQ 让手持超声波设备成为现实,是医疗科技的标杆应用;HomeCourt 用 AI 追踪篮球投篮数据,是运动科技的创新典范。年度最佳应用和游戏方面,Spectre Camera(长曝光相机)和 Sky: Children of the Light(陈星汉团队新作)都是当年的亮点——Sky 的画风让人想起《风之旅人》,而 Spectre 则用计算摄影把长曝光变得简单优雅。

年底 Apple 还发布了 Best of 2019 官方榜单:iPhone 年度 App 是 Spectre Camera,iPad 年度 App 是 Flow by Moleskine,Mac 年度 App 是 Affinity Publisher;游戏方面 iPhone 年度游戏是 Sky,iPad 年度游戏是 Hyper Light Drifter,Mac 年度游戏是 GRIS。现在回头看,这份榜单其实把 2019 的几条主线(计算摄影、手写创作、独立游戏审美)抓得很准。

iOS 上还有不少现象级应用:剪映 凭借简单易用的剪辑功能迅速走红,成为短视频创作者的标配;飞书(字节跳动出品)开始在企业协作市场发力;滴答清单Sorted³ 在 GTD 工具圈很受欢迎。

macOS 上,Bear(熊掌记)继续是 Markdown 笔记的首选;Things 3 依然是 GTD 工具的标杆;Notion 在国内也开始流行起来,“All-in-One Workspace”的理念吸引了很多效率爱好者;Craft 虽然要等到 2020 年才正式发布,但相关理念已经在酝酿。开发工具方面,Paw(API 调试)、Proxyman(网络抓包,这年正式发布 1.0)、TablePlus(数据库客户端)继续是很多开发者的首选;Alfred 4 发布,效率党狂喜;CleanMyMac X 依然是清理 Mac 的不二之选。设计工具方面,Figma 持续蚕食 Sketch 的市场份额,“协作设计”成为新的关键词。

这一年我的工作重心已逐渐转向更基础的技术工作:解决疑难杂症、对工程做性能优化。我更多地关注一码多端的动态化技术——除了 JavaScriptCore 外,还深入了解了 React NativeWeex 的具体实现,以及字节跳动的 Lynx(后来在 2025 年重新发布)。除了引擎层面外,Bundle 版本发布管理平台也是很重要的一环。

2019 年末的时候,谁也没想到接下来的一年会如此不同。

2020年:疫情与 Apple Silicon 革命

2020 像一次“强制重排”:疫情把生活与工作方式拉进不确定性,Apple Silicon 的落地则让苹果生态按下新的加速键。

2020 年新冠疫情席卷全球。年初武汉封城让所有人猝不及防,远程办公、在线教育、直播电商一夜之间成了刚需;东京奥运会也史上首次被迫延期,很多人的“2020 计划”就这样被按下暂停键。1 月 26 日,科比·布莱恩特 在直升机事故中不幸离世,整个体育界为之震动——“凌晨四点的洛杉矶”成为永恒的记忆。3 月美股经历了史无前例的 四次熔断,巴菲特也感叹“活了 89 年没见过这场面”。

直播带货在这一年彻底爆发——薇娅李佳琦 成为现象级主播,“OMG,买它!”成为年度金句;罗永浩 4 月进军直播带货还债,“真还传”开始上演。

内容层面,《隐秘的角落》的“爬山”梗刷屏全网,秦昊、张颂文的演技让人直呼过瘾;《安家》以房产中介视角讲述都市百态,孙俪的演绎深入人心。B 站跨年晚会《最美的夜》在 2019 年底播出后口碑爆棚,也让更多人意识到 B 站不只是二次元。

居家期间,Switch 的《动物森友会:新地平线》在 3 月发售后爆红,“上岛”“大头菜”成了社交媒体的共同语言;《健身环大冒险》则把客厅变成健身房,缺货涨价也成了那年的另类记忆。而《Among Us》这款 2018 年上线的“狼人杀”风格游戏,在 2020 年因主播推广意外翻红,10 月全球下载量达到 7480 万次,“There is 1 Impostor among us”也成了年度经典台词。

Zoom 从一家视频会议公司变成了“疫情基建”,每日会议参与者(daily meeting participants)从年初的 1000 万跃升到 3 亿量级;钉钉、飞书、企业微信在国内竞争异常激烈。GPT-3 在 6 月由 OpenAI 发布(若该链接不可访问,可参考 GPT-3 论文:Language Models are Few-Shot Learners),1750 亿参数的规模让业界震惊,虽然普通开发者当时还用不上,但“大模型”的种子已经埋下。产业侧,年末 特斯拉 被纳入 S&P 500 指数,市值一度超过丰田成为全球最有价值的汽车公司;小鹏理想 在美股上市,蔚来 则在交付与股价上迎来高光时刻,中国新能源汽车“造车新势力”被推到聚光灯下。

同一年 10 月 20 日 React 17 发布,主打“更易渐进升级”(对大型项目而言,这比新特性更重要);Node.js 14 在 4 月发布并于 10 月进入 LTS,让一大批工具链与生产项目更稳地站上现代 JavaScript 语法与运行时。

开源社区这一年也很活跃。前端方面,Next.js 10 带来图片优化与国际化路由;Tailwind CSS 2.0 让原子化 CSS 成为主流。后端方面,FastAPI 凭借类型提示和自动生成 OpenAPI 文档迅速走红。iOS/Swift 生态方面,TCA(单向数据流、State/Action/Reducer/Effect、可测试 SwiftUI 架构)正式发布,SwiftFormat(自动代码格式化、可配置规则)、Pulse(网络请求日志、持久化与可视化调试)等工具开始被更多团队采用。疫情也带火了远程协作工具:Excalidraw(手绘白板)在疫情期间大受欢迎;Foam(VS Code 上的 Roam 替代品)让双向链接笔记在开发者圈子里流行起来。远程协作的另一面是“能自建就自建”:开源视频会议 Jitsi Meet 和直播推流神器 OBS Studio 进入更多人的工具箱;AI/NLP 领域 Transformers 进一步出圈,让“预训练模型 + 微调”成为工程常识;可观测性方向 OpenTelemetry 逐步成为 tracing/metrics/logs 统一语义的事实标准。

硬件方面,2020 年是苹果“改朝换代”的开始。11 月 10 日,苹果发布自研的 Apple Silicon M1 芯片,这颗 5nm 制程的 SoC 把 CPU、GPU、神经引擎和统一内存集成在一起,性能与能效双双跃升:搭载 M1 的 MacBook Air 无风扇设计却依然能大幅超越上一代,续航达到 18 小时;13 英寸 MacBook Pro 续航更是长达 20 小时,创下 Mac 历史之最;Mac mini 也不再只是性能“入门款”,CPU 性能提升 3 倍、GPU 提升 6 倍。苹果用实际行动证明:ARM 架构不仅能做手机,也能做电脑。对 Intel 的这次“断舍离”,也让整个 PC 行业重新审视“架构”这件事。

10 月 13 日,iPhone 12 系列发布,苹果终于拥抱 5G——A14 仿生芯片(业界首款 5nm 手机芯片)、超瓷晶面板、全新的直角边框设计(致敬 iPhone 4/5),以及 MagSafe 磁吸充电的回归,都让人眼前一亮。同时发布的还有 iPhone 12 mini(5.4 英寸小屏党狂喜)、iPhone 12 Pro 和 iPhone 12 Pro Max(LiDAR 扫描仪加持)。9 月的发布会上,Apple Watch Series 6 带来了血氧监测功能,入门款 Apple Watch SE 让更多人能买得起苹果表;iPad Air(第四代) 换上全面屏设计,首次把 Touch ID 塞进电源键,A14 芯片也让它性能直逼 iPad Pro。

WWDC 2020 首次完全线上举办,没有现场观众的 Keynote 反而更加紧凑高效;更关键的是,苹果也在同一届 WWDC 正式宣布 Mac 向 Apple Silicon 迁移,年末 M1 的发布就是第一批量产落地。iOS 14 带来了呼声最高的 桌面小组件(Widgets)——终于可以在主屏幕上放天气、日历、快捷方式了;App Library 自动整理所有 App,强迫症患者狂喜;App Clips 让用户无需下载完整 App 就能体验核心功能,扫码即用的体验非常丝滑;画中画 功能从 iPad 来到了 iPhone;Siri 和来电界面也终于不再全屏霸占了。macOS Big Sur 带来了近十年来最大的视觉重设计,圆角、半透明、新图标,整体更接近 iOS/iPadOS 的设计语言,也为 M1 Mac 上运行 iOS App 做好了准备。

SwiftUI 2.0 是这届 WWDC 的另一个亮点:新增了 WidgetKit(小组件开发框架)、LazyVStack/LazyHStack(懒加载布局)、LazyVGrid / LazyHGrid(网格布局)、App 协议@main 入口点)、@StateObject@AppStorage@SceneStorage 等新特性,让 SwiftUI 终于可以不依赖 AppDelegateSceneDelegate 独立构建完整 App 了。Swift 5.3 在 9 月 16 日随 Xcode 12 发布,带来了 @main 属性、多尾随闭包、Float16、更简洁的 #file 字符串、Swift Package Manager 对资源和本地化的支持,以及官方 Windows 支持。编译后二进制体积也显著减小——一个 SwiftUI App 的代码大小比 Swift 5.1 时代减少了 40% 以上。

WWDC20 的 What’s New in Swift(10170) 把 clean/dirty memory 的视角摆到台前,并展示了在 iOS 14 的 Swift 运行时上如何把一部分原本会变成堆上 dirty memory 的开销(比如元数据与缓存)压下去,示例里甚至能让某些场景的运行时堆开销降到“更早版本的三分之一以下”。同一届的 Advancements in the Objective-C Runtime(10163) 则从 ObjC runtime 的角度继续“把可不变的东西留在只读页、把可变部分延迟/隔离”,核心目的同样是减少 iOS 上更昂贵的 dirty pages。中文圈也有人把这场分享拆解得很细,比如这篇《优化 Objective-C runtime(WWDC2020)》就按 clean/dirty memory 把 class_ro_t/class_rw_t 的拆分与 class_rw_ext_t 的按需分配讲清楚,也解释了 relative method list(方法列表用 32 位偏移)与 swizzling mapping table 的取舍,最后落到一个很实用的提醒:别直接摸私有结构,尽量通过 class_copyMethodList/method_getImplementation 这类官方 API 来访问。

作为产品形态的变化,iOS 14 把主屏幕变成新的产品入口,小组件成为年度最大增量——从官方的 Widgets / WidgetKit,到 WWDC 的 Build SwiftUI views for widgets(WWDC20 10033),再到 @samwize 写的 Guide to WidgetKit(从 Widget Extension target 的签名/Provisioning,到 TimelineProviderplaceholder/getSnapshot/getTimeline 与 reload policy,再到 widgetFamily 适配、widgetURL/Link 的点击限制(小尺寸只能用前者)等细节整理成 checklist),你几乎能完整看到“新入口”如何被迅速工程化。与小组件一起出现的还有系统级的“更轻、更快”的体验取向:App Clips 让用户“先用再装”,App Library 则把主屏幕管理交给系统,产品形态在 2020 明显更强调低摩擦的触达与收纳。

开发细节层面,肘子的两篇 HowTo(SwiftUI2.0 使用 ToolBar 替代 navigationbarItems / 使用 ScrollViewReader 定位滚动位置)属于那种“拿来就能用”的小而美补丁:前者把 .toolbar + ToolbarItem(placement:) 如何覆盖 navigationBarItems(尤其是 .automatic 这类多平台 placement 的行为差异,以及 ToolbarContentBuilder 还不支持逻辑判断这类限制)讲清楚;后者则用 ScrollViewReader/ScrollViewProxy.scrollTo(_:anchor:) 演示基于 .id 的滚动定位,并点明“想反向记录当前滚动位置”仍然不太优雅。

工具链这边,Swift 5.3 + Xcode 12 把 Swift Package Manager 推到“生产可用”的关键一档:官方的 Swift 5.3 Release ProcessSwift 5.3 Released!(以及 SwiftGG 的对应译文:Swift 5.3 发布流程 / Swift 5.3 released!)把发布节奏与工程向特性讲得很完整;当你真的要把包“用进项目”时,最容易翻车的往往不是写 Swift,而是“包里带资源/做本地化/引入二进制依赖”。这时配合官方文档 Bundling resources with a Swift packageBundle.module)和提案 SE-0272 Package Manager Binary Dependencies,再读 Vincent Tourraine 的 Notes WWDC 2020 : Swift packages - Resources and localization 与 @samwize 的 How to localize resources in Swift packages,就能把“宿主 App 也要声明 Localizations”这类隐蔽坑点一次性扫掉。

在体验红线上,2020 仍然绕不开启动优化:年初那两篇被高频转发的文章——《一行代码解决!iOS二进制重排启动优化》与《抖音研发实践:基于二进制文件重排的解决方案,APP启动速度提升超15%》——把“二进制重排(order file)如何落进构建链路”以及“Page Fault 视角下为什么重排有效”的因果链路从原理到数据都讲透了:cold launch 时 dyld 把 Mach-O mmap 到虚拟内存后,随着 pc 寄存器不断跳转取指执行,会频繁触发缺页中断来加载物理页(A9+ 处理器 16KB/页),一次 page fault 耗时 0.3~1ms,启动期间可能发生 2000+ 次、累计 300ms 以上;通过 hook objc_msgSend、插桩 +load/C++ constructor,或借助 SanitizerCoverage 编译时插桩拿到真实执行顺序,把启动期调用的函数符号紧凑排列进 order file,就能大幅减少 page fault 与 disk thrashing 风险。

如果你想把 pre-main 再往下拆到 dyld/Mach-O/initializers 的具体成本,美团外卖那篇《美团外卖 iOS 冷启动治理》至今仍是绕不过的底层教材:文章把启动划分为 T1(main 前的加载链接)、T2(main 到 didFinishLaunching)、T3(首页渲染完成)三个阶段,用 DYLD_PRINT_STATISTICS 与火焰图做精准定位,再通过减少动态库、减少 +load、清理无用类/方法等手段压缩 T1;更有价值的是它提出的”分阶段启动 + 启动项自注册”机制——借助 Clang 的 __attribute__((section())) 把函数指针写入 __DATA 段、运行时按阶段触发,既实现解耦又能跨端复用;闪屏页的并行构建、缓存定位 + 首页预请求等”变串行为并行”的策略,同样是可直接落地的工程抓手。

而启动之外,2020 另一个在大厂里被反复打磨的主题是”卡顿/无响应”的线上归因:iOS 没有 Android 那样的 ANR 系统回调,工程上通常走”RunLoop 状态机 + 子线程采样堆栈”这条路——在主线程 RunLoop 的 kCFRunLoopBeforeSources/kCFRunLoopAfterWaiting 等状态切换时打点计时,超时则通过 Mach 线程接口(task_threads/thread_get_state)抓取主线程调用栈。微信团队把这套方案在 Matrix 的 iOS 端落成可复用的插件体系(卡顿检测、内存监控、耗电归因等模块可独立接入);美图在 2020 开源的 MTHawkeye 则把 ANRTrace/UITimeProfiler/FPS 等能力做进 App 内的 profiling 面板,还支持网络请求监控与 GPU 过度绘制检测(发布介绍:MTHawkeye 重磅发布);滴滴的 DoKit(DoraemonKit) 也把 FPS/CPU/内存/卡顿等”开发期必备仪表盘”统一收口到一套工具入口里,并支持 mock 数据、H5 任意门、视觉对比等便捷调试功能。

内存这条线在 2020 也明显”从泄漏排查走向 OOM 归因”:除了 Instruments 的 memgraph/leaks 这些老牌工具,腾讯开源的 OOMDetector 把”大内存分配监控(通过 hook malloc/vm_allocate 等接口记录分配调用栈)、泄漏扫描、爆内存前 dump 调用栈”做成组件,特别适合拿来解释”为什么触发了 jetsam 被系统杀掉但看不到 crash 日志”这种线上 FOOM(Foreground Out Of Memory)疑难问题;而当它与 Apple 在 WWDC20 强调的 clean/dirty memory、ObjC runtime 只读化/延迟化策略放在同一张图里,你会更容易把内存优化从”省点内存”升级为”降低 dirty pages、减少可变页写入、把高成本工作推迟到真正需要的时候”。

如果说启动优化偏“工程内功”,那么 2020 也是 Apple 把“线上真实用户的性能问题”正式工程化的一年:Xcode 12 的 Organizer 能直接看到按版本聚合的滚动卡顿(scroll hitches)与磁盘写入异常等指标(Diagnose performance issues with the Xcode Organizer(WWDC20 10076));而 MetricKit 2.0(What’s New in MetricKit,WWDC20 10081) 则把这些能力下沉到 API 层面:除了新增 CPU instructions、scroll hitches、app exit reasons 等指标外,还补齐了 MXDiagnosticPayload 这类诊断 payload(hang/crash/CPU exception/disk write exception),让“发现问题”不止停留在曲线上,而是能拿到 call stack tree 去定位(文档入口:MetricKit,例如 MXDiagnosticPayloadhangDiagnostics)。与之配套的是“把卡顿写进测试”:在 Eliminate animation hitches with XCTest(WWDC20 10077) 里,Apple 把 hitches 指标(ms/sec)和 XCTOSSignpostMetric(比如滚动拖拽/减速)交到 XCTest 的性能测试里,让“顺不顺”第一次更像一条可回归的 CI 指标;社区里也有不少好笔记/翻译,比如《Eliminate animation hitches with XCTest》与基于 Organizer 的实战拆解《Diagnose Performance Issue》,以及 Donny Wals 的《Measuring Performance with os_signpost》(从 OSLog/signpostID 的写法,到 Instruments 里添加 os_signpost + Points of Interest 两条 track、按 begin/end 自动聚合区间并统计耗时,基本把 signpost→Instruments 的链路讲透)。

图形/渲染这条线在 2020 也有一套更“硬”的官方教材:Apple GPU 的 TBDR 架构、GPU counters 的定位方法,以及 Xcode 12 的 Metal Debugger/Metal System Trace(含 summary insights)如何落到“找瓶颈→改一处→复测”的闭环,基本都在这几场里讲透了——Harness Apple GPUs with Metal(WWDC20 10602)Optimize Metal apps and games with GPU counters(WWDC20 10603)Gain insights into your Metal app with Xcode 12(WWDC20 10605)

对我个人而言,2020 几乎是围着“启动”在转。年初受前滴滴同事邀请去快手做分享时,我把启动阶段耗时分析的思路与工具链梳理成 Slides,并把方法耗时分析工具 MethodTraceAnalyze 开源出来(记录在这篇文章里:在快手做分享、无用类检查、在广州做 SwiftUI 学习笔记分享、InfoQ二叉树视频)。同一时期我也在广州做了一次 SwiftUI 学习笔记分享,但“为了讲清楚”而反复梳理的过程,对做性能优化同样重要。

那次整理对我最大的价值,是把“启动优化”从经验活变成工程活:不再泛泛地说“启动慢”,而是把链路拆到 dyld / ObjC runtime / +load / C/C++ 静态初始化这些具体环节,再用数据说话。也因此我开始更认真地做无用类/无用代码治理——它表面上在做包体积,最后却会落到启动加载成本上;光靠静态扫描远远不够,还需要结合运行时证据做灰度验证。

年底我把高德 App 启动优化专项里用到的手段与一些想法系统写成了《App 启动提速实践和一些想法》:从唤端与 H5 启动页体验,到 pre-main 阶段的加载与链接开销,再到用 SanitizerCoverage/objc_msgSend 采样生成 order file,以及线程调度与任务编排,最后落到“把指标做成自动化管控,才能长期守住收益”。

SwiftUI 的兴起也带火了更可组合、可测试、可观测的架构范式:Point-Free 用《Composable Architecture, the library》把 TCA 作为库正式推出,又在《Instrumenting features built in the Composable Architecture》里把 os_signpost 这类性能度量做成一行可插拔的 .signpost() higher-order reducer——既能统计 action 的发送频次与 reducer 的执行耗时,也能把 effect 的开始/完成/取消纳入同一套 Instruments 观测;而年底 Swift by Sundell 的《The lifecycle and semantics of a SwiftUI view》则从“View 是值类型、body 只是描述而不是生命周期回调”的角度,提醒大家别在 body 里触发副作用,而应使用 onAppear/onReceive 等修饰符承载时序,并在 UIViewRepresentablemakeUIView/updateUIView 里正确复用 UIKit 视图,避免把 UIKit 的生命周期直觉硬套进去。

系统化学习资源方面,国际上 objc.io 在这一年出版了 《Thinking in SwiftUI》——更强调用 SwiftUI 的数据流与语义来组织界面(状态、绑定、环境值这些概念该怎么“顺着框架”去用);喵神在 objccn.io 出版的《SwiftUI 与 Combine 编程》则用实践把 SwiftUI 的状态驱动和 Combine 的响应式链路(Publisher/Operator/Scheduler)串起来;而追踪资讯最省力的入口之一依旧是《老司机 iOS 周报》。

2020 年 Apple Design Award 的获奖作品很有代表性:Sayonara Wild Hearts(Simogo/Annapurna Interactive)把复古街机与音乐 MV 融为一体,视觉和听觉都是顶级享受;Sky: Children of the Light(thatgamecompany)延续了《风之旅人》的治愈风格,强调合作与分享;Song of Bloom(Philipp Stollenmayer)是一款脑洞大开的解谜游戏;Where Cards Fall(The Game Band/Snowman)用纸牌屋构建梦境,美术风格独树一帜。年底 Apple 发布的 Best of 2020 榜单 中,iPhone 年度 App 是 Wakeout!(居家健身)、iPad 年度 App 是 Zoom(疫情刚需)、Mac 年度 App 是 Fantastical(日历神器);游戏方面,iPhone 年度游戏是《原神》、iPad 年度游戏是《Legends of Runeterra》(拳头的卡牌游戏)、Mac 年度游戏是《Disco Elysium》(神作 RPG)。

国内 App 方面,腾讯会议 在疫情期间迅速崛起,成为国内在线会议的首选;钉钉 被“网课”带火,虽然收获了大量一星差评(学生党的怨念),但 MAU 突破 3 亿;学习通 也因网课需求下载量暴涨,虽然用户体验一言难尽,但确实成了很多学生的“噩梦”和日常。小宇宙 播客 App 在 3 月上线,凭借简洁的设计和优秀的收听体验,成为中文播客圈的现象级产品。飞书 凭借字节跳动内部沉淀的协作理念,在企业市场持续发力。美团外卖饿了么 在疫情期间成为刚需,骑手们成了城市里最忙碌的人。

疫情也让一批“基础设施型 App/服务”从“装不装无所谓”变成了“不装不行”:各地的健康码/通行码几乎成了出门标配;在线教育方面,从“停课不停学”到“直播上课”,腾讯课堂、雨课堂、ClassIn 等把课堂搬进手机;文档协作方面,腾讯文档 / 石墨文档 这种多人实时编辑工具也变成了远程协作的日常。

iOS 上小组件的开放带火了一批定制化应用:Widgetsmith 让用户自定义各种风格的桌面小组件,一度登顶 App Store 免费榜;Color Widgets 也凭借精美的预设设计吸引了大量用户;主屏幕美化一时成为社交媒体上的热门话题,“iOS 14 美化教程”刷屏各大平台。健身应用方面,Keep 在居家期间用户激增,“自律给我自由”成为很多人的桌面壁纸;Nike Training Club 宣布免费开放高级功能,也收获了一波好感。

Mac 端,Notion 在国内的使用者越来越多,“All-in-One”的知识管理理念开始深入人心;Craft 在年底发布,以原生 Swift 开发、精美的设计和流畅的体验,成为 Notion 的有力竞争者;Obsidian 以本地 Markdown 文件 + 双向链接的方式,吸引了一批注重数据所有权的用户。效率工具方面,Raycast(年底公开测试)开始挑战 Alfred 的地位,凭借现代化的设计和丰富的扩展生态迅速积累口碑;Rectangle(开源的窗口管理工具)成为很多人 Spectacle 的替代品;CleanShot X 让截图和录屏变得更加优雅。远程协作工具也几乎成了“上班必备”:ZoomMicrosoft TeamsSlackFigmaMiro 这些软件在 2020 频繁出现在屏幕共享里。开发工具方面,Fig(终端自动补全,后被 AWS 收购)开始在开发者圈子里流行;TablePlusProxyman 继续是数据库和网络调试的首选。

2021年:并发与元宇宙

如果用两个关键词概括 2021:一个是“元宇宙”点燃的想象力与泡沫;另一个是 Swift 5.5 让并发从“写法问题”升级为“语言级范式切换”。

2021 年我最喜欢的电视剧是 《觉醒年代》,豆瓣 9.3 分几乎是年度口碑之王。《你好,李焕英》 作为贾玲的导演处女作,票房冲到 54 亿,母女情催泪无数;《山海情》 则用扶贫题材打动人心。

2 月 Clubhouse 音频社交横空出世,马斯克在 Clubhouse 上聊天带火了这款应用,邀请码一度被炒到几百块。虽然热度只持续了几个月,但“音频社交”的概念从此进入主流视野。

同样被马斯克带火的还有加密货币。年初 狗狗币(Dogecoin) 在他推特带货下暴涨,一度冲进加密货币市值前十;3 月数字艺术家 Beeple 的 NFT 作品在佳士得拍出 6934 万美元,“NFT”一夜之间成为热词,Bored Ape Yacht Club(无聊猿) 等 PFP 项目爆火——虽然后来泡沫破裂,但 2021 的确称得上“NFT 元年”。

体育与航天方面同样有高光时刻。7 月 23 日延期一年的 东京奥运会 终于开幕,空场比赛的画面成了史无前例的奥运记忆,苏炳添 9 秒 83 闯入百米决赛让无数人热泪盈眶。航天领域,4 月 29 日 空间站天和核心舱发射升空,中国空间站建设正式拉开序幕;5 月 15 日 天问一号着陆巡视器成功着陆火星(祝融号火星车着陆火星乌托邦平原),中国成为继美国之后第二个成功让火星车着陆的国家。

10 月 28 日,Facebook 宣布更名为 Meta,扎克伯格押注“元宇宙”成为下一代互联网入口——这一动作点燃了全球对元宇宙的狂热讨论,“元宇宙元年”成为年度关键词。国内科技巨头也迅速跟进:8 月 字节跳动以 90 亿元收购 VR 厂商 Pico百度 12 月推出元宇宙产品“希壤”;腾讯网易 也纷纷申请元宇宙相关商标。3 月,被称为“元宇宙第一股”的 Roblox 在纽交所上市,首日市值突破 400 亿美元,让资本市场看到了这个赛道的想象空间。

与元宇宙的喧嚣形成对比的,是中国互联网行业的监管风暴。4 月 10 日,阿里巴巴因“二选一”垄断行为被罚 182.28 亿元,创下中国反垄断罚款纪录。7 月 滴滴 赴美上市后旋即遭遇网络安全审查,App 下架整改。7 月 24 日 “双减” 政策落地,教培行业一夜入冬——新东方好未来 等巨头股价腰斩,很多程序员朋友也在这一年经历了“教育行业大逃离”。这一年也被称为中国平台经济的“反垄断元年”,《国务院反垄断委员会关于平台经济领域的反垄断指南》 在 2 月正式发布,全年反垄断处罚案例超过 120 起。

GitHub Copilot 在 6 月以技术预览版亮相,第一次让“AI 写代码”从概念变成了能用的工具——虽然当时只支持 VS Code,但已经让很多人惊呼“这才是未来”。Rust 继续蝉联 Stack Overflow 最受喜爱编程语言,Rust Foundation 也在 2 月正式成立,让 Rust 的治理更加正规化。Tailwind CSS 持续流行,“原子化 CSS”成为新的设计范式;Vite 2.0 在 2 月正式发布,成为前端构建工具的新宠。

前端与全栈方向,Next.js 12Remix 把“全栈框架”的叙事推到台前;Astro(静态站点生成器,”岛屿架构”、零 JS 默认、多框架组件)在这一年开始崭露头角;pnpm(硬链接共享 node_modules、严格依赖隔离、workspace 支持)凭借高效的硬链接和严格的依赖管理迅速流行,很多项目开始从 npm/yarn 迁移;Playwright(微软出品的 E2E 测试框架,跨浏览器、Auto-wait、Codegen 录制)成为 Cypress 的有力竞争者;Tauri(Rust + Web 构建桌面应用)作为 Electron 的轻量替代开始被更多人关注。后端方面,Prisma(TypeScript ORM)在 Node.js 项目中越来越流行;NocoDB(开源 Airtable 替代)、Appwrite(开源 BaaS)也在这一年获得大量 Stars。DevOps/工具方向,Fig(终端自动补全)开始在开发者圈子里流行;GitHub CLI 持续完善,gh 命令成为很多人的日常。

对 iOS 开发者来说,2021 还有一条“业务强相关”的暗线:隐私合规。4 月 iOS 14.5 正式启用 App Tracking Transparency(ATT)(框架文档:AppTrackingTransparency,官方时间点见:Upcoming requirements: App Tracking Transparency),IDFA(ASIdentifierManager)不再是“默认可用”,广告归因更多要转向 SKAdNetwork。这不是纯技术话题,却直接决定了很多 App 的增长、投放与变现策略,也让“上线前对照清单做适配”变成了当年的新常态。

9 月 14 日,苹果发布 iPhone 13 系列:A15 仿生芯片、电影效果模式(Cinematic Mode)、更小的刘海、更长的续航,以及 Pro 系列的 ProMotion 120Hz 自适应刷新率——那段时间朋友圈被“丝滑”刷屏。但真正让开发者沸腾的是 10 月 18 日发布的 MacBook Pro(14/16 英寸)M1 Pro 和 M1 Max 芯片把性能拉到新高度,统一内存最高 64GB,MagSafe 充电回归,HDMI 和 SD 卡槽回归,刘海屏设计虽然争议不断但 Liquid Retina XDR 显示效果确实惊艳。这一代 MacBook Pro 成为很多开发者“等了五年终于换机”的答案——Touch Bar 终于退场,剪刀脚键盘和实体功能键回归,编译速度提升肉眼可见。4 月还发布了搭载 M1 芯片的 iMac(24 英寸),七彩配色让桌面焕然一新。

WWDC 2021 依旧以线上形式举办,但内容密度丝毫不减。这届 WWDC 最重磅的发布当属 Swift 5.5 带来的全新并发模型:async/await 让异步代码终于能像同步代码一样写,告别回调地狱;Actor 用类型系统保护共享可变状态,让并发安全成为编译期保证;Structured ConcurrencyTaskTaskGroupasync let)让并发任务的生命周期管理更可控。这套并发系统从提案(SE-0296SE-0306 等)到落地,经历了数年讨论,终于在 2021 年正式交付——这也是 Swift 自 2014 年发布以来最重大的语言特性变化之一。

iOS 15 / iPadOS 15 带来了 SharePlay(同播共享,可以和朋友一起看视频、听音乐)、Focus Modes(专注模式,按场景过滤通知)、Live Text(实况文本,图片里的文字可以直接复制)、重新设计的通知系统和天气 App。macOS Monterey 则带来了 Universal Control(通用控制,一套键鼠控制多台设备)、AirPlay to Mac、以及 Safari 的标签页组等功能。开发者工具方面,Xcode Cloud 作为苹果官方的 CI/CD 服务首次亮相,Object Capture(3D 扫描 API)让大家开始畅想苹果眼镜的未来。

SwiftUI 3.0 这一版补齐了很多实用能力:AsyncImage(异步加载图片)、refreshable(下拉刷新)、searchable(搜索框)、swipeActions(滑动操作)、listRowSeparator(分隔线控制)、confirmationDialog(确认对话框)等修饰符让很多以前需要 UIKit 桥接才能实现的功能变得原生可用。AttributedString 也在这一版正式落地,配合 Text 可以直接渲染富文本。同时,SwiftUI 与 async/await 的集成也非常自然——task 修饰符让在视图里发起异步请求变得优雅。Swift Playgrounds 4 也在年底发布,第一次可以在 iPad 上直接开发并提交 SwiftUI App 到 App Store。

同届 WWDC 还带来了 StoreKit 2(更现代的订阅与交易 API)、以及更完善的订阅状态与校验链路(配合 StoreKit Transaction / VerificationResult 这类类型安全的表达)。对做会员/订阅的 App 来说,这是 2021 另一个“看起来是框架升级,实际是商业底座升级”的变化。

2021 年下面几篇值得一读的文章:

  • 官方的 Swift 5.5 Released! 把 Swift Concurrency 的主线(async/await、structured concurrency、Actors)以及一系列配套提案(Objective-C 并发互操作、continuations、Task Local Values、async let)串在一起,读完能对“哪些写法是语言保证、哪些仍是约定俗成”有清晰边界;中文圈里喵神的 Swift 结构化并发 从 goto 语句与结构化编程的历史切入,延伸到任务树的父子关系、withUnsafeCurrentTask 的取消/优先级语义,以及 withTaskGroup + for await 的收束方式,把取消传播(cancellation propagation)和错误传播如何沿任务树自动级联讲得很透;avanderlee 的 Actors in Swift 则以 data race 为切入口,解释 actor 的隔离状态(isolated state)为什么要求 await 跨边界访问、如何用 nonisolated 放开只读成员,以及如何减少不必要的 suspension point(比如避免重复 await)来兼顾安全与性能。
  • Majid 的 Mastering AsyncImage in SwiftUI 基本是最常被抄作业的一篇,从多个 initializer 入手讲清楚怎么拿到下载后的 Image 去做 resizable/contentMode/clipShape 等定制,再用 AsyncImagePhase 组织 loading/success/failure 的状态机,配合 placeholderTransaction 把过渡动画也一并管起来;中文圈里喵神的 TCA - SwiftUI 的救星? 从 Elm 架构启示讲起,把 Feature 拆解为 State/Action/Reducer/Store(以及 Environment、Effect)、ViewStore/WithViewStore 的观测方式、TestStore 的断言式测试与 .scope 的切分策略串成一条线,让“单向数据流 + 副作用 + 可测试”在 SwiftUI 里有了更可落地的形态;肘子的 用 Async-Await 重建 SwiftUI 的 Redux-like 状态容器 则把 Store 提升到 @MainActor 的语义边界,用即发即弃的 Task 管副作用生命周期,并在副作用方法中返回 Task<Action, Error>,把原本需要 Combine Publisher/订阅管理的样板代码收敛到更简洁的 async/await 写法。
  • ATT 适配踩坑看 @samwize 的 Pitfall: ATT prompt not showing,它指出 iOS 15 起如果在 launch 阶段、applicationState == .inactive 时调用 ATTrackingManager.requestTrackingAuthorization,可能导致系统弹窗不出现并引发审核问题;更稳妥的做法是把触发点放到 applicationDidBecomeActive(或延迟一拍)再请求;StoreKit 2 看 wwdcbysundell 的 Working with in-app purchases in StoreKit 2,除了 Product/Transaction/VerificationResult 的类型安全链路,还覆盖了 Xcode 的 StoreKit configuration file 测试、purchase() 的多状态结果处理、Transaction 的 async sequence 监听(pending/更新)、currentEntitlements + revocationDate 的权益判断,以及应用内发起退款流程的 API。
  • WWDC21 的 Ultimate Application Performance Survival Guide 把 MetricKit、Instruments、Xcode Organizer 等性能指标和工具链串成闭环。国内大厂的输出同样值得关注:字节的《抖音 iOS 启动优化实战》从分阶段埋点监控讲起,详解 +load/initializer 调用顺序与链接顺序的关系、Runloop 回调时机(kCFRunLoopBeforeTimers)、CA::Transaction::commit 首帧检测等无侵入方案;《OOM 崩溃率下降 50%+》则从 iOS 的 Jetsam 内存管控机制切入,把 FOOM/BOOM 区分、pageSize/rpages 内存页计算、XNU 内核清理策略讲透;腾讯的 Matrix 以插件化 APM 覆盖卡顿、FPS、内存等维度;滴滴的 DoKit 提供端内调试面板;美团的 cocoapods-hmap 编译提速 用 Header Map 技术替代传统 Header Search Path,通过 .hmap 头文件映射表减少文件 IO、解决 400+ 组件工程的编译瓶颈——这些都把”性能问题”做成了可工程化落地的方案。
  • 更多阅读可参考 老司机 iOS 周报 的 2021 年归档,以及 SwiftGG 的中文翻译。

2021 年开源社区也很活跃。并发相关的项目开始涌现,比如 Apple 官方的 swift-async-algorithms(异步序列算法库,提供 merge/zip/combineLatest/debounce/throttle 等操作符)开始酝酿;底层与系统编程方向,Apple 也把 swift-atomics 推到 1.0(原子操作与无锁并发基石),并在 Swift Forums 宣布 Swift System 1.0swift-system)发布,让 Swift 在“系统接口/文件描述符/路径”这层也开始有了更标准的抽象。工程化工具方面,SwiftFormatSwiftLint 持续是代码规范的首选;XcodeGen(YAML 定义项目结构、生成 .xcodeproj)和 Tuist(Swift DSL 项目描述、依赖图可视化、模块化缓存加速)在项目配置管理上各有拥趸;Kingfisher 5.15 / 6.0 版本持续更新,适配 async/await;Alamofire 也在这一年开始适配 Swift Concurrency。SPM(Swift Package Manager)的使用也在这一年显著增长,越来越多的库开始优先甚至只支持 SPM。

文档与基础设施也在补齐:WWDC21 推出的 DocC 文档系统(基于 Markdown、支持教程与 API 文档、Xcode 集成)以及随后开源的 swift-docc / swift-docc-render 让”文档即代码”更像官方正统;同年 Apple 还开源了 swift-collections(Deque/OrderedSet/OrderedDictionary 等)与 swift-markdown(Markdown 解析),Swift 的基础设施越来越厚。

Apple Design Award 2021 的获奖者很有代表性:Voice Dream Reader(文字转语音,无障碍设计典范)、Pok Pok Playroom(儿童创意应用)、CARROT Weather(毒舌天气,年度 Apple Watch App)、原神(视觉与图形奖)、NaadSadhana(AI 音乐创作)、英雄联盟手游(创新奖)。年底 Apple 发布的 App Store Awards 2021:iPhone 年度 App 是 Toca Life World(儿童创意游戏)、iPad 年度 App 是 LumaFusion(专业级视频剪辑)、Mac 年度 App 是 Craft(原生文档工具,凭借丝滑体验成为 Notion 的有力竞争者)、Apple Watch 年度 App 是 CARROT Weather。游戏方面,iPhone 年度游戏是英雄联盟手游、Apple Arcade 年度游戏是 Fantasian(坂口博信新作)。Apple 还把“年度趋势”定为“Connection”(连接),致敬那些在疫情期间把人们连接在一起的应用,比如 Among Us!BumbleCanva 等。

iOS 上值得关注的应用还有:Widgetsmith 延续上一年的热度,小组件定制依然火爆;Day One(日记应用)获得了更多用户;Craft 凭借年度 Mac App 的声誉在 iOS 上也收获了大量用户;GoodNotes 5Notability 继续是 iPad 手写笔记的双子星(虽然 Notability 年底宣布转为订阅制引发争议);Telegram 在隐私意识觉醒的背景下用户量持续增长;Discord 从游戏社区扩展到更广泛的兴趣社群。

Mac 端,Raycast 在这一年持续迭代,扩展生态越来越丰富,正式成为 Alfred 的强力竞争者——很多人是在这一年完成了从 Alfred 到 Raycast 的迁移;Obsidian 的双向链接笔记在开发者圈子里持续流行,成为知识管理的新范式,社区插件生态也在这一年爆发式增长;Craft 凭借原生 Swift 开发、精美设计和流畅体验获得年度 Mac App,也让很多人开始重新审视”原生 vs Electron”的选择;Logseq(开源双向链接笔记)作为 Obsidian/Roam 的开源替代也开始被更多人关注。

还有一类“低调但装机率极高”的小工具:视频播放器 IINA;键盘改键神器 Karabiner-Elements;外接显示器亮度控制 MonitorControl;以及菜单栏系统监控 Stats 等——它们不一定上榜,但很多人的 Mac 没它们会少点顺手。

效率工具方面,CleanShot X 在截图和录屏领域越来越强大;Bartender 4 继续是菜单栏管理的不二之选;Rectangle(开源窗口管理)已经基本取代了停更的 Spectacle;Paste(剪贴板管理)和 PopClip(文本增强)仍是很多人的装机必备;Fantastical 继续是日历应用的标杆;Things 3OmniFocus 3 依然是 GTD 工具的两大顶流;Linear(项目管理)凭借流畅的体验和现代化的设计在科技公司中迅速流行。

开发工具方面,Proxyman 持续是网络调试的首选,2.0 版本带来了更多专业功能;TablePlus 在数据库管理上越来越成熟;Warp(基于 Rust 的现代终端)开始内测,虽然要等到 2022 年才公开发布,但已经在开发者圈子里引发期待;iTerm2 依然是很多老用户的选择;VS Code 在 M1 Mac 上的原生支持让体验更加流畅,Remote Development 功能也让远程开发成为日常;JetBrains 全家桶在这一年也完成了 Apple Silicon 原生适配。设计工具方面,Figma 持续蚕食 Sketch 的市场份额,“协作设计”成为主流;Framer 转型为无代码建站工具后也获得了新生。

这年我研读了下 QuickJS 代码,写了篇 深入剖析 JavaScript 编译器/解释器引擎 QuickJS - 多了解些 JavaScript 语言 从 JavaScript 与作者 Fabrice Bellard 的背景切入,解释 QuickJS 如何用约 210KB 的体量覆盖最新 ECMA-262,并通过 Test262 把兼容性“跑出来”;再从工程视角补上 makefile、qjsc、字节码、以及如何用 Xcode 编译安装和调试源码。

另外我还写了篇 我写技术文章的一点心得:用“熵增/逆熵”的视角,把写作拆成“独特性、真实感、故事性、新意”四个维度,并给出从记录→归类→搭骨架→完善与包装的四步法,以及一套写完后的自检问题清单与常用工具链。

2021 这一年是“并发元年”——Swift 5.5 带来的 async/await 和 Actor 模型,从根本上改变了 iOS/macOS 异步编程的范式,影响将在接下来几年持续发酵。M1 Pro/Max 的发布让 Apple Silicon 从“够用”变成了“真香”,很多开发者在这一年完成了设备换代。

这一年我也开始更认真地关注 SwiftUI 的演进——虽然业务代码还是以 UIKit 为主,但 SwiftUI 明显在补齐短板,感觉“下一个项目可以试试”的时机越来越近了。年末的时候,大家还在讨论元宇宙能不能成,没人预料到一年后的 ChatGPT 会让整个行业的话题彻底转向。

2022年:AI 元年与 SwiftUI 工程化

2022 年是新旧时代的分水岭:一边是生成式 AI 的点火,把“写代码/写内容”的方式推向新范式;另一边是 SwiftUI 4、Swift Concurrency 与 Xcode 14 把苹果生态的工程化底座补齐。沿着这两条主线回看,会更容易理解这一年为什么重要。

11 月 30 日,ChatGPT 发布,两个月内用户数突破 1 亿,成为互联网历史上增长最快的应用。我此前一直在使用 GitHub Copilot,但 ChatGPT 带来的震撼更强——它像一个随叫随到的“知识型同事”,能即时回答各种技术问题,也能帮你把思路梳理成可执行的路径。

把时间线往前拨,图像生成领域迎来三巨头:3 月 Midjourney 面世、4 月 DALL·E 2 发布、8 月 Stable Diffusion 开源。Stable Diffusion 作为 latent diffusion(LDM)路线的代表,通过 CLIP 文本编码、U-Net 去噪与 VAE/autoencoder 的潜空间压缩组合把“文生图”落到可运行的工程形态;围绕它的生态(如 AUTOMATIC1111/stable-diffusion-webui)也在几个月内迅速成熟,把 txt2img/img2img、inpainting/outpainting 这类生成模式,以及 negative prompt、seed、采样器与 guidance scale(引导强度)等常用调参入口做成可复用的本地工作流,让“人人都能本地跑图”成为现实。

语音领域同样取得突破:9 月 OpenAI 开源 Whisper,它是基于 Transformer 的 sequence-to-sequence 多任务模型,不只覆盖多语言语音识别/转写,也支持语音翻译(speech translation)和语种识别等任务;配合 ffmpeg 等基础依赖,用命令行或脚本就能在本地按滑动的 30 秒窗口做推理,把“语音转文字”真正接进内容生产、检索与质检等产品工作流。

科技行业迎来一轮 互联网寒冬。10 月,马斯克以 440 亿美元完成对 Twitter 的收购后,立即裁员 50%;随后 Meta、亚马逊、Salesforce 等巨头相继宣布裁员或冻结招聘,国内互联网公司也在“降本增效”。11 月 FTX 交易所暴雷,加密货币市场一夜入冬。

监管层面,欧盟 9 月正式通过 Digital Markets Act(DMA),“iOS 是否要开放侧载和第三方应用商店”开始从讨论变成实打实的时间表。

12 月国内疫情政策调整,刘畊宏毽子操和“羊了个羊”小游戏成为居家期间的集体记忆。

系统编程领域,Rust 持续崛起,Linux 内核开始接受 Rust 代码,Rust 连续第七年蝉联 Stack Overflow 最受喜爱编程语言。

JavaScript 生态迎来多个重要发布:6 月 Bun(Zig 编写的 JS 运行时,内置打包器/测试/包管理)以极快的启动速度震动社区;10 月 Next.js 13 引入号称比 Webpack 快 700 倍的 Turbopack(Rust 编写的增量打包器)。桌面应用框架方面,6 月 Tauri 1.0 正式发布(Rust 后端 + Web 前端、调用系统 WebView、极小体积),以极小体积和原生性能成为 Electron 的有力替代;8 月 Astro 1.0 带来“岛屿架构”,让静态站点也能拥有局部交互。

几个“生态级版本号”也在这一年集体换挡:3 月 React 18 将并发渲染带入主流;5 月 Flutter 3 完成“六端稳定”拼图;10 月 Python 3.11 以解释器性能提升出圈;12 月 SvelteKit 1.0 落地,“更轻的框架 + 更快的构建”成为前端热议话题。

这些变化发生在整个技术世界,而苹果生态在 2022 的关键词则更集中:iOS 16 把“入口”推到锁屏与灵动岛,SwiftUI 4 把“导航”重写,Xcode 14 把“诊断”下沉成可闭环的工具链。

9 月 7 日,苹果发布 iPhone 14 系列:Pro 系列首次采用“灵动岛”(Dynamic Island)取代刘海,把“挖孔”变成“交互入口”;同时首次支持 48MP 主摄、Always-On 显示,以及卫星紧急求救(SOS)功能。同期发布的 Apple Watch Series 8 新增体温感应和车祸检测,Apple Watch Ultra 则瞄准户外极限运动市场;AirPods Pro(第二代) 以两倍降噪和自适应通透模式再次刷新真无线耳机体验。

10 月,苹果更新 iPad 线:搭载 M2 芯片iPad Pro 与全新设计的 iPad(第十代) 一起发布——iPad 终于告别了 Lightning 接口。

WWDC 2022 对开发者来说干货满满。iOS 16 最大的亮点是 锁屏小组件(Lock Screen Widgets)和高度可定制的锁屏界面——开发者可以通过 .accessoryInline.accessoryCircular.accessoryRectangular 三种新 widget family 把信息直接展示在锁屏上。macOS Ventura 带来了 台前调度(Stage Manager)和 接力相机(Continuity Camera)。

同样在 WWDC22 上被推到台前的还有 Passkeys:这套基于 WebAuthn/FIDO 的公钥凭证体系,把“登录”从共享口令变成每个账号一对公私钥(公钥上送服务端、私钥留在设备/iCloud Keychain,解锁依赖 Face ID/Touch ID 等生物识别)。落到 iOS 工程实现时,通常会通过 AuthenticationServices 的 ASAuthorizationPlatformPublicKeyCredentialProvider 发起 registration/assertion(challenge → 注册/断言)的闭环,并且必须配置 associated domains(webcredentials)才能在 App 与 WKWebView 场景里跑通——因此很多团队也第一次认真评估“短信验证码/传统密码”之外的身份体系。

Swift 5.7 在并发和类型系统上继续演进:正则表达式字面量和 RegexBuilder 让字符串处理更加直观(配合 Swift Regex);分布式 Actor(Distributed Actors)简化了分布式系统开发;if let 简写、不透明参数类型、泛型改进等让日常代码更简洁。配合 iOS 16 在 dyld/Swift runtime 上的优化(见 WWDC22 110363),大型 Swift App 的启动时间也能获得显著改善。

SwiftUI 4.0 是这一版最大的亮点:NavigationStackNavigationSplitView 取代了旧的 NavigationView,带来数据驱动、可编程的导航体验,终于解决了困扰开发者多年的导航难题;GridLayout 协议让自定义布局更加灵活;ShareLinkPhotosPickerGaugeMultiDatePicker 等新组件补齐了很多业务场景。

2022 年苹果还首次开放了几个“业务级”框架:Swift Charts 让绑定数据画图表变得和写 SwiftUI 视图一样简单,UI 与苹果自家 App(如股票、健康)一模一样;WeatherKit 提供了与系统天气 App 相同的高精度天气数据(每月 50 万次免费调用);Live ActivitiesActivityKit 则让动态数据能够出现在锁屏和灵动岛上。这些框架的出现,让开发者真正能够“站在巨人肩膀上进行创作”。

如果想从官方口径弄清“这一年到底变了什么”,语言层面 Swift 团队用 Swift language updates from WWDC22 把 WWDC22 的语言演进讲成一条清晰的脉络,随后 Swift 5.7 Released! 再把并发、类型系统与正则等变化落到版本发布说明上;同时大家也会借助“总览型复盘”来快速补课,比如 WWDC 2022 Platform State of the Union Recap 或中文圈的 WWDC2022-iOS 篇完全解析,用更低的时间成本把 session 重点串起来。工具链与工程侧的关键词也因此浮出水面:从 SPM 的依赖治理,到 Xcode 14 的日常升级,再到 dyld 相关的启动优化(dyld/启动优化(WWDC22 110363)),都在提醒团队:新系统版本的价值,很多时候是“性能与工具链”这种看不见但长期收益很高的底座改进。

同样在工程与工具链侧,我也把之前偏“原理向”的 LLVM/Clang 学习,整理成更偏“怎么用”的实践笔记《使用 LLVM》:从 llvm-cov/llvm-profdata 的 source-based coverage,到 SanitizerCoverage 的插桩回调,再到自制 Pass 控制插桩范围,把“函数/基本块级执行证据”真正跑出来,用来做代码新陈代谢(识别无用函数/路径、辅助瘦身与启动治理)并形成可灰度验证的闭环。

另外一个偏工程化但很实用的变化是 Swift 5.6 把 Swift Package Manager 的可扩展性补齐:通过 SwiftPM Plugins(Build Tool / Command Plugins,对应 SE-0303/SE-0332)把代码生成、格式化、Lint、文档构建(比如 Swift-DocC plugin)这类任务收敛到 swift package/IDE/CI 的同一条流水线上,很多团队也因此把原来散落在脚本里的工具链变成可复用、可审计的插件。

2022 其实是 Apple 也很重视“卡顿/无响应(hang)”:

并发性能也在 2022 被官方“扶正”。Instruments 14 新增的 Swift Concurrency template 能把 Swift Tasks/Actors、Task Forest、Actor queue contention 等信息直接可视化,定位“为什么 UI 不响应”“为什么并行没跑起来”这类问题的效率大幅提升(Visualize and optimize Swift concurrency(WWDC22 110350);Swift 团队在 Swift language updates from WWDC22 里也专门提到这套新模板)。从这个角度看,Swift Concurrency 的落地不仅是语法迁移,更是工具链把“并发正确性/并发性能”纳入日常工程的一次升级。

字节跳动的 DanceCC 工具链系列把“调试性能”这种经常被忽视但极影响效率的痛点掰开揉碎:一方面给出《Swift 调试性能的优化方案》(2022-05-07),从 po/p/v 命令的工作流程(IR 编译、JIT 执行、Dynamic Type Resolve 通过 remoteAST 或 Swift Reflection 解析内存布局)讲起,针对大型 Swift 项目”厚主二进制”(主二进制超 1GB、数百个 Swift Module)的场景,通过关闭 swift-typeref-system / swift-dwarfimporter 开关、修复静态链接库误用 dlopen 的 O(N×M) 搜索问题、用 fstat 前置判断优化 External Module 查找路径、增加 Symbol File 共享缓存等手段,把断点陷入后变量显示耗时从分钟级别压缩到秒级;另一方面又补上《Xcode LLDB 耗时监控统计方案》(2022-09-07),通过 LLDB Plugin 机制注入动态库、用 Fishhook 拦截 lldb-rpc-server 对 LLDB.framework 的 Script Bridge API 调用(如 SBFrame::GetVariables、SBValue::GetChildAtIndex),在调用前后打点计时,配合 OverrideCallback 拦截 expr/po/p 命令、log timers dump 采集极端耗时场景的堆栈,形成”怎么把耗时测准、测全、持续测”的完整监控体系。另一条更偏线上治理的路径则由微信团队的 Matrix 代表:把卡顿、内存(FOOM)、FPS 等问题做成插件化 APM,形成“监控→取证→归因→修复→回归”的标准链路(例如他们对 RunLoop 卡顿监控与耗时堆栈提取的工程细节公开得非常彻底:Matrix-iOS 卡顿监控;内存/FOOM 体系同样是工业级长文:Matrix-iOS 内存监控)。

把“新范式”落进真实工程,而 2022 最典型的落点就是 SwiftUI 4 的导航体系。围绕 NavigationStack / NavigationPath / NavigationSplitView,大家关心的不再是“能不能写页面”,而是“复杂业务流如何组织、怎么做程序化导航、怎么适配 iPad 与 Stage Manager”。国际圈里,What is new in SwiftUI after WWDC22What’s New in SwiftUI 4 for iOS 16 负责把新增 API 的覆盖面铺开;当讨论进入“怎么做”的层面,Essential Developer 的两篇文章(Programmatic iOS App Navigation with SwiftUI NavigationLink (like in UIKit!) / How senior iOS devs do SwiftUI navigation programmatically (Patterns & Flows))把复杂导航抽象成可复用的 patterns/flows。中文圈也几乎同步给出了“能直接抄作业、还能讲清取舍”的落地版本,比如 SwiftUI 4.0 的全新导航系统 把概念和 API 迁移讲透,肘子的 在 SwiftUI 中创建自适应的程序化导航方案 则更贴近真实项目里 Stack/SplitView 的决策过程——从 NavigationPath(对 Hashable 数组的包装)如何在”栈”中推送和弹出数据,到 NavigationSplitView 与 List(selection:) 的深度绑定(只有用 List 修改状态才能在自动转换后保持程序化导航能力),再到如何用 horizontalSizeClass 判断当前视觉模式、在台前调度(Stage Manager)场景下根据 onAppear 时机在 NavigationStack 与 NavigationSplitView 之间平滑切换状态,把”一次编写适配多设备”这个 SwiftUI 承诺落到了可执行的工程方案里。年末再用 Point-Free 的 2022 Year-in-Review 做一次回看,很多团队会更容易判断:哪些写法该沉淀成规范,哪些只是阶段性过渡。

而在 SwiftUI 侧,我在 3 月受 Apple 加速器 SwiftUI 活动邀请做了一次分享《在苹果加速器活动做的 SwiftUI 开发分享》,并把幻灯片做成可交互 demo(支持解释执行代码片段)。内容既包含 iOS 常用组件的系统梳理,也把视角扩展到了 macOS:多栏导航(Sidebar/Detail)、Toolbar 的语义化布局、树形目录(List(children:)/DisclosureGroup)、文本/代码编辑器(NSTextView + NSViewRepresentable)、Grid 与一些视觉效果。对我自己而言,这次“把内容讲清楚”的过程也像一次工程化演练:把零散的 API 经验抽成可复用的组件清单和 demo,便于后续在真实业务里评估与落地。

与 SwiftUI 4 并行推进的,是并发体系继续从“语言特性”变成“工程规范”。Swift Concurrency 相关的讨论不再停留在语法层,而是围绕 Actor / Sendable 这些约束,以及“任务取消”这种容易被忽略的工程细节(在 async 流程里及时检查取消信号),去建立团队层面的并发边界与 code review 共识;avanderlee 的 The 5 Biggest Mistakes iOS Developers Make with Async/Await 之所以被反复引用,本质上是它把迁移期最容易翻车的细节讲得很“可对照”:把 for await 异步序列当普通循环、误以为 async 默认在后台执行导致 UI 卡死、忽略取消检查让请求/计算白跑、手工迁移旧代码导致行为与原来不同,以及在 onAppear 里随手创建 Task 造成重复请求等。与此同时,Swift 5.7 把 RegexBuilder 推到标准库能力里,也让不少“原来靠三方/靠手写”的字符串处理场景有了更可维护的表达方式。

iOS 16 把新的交互入口推到所有团队面前:锁屏与灵动岛。围绕 锁屏小组件ActivityKit / Live Activities,要做的事既包括“把展示形态跑起来”,也包括“把数据模型与生命周期管起来”。国际圈里,Lock screen widgets in SwiftUIDisplaying live activities in iOS16 把关键机制讲得很透——从 ActivityAttributes 协议如何把静态数据(如订单 ID)与动态数据(ContentState,如配送进度)分离,到 ActivityConfiguration 如何在 WidgetKit 中定义锁屏与灵动岛的展示视图,再到 8 小时自动超时 / 12 小时最大显示时长的系统限制、Info.plist 需添加”Supports Live Activities”键、前台启动后可用 BackgroundTasks 或 Push Notification 更新状态等工程细节;想走最短路径上手时,Dynamic Island (and Live Activities): Quick Start Tutorial 基本就是从配置到 ActivityConfiguration 的直达路线。中文圈对应的“落地链路”也很快补齐:从 Live Activities - Dynamic Island Dev 的工程配置与展示区域拆分,到 Live Activity - 创建你自己的灵动岛 App 的可抄作业实践,再到 iOS16适配指南之Live Activity 这种偏适配/排雷的总结,基本覆盖了从 demo 到业务上线会遇到的主要坑点。

2022 年 iOS/Swift 开源社区热度依旧:AlamofireKingfisherSnapKitRxSwift 等“老牌”库持续更新并逐步拥抱 Swift Concurrency;动画场景里 Lottie 依然是首选;在需要兼容 iOS 15 及以下系统时,Charts 仍是很多团队的现实选择(即使 Swift Charts 已发布);网络抽象层的 Moya 继续流行。工程侧,SwiftLintSwiftFormat 仍是团队标配;架构侧,swift-composable-architecture(TCA) 在 SwiftUI 项目中越来越受欢迎,Point-Free 团队持续迭代,swift-dependencies 也开始被独立使用,TCA 的 Navigation 支持在这一年趋于成熟。Swift Package Manager 的使用继续增长,越来越多的库开始优先甚至只支持 SPM。

Apple Design Awards 2022 的获奖者很有代表性:Procreate(多元包容)、Slopes(优越互动)、Halide Mark II(视觉图像)、(Not Boring) Habits(乐趣横生)、Rebel Girls(社会影响)、Odio(创新思维)。年底 Apple 发布的 App Store Awards 2022 中,iPhone 年度 App 是 BeReal——这款主打“真实社交”的 App 在年轻人中迅速走红,每天随机时间提醒你拍一张“此刻”照片的设计颇有新意;iPhone 年度游戏是 Apex Legends Mobile;iPad 年度 App 是 GoodNotes 5;Mac 年度 App 是 MacFamilyTree 9。中国区的年度 iPad App 同样是 GoodNotes 5,年度游戏是 《英雄联盟电竞经理》喝水羊驼 因帮助用户养成饮水习惯获得“文化影响力”表彰。

Mac 端应用,Raycast 在这一年持续迭代,扩展生态越来越丰富,成为很多人替代 Alfred 的首选;Arc 浏览器 在 4 月发布,垂直侧边栏和“万维网操作系统”的理念让人耳目一新,成为 Chrome 的有力竞争者;Warp 终端在 4 月公开发布,基于 Rust 构建、AI 辅助命令提示的设计让终端体验焕然一新;Figma 在 9 月被 Adobe 宣布以 200 亿美元收购,震动整个设计圈(虽然最终因监管问题未能完成)。效率工具方面,Obsidian 的双向链接笔记生态继续繁荣,10 月发布了 1.0 正式版,标签面板和 Canvas 画布功能让它更像一个完整的知识管理系统;Logseq 作为开源替代也在这一年获得更多关注;CraftNotion 在知识管理领域各有拥趸;CleanShot X 成为截图录屏的标杆;ProxymanTablePlus 在开发工具领域持续流行。开发者工具方面,GitHub DesktopForkTower 在 Git 客户端中各有用户群;Cursor 在这一年开始进入早期开发者的视野,AI 辅助编程的理念已经萌芽。菜单栏工具方面,Bartender 4Hidden Bar(开源)继续是管理菜单栏图标的首选;iStat Menus 在系统监控领域依然强大;PasteMaccy(开源)在剪贴板管理上各有拥趸。

微信 在这一年也开始尝试在搜一搜、视频号等场景融入更多 AI 能力。随着 Stable Diffusion 等 AI 图像生成模型的开源,国内也涌现了一批 AI 绘图 App,如 意间AI绘画6pen 等,“AI 作画”成为年度热词。居家期间,Keep 配合刘畊宏直播再次迎来用户增长高峰;得物(毒 App)在年轻人中持续流行;得到 在知识付费领域仍有忠实用户群。Locket Widget 把朋友的即时照片直接挂在桌面/小组件上,简单到极致,却又足够上头。

Bear 发布了 Panda 编辑器(Bear 2.0 预览版),新的表格和代码块支持让人期待。1Password 8 在这一年发布,虽然争议不断(Electron 化),但密码管理的核心功能依然强大;Drafts 继续是“文字处理的起点”;Carrot Weather 的毒舌天气依然有趣;Overcast小宇宙 在播客领域各有拥趸。

2023年:AI 常态化与空间计算登场

科技行业这一年的大事件不少:7 月 Twitter 正式更名为 X,小蓝鸟图标成为历史,API 政策的收紧也让不少第三方客户端开发者黯然离场;10 月微软完成了对 动视暴雪的 687 亿美元收购

影视方面,《漫长的季节》 写尽时代洪流下的普通人命运,《繁花》 在年末开播;《长安三万里》 用动画写李白与盛唐。生活层面,“淄博烧烤”和“酱香拿铁”先后出圈,年末董宇辉与东方甄选的“小作文”风波引爆舆论。

这些更像 2023 的背景噪声;真正改写这一年底色的,还是 AI。

3 月 14 日,GPT-4 发布(可参考其技术报告:arXiv:2303.08774)。多模态能力让它在律师资格考试中的成绩从 GPT-3.5 的“垫底”跃升到前 10%,“AI 能做什么”的边界被重新定义。

ChatGPT 从 2022 年底的现象级产品,在这一年彻底渗透进开发者的日常:写代码、查文档、头脑风暴、改 Bug,很多人发现“先问 GPT”已经成了新的肌肉记忆。12 月,GitHub Copilot Chat 正式面向所有订阅用户开放,AI 辅助编程从“尝鲜”变成了“标配”。

AI 图像生成领域,Midjourney V5 在 3 月发布,画面质量再上一个台阶;Stable Diffusion XL 也在这一年推出,开源社区围绕它构建了更成熟的工具链(如 ComfyUI,节点式可视化工作流)。语音领域,基于 Whisper 的本地化方案开始普及,“语音转文字”变得前所未有地简单。

AI 领域的竞争格局在这一年急剧变化:

  • 7 月,Anthropic 发布 Claude 2,100K 上下文窗口让它在处理长文档时大放异彩。
  • 同月,Meta 开源了 LLaMA 2,从 7B 到 70B 参数的全系列模型让开源社区沸腾——第一次有顶级大厂把这种级别的模型以更可复用的形式交到开发者手里。
  • 9 月,Mistral AI 发布 Mistral 7B,用更小的参数量达到了 LLaMA 2 13B 的水平,“小模型大智慧”的路线开始被认真对待。
  • 11 月 6 日,OpenAI 在首届开发者大会 DevDay 上发布 GPT-4 Turbo(128K 上下文、更便宜的价格),并推出“自定义 ChatGPT”的 GPTs 功能。
  • 11 月的 OpenAI 董事会风波也让全球科技圈目瞪口呆——Sam Altman 被董事会解雇、员工集体威胁离职、微软抛出橄榄枝,最后 Sam Altman 在短短 5 天内回归并重组董事会。
  • 12 月,Google 发布 Gemini,声称在某些基准测试上超越 GPT-4,AI “三国杀”正式开打。

AI 相关的开源项目也迎来井喷式爆发,很多“做 LLM 应用需要的基础积木”在这一年被迅速补齐:

  • 应用框架:LangChain(Python,链式调用、Prompt 模板、Memory 上下文、Agent 工具调用)和 LangChain.js(JavaScript)成为构建 LLM 应用的首选框架,“链式调用”的思路让复杂的 AI 工作流变得可组合、可调试。
  • Agent 概念:AutoGPT 在 4 月横空出世,让 AI 自主完成任务链的概念一夜爆火,虽然实用性有限,但它打开了“AI Agent”的想象空间。
  • 本地运行:llama.cpp 让大模型可以在普通 CPU 上运行,配合 GGUF 格式和量化技术,“本地部署 LLM”从奢望变成现实;基于它的 Ollama 更是把本地运行大模型做到了“一行命令启动”的简洁。text-generation-webui 提供了开箱即用的 Web 界面,让非技术用户也能体验本地 AI。
  • 私有数据与 RAG:PrivateGPT 让“本地知识库问答”成为可能——把文档丢进去,AI 就能基于你的私有数据回答问题。向量数据库领域,ChromaMilvusQdrant 都在这一年获得大量关注,RAG(检索增强生成)架构成为 LLM 应用的标配。
  • 图像工作流:ComfyUI 的节点式工作流让 Stable Diffusion 的使用更加灵活,Fooocus 则走简化路线,号称“Midjourney 开源平替”。

前端与全栈领域同样热闹。9 月,Bun 1.0 正式发布,这个用 Zig 写的 JavaScript 运行时号称比 Node.js 快数倍,内置打包器、测试运行器、包管理器,“all-in-one”的野心让人侧目。Next.js 14 带来了 Server Actions,让服务端函数可以直接在组件里调用,全栈开发的边界再次模糊。shadcn/ui 火遍前端圈,它不是一个组件库,而是“组件代码的集合”:把代码复制到你的项目里、随便改——这种“反框架”的理念让很多人眼前一亮。

6 月 5 日的 WWDC 2023 注定载入苹果史册——不是因为 iOS 17,而是因为 Apple Vision Pro 的发布。它把 AR/VR 的讨论从概念拉进了苹果生态,visionOS 成为苹果第六大操作系统。虽然 3499 美元的售价和 2024 年初才发售的时间表让很多人只能“云体验”,但苹果用 Reality Composer ProRealityKitARKit 的组合告诉开发者:空间计算的开发工具已经准备好了。WWDC 上发布的 visionOS SDK 在 6 月下旬向开发者开放,全球多地的 Vision Pro Labs 也让开发者提前上手真机。

对 macOS 开发者/玩家来说,WWDC 2023 还有一个被低估的惊喜:Game Porting Toolkit 让很多 Windows 游戏可以在 Apple Silicon 上更快跑起来(配合 Wine/CrossOver 社区),一度出现“Mac 也能玩 3A”的小高潮;这也为后续《死亡搁浅》《生化危机》等原生移植的讨论提前预热。

9 月 12 日,iPhone 15 系列发布,最大的变化是全系换装 USB-C 接口——Lightning 时代正式落幕。iPhone 15 Pro 和 Pro Max 搭载 A17 Pro 芯片(业界首款 3nm 手机芯片),USB 3 传输速度提升到 10Gbps,还新增了可自定义的 Action Button(动作按钮)取代静音开关。灵动岛(Dynamic Island)也从 Pro 系列下放到全系,成为 iPhone 的标志性交互。同期发布的 Apple Watch Series 9Apple Watch Ultra 2 带来了双指互点(Double Tap)手势,让单手操作更方便。

10 月,Apple 发布 M3 芯片家族,同时更新了 MacBook Pro 与 iMac。3nm 工艺、硬件光线追踪和 Dynamic Caching 让“Mac 图形性能”第一次有了更具体的工程指标,也直接影响到 Metal/渲染/游戏相关开发者的关注点。

iOS 17 带来了不少“小而美”的更新:StandBy 模式让横置充电的 iPhone 变成床头时钟;NameDrop 让交换联系方式只需碰一碰;Journal App 在年底上线,主打“记录生活中的小美好”。开发者侧,交互式小组件(Interactive Widgets)终于来了——用户可以直接在小组件上操作,不用打开 App;TipKit 让“功能引导”有了官方方案。macOS Sonoma 则把交互式小组件带到了桌面,还加入了屏幕保护壁纸和 Game Mode。

产品层面,苹果也在 3 月推出了 Apple Music Classical,把古典乐的曲库与元数据做成独立 App;5 月,Final Cut Pro for iPadLogic Pro for iPad 上线,“iPad 能不能当生产力主力机”又被讨论了一整年。

SwiftUI 5.0 的更新围绕“让现有能力更好用”展开:ScrollView 获得了大量新修饰符(如 scrollPositionscrollTargetBehaviorscrollTransition),终于能精细控制滚动行为;新的 Inspector API 简化了侧边栏检查器的实现;新的 Observation 框架配合 @Bindable@Observable 让状态管理更清晰;动画方面,弹簧动画(spring animation)成为默认,.animation 修饰符也支持了更细粒度的控制。MapKit for SwiftUI 也在这一年变得真正可用,不再只是“能跑”而是“能用到业务里”。

这届 WWDC 最让人兴奋的框架级更新当属 SwiftData——苹果用纯 Swift 重新设计的数据持久化框架。和 Core Data 相比,SwiftData 用 @Model 宏声明模型、用 @Query 宏获取数据、用 ModelContainerModelContext 管理存储上下文,语法更贴近 SwiftUI 的声明式风格,学习曲线也更平缓。官方的 Meet SwiftDataBuild an App with SwiftDataDive Deeper into SwiftData 三个 Session 基本覆盖了从入门到进阶的完整链路。当然,SwiftData 1.0 也有不少坑点(比如复杂关系的性能、CloudKit 同步的稳定性),但作为“Core Data 接班人”的第一版,它已经展示出了足够的潜力。

Swift 5.9 在 9 月随 Xcode 15 发布,带来了两个重磅语言特性:宏(Macros) 和 参数包(Parameter Packs)。宏分为独立宏(# 前缀)和附着宏(@ 前缀),让开发者可以在编译期生成代码,减少样板代码的同时保持类型安全。@Observable 宏就是最典型的应用——它让类自动获得观察能力,彻底取代了 ObservableObject + @Published 的组合。参数包则让泛型函数可以接受任意数量的类型参数,解决了之前需要写多个重载的尴尬。社区也迅速跟进,swift-power-assertswift-spyableMetaCodable 等基于宏的工具开始涌现。除了宏/参数包,Swift 5.9 也把“所有权模型”往前推了一步(borrowing/consumingconsume~Copyable 等),让性能敏感与系统级代码能更少拷贝、更少 ARC,虽然这些能力在 5.9 仍有不少限制,但方向已经很明确。

数据层的 SwiftData,状态层的 Observation 与语言层的 Swift Macros,界面层围绕 SwiftUI ScrollView 的“滚动补齐”、iOS 17 的 交互式小组件TipKit,再加上平台叙事的 visionOS;所有这些要落到工程里,最终又会回到 Xcode 15 Release Notes 这类“版本事实”。沿着这条主线回看社区文章,会读出一条很清晰的节奏:先在 WWDC 后快速建立全景与共识,再把新能力拆解到可抄作业的落地细节,最后回到工程化与架构稳定性。

WWDC23 的 Demystify SwiftUI Performance 把 SwiftUI 的依赖追踪、identity 与刷新成本讲清楚;Analyze Hangs with Instruments 则把“卡顿”拆成 Busy Main Thread / Blocked Main Thread / Async Hang 三类,用 Instruments 的 Time Profiler、Thread State Trace、Hangs 轨道(以及 SwiftUI 的 View Body 轨道)把证据链拉直。并发这条线也终于能被观察:在 Beyond the Basics of Structured Concurrency 里,task tree、取消与优先级不再只是语法,而是可被工具捕捉的运行时行为。

开发期用 Instruments 把点状问题定位清楚,上线后则要有“面状”的指标闭环:Apple 的 MetricKit 提供 MXMetricPayload / MXDiagnosticPayload 等系统聚合数据,把启动、CPU、内存、hang、崩溃等问题从“个案”变成“分布”;配合 iOS 15+ 的 OSSignposter(signpost)把关键路径打点,才有可能把优化变成可量化的迭代。社区侧也有不少踩坑贴把“性能陷阱”具体化,比如 iOS 17 新引入的 Observation 框架与 @Observable 宏在 SwiftUI 中的内存泄漏问题——Observation 为了追踪属性变化会对被追踪的引用对象维持强引用,而 SwiftUI 在 .sheet/.fullScreenCover dismiss 后的释放时机 bug 会让这些对象“该放不放”,尤其当被捕获对象较大时,内存占用很容易被滚到数 GB。bafford 在 2023-10-12 的 SwiftUI memory leak workaround 记录了该问题在 iOS 17.0..17.1b3 期间的表现,并给出一个 UIKit 兜底:通过在 environment 注入带 UIViewController 的 coordinator,把 sheet 的呈现交给 UIKit,并提供 leak_workaround_sheet / leak_workaround_fullScreenCover 这类 view modifier 来触发(该 bug 在 iOS 17.1 之后被修复)。另外,抖音/字节的“二进制重排(order file)做启动优化”也长期被工程团队复用:用 System Trace 在 VM Activity 里看 File Backed Page In(Page Fault)把“启动期缺页”量化出来,再结合 LinkMap 的 __TEXT,__text / __DATA,__mod_init_func 静态扫描与启动期 trace 生成 ld -order_file 的符号顺序,把热路径函数聚拢以减少 Page Fault;落地时还会用 os_signpost 给 Mach-O load / C++ 静态初始化等阶段打点,并用 -order_file_statistics 校验命中(可参考 InfoQ 的整理:抖音研发实践:基于二进制文件重排的解决方案,APP 启动速度提升超15%)。

社区围绕这些新能力产出了大量高质量内容。WWDC 后快速补课,Majid 的 What is new in SwiftUI after WWDC 23 用一篇把 SwiftUI 5 的重点更新串起来:数据流从 Combine 走向 Observation(@Observable 宏、@Bindable、environment 注入),动画补齐了 PhaseAnimator 与带 completion 的 withAnimation,ScrollView 也终于能通过 scrollPosition/scrollTargetBehavior 做更可控的滚动定位与分页行为;同时像 ContentUnavailableView、List 间距控制、#Preview 宏等“小但常用”的改动也都点到。中文圈可以先看 fatbobman 的 WWDC 23,SwiftUI 5 和 SwiftData 的初印象 来校准预期:他一方面从 @Observable 的“按属性粒度响应变化”切入,解释这套新数据流对过渡计算等性能痛点的意义;另一方面也把 SwiftData 明确为“基于 Swift 5.9 新特性实现的 Core Data Swift 封装”,并提前标出 ModelContext 合并、Sendable/线程限制、CloudKit 同步与 .unique 约束等第一波坑。ScrollView 新 API 的落地细节,fatbobman 的 深入了解 SwiftUI 中 ScrollView 的新功能 覆盖了 contentMarginsscrollPosition(id:)scrollTargetBehavior(分页与视图对齐)、scrollTransition 过渡动画等新 API,基本可以直接“抄作业”。宏系统入门,SwiftLee 的 Swift Macros: Extend Swift with New Kinds of Expressions 详细介绍了 freestanding 宏(# 前缀)与 attached 宏(@ 前缀)两种类型,以及七种宏角色的用法,并演示如何通过 AST 操作实现编译期代码生成与验证。想判断 SwiftUI 该押多重,timac 的 Apple’s use of Swift and SwiftUI in iOS 17 通过逆向分析 dyld shared cache 给出了可量化数据:iOS 17 包含 385 个使用 SwiftUI 的二进制文件,Swift 代码占比较 iOS 16 增长 50%,使用 SwiftUI app lifecycle 的 App 从 4 个增至 14 个,Settings、Health、Home、Calendar、Reminders 等系统应用已大规模采用 SwiftUI。年末 SwiftLee 2023: A Year in Review 复盘时也给了一个很直观的信号:年度热门依然是 async/await 的落地、@MainActor 的主线程调度,以及“如何在 iOS Simulator 测推送”这类工程实操——从侧面印证了社区重心正从“看新能力”转向“做迁移、补工具、提效率”。

2023 年 iOS/Swift 开源社区依然活跃。swift-composable-architecture(TCA) 发布了 1.0 版本,围绕 Reducer/Store/Effect 与 TestStore 把“可组合状态管理 + 可测试副作用”沉淀成一套工程化打法,甚至用 @Reducer 宏把样板代码再压一层;Point-Free 团队用 Modern SwiftUI 系列视频把这套理念讲得更完整。Kingfisher 7.x 版本持续适配新系统,依旧是“异步图片下载 + memory/disk hybrid cache + processor + prefetch”的事实标准,并把 SwiftUI 侧的 KFImage 做得足够顺滑。新兴项目方面,swift-dependencies 把 SwiftUI “environment” 风格的依赖注入抽象成可控依赖(DependencyValues 注册、withDependencies 覆盖),让测试与 Preview 的依赖替换更系统;swift-navigation 则把“用状态驱动导航”的模式从 SwiftUI 扩展到 UIKit/AppKit,尤其强调用 enum destination 获得编译期约束,避免同时 push/sheet/alert 的无效组合。宏相关的工具开始涌现:swift-macro-testing 把宏展开(expansion)做成可记录/可回归的测试,并能校验 warnings/errors/fix-its;swift-spyable@Spyable 自动为协议生成 spy,追踪调用/参数/返回值;swift-power-assert 则用 #assert() 这种 diagrammed assertions 把失败信息“图解化”,连 async/await 表达式也能直接断言。

把视角从框架与工程落地拉回生态与产品侧,会更直观看到这些变化如何进入日常。

Apple Design Awards 2023 的获奖者很有代表性:Universe — Website Builder(多元包容)让建站变得触手可及;Duolingo(乐趣横生)以游戏化设计让语言学习上瘾;Flighty(绝佳互动)把航班追踪做到极致;Headspace(社会影响)让冥想走进大众;SwingVision(创新思维)用 AI 分析网球动作;Any Distance(视觉图像)把运动追踪与 Live Activities 完美结合。年底 App Store Awards 2023 的获奖名单中,iPhone 年度 App 是 AllTrails(户外徒步)、iPad 年度 App 是 Prêt-à-Makeup(妆容模板)、Mac 年度 App 是 Photomator(照片编辑);游戏方面,iPhone 年度游戏是 《崩坏:星穹铁道》、iPad 年度游戏是 Lost in Play、Apple Arcade 年度游戏是 Hello Kitty Island Adventure。文化影响力获奖者包括 Pok Pok(儿童创意)、Proloquo(辅助沟通)、Too Good To Go(减少食物浪费)。

Mac 端应用也有不少亮点。Arc 浏览器 在这一年彻底出圈,垂直标签栏、Spaces 工作区、Boosts 自定义样式让它成为很多人的主力浏览器,年底还推出了 Windows 版。Raycast 继续进化,Raycast AI 功能让启动器也能调用大模型,扩展生态越来越丰富。Warp 终端凭借 AI 命令提示和现代化设计吸引了大量开发者。Obsidian 发布了 1.4 版本,Properties 功能让元数据管理更规范。效率工具方面,CleanShot X 在截图录屏领域依然是标杆;Bartender 虽然被收购后引发争议,但功能上依然强大;ProxymanTablePlus 继续是开发者的网络调试和数据库管理首选。AI 相关工具方面,MacGPTMacWhisper 等本地化工具开始流行。写作工具领域,iA Writer 持续迭代、Ulysses 依然是长文写作的标杆、Notion 推出了 Notion AI 功能让笔记工具也能调用大模型、Craft 在文档协作上越发成熟。设计工具方面,Figma 依然是 UI 设计的事实标准,虽然 Adobe 收购案被欧盟否决,但也让更多人开始关注它。Linear 以极简的设计和流畅的体验成为很多团队的项目管理首选。

iOS 平台上 Perplexity AIApp Store)以“AI 搜索引擎”的定位吸引了大量用户,搜索结果附带来源引用,让人耳目一新。Claude(Anthropic 的官方 App)在 iOS 上提供了流畅的对话体验。Character.AI 让用户可以和各种虚拟角色聊天,成为 Z 世代的社交新宠。Luma AI 用 NeRF 技术把照片变成 3D 模型,“扫一圈就能建模”的体验让人惊叹。Remini 用 AI 修复老照片,让很多人翻出了压箱底的旧照“修复青春”。健康领域,Gentler Streak 以“温和健身”的理念赢得了 Apple Design Award,强调休息和恢复同样重要。Copilot(个人财务) 不是 GitHub 那个,而是一款精美的记账 App,把账单追踪做得赏心悦目。Timery 配合 Toggl 让时间追踪变得更顺手。播客领域,小宇宙 继续深耕中文播客生态,Snipd 用 AI 提取播客精华片段,让“听后整理”变得轻松。

文心一言通义千问讯飞星火智谱清言Kimi(月之暗面) 等国产大模型相继发布 App 或网页版,虽然与 GPT-4 仍有差距,但进步速度令人欣喜,“国产大模型”成为投资热词。Poe(平台本身可能需要翻墙)作为多模型聚合平台也吸引了不少用户。移动端,ChatGPT 官方 App 在 5 月上架 App Store,让 iOS 用户终于有了原生体验。《蛋仔派对》 在年轻人中流行,社交玩法让它成为又一款现象级手游。

看过这些工具和应用的繁荣,回头想想 AI 对日常开发的影响,其实更值得细说。ChatGPT 的到来确实改变了很多开发者的工作方式:写代码时先问 GPT、改 Bug 时先让 AI 分析、写文档时用 AI 辅助——这些已经成了新的日常。但与此同时,AI 更像是“放大器”:基础扎实的人用 AI 效率飙升,基础薄弱的人可能只是得到了“看起来对但跑不通”的代码。Vision Pro 的发布让人兴奋,但 3499 美元的售价和“开发者先行”的策略也说明,空间计算离普及还有距离。苹果在发布会上对 AI 的沉默,可能不是没准备好,而是在等一个“苹果式”的答案。

4 月我把用 SwiftUI 做 RSS 阅读器的实践写成《使用 SwiftUI 开发 RSS 阅读器》:从订阅源识别(mimeType + SwiftSoup 兜底解析)、多种 RSS 规范解析,到 Core Data 建模/批量写入、Core Spotlight 索引,以及 CloudKit 同步订阅、已读与收藏,算是一次把“界面 + 数据 + 同步”串成完整产品链路的练习。

5 月在我家孩子小学的家长讲堂,我做了一次《给孩子小学的家长讲堂做了一个计算机科普分享》,用图灵与 Enigma 破译的故事引出“计算机是什么/能做什么/不能做什么”,顺带从 DES/AES、Diffie-Hellman 密钥交换讲到 HTTPS 与 iPhone 的数据保护——这类“把基础讲清楚”的输出也提醒我:越是 AI 时代,越要把底层概念讲透、做实。

在这些实践之外,我也继续关注 SwiftUI 的演进,SwiftData 的出现让我开始认真考虑在新项目里用纯 Swift 方案替代 Core Data。宏系统的引入让 Swift 的元编程能力上了一个台阶,虽然学习曲线陡峭,但带来的收益是实打实的。AI 工具的普及也让我重新思考“开发者的核心竞争力”——代码生成可以交给 AI,但架构设计、性能优化、问题定位,这些还是需要扎实的基本功。

2024年:Apple Intelligence 与空间计算落地

2024 年年初,《繁花》 收官,王家卫镜头下的上海让人感叹“原来那个年代那么美”;8 月,《黑神话:悟空》 发售,首周销量破 1000 万份,成为中国游戏史的里程碑。电影方面,《热辣滚烫》《飞驰人生2》 承包了春节档的笑与泪。

7 月底至 8 月,巴黎奥运会 在塞纳河畔开幕,霹雳舞 首次入奥。中国代表团以 40 金与美国并列榜首,郑钦文 网球女单夺冠创造历史,潘展乐 成为男子 100 米自由泳首位“破 47 秒”的人。闭幕式上,汤姆·克鲁斯 从体育场屋顶滑降,为 2028 洛杉矶奥运会 造势。

11 月,特朗普 再度当选美国总统,马斯克 全程站台——科技与政治的交织愈发明显。

这些更像 2024 的背景噪声;真正改写工作方式的,还是 AI 的落地速度。

OpenAI 动作频频:5 月发布原生多模态的 GPT-4o;9 月推出专注推理的 o1 模型;12 月正式发布文生视频模型 Sora,标志着这一能力从实验室走向产品。

国产大模型展现惊人的追赶速度:Kimi 以 20 万字上下文和出色的中文理解迅速走红;智谱清言文心一言通义千问讯飞星火 纷纷推出更强版本;豆包 凭借免费策略吸引大量用户。“国产大模型可堪一用”成为共识。

AI 模型与生态的竞争也更白热化:

与此同时,科技行业的裁员潮并未停止,全球裁员总数接近 28 万人。3 月,Reddit 上市(股票代码 RDDT),对于经历了 2023 年 API 收费风波(导致 Apollo 等第三方客户端关闭)的用户来说,颇具讽刺意味。

前端生态持续演进。React 19 带来了 Actions、use() Hook 和 Server Components。Python 生态掀起“更快工具链”浪潮:Astral 的 uv 把 pip/venv/lockfile 压缩成一个极速二进制,Ruff 让 lint + format 收敛为一套默认选项。

AI 编程工具方面:

  • Cursor(VS Code fork、AI 代码补全、多文件编辑、Codebase 上下文理解)以 AI-first 的编辑器体验迅速走红,成为很多开发者的 VS Code 替代品。
  • GitHub Copilot 的 Workspace 功能让 AI 辅助从“代码补全”扩展到“项目理解”。
  • Devin(号称“首个 AI 软件工程师”)在 3 月发布,虽然实际能力有限,但打开了“AI Agent 做完整任务”的想象空间。

开源工具方面,LocalSend(Flutter 跨平台、局域网 P2P 传输、端到端加密)凭借跨平台、免费、无需互联网的特点获得超过 40k Star,成为 AirDrop 的最佳替代品;Stirling-PDF 提供本地部署的 PDF 处理工具集;Open WebUI 为本地 LLM 提供类似 ChatGPT 的 Web 界面。AI 应用开发平台 Dify(可视化 Prompt 编排、RAG 流水线、Agent 工作流)和 FastGPT(知识库问答、工作流编排)在国内外都获得大量关注。

本地运行大语言模型变得前所未有的简单。Ollama 凭借“一条命令跑 Llama”的极简体验迅速走红,ollama run llama3 成为很多开发者接触本地 LLM 的第一步;LM Studio 则提供了更友好的 GUI。RAG 应用构建方面,LangChainLlamaIndex 持续主导;Vercel 的 v0.dev 用 AI 生成 UI 组件,让“描述即开发”更近一步。

前端 UI 库方面,shadcn/ui 以“复制粘贴而非安装依赖”的理念成为 React 社区新宠;HTMX 的复兴让“返璞归真”的开发方式重新获得关注。编辑器领域,Zed 在 1 月正式发布 1.0 并开源,以极致性能和原生 AI 集成挑战 VS Code。代码质量工具方面,Biome(原 Rome)作为 ESLint + Prettier 的 Rust 替代品持续获得关注。

把视角从通用工具链拉回 Apple 生态,2024 的平台侧关键词也很明确:Apple Intelligence 与空间计算落地。

6 月 10 日的 WWDC 2024,苹果发布了 Apple Intelligence——一套深度集成到 iOS 18、iPadOS 18 和 macOS Sequoia 的个人智能系统,涵盖写作工具、Image PlaygroundGenmoji、Siri 升级,以及与 ChatGPT 的集成。苹果没有追逐“最强模型”的军备竞赛,而是把重心放在隐私保护(Private Cloud Compute)和端侧智能上。

2 月 2 日,Apple Vision Pro 在美国发售,3499 美元的售价和“空间计算”定位让它成为年度最受关注的硬件。配备双微 OLED 显示屏、M2 主芯片和 R1 芯片,通过眼动追踪、手势和语音交互——这是苹果十年来的首款全新品类。上市时已有超过 600 个专为 visionOS 设计的空间应用。6 月发布的 visionOS 2 引入空间照片、新手势、Mac 虚拟显示等功能。不过,Vision Pro 在销量和佩戴舒适性方面也面临挑战。

9 月 9 日,苹果发布 iPhone 16 系列,全系搭载 A18 芯片,专为 Apple Intelligence 优化。iPhone 16 Pro/Pro Max 采用更大显示屏(6.3/6.9 英寸),新增 相机控制按钮
同期更新的还有 Apple Watch Series 10(更薄、屏幕更大,新增睡眠呼吸暂停检测)和 AirPods 4(入门款首次提供带主动降噪的版本)。
10 月,搭载 M4 芯片 的新款 MacBook ProMac miniiMac 发布。

iOS 18 围绕“个性化”和“AI 能力”展开:主屏幕图标可自由排列并自定义颜色;控制中心完全重新设计,支持多页面和第三方控件;Photos 应用迎来最大改版;Messages 支持 RCS 协议;Safari 新增 Highlights 功能。开发者侧,Controls 让第三方 App 也能把动作放进控制中心;App Intents 框架的扩展让 App 与 Siri 和 Apple Intelligence 的集成更深入。

SwiftUI 6.0 迎来多项重要更新:

Swift 5.10 在 3 月发布,完全支持数据隔离(Data Isolation),为 Swift 6 做最后铺垫。新增 nonisolated(unsafe) 关键字;启用 -strict-concurrency=complete 后,编译器会检测潜在的数据竞争。

9 月 17 日,Swift.org 发布 Announcing Swift 6,数据竞争安全成为语言级能力——Swift 6 模式下,编译器将数据竞争视为编译错误。非可复制类型(Noncopyable Types)获得泛型和标准库支持;Typed throws 让错误处理可指定具体类型;C++ 互操作性进一步增强。配套发布的 Swift Testing 框架用更现代的语法(@Test#expect)替代传统 XCTest。

SwiftData 在 iOS 18 中迎来重要更新:ModelContext.didSave 通知终于正常工作;#Index 宏让数据库索引声明更直观;CloudKit 同步稳定性改善。根据 timac 的统计,iOS 18 中使用 SwiftUI 的系统二进制文件数量再创新高,SwiftUI 在系统级应用中已成“标配”。

社区对 WWDC 2024 / iOS 18 / Swift 6 的讨论热度非常高。

  • Majid:What Is New in SwiftUI after WWDC24——梳理了 App/Scene/View 协议的 @MainActor 隔离变化、View collections(Group/ForEach 新重载与 SubviewsCollection)、新的 Tab 类型与侧边栏流畅过渡、Hero 动画(matchedTransitionSource/navigationTransition)、ScrollPosition 精确滚动控制,以及 @Entry/@Previewable 宏的简洁用法
  • AppCoda:What’s New in SwiftUI for iOS 18——用可运行示例串起 iOS 18/SwiftUI 6 的新能力:TabView 通过 .tabViewStyle(.sidebarAdaptable) 实现浮动 Tab Bar 与侧边栏的自适应切换,Sheet 用 .presentationSizing 统一尺寸行为,视觉上有 MeshGradientmatchedTransitionSource 的 Zoom transition,滚动交互补上 onScrollGeometryChange,控制中心则用 ControlWidget 把控件小组件变成系统入口
  • 官方入口:WWDC24 SwiftUI 与 UI 框架指南——把 WWDC24 的 SwiftUI/AppKit/UIKit 相关 session 按主题编排:先从 SwiftUI/UIKit 的新能力入手,再延伸到容器与布局、动画与过渡、iPad 标签页与边栏体验和辅助功能,最后覆盖 visionOS 的空间容器与沉浸式空间,适合当作“查新 API 的索引页”
  • 肘子:WWDC 2024 观后感——从开发者视角评价 Apple Intelligence、Swift 6 严格并发、SwiftData 改进等年度重点变化
  • Swift.org:Swift 5.10 Released(完全数据隔离支持、nonisolated(unsafe) 关键字)、Ready for Swift 6Announcing Swift 6(数据竞争编译时检测、Typed throws、Noncopyable Types 泛型支持)
  • SwiftGG 中文版:Swift 5.10 ReleasedAnnouncing Swift 6——官方文档的高质量中文翻译
  • onevcat:Swift 6——详细记录迁移体会,包括 @MainActor 后向兼容策略(assumeIsolated/assertIsolated)、用 OSAllocatedUnfairLock 实现线程安全的 Sendable class、@unchecked Sendable 的合理使用、@Sendable 闭包的”传染”问题、以及 deinit 隔离的社区讨论与变通方案
  • Medium:Mastering Modern Swift Concurrency——结合 Swift 5.10/6 讲解 actor hopping、Sendable 约束、async/await 最佳实践
  • 官方:Concurrency Adoption Guidelines——面向库作者的并发“迁移清单”:从 API 设计和 #if canImport(_Concurrency) 的兼容守护开始,逐步引入 Sendable/@Sendable 并用 -warn-concurrency 观察风险,再到 Task cancellation/Task Local 的工程化落地,以及对 Swift 6 全量检查时代的预期与破坏性变化
  • SwiftLee:Swift Testing: Writing a Modern Unit Test@Test 宏与测试函数命名)、Using the #expect macro(用布尔表达式替代 40+ 种 XCTest 断言、异常捕获的多种形式)、MVVM in SwiftUI(结合 @Observable 宏的现代架构实践)
  • Majid:Introducing Swift Testing. Basics.——从 @Test/@Suite 宏、#expect/#require 断言到参数化测试与并行执行,提供完整入门指南
  • 肘子:Mastering the Swift Testing Framework(深入剖析 Swift Testing 与 XCTest 的差异、Xcode 16 集成方式、命令行 --enable-swift-testing 选项)、理解 SwiftUI 的视图刷新机制(探讨视图依赖追踪与 body 重算时机)、SwiftUI onAppear 异常调用的陷阱(分析生命周期回调的时序问题与规避策略)、写在 WWDC 2024 之前:SwiftData 的未来潜力与现实挑战(讨论 #Index 宏、CloudKit 同步稳定性、ModelContext.didSave 通知等痛点)
  • timac:State of Swift and SwiftUI in iOS 18——通过二进制分析得出硬数据:iOS 18 含 6800 个二进制,592 个使用 SwiftUI(较 iOS 17 增长超 50%);19 个 App 采用纯 SwiftUI 生命周期(含 Calculator/Passwords);Apple Intelligence 框架(ProactiveSummarization/PrivateCloudCompute)均用 Swift 编写;新增 SwiftUICore 供 UIKitCore 底层调用
  • SwiftLee:SwiftLee in 2024——年度复盘里主要讲“独立开发者如何把产出做成系统”:全职独立后的节奏与优先级取舍、Newsletter 周更习惯,以及 RocketSim(含 Teams 版)、课程与 Podcast 的推进路径

iOS/Swift 开源社区持续活跃。swift-composable-architecture(TCA) 在大型 SwiftUI 项目中的采用率持续上升,Point-Free 团队也发布了 swift-navigationswift-perceptionExyte 团队的 SwiftUI 库(ChatPopupViewAnimatedTabBar)因设计精美、接口简洁而广受欢迎。SwiftLintSwiftFormat 依然是团队标配;SwiftUI-Introspect 在访问底层 UIKit/AppKit 视图时仍不可或缺。

Apple Design Awards 2024 获奖者:Procreate Dreams(创新)、oko(多元包容)、Bears Gratitude(乐趣横生)、Crouton(绝佳互动)、Gentler Streak(社会影响)、Rooms(视觉图像)。首次设立的“空间计算”类别由 djayBlackbox 获得。

App Store Awards 2024:iPhone 年度 App 是 Kino(把 iPhone 变成专业摄影机);iPad 年度 App 是 Moises(AI 分离音轨);Apple Vision Pro 年度 App 是 What If…? An Immersive Story。Apple Arcade 年度游戏是 Balatro+——扑克牌 + Roguelike 的“构建卡组”游戏,在独立游戏圈爆火。

Mac 端,Arc 浏览器 推出 Windows 版和 Arc Search(iOS),但创始团队宣布不再投入桌面版引发争议;Raycast 的 AI 功能和扩展生态持续繁荣;OpenAI 推出 ChatGPT for macOSWarp 终端发布 Linux 版本;Obsidian 的 1.5/1.6 版本带来全新表格编辑器和 PDF 标注体验。Bear 2.0 终于发布,Markdown 表格、绘图和全新编辑器让老用户等得值。Bartender 被收购后争议不断,很多用户转向开源替代 Ice

iOS 平台上,AI 应用继续爆发:ChatGPT 支持语音对话和图像理解;Perplexity AI 以“AI 搜索引擎”定位获得青睐;国产 Kimi豆包通义千问 纷纷推出移动端 App。苹果在 2 月发布了 Apple Sports——更轻更快的比分/赛程 App。小红书 在海外以“RedNote”名义走红,TikTok 禁令风波让它意外收获大量美国用户。

4 月苹果更新 App Store Review Guidelines,允许“复古游戏主机模拟器”上架,Delta 等模拟器迅速走红——很多人第一次在 iPhone 上用更“官方”的方式重温 GBA/FC/SFC 的童年。

对 iOS 工程师来说,2024 另一个实感很强的变化是:工具链把“取证”前移到了日常。

Xcode 16 把一批“本来需要资深工程师才能搭出来的诊断链路”前移到日常工作流:Thread Performance Checker 能直接提示主线程 hang、优先级反转、过量磁盘写入;配合 Instruments 的 Flame Graph,Time Profiler 更像“一眼能看懂的热点地形图”(What’s New in Xcode 16)。

Apple 在 Analyze Heap Memory(WWDC24 10173) 里把堆内存问题写成可复用流程:先把现象分成 transient growth / persistent growth / leaks,再用 Instruments 标定时间段、找持有链,必要时下沉到 vmmap / malloc_history

Explore Swift Performance(WWDC24 10217) 用函数调用、内存布局、值拷贝这些底层成本解释泛型、协议类型、闭包与 async 何时会变贵——提醒我们别靠直觉做微优化,而要用 profiling 证明。

Swift 6 把“底层约束”写进语言:Noncopyable Types 用 ~Copyableborrowing/consuming 把资源所有权和“少拷贝/少 ARC”变成语义的一部分(What’s New in Swift);Strict Concurrency / Sendable / @MainActor 的迁移会直接影响模块边界与性能折中——onevcat 在《Swift 6》把迁移成本与渐进策略讲得很落地。

值得注意的是,工具链本身也会“改变运行时”。Nutrient 在《Investigating a dynamic linking crash with Xcode 16》记录了一次 Xcode 16 下的动态链接崩溃排查:他们用 dlsym/dladdr 校验 SQLite 符号是否都来自同一 libsqlite3.dylib,却发现 Dl_info.dli_fname 有时指向 libRPAC.dylib;进一步定位到 Thread Performance Checker 会通过 interpose 介入部分 SQLite 符号,最终让 sqlite3_threadsafe 成为触发点——排查这类问题时要把“诊断开关/环境变量(如 DYLD_PRINT_LINKS_WITH)”也当成变量。

美团在《Recce in Meituan:大前端·如何突破动态化容器的天花板?》以 iOS “无 JIT” 为前提重做动态化容器:运行时采用 “Wasm 为主(Wasm3)+ JavaScript 为辅(QuickJS)” 的解释执行策略,业务侧用 Rust 编译到 Wasm;渲染层复用 React Native 的原生渲染与组件封装、保留 UIManager/Yoga 布局,并把优化重点放在“属性设置/渲染通信”(避开 RN 属性转换瓶颈)与平台抽象接口(借鉴 WebIDL/LLVM 的分层思路)上,目标是在动态化能力和接近原生的渲染管线效率之间重新找平衡。

10 月我在韩国首尔 KWDC24 做了《我在韩国首尔 KWDC24 做的技术分享》,把 iOS 性能优化的演进拆成三块:内存问题(leak/peak/thrashing)的定位与取证、启动阶段(Pre-main/Post-main)的度量与治理、以及通过识别未执行代码/未使用资源来压缩包体。文章里也把常用抓手串了一遍:Memory Graph、MallocStackLogging / leaks / malloc_history、fishhook / malloc_logger、MetricKit 的 os_signpostobjc_msgSend hook、LLVM Pass 等。

这是我第一次出国做分享,认识了很多朋友,收获颇丰。

2025年:Liquid Glass 与 AI Agent

2025 对我而言像一条“交叉路口”:一边是 AI 从“对话”走向“执行”,Agent 把任务拆解、工具调用、验证迭代串成流水线;另一边是苹果用 Liquid Glass 重写平台视觉语言,把“像系统”推到跨设备的一套统一标准。对 iOS 工程师来说,它们分别改变了“怎么做事”和“做出来长什么样”。

AI Agent:从“被动工具”到“主动执行者”

2025 年是 AI Agent 全面落地的一年。根据 SuperAI PULSE 的报告,约 65% 的 AI 从业者认为 AI Agent 将是最具影响力的发展趋势——AI 正从“被动工具”变成“主动执行者”。2 月 AI Action Summit 在巴黎召开,汇聚超过 100 个国家的代表讨论 AI 政策与伦理;3 月 Nvidia GTC 大会 上,黄仁勋发布全新 Rubin AI 芯片架构,宣布 AI 已进入“拐点时代”。

国产大模型迎来“国运级”时刻。DeepSeek 凭借 V3 模型在数学、编码与中文任务上对标 GPT-4o/Claude-3.5-Sonnet,在 App Store 下载榜登顶,被外媒称为“中国 AI 的 Sputnik 时刻”;Kimi(月之暗面) 发布 K2 模型——1 万亿参数的 MoE 架构,激活仅 320 亿参数,在 SWE-Bench、LiveCodeBench 等基准上达到开源 SOTA,训练成本约 460 万美元,被评价为“又一个 DeepSeek 时刻”。豆包通义千问文心一言智谱清言 等国产大模型持续迭代,“国产大模型可堪大用”逐渐成为共识。

这一年模型与基础设施仍在加速迭代。OpenAI 在年末推出了 o3 推理模型,在数学、编程、科学推理上取得重大突破;Google 发布了 Gemini 2.0,原生多模态能力让它在视觉理解上达到新高度。AI 硬件方面,Nvidia 的 Blackwell 架构 GPU 和 AMD 的 MI300X 加速卡在数据中心和 AI 训练市场展开激烈竞争;苹果的 M4 系列芯片 在端侧 AI 推理上表现亮眼,Mac mini M4 成为性价比之选。特斯拉 的人形机器人 Optimus 开始在工厂进行测试部署,Elon Musk 宣称 2025 年底前将有数千台 Optimus 投入使用。

开源社区在 2025 年迎来了 AI 基础设施工具的爆发:

  • LangChain(~100k Stars)成为构建 LLM 应用的事实标准框架,从 RAG 到 Agent 几乎无所不能
  • LlamaIndex(~38k Stars)专注于数据索引和检索增强生成
  • Ollama(~105k Stars)让在本地运行开源大模型变得简单,“一行命令跑大模型”成为日常
  • vLLM(~35k Stars)的 PagedAttention 技术大幅提升了 LLM 推理吞吐量,成为生产环境部署的首选

同一时间,通用开发工具链也在加速演进:

  • 前端与运行时: Next.js 15(Turbopack 让冷启动与热更新更快)、Bun 1.x(“all-in-one JavaScript runtime”)
  • 内容与组件化: Astro(内容驱动架构)、shadcn/ui(“复制粘贴”而非安装依赖)
  • Rust 生态与跨端桌面: Tauri 2.0(支持移动端)、Zed(极致性能 + AI 集成)
  • Python 工具链: Ruff(Rust 重写的 linter/formatter)
  • 基础设施: Deno 2.0(npm 兼容)、Podman(Docker 的无守护进程替代)

如果把“AI Agent 元年”拆到开源栈,你会发现 2025 年大家补齐的不只是模型能力,而是“连接”和“编排”:像 Model Context Protocol(MCP)(Anthropic 开源、标准化工具与资源接入、JSON-RPC 通信)把工具/数据源接入抽象成开放协议;LangGraph(状态机图、循环与分支、持久化检查点)把 Agent 的控制流变成可视化、可回放的图;CrewAI(角色定义、任务分配、协作流程)让多 Agent 的角色分工更像“团队协作”;微软也推出了 Microsoft Agent Framework文档),把 AutoGen/Semantic Kernel 的经验往“可治理、可观测”的企业方向收敛。

AI 编程工具在这一年深度融入开发流程。GitHub CopilotCursorClaude Code 成为很多开发者的标配。Claude Code 支持代码库全局理解、多文件编辑、自动运行测试,可以直接把 Issue 变成 Pull Request。Anthropic 在这一年发布了 Claude 4.5 系列模型(Opus 4.5Sonnet 4.5Haiku 4.5),在代码理解和生成上达到新高度。写简单的逻辑、生成测试用例、重构代码,AI 都能帮上忙。但 AI 不能替代思考——架构设计、性能优化、问题定位,这些还是需要深厚的基础知识。AI 是工具,不是替代品。正如 SwiftLee 在 SwiftLee in 2025 中总结的:“AI 能帮你写代码,但维护代码质量仍需要 SwiftLint、SwiftFormat 等工具来保障。”

6 月 9 日的 WWDC 2025 是苹果十年来最大的设计语言变革。苹果发布了全新的 Liquid Glass 设计语言——一套跨 iOS、iPadOS、macOS Tahoe、watchOS、tvOS、visionOS 的统一视觉体系。Liquid Glass 以半透明、类玻璃的材质为核心,能反射和折射周围环境,并随设备运动、光照和内容动态变化。按钮、导航栏、工具栏、小组件、图标、锁屏、主屏幕——几乎所有界面元素都采用了这种新材质。iOS 26 于 9 月 15 日正式推送,支持 iPhone 11 及更新机型。

Liquid Glass 带来的变化是全方位的:锁屏时间显示会根据壁纸空间动态缩放,照片壁纸可以变成“空间场景”随设备移动呈现 3D 效果;App 图标变成多层结构并带有玻璃质感,新增“透明”图标风格;工具栏和导航控件采用更圆润、流动的形态,滚动时自动收缩以突出内容。不过,这套新设计也引发了争议——部分用户反映 眼睛疲劳和眩晕感,尤其是在深色壁纸和透明图标模式下。苹果在 iOS 26.1/26.2 中回应了这些反馈,增加了“玻璃透明度”滑块和回退选项。

SwiftUI 7 围绕 Liquid Glass 进行了全面适配:新增 .buttonStyle(.glass) 玻璃按钮样式、glassEffect 修饰符、更新的 Tab 和 Toolbar API。原生 WebView 组件首次内置 SwiftUI,无需桥接即可加载网页;TextEditor 新增富文本编辑支持;macOS 上的滚动列表性能显著改善,配合新的 Instruments 模板让 SwiftUI 性能调优更直观。Swift 6.2 在并发模型上继续演进,编译器功能增强。

开发者工具层面,苹果在 WWDC 上发布了 Xcode 26,并在 Coding Intelligence 上把 AI 集成推到新高度:可以直接连接 LLM(ChatGPT 或其他)来编写代码、生成测试、修复错误,还支持在 Apple Silicon 上本地运行模型。全新的 Foundation Models 框架 让开发者可以在 App 中嵌入本地 LLM,支持离线运行并保护隐私。Human Interface Guidelines 也随 Liquid Glass 全面修订。

也许是因为 AI 让开发者从繁杂的手敲代码中抽身出来,那些“不那么简洁但性能和稳定性更好”的框架又重回到开发者身边——很多开发者重新发现了 UIKit 的价值。SwiftUI 虽然声明式很优雅,但在复杂交互、性能优化方面 UIKit 更可控。早在 iOS 18,Apple 就为 UIKit 带来了 @Observable 支持,配合 updateProperties() 方法,让“状态驱动 UI”在 UIKit 里也更顺滑。社区里关于“UIKit 回归”的讨论热度很高,SwiftUI vs UIKit in 2025 等文章会把两者的性能基准、迁移成本和混合方案讲得更量化。

如果说 Liquid Glass 是表层的 UI 语言,2025 年对 iOS 工程师更“底层”的变化,是性能工具链把“调优”从 CPU/内存进一步推进到了 SwiftUI 更新因果、能耗预算与硬件级瓶颈分析。Instruments 26 强化了 SwiftUI 专用模板,Optimize SwiftUI Performance with Instruments(WWDC25 306) 把 view body 更新、Representable 更新、以及 “Cause & Effect Graph(为什么会更新)” 做成了可视化证据链;能耗层面,苹果用 Profile and optimize power usage in your app(WWDC25 226) 把 Power Profiler 与“真机离线采集 Performance Trace”带进日常工作流——终于可以把“省电”从口号变成 trace 对比。

更硬核的是 CPU 微架构调优的入口:Optimize CPU performance with Instruments(WWDC25 308) 把 Processor Trace(记录每一条分支/指令的执行路径)和 CPU Counters 的 Bottleneck Analysis(Useful/Instruction Delivery/Instruction Processing/Discarded)做成了面向开发者的工具;配合 Xcode Organizer 的指标推荐与趋势洞察(WWDC25 247),性能问题开始更像“可以被持续治理的工程债”,而不是事后背锅的玄学。

系统底层这边,苹果在 9 月发布了 Apple Security Research 的 Memory Integrity Enforcement:把硬件级内存标记(EMTE)与 typed allocator 等机制组合成更完整的内存安全体系——对我们这些“写 App 的人”来说,它既是安全范式的变化,也意味着未来在性能/稳定性问题定位上会遇到更多“系统帮你兜底”与“系统更严格”并存的新边界。

  • 总览/速览:Majid 的 What’s new in SwiftUI after WWDC25 与 SwiftProgramming.com 的 What’s new in SwiftUI and iOS 26 at WWDC25——前者梳理了 Liquid Glass 如何让 Xcode 26 构建的 App 自动适配新设计,并逐一讲解 Tab/Toolbar 新 API、GlassButtonStyleglassEffect() 修饰符、AttributedString 富文本编辑与原生 WebView 支持;后者补充了 @Animatable 宏、List section index、场景感知 padding、SF Symbols “draw on” 动画等更细粒度的 API 变化
  • 可运行示例:Exploring WebView and WebPage in SwiftUI for iOS 26(AppCoda)——演示了 WebView 一行代码加载网页(告别 UIViewRepresentable 包装),以及 WebPage 的程序化控制方式,包括 title/url/estimatedProgress 等属性获取与加载进度追踪
  • 中文“校准取舍”:fatbobman 的 WWDC 2025 初印象:意料之中,预想之外 与 CSDN 的 WWDC25开发新秘技揭秘——前者指出实机动效远胜静态截图、Liquid Glass 需要重新设计背景而非简单开关,还提到 Apple 给了一年过渡期可在 Xcode 26 中关闭新设计;此外点评了 TabView 大改、macOS SwiftUI-AppKit 互操作增强、以及 AttributedString 富文本编辑是今年最惊喜的 API 补齐;后者更像一组“按主题拆解”的 WWDC25 合集:从 Liquid Glass/SwiftUI 7 的视觉与组件实现,到 @Animatable 宏、SwiftData3 的 @Model 与迁移/并发安全,再到 Chart3D 与 SnippetIntent 这类新能力,适合先扫一遍技术地图再按需深挖
  • 玻璃效果实操:Majid 的 Glassifying custom SwiftUI viewsGlassifying custom SwiftUI views. Groups——第一篇介绍 glassEffect 三种预设(regular/clear/identity)及 tint() 着色、interactive() 手势响应;第二篇讲解 GlassEffectContainer 将多个玻璃视图合并渲染以提升性能,spacing 参数控制形变距离,glassEffectUnion 则用于跨视图统一玻璃效果
  • 迁移与架构:Vlad Khambir 的 The Most Valuable WWDC 2025 Sessions for iOS EngineersHow to Migrate from UIKit to SwiftUI in Large Projects——前者精选了 10 个高价值 Session(含 Foundation Models、SwiftUI 性能优化等);后者提出”从叶到根”渐进迁移策略:先迁移子视图、再迁移页面、最后处理根导航,通过 UIHostingController/UIViewRepresentable 双向桥接实现混合架构
  • 并发变化与迁移:SwiftLee 的 Default Actor Isolation in Swift 6.2 与 Donny Wals 的 Exploring concurrency changes in Swift 6.2(对照 Swift 6.2 Released)——核心变化是 nonisolated(nonsending) 让异步函数默认在调用者执行器上运行、不再强制 Sendable;新项目默认启用 @MainActor 隔离,老项目可通过 Xcode 26 的”Approachable Concurrency”设置开启;@concurrent 属性则用于显式切回全局执行器
  • 端侧 AI 落地:SwiftCafe 的 iOS 26 开发者可以调用 Apple Intelligence 本地模型,Foundation Models 框架、Superwall 的 An Introduction to Apple’s Foundation Model Framework、Artem Novichkov 的 Foundation Models profiling with Xcode Instruments——三篇分别覆盖 SystemLanguageModel 基础调用、Guided Generation 结构化输出与 Instructed Prompts 系统指令、以及 Instruments 性能剖析;模型内置系统不增加包体积,支持 Streaming API 与 Tool Calling
  • 端侧 AI(业务视角):Majid 的 Building AI features using Foundation Models——从业务场景出发演示 LanguageModelSession 初始化、instructions 系统指令配置、GenerationOptions 生成参数调优,以及如何用 respond() 实现多轮对话
  • 团队知识底稿:信逆云科技的 iOS Swift开发实战:从UIKit到SwiftUI现代化应用(2025)——更像一份“现代化改造目录”,把落地拆成可复用的模块:架构侧覆盖 MVVM 与 Clean Architecture(分层、依赖注入、状态管理),工程侧给出 CI/CD(GitHub Actions/GitLab CI、Fastlane、自动签名打包与发布)与测试体系(TDD、Mock、UI/集成测试、测试金字塔)的组织方式,同时补齐安全(HTTPS、本地存储、混淆/逆向、防护与证书 Pinning)与性能(启动、内存、流畅度、网络指标与监控)这些“最后一公里”的清单
  • 工具链与验证:SwiftLee 的 #Playground Macro: Running Code Snippets in Xcode’s canvas 与 MJ Tsai 的 Xcode 26——前者介绍 #Playground 宏:import Playgrounds 后可在源码中直接运行代码片段并在 Canvas 预览结果,无需单独创建 .playground 文件,实现”所写即所得”的即时反馈;后者记录了 Xcode 26 正式版与 RC 的 build number / .xip 包差异与下载细节,评论区还补充了可通过 Info.plist UIDesignRequiresCompatibility 临时保持旧视觉样式的兼容开关,适合做过渡期的风险兜底
  • 上线闭环:Majid 的 Monitoring app performance with MetricKit——讲解如何通过 MXMetricManagerSubscriber 订阅 MXMetricPayload(性能指标)与 MXDiagnosticPayload(崩溃诊断),捕获传统 Crash Reporter 遗漏的 OOM 与后台终止,applicationExitMetrics 可追踪内存压力退出,支持 JSON 导出便于接入后端

迁移与架构讨论也是 2025 的高频话题:一类是“从 UIKit 渐进迁移到 SwiftUI”的工程方法论(桥接、灰度、边界);另一类是“SwiftUI + Swift Concurrency”在大项目里的分层与可测试性——Vlad 的两篇与信逆云科技那篇基本覆盖了这两条路线。

周报类内容在 2025 年仍然是快速补课的最佳入口。国际圈里,Majid 的 SwiftUI Weekly 每周精选社区好文,近期讨论了 Feature Flags、玻璃效果的自动应用时机、zIndex 等实用技巧;SwiftLee Weekly 每周二发送,精选五篇文章配合 Swift Evolution 动态,订阅者已超 3 万。中文圈里,肘子的 Swift 周报(fatbobman)持续输出高质量原创,第 116 期《Swift、SwiftUI 与 SwiftData:走向成熟的 2025》回顾了这些技术的年度演进,第 109 期包含 iOS 性能优化问答汇总;老司机 iOS 周报 第 358 期讨论了 objc4-950 源码更新、App Store Mini App Partner Program 等话题。两份周报都是中文开发者“每周必读”的存在。

年末回顾类文章也很有价值。SwiftLee 的 SwiftLee in 2025: A full year as an indie developer 记录了 Antoine 全职独立开发的第一年:RocketSim for Teams 达到 250+ 团队用户,Swift Concurrency 课程 被 Airbnb、Garmin、Monzo 等公司采购,Newsletter 订阅者突破 3 万。他也坦言博客流量因 AI 内容聚合而增长放缓,转而加大 YouTube 视频投入。Majid 在 2025-12-09 的 Monitoring app performance with MetricKit 中深入探讨了 MetricKit 的崩溃、启动时间、内存监控能力;在 2025-07-23 的 Glassifying custom SwiftUI views. Groups 则演示了如何为自定义视图组应用玻璃效果。

2025 年 iOS/Swift 开源社区持续活跃:

同一个名单里还有不少“看一眼就知道今年大家在用什么”的 finalists。Apple 在 6 月的 winners and finalists 公告 里把它们完整列了出来:效率与写作类有 iA Writer,信息消费里有 Ground News,专注力工具里有 Opal;游戏侧除了大热的 Balatro,还能看到 Prince of Persia: The Lost Crown、Skate City: New York、Control Ultimate Edition 这类更偏“主机级体验”的作品——很能代表 2025 年 Apple 平台生态一边追求更精致的个人生产力、一边追求更沉浸的内容消费。

说完行业和社区,聊聊我自己这一年。2025 对我而言像一条“二分线”:一头是继续把性能问题量化到每一毫秒,另一头是把 AI 变成可以共同产出的搭档。

1 月我在新加坡的 iOS Conf SG 25(大会 10 周年)做了《我在 iOS Conf SG 25 的演讲》分享,主题仍然是启动优化:先把 Launch Time 拆成 Pre-main/Post-main,再分别讲怎么度量和治理——用 Instruments 的 App Launch 模板做快速定位,用 os_signpostsysctl、MetricKit 把“手工分析”推进到“可日常化采集”;最后通过 hook objc_msgSend(fishhook)与 Swift 函数插桩,把耗时归因到函数与调用链。原文里也放了视频以及三个 Demo/工具(AppLaunchDemoDevToolsRSSReader)。

3 月在上海的 Let’s Vision 25,我分享了《使用 AI 突破 iOS 开发者能力边界》。AI 带来的改变很特殊——它第一次让很多场景接近“所想即所得”:过去需要翻文档、试参数、读源码的部分,现在可以先快速生成一个可运行的骨架,再用验证把它推到可维护。在我的实践里,SwiftUI 动画开发场景中,AI 工具能减少约 70% 的 API 查阅时间。工具上我常用 Cursor + Claude 做多文件理解与重构;提示词上更依赖一个稳定骨架:角色设定 → 任务描述 → 约束条件 → 输出格式,再配合“逆向 Prompt”(先让 AI 给实现,再让它解释风险/边界)把结果从“能跑”推进到“可维护”。甚至可以把 Keynote 动画导出成视频,用 Vision 框架分析帧差异反推 CAAnimation 参数,再让 AI 转成可编辑的 SwiftUI 代码。我最大的感触是:用 AI 去学习喜欢却不擅长的,用 AI 去做必要却枯燥繁琐的,把时间留给更少但更重要的事。

我越来越确信:当 UI 语言在表层被重写、当工作流在工具侧被重写,真正不变的是工程能力本身——把问题拆到可度量、可定位、可回归,把新工具当成放大器而不是拐杖。带着这种确定感,回头看这十年,会更清楚什么值得长期投入。

下一个十年已经开始,好看的剧《太平年》开始了,好的模型Opus4.6、GPT5.3 Codex也来了,好使的工具 OpenClaw 开始浮现,好用的方法论 Skill 也用了起来。对后面的日子更加期待起来了。

十年回望

写到最后,想起周末带娃去游乐场,孩子在玩,我在旁边用手机看斯坦福公开课的日子。那时候 iOS 9 刚发布,3D Touch 还新鲜,适配和约束把人折腾得够呛。

十年过去,iOS 已经到 26 了,SwiftUI 逐渐成熟,Vision Pro 开辟了新的战场,Liquid Glass 带来了十年来最大的视觉变革。技术在变,但学习的方法没变——保持好奇,解决问题,持续输出。

回头看这十年,有几个感悟:

  • 底层原理永远不过时。新框架层出不穷,但内存管理、多线程、渲染原理这些底层知识是不变的。理解它们,才能在新框架里游刃有余。
  • 不要被新技术焦虑绑架。SwiftUI 很好,但 UIKit 也不过时;Swift 很好,但 Objective-C 也还能用。关键是能解决实际问题,而不是追逐热点。
  • 输出是最好的学习。每次深入研究一个问题,写下来,分享出去。”教”是最好的”学”,这些年写博客、做分享,受益最大的其实是我自己。
  • 把问题拆到可验证的闭环。十年前看 Auto Layout 头疼,现在看并发模型一样头疼——但“测量 → 定位 → 改动 → 回归”这条路径一直有效。

十年前在游乐场看公开课的那个新手,现在还是会为新技术兴奋、为难题头疼。也许这就是做开发者最好的状态吧。

使用 AI 突破 iOS 开发者能力边界

作者 戴铭
2025年3月6日 14:54

之前在 KWDC 和 iOS Conf SG 用英文做过性能优化进化启动优化的技术分享。以下是我这次在上海举行的 Let’s Vision 25 大会上分享的内容。

前言

从变形金刚的机械美学启蒙,到用 Objective-C 构建第一个 iOS 应用,每个技术突破都带来新的创作可能。AI带来的变革尤为特殊——它首次实现了”所想即所得”的开发体验。过去需要数周研究的 CoreAnimation 动画,现在通过自然语言描述即可生成基础实现;曾经需要阅读大量 RFC 文档的网络协议,如今通过 AI 助手可以快速理解关键点。

根据GitHub官方统计,使用 Copilot 的开发者代码完成速度提升55%,而我的实践数据显示,在SwiftUI动画开发场景中,AI工具可减少70%的 API 查阅时间。

AI 编程工具

工具类型 我的选择 核心优势 适用场景 候选方案
本地推理 Ollama+Deepseek-r1 32B参数平衡性能/显存 代码审查 LM Studio
知识管理 AnythingLLM 支持PDF/代码库索引 技术文档检索 OpenWebUI
云端开发 Cursor+Claude3.7 实时联网/多文件分析 全栈调试 VSCode+Copilot

推荐在 Xcode 中配置 Copilot 双面板布局:左侧编写业务逻辑,右侧自动生成单元测试用例。对于 CoreData 等 ORM 操作,尝试用”逆向Prompt”技巧:先让AI生成代码,再要求它解释可能出现的问题。

使用本地大模型的好处是可以使用 Swift 项目代码作为训练集,进行模型微调。

提示词

提示词非常重要,现在推理模型出来后,我觉得最重要的是描述清楚想要得到结果是什么样的。然后再设定好范围和边界。

比如

1/ 角色设定你是有10年经验的CoreAnimation专家,擅长用显式动画优化交互体验2/ 任务描述我需要实现类似Apple Music专辑封面的3D翻转效果,要求:- 使用CATransform3D实现透视投影- 支持手势控制翻转角度- 优化iPad多任务场景下的性能3/ 约束条件- 目标系统iOS15+- 避免使用第三方库- 优先考虑Metal加速方案4/ 输出格式分步骤给出实现方案,标记出需要特别注意的点

有了工具,了解了提示词,接下来就可以做一些能够开心的事情了。对于我来说提升技能是最能让我开心的事。

想提升的技能

我感兴趣的事情其实挺多,比如画漫画,做幻灯片,游戏相关的 Blender 、Godot 和 Vision OS。

开发相关的有密码技术、网络技术、编译器和虚机等。

另外开发各类的 APP 技能也是我想要掌握的。比如日历、日记、天气、健康、拍照、漫画阅读器和音视频播放器等等。

当然,我当前眼前最想提升的就是幻灯片的质量,因为我要用它在会上来做分享呀。

开发幻灯片 APP 需要什么知识

以前我为了能够更好的分享内容,我做了个幻灯片软件。

接下来我想完善这个幻灯片软件,于是我这样问了 AI

我想开发一个 PPT 这样的 macOS 应用,这个应用有哪些功能,分别需要什么方面的技术知识

AI 的回答分为基础功能、进阶、高级、技术架构、挑战等部分。基础部分主要包括了界面、文档和幻灯片编辑的功能和使用的技术。

进阶里有模板、动画和多媒体的功能和技术。

  • 基础功能
    • 界面
      • 功能:窗口、菜单栏、工具栏、幻灯片缩略图、编辑区
      • 技术:Swift/SwiftUI、AppKit、NSWindowController、NSDocument、NSDraggingDestination
    • 文档
      • 功能:新建、打开、保存、自动保存、版本恢复
      • 技术:FileManager、NSSavePanel、NSOpenPanel
    • 幻灯片编辑
      • 功能:添加/删除幻灯片、调整顺序、编辑文本/形状/图片
      • 技术:NSView 或 CALayer 实现画布、NSTextView 或自定义文本渲染、NSBezierPath 或 Core Graphics 进行图形绘制。
  • 进阶
    • 模版与主题
      • 功能:预置模板、自定义主题
      • 技术:NSUserDefaults、动态渲染实时应用到幻灯片元素
    • 动画与过渡效果
      • 功能:元素入场动画、幻灯片切换效果
      • 技术:Core Animation、CAAnimation 和 CALayer 实现关键帧动画、时间轴控制
    • 协作
      • 功能:多人实时编辑、版本历史、云端存储
      • 技术:WebSocket、冲突解决(OT)算法、CloudKit、NSUbiquitousKeyValueStore
    • 多媒体支持
      • 功能:插入图片、音频、视频、形状、图标
      • 技术:AVFundation、NSImageView、Core Plot
  • 高级
    • 导出和分享
      • 功能:导出 PDF、视频、HTML、图片、Markdown
      • 技术:NSPrintOperation 或 PDFKit,AVAssetWriter 逐帧渲染幻灯片
    • 智能建议
      • 功能:自动排版、配色建议、AI 生成内容
      • 技术:Core ML、NaturalLanguage、LLM
    • 插件系统
      • 功能:第三方插件扩展(图表库、AI 工具)
      • 技术:NSBundle 加载插件、定义插件接口协议
  • 技术架构
    • 设计模式:MVVM、响应式编程
    • 数据存储:SwiftData、NSCache 缓存
  • 适配 macOS 特性
    • Dark Mode:通过 NSAppearance 适配深色主题
    • 沙盒机制:处理文件访问权限和安全性
  • 关键挑战
    • 复杂动画:高性能渲染需要 Metal 或 Core Animation 优化

目前我最希望能够学习到的就是其中的动画部分,因为我希望 PPT 能够通过动画看起来更生动些。

AI 能够解决传统动画开发的一些痛点:

功能模块 传统开发痛点 AI解决方案 效率提升
动画系统 关键帧参数调试耗时 自然语言转CAKeyframeAnimation代码 65%
布局引擎 多设备适配复杂 深度学习预测最佳constraint组合 80%
协作同步 冲突解决算法实现难度高 生成OT算法Swift实现模板 70%
性能优化 Metal Shader调试困难 自动生成性能分析报告及优化建议 90%

一些操作技巧:

  • 逆向工程法:将Keynote动画导出为视频,用Vision框架分析帧差异生成CAAnimation参数

    Screen Recording → (Vision分析) → CoreAnimation指令集 → (AI转换) → SwiftUI可编辑代码
  • 提示词演进

    • 初级:”如何实现页面翻转动画”
    • 进阶:”生成支持手势控制的CATransform3D动画,要求60fps流畅运行”
    • 专家:”创建可中断的物理动画系统,模拟纸张的弯曲刚度和空气阻力”
  • 调试技巧

    • 在Xcode中配置CA_DEBUG_TRANSACTIONS环境变量
    • 使用Instruments的Core Animation分析模板
    • 通过AI解释CAMediaTimingFunction的贝塞尔曲线参数

基础提示词

接下来就看看我是怎么使用 AI 的来学习动画的吧。

我们先看看基础提示词,框定一些基本技术,这里列出是我常用的技术,比如 SwiftUI、Swift Concurrency、SwiftData 和 Observation。

动画描述提示词

下面我对我要实现的动画做一个描述,现有幻灯片有个比较大的痛点,就是当我想在幻灯片上指出某个区域时,需要用到银光笔。如果能够直接在幻灯片中实现点击,然后通过一些效果表示点击的地方那不是更好。

于是我做了这样一个描述的提示词。让其在点击拖动时会产生变色的光晕效果,这个光晕会随着时间渐渐消失,可以多点触控,能够有多条轨迹,发光效果使用模糊滤镜实现的,颜色会随时间自动的变化。

代码可以看到颜色随时间变化这个需求主要是通过 truncatingRemainder 这个方法来实现的。使用 Canvas 的上下 addFilter 方法设置模糊效果,然后形状设置成圆形。

下一个例子。提示词是点击或拖动会产生随机的形状,这些形状包括圆形、矩形、圆角矩形、胶囊形、椭圆形和三角形,每个形状都有随机的颜色和大小,形状出现时有缩放动画,然后渐渐消失。最多同时显示50个形状,形状之间保持最小间距。

得到了代码。

ForEach(shapes) { shape in    // 描边和尺寸设置    shape.view        .stroke(shape.color, lineWidth: 2) // 描边样式        .frame(width: shape.size * shape.scale, height: shape.size * shape.scale) // 动态尺寸        .position(shape.position)        .opacity(shape.opacity)        .animation(.easeOut(duration: animationDuration), value: shape.scale)}// 随机图形static func randomShape() -> some Shape {    let shapes: [AnyShape] = [        AnyShape(Circle()),         AnyShape(Rectangle()),        AnyShape(RoundedRectangle(cornerRadius: 25)),        AnyShape(Capsule()),        AnyShape(Ellipse()),        AnyShape(Triangle()) // Added Triangle    ]    return shapes.randomElement()!}// 三角形状 Shapestruct Triangle: Shape {    func path(in rect: CGRect) -> Path {        var path = Path()        path.move(to: CGPoint(x: rect.midX, y: rect.minY))        path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))        path.closeSubpath()        return path    }}extension Color {    static var random: Color {        Color(red: .random(in: 0...1),              green: .random(in: 0...1),              blue: .random(in: 0...1))    }}

从代码中可以看出颜色是通过 rgb 三个值随机组合获得的随机颜色,在 randomShape 这个函数中实现了各个形状,基本都是内置的 Shape 形状。

点击运行可以看到效果。

从其它语言代码获取提示词

这是一个 Rect 的动画代码,完整代码地址在这里。代码不少,如果对 CSS 动画不熟还挺难看懂,那么我们可以让 AI 帮我们获取这段代码的动画描述,让他通过描述转化成 SwiftUI 动画的提示词。这就是得到的提示词。

可以看到 AI 生成的提示词比我们写的更有条理,他会先说最终的视觉效果是什么样的,然后会对动画的特点做详细的说明,比如是按照什么速度和方向旋转,完成一圈的时间,是否循环。还生成了技术要点,比如环形渐变用 AngularGradient ,边框效果用 mask 和 strokeBorder,blur 效果模拟发光。用 GeometryReader 来确保边框大小。还有布局上一些值的设置。可以说是非常精确和详细了。

通过这些提示词得到的 SwiftUI 代码我们就容易看懂了。比如 GlowingCardBorder 这个边框视图的颜色设置,尺寸的计算等,还有 TimelineView 如何计算时间。

运行这段代码的效果如下图:

我们可以学到 CSS → SwiftUI 转换法则

protocol AnimationTranspiler {    func convert(keyframes: CSSKeyframe) -> CAKeyframeAnimation    func translate(easing: BezierCurve) -> CAMediaTimingFunction    func adapt(layout: FlexBox) -> SwiftUI Layout}// AI生成的转换中间件示例class CSS2SwiftAITranspiler {    func parseTransform(transform: String) -> CATransform3D {        // 使用NLP识别translateX/rotate等操作        // 生成优化后的CATransform3D链式调用    }}

代码复用

这是一个会发光和改变形状的动效,左边是代码,可以看到很多值的设置都是写在各处表达式中的,如果想复用这段动画代码进行一些调整就很费劲。于是我写出这样的提示词。先让他将动画的部分进行复用,实现更多自定义,然后利用提示词再优化下代码。比如减少重复的部分,编写已用接口等。

AI 依据提示会帮着生成一个接口配置结构体,里面可以对动效做详细的设置。比如角的弧度,变换的幅度,颜色和阴影等。还可以对不同参数组合进行配置,方便复用。

这里是一个新的设置,主要是将背景设置为黄色,改变了角的弧度还有阴影部分。拖动的时候会发现,对拖动后的弹性做了些参数调整,弹起来更加流畅些了。

学习技术

这个效果,可以看到一群散开的小圆,慢慢聚合点缀成文字。

如果我想了解这个动画效果后面的技术,可以这样问 AI。先让他系统性的检查代码中的动画实现,然后说下每个动画的技术。最后说下每个技术的最佳实践。

AI 的回答如下:

主要使用了以下几种动画技术:## **SpriteKit 粒子动画**:- 使用 `SKShapeNode` 创建粒子节点- 通过 `SKAction` 实现粒子的移动动画let move = SKAction.move(to: targetPosition, duration: duration)let scale = SKAction.scale(to: 1.0, duration: duration)particle.run(SKAction.group([move, scale]))## **粒子动画效果**:- 初始化时粒子随机分布- 文字组装动画(1.5-3.0秒)- 触摸爆炸效果(0.3秒)- 重新组装动画(1.5-2.0秒)## **SwiftUI 动画**:- 使用 `withAnimation` 制作按钮滑入动画withAnimation(.spring(duration: 0.6)) {    buttonOffset = 0}## **核心动画技术**:- 使用 Core Text (`CTLine`, `CTRun`) 创建文字路径- 使用 `CGPath` 进行路径采样,确定粒子目标位置## **动画时序控制**:- 使用 `DispatchQueue.main.asyncAfter` 控制动画延时- 设置动画缓动效果:`.easeOut`- 使用不同的动画持续时间创造自然的视觉效果这个示例综合运用了 SpriteKit、SwiftUI 和 Core Graphics 的动画能力,创造了一个复杂的交互式文字粒子动画效果。

通过 AI 的回答可以看到动画中的粒子节点时使用 SpriteKit 创建的。粒子不同时段的动画的参数也给了出来。还有文字形状使用 Core Text 的 CTLine 和 CTRun 创建的文字路径,用 CGPath 进行了路径的采样确定粒子目标位置。

从开源库学习

背景的这个颜色会变的效果来自一个开源库,你可以看到颜色的变化的节奏并不死板,而且颜色融合的效果也很好。我们可以通过 AI 来了解背后是怎么做到的。

AI 分析后,会告诉我各个动画的效果是在哪个文件实现的。

我们能够一目了然用了哪些动画,以及是在哪个文件中写的。比如弹性动画效果是在 BlobLayer.swift 里用 CASpringAnimation 创建的,用到了哪些属性。定时器是在 FluidGradientView.swift 里通过 Combine 创建的,触发时机是随机的。

渐变动画是基于 CAGradientLayer 创建的子类 BlobLayer 来创建的径向渐变。

如何使用这个库,也可以让 AI 提供范例,这样应用到自己项目也会很容易。下面就是它提供的使用范例。

可以看到这个库的接口非常清晰,除了基础颜色可以定义外,还能够添加叠加高亮色,其中加白是为了让颜色不要太艳丽,加黄是为了让颜色偏暖些。

接口使用

下面我想说说接口的使用,比如 SF Symbol 新增了 symbolEffect 来给 SF Symbol 做动效,第一个是不断的晃动,第二个是给晃动加了延时,第三个是在两个 SF Symbol 切换时加上过渡效果。那么怎么全面掌握 symbolEffect 接口所有动效及用法呢?

我们可以直接这样问 AI。它会整理好所有的基础动画效果。

在 SwiftUI 中,`symbolEffect` 修饰符可以为 SF Symbols 添加动画效果。以下是主要的 symbolEffect 类型:## 基础动画效果:.symbolEffect(.bounce) // 弹跳.symbolEffect(.scale) // 缩放.symbolEffect(.pulse) // 脉冲.symbolEffect(.wiggle) // 摇摆.symbolEffect(.appear) // 出现.symbolEffect(.disappear) // 消失.symbolEffect(.replace) // 替换## 变体动画效果:.symbolEffect(.bounce.up) // 向上弹跳.symbolEffect(.bounce.down) // 向下弹跳.symbolEffect(.variableColor) // 变色.symbolEffect(.iterative) // 迭代动画## 常用选项设置:// 重复选项.symbolEffect(.bounce, options: .repeat(3)) // 重复3次.symbolEffect(.bounce, options: .repeat(.forever)) // 无限重复.symbolEffect(.bounce, options: .repeat(.continuous)) // 连续重复// 速度选项.symbolEffect(.bounce, options: .speed(2)) // 2倍速## 组合效果:// 可以组合多个效果.symbolEffect(.bounce).symbolEffect(.variableColor)你当前代码中使用的是:.symbolEffect(.wiggle, options: .repeat(.continuous))这会让图标持续不断地进行摇摆动画。注意:某些效果可能需要 iOS 17.0+ 或 macOS 14.0+ 及以上版本才能使用。结合 transition 来使用import SwiftUIstruct ContentView: View {    @State private var isPresented = false    var body: some View {        if isPresented {            Image(systemName: "swift")                .font(.system(size: 100))                .transition(.symbolEffect(.appear))        } else {            Button("Show symbol", action: { isPresented.toggle() })        }    }}// contentTransitionimport SwiftUIstruct ContentView: View {    @State private var isPresented = false        var body: some View {        VStack(spacing: 50){            Image(systemName: isPresented ? "checkmark.circle.fill" : "faceid")                .font(.system(size: 100))                .contentTransition(.symbolEffect(.replace))                        Button("Show symbol", action: { isPresented.toggle() })        }    }}

会说变体动画有哪些,组合效果,有哪些选项,比如设置重复次数,速度等。最后还会举例怎么使用。

修改动画效果

下面我们看看如何通过 AI 来修改动画的效果,左边代码会显示一个火焰的动效,现在我们让颜色有变化。提示词指明增加颜色的随机性,然后让粒子大小也能够有变化。

可以看到右侧火焰的颜色变成随机的来了。

接下来再看看怎么让粒子的轨迹能够有变化,提示 AI 让运动轨迹变成螺旋运动,带些波浪效果,然后让运动的中心点移到画布的中心。

会生成对应的运算表达式。

// 添加螺旋运动let spiralRadius = canvasSize.width/3 * (1-time)let spiralAngle = rotations * time * .pi * 2 + startingRotation// 添加波浪效果let waveAmplitude = canvasSize.width/6 * Darwin.sin(time * .pi * 4)let x = canvasSize.width/2 + Darwin.cos(spiralAngle) * spiralRadius + waveAmplitudelet y = canvasSize.height/2 + Darwin.sin(spiralAngle) * spiralRadius * 0.8// 使用非线性alpha衰减let alpha = Darwin.pow(1-time, 1.5)

运行后看到粒子的轨迹已经变了。

编辑器的开发

接下来我要讲的是这个用来展示代码的编辑器,以前用的三方的,定制起来不方便,还需要考虑升级和兼容问题。

左侧代码是我幻灯片中标题文字动画选择的部分,目前这个编辑器不光可以浏览代码,还能够修改代码,比如更改 animationType 的值,就可以更改文字动画效果。

这个其中一种动画效果,也是我用在提示词上的文字动画。我们用这个编辑器来改改文字内容,文字大小。

那么这个编辑器的提示词是什么呢?我希望这个编辑器是原生的,但是 SwiftUI 的 TextEditor 现在还无法进行深入的定制,我发现很多好的原生编辑器都是使用 AppKit 的控件来做的。所以我希望它是在 SwiftUI 下用 NSViewRepresentable 来包装现有的 AppKit。

功能上我需要的是能够显示语法高亮,而且是多语言的,Swift 语法用来显示开发代码,Markdown 语法用来显示提示词问题和答案之类。高亮逻辑会在文本改变时触发。这样能够保证修改代码后依旧能够正确显示高亮。确保 NSTextView 和 SwiftUI 的数据流能够双向绑定。再让其支持 UndoManager 撤销操作和快捷键,因为代码会经常敲错,需要快速回退。

另外是对语法高亮支持的说明,说明需要支持多语言。支持配色主题等。

最后就是换行到语法格式对应的地方。

总的来说

这几年用 AI 的工具以来,我最大的感触就是用 AI 去学习喜欢却不擅长的,用 AI 去做必要却枯燥繁琐的。这样就有更多的时间去做很少的事情,因为

只有很少的书值得去读,
很少的作者值得关注,
很少的朋友值得交往,
很少的事情值得投入,
很少的道理值得明白,
很少的资产值得投资,
很少的目标值得追逐。

我在 iOS Conf SG 25 的演讲

作者 戴铭
2025年1月21日 10:14

大会前,vina 跟我说 iOS Conf SG 的受众很大,她希望能够讲些可以让大家更加兴奋,可以在日常工作中应用的内容。因此,我也是专门写了些 Demo 和工具,共三个,123。那些难理解的内容我都去掉了。这次的画的图也是我花费时间最长的一次,学习了些时尚杂志的设计和布局。有些来不及调配色的图,我就参考媳妇买的巧克力包装配色。

下面是分享的内容。视频已放出点击查看

I’ll be talking about how to reduce app launch times.

I’ll first explain what app launch time is.

Then, I’ll cover how to collect launch time data using tools like Instruments, os_signpost, sysctl, MetricKit, and by hooking objc_msgSend and Swift functions.

I’ll also go over how to solve common performance issues.

Finally, we’ll dive into advanced ways to reduce launch times, with optimization strategies and code examples.

Let’s first understand launch time.

Launch time has two main parts: pre-main and post-main.

  • Pre-main happens before the main function. This is when the Mach-O file is loaded and dynamic libraries are read. To optimize here, we can reduce the size of the Mach-O file and cut down the number of dynamic libraries.

  • Post-main happens after the main function. This is when the UI is rendered and data is loaded until the app becomes interactive. Here, we can optimize task priority.

So, how can we measure the time spent during these stages?

We can use Xcode’s Instruments to analyze launch time.

The method is to use the App Launch template in Instruments, collect data for the first 20 seconds of the app launch, filter the data, and then analyze it.

Since the launch phase calls many system library methods, to get better results, it’s important to filter out system library data and track time usage per thread. Instruments can do this by setting up the Call Tree to filter system libraries and view data by thread.

Keep in mind that Instruments collects data through periodic sampling, so it may miss some details.

So, we need to do manual analysis. The benefit of this approach is that it lets us collect data automatically, gathering it daily.

It also allows us to customize time tracking, like measuring time at the function level, which gives us more detailed stats.

The methods for manual analysis include os_signpost and MetricKit.

Let’s first look at how to use os_signpost.

First, import os.signpost into your code. Then, where you want to track time, add start and end markers to log the duration.

Data collection with os_signpost is done through Xcode’s Profile feature, using the Instrument’s Logging template.

The limitation of os_signpost is that it can’t track pre-main timing. Another limitation is that it still relies on Instruments.

How do we solve these limitations?

To handle this, we can use the sysctl system interface to get pre-main timing.

And with MetricKit, we can gather launch time data without relying on Instruments.

Let’s talk about sysctl. sysctl provides an interface to fetch process information.

When a process is created, it initializes kernel data and records the creation time. This is the start time of the process.

To measure time with sysctl, we first get process info and then calculate the elapsed time.

We do this by setting up sysctl, creating an MIB array, and getting the p_starttime value from the kinfo_proc structure.

The p_starttime gives us the process start time. To get the elapsed time, we need the current time and then calculate the difference.

In the getProcessRunningTime function, we find the address offset for the current process’s PID in the process’s memory layout. This gives us detailed information about the current process, stored in kinfo.

We then get the current time when the function is called. By subtracting the process start time from the current time, we get the runtime since the process was created.

Now that we’ve solved the issue of not being able to track pre-main time, let’s move on to solving how to get this data without relying on Instruments.

To obtain the pre-main time, you need to first gather information about the process, extract the process creation time, and then calculate the app’s running time.

Now that we’ve solved the issue of not being able to track pre-main time, let’s move on to solving how to get this data without relying on Instruments.

To use MetricKit, you first create an MXMetricManager and add a subscriber to collect data.

Data is collected when the app enters the background or when the device is idle.

The data processing happens in MXMetricManagerSubscriber and supports batch processing.

You can view the collected data in Xcode’s Organizer, and it also supports custom analysis.

Manual analysis has many benefits, but it’s time-consuming, error-prone, and can lead to messy code. So, we need an automated solution.

The automated process involves using tools to parse the code, find method definitions, and insert timing logic. This saves development time and makes the code easier to maintain.

Tools available for this include source code analysis tools and build integration tools.

Next, I’ll cover some automated ways to measure time, including how to hook objc_msgSend to track the time of Objective-C function calls.

For Swift projects, I’ll also explain how to track the time of each Swift function.

Let’s first see how to track the time of Objective-C functions. Since all Objective-C functions are called through objc_msgSend, we can hook this method to track the time of all Objective-C functions.

The approach is to use fishhook to replace the objc_msgSend C function.

Since objc_msgSend is written in assembly, we also need to use assembly to do the method replacement.

In the replacement, we save the necessary registers before the method call and restore them afterward.

We track the time before and after the method call, save the time for each function, and generate a report.

You can view the full code at the link below.

Here is the code. In the smCallTraceStart function, we use fishhook‘s rebind_symbols to replace the method. The original objc_msgSend is saved as orig_objc_msgSend, and the hook logic is in hook_Objc_msgSend.

In the hook_Objc_msgSend method, we first save the method call parameters, then record the start time with before_objc_msgSend. After reading the parameters, we call the original objc_msgSend, save its return value, and calculate the function execution time.

Finally, we return the value from objc_msgSend and wrap everything in an interface for easy use.

After running it, you’ll see that the execution time of all functions is recorded.

The code summary is shown in the diagram. We first replace objc_msgSend and calculate function execution time in the replacement. Then, we save the data and generate a report.

This is the method we use in our company to check startup time.

This method only works for tracking the execution time of Objective-C functions. But what about Swift functions?

To track the runtime of Swift functions, I wrote a tool.

Simply drag your Swift project folder into the tool, click a button, and the tool will parse the Swift files in the project, find function definitions, and insert the time tracking code.

When your app runs, the tool starts collecting data, including function names, call counts, and execution times.

This is the tool’s interface. Just drag your project in. In the top right corner, there’s a button for time tracking. Click it, and it will insert the tracking code.

Once your project runs, the tool will sort the function’s execution time, showing the average time, call count, and total time for each function.

From what we’ve covered so far, we know how to identify where startup time is spent.

There are a few common issues that can impact launch time.

There are several common situations that can affect function execution time, as shown in the image.

The first one is expensive operations, like reading large files, making network requests, or querying the database.

The solution here is to move these operations to the background or break them into smaller tasks that run as needed.

The second issue is displaying large images. You can asynchronously load and decode large images using Swift Concurrency, or use more optimized formats to reduce I/O and memory usage.

The third issue is frequent UI updates. The solution is to use lazy loading to only update the UI visible on the screen, and use default placeholders for UI elements off-screen.

The last issue is DispatchSemaphore, which can block the main thread. The solution is to use Swift Concurrency’s Task Group, move the wait into async tasks, and free up the main thread.

The relevant examples and solutions are available in the link below.

In the demo app, I’ve included all the bad cases. The app’s launch time was huge, over 10 seconds.

After optimizing the code, the main thread finish time is down to just 1 second, and the async completion time is also much shorter.

You definitely want to download this demo and see the difference before and after optimization. The link is below on this page.

We’ve used tools to pinpoint startup issues, and now we’ve solved those costly problems.

But can we further reduce the startup time?

Next, I’ll introduce two more techniques that can reduce startup time even further: optimizing the launch path and merging libraries.

The principle of Launch Path optimization is that when an external launch is triggered, we bypass the home page’s reading and rendering, directly opening the target page.

The benefit of this approach is that it saves the overhead of reading and rendering the home page.

Next is the Mergeable Libraries optimization technique.

Traditionally, dynamic libraries were loaded one by one, processing symbols and then initializing each library.

With Mergeable Libraries, dynamic libraries are merged, removing redundant and duplicate symbols, and turning them into static libraries.

This is a new feature in Xcode that can be enabled through Build Settings.

In Build Settings, you can find the “Merged Binary” option and set it to “Automatic.”

At this point, we’ve identified the problems and understand how to address them. We also know how to further reduce startup time.

However, as the app evolves, the tasks that run during startup can become more complex and numerous.

We need a way to manage these tasks effectively, so we can control the system resource usage during startup and prevent the launch time from getting worse.

CPU and memory are limited resources.

If we don’t manage multithreading tasks properly, tasks can pile up at times, causing the CPU to switch between threads frequently, which wastes time.

When threads aren’t busy, the CPU isn’t fully utilized, causing delays and slowing down startup time.

The larger the codebase, the more obvious these issues become.

So, how can we better manage multithreading tasks and make full use of the CPU?

We divide tasks into high-priority and low-priority ones. High-priority tasks should run concurrently and can have dependencies managed.

Low-priority tasks can be delayed and run only when system resources are available.

I created two functions: executeTasksConcurrently and performLowPriorityTasks.

executeTasksConcurrently runs high-priority tasks concurrently using Swift Concurrency’s withTaskGroup, and the order of calling this function controls task dependencies.

performLowPriorityTasks runs low-priority tasks using Task.detached and sets the task’s priority to background.

Once we create three high-priority task groups, they will execute sequentially, and tasks within each group will run concurrently. Low-priority tasks will run when system resources are free.

So far, we’ve mostly covered Post-main optimizations. For Pre-main, we can optimize startup time by reducing the app size.

There are many ways to reduce app size, mainly through static analysis. Today, I’ll share how we can analyze at runtime to find unused code, expanding the scope of our optimizations.

Let me introduce a solution that can help identify which classes are not being used during runtime.

The process involves checking all classes when the app goes to the background and determining which ones have been initialized.

We use the objc_getClassList API to get a list of all classes, and NSClassFromString to find the metaclass of each class. The metaclass’s flag field, when shifted 29 bits, tells us if the class was initialized during runtime.

In the code, the metaClass struct’s data method returns a class_rw_t metaclass struct. The flag field is shifted left by 29 bits. A value of 0 means the class hasn’t been initialized, while 1 means it has.

In the initializedClassesInArray method, we use NSClassFromString to get class data, then call isInitialized to check if the class was initialized. We add initialized classes to an array, and the remaining classes are the ones not used during this app session.

Here, I’ve printed out all the initialized classes.

It’s also how we check for unused code in our company.

From the results of the analysis, this solution indeed detects a lot of unused code, especially older code.

However, there’s one issue. If a class contains many functions, as long as one of them is used, the entire class is considered “in use.”

So, we need to take it a step further and find even more unused code.

Do you remember the tool I created to collect Swift function data?

That tool can also collect data on all the functions in your app.

Every function your app calls during execution gets logged.

By subtracting the functions that are actually called from the total list, we can identify unused functions.

Click the button in the top-right corner of the tool, and it will show a list of all functions, with the ones that were executed marked.

We’ve gone over the built-in tools in Xcode for checking startup issues and how to create custom tools for automating the checks.

We also looked at some bad cases and discussed optimization techniques. To make every millisecond count, we shared more practical optimization tips.

I hope you found this helpful.

上面就是我分享的内容。另外这次主题是个大话题,还有很多相关知识可能需要花费更多时间学习,我也整理了些官方内容和一些工具。

很多嘉宾的博客我都订阅过,看过他们很多的分享。

这次也是 iOS Conf SG 大会的10周年。很多上次 KWDC 大会认识的韩国朋友也来了。思琦说这次上海的 Let’s Vision 25 也会有很多有意思的国内外嘉宾过来,真是非常期待。

我在韩国首尔 KWDC24 做的技术分享

作者 戴铭
2024年10月28日 10:58

韩国朋友真是太热情了。下面是这次分享的内容,文章后面我还会记录些这次首尔的见闻。

The topic I’ll be discussing is the evolution of iOS performance optimization. I hope you can take away some insights from my talk.

Let’s first talk about a few situations where an app becomes unusable, which can be simplified into app crashes or freezes. There are three main reasons, the first being OOM, meaning memory exhaustion.

When an app consumes too much memory, the system can no longer allocate more, leading to OOM. This issue doesn’t produce crash logs, making it tricky to trace.

The second reason is a null pointer, where the pointer points to an invalid memory address. The third common issue is accessing a nil element in an array, which is another frequent cause of crashes.

These are the three most common causes of crashes, with memory issues being the hardest to resolve. Next, I’ll focus on how to address memory issues.

In addition to crashes, performance issues can also affect the user experience, such as lagging or overheating.

  • Lag can be identified through Runloop monitoring to locate the part of the stack where execution takes too long;
  • Overheating can be addressed by monitoring CPU usage in threads to find the threads or methods causing CPU overload.

Slow app startup and large package sizes also impact user experience. As projects grow in complexity, solving these problems becomes increasingly challenging.

The above four issues lead to a poor user experience.

Upon analysis, these three problems are the hardest to solve: memory issues, slow startup, and large package sizes. I will focus on sharing some of the latest solutions to these problems next.

Memory issues fundamentally stem from improper memory usage. Memory is a finite resource, and if we misuse it, problems will inevitably arise.

The most common memory issues are threefold: the first is memory leaks, where memory is not released after being used, leading to increasing memory consumption.

The second issue is high memory peaks. When memory usage suddenly spikes at a certain point, the system may trigger the Jetsam mechanism, killing the app directly.

The third issue is memory thrashing, which refers to frequent garbage collection, causing performance corruption.

So, memory leaks, high memory peaks, and memory thrashing are the most common memory issues.

To solve memory issues, the first step is to understand memory usage. We can retrieve this information using system APIs, such as mach_task_basic_info, the physicalMemory property of NSProcessInfo, and the vm_statistics_data_t structure.

In addition to APIs, Xcode’s Memory Graph feature is very intuitive, allowing you to view the app’s memory usage in real-time, making it a very handy tool.

There are also some open-source libraries, such as KSCrash, which provide freeMemory and usableMemory functions to retrieve information about the system’s free and available memory.

Using these methods, we can clearly monitor the app’s memory usage.

What may seem like a small memory leak can accumulate over time, eventually causing system performance worse or even triggering an OOM crash.

The most common cause of memory leaks is retain cycles. Here are two open-source tools that can help us detect retain cycles.

The first is MLeaksFinder. It hooks the dealloc method to check whether an object still exists after being released, thereby determining if there is a memory leak.

The second tool is FBRetainCycleDetector. It traverses strong references between objects and builds a reference graph. If it detects a cycle, it indicates a retain cycle issue.

Retain cycles are relatively easy to detect. In addition to these open-source tools, Xcode’s tools can also help us detect memory leaks in a visual way.

In contrast, memory peaks and memory thrashing are like hide “little monsters” and are harder to detect. So, how do we track down these problems like detectives?

Here’s one method: by repeatedly sampling memory usage, we can calculate the differences and identify the objects with the fastest memory growth.

Rank the top 100 objects with the most significant growth. Specifically, this can be done by hooking the alloc and dealloc methods to track the allocation and release of objects.

Each time memory is allocated, we can maintain a counter—incrementing the counter on alloc and decrementing it on dealloc—this way, we can keep track of the number of currently active objects.

With this method, we can pinpoint the objects with the fastest memory growth, making it easier for further analysis.

Next, let’s introduce hook malloc, which allows us to capture every memory management operation. It’s like planting a “secret agent” to monitor each memory allocation action.

Below are some common methods to hook malloc, including macro definitions, symbol overriding, and function attributes. The most flexible method is using fishhook, which allows dynamic toggling.

fishhook is a technique that modifies Mach-O file symbols to achieve function replacement. We can use it to replace the malloc function.

In the code above, the purpose of rebind_symbol is to replace the malloc function with our custom-defined custom_malloc function. The second parameter, original_malloc, indicates that after replacing the function, the original function will continue to be executed.

This way, with each memory allocation, through the custom_malloc function, we can capture the size and address of every memory allocation.

Additionally, the system’s built-in malloc_logger tool can also comprehensively record the memory allocation process, offering a more straightforward solution.

malloc_logger is essentially a callback function. When memory is allocated or released, it will callback and log relevant information.

By tracking malloc and free operations, we can discover memory blocks that haven’t been correctly released.

After solving memory issues, remember to retest to ensure the problem is completely resolved.

Next, let’s look at how to customize this malloc_logger function to capture memory allocation and release information.

First, define a callback function with the same signature as malloc_logger, for example, custom_malloc_stack_logger.

The type indicates the type of memory operation, such as malloc, free, or realloc; arg1 represents the memory size, arg2 is the memory address, and result indicates the reallocated memory address.

Based on different type values, we can obtain this parameter information and record memory allocation details, especially for large memory allocations. We can also capture stack information to facilitate issue analysis.

Of course, a memory snapshot is also a comprehensive solution that captures complete memory information.

First, by traversing the process’s virtual memory space, we can identify all memory regions and log information like the start address and size of each region.

Using the malloc_get_all_zones function, we can retrieve all heap memory regions and analyze each region’s memory nodes one by one, ultimately identifying memory reference relationships.

With this more comprehensive information, we can resolve memory leaks, optimize memory usage, and prevent OOM crashes in one go.

Here is a code example for finding all memory regions. As you can see, the vm_region_recurse_64 function’s info parameter contains information like the memory region’s start address and size.

Using this information, we can construct a memory layout map to analyze the app’s memory state when issues occur, such as using the protection property to check if the app accessed unreadable or unwritable memory regions.

Compared to other methods, the benefit of malloc stack logging is that it automatically records data without needing to write code manually to capture memory information. You just need to enable it when necessary and disable it when not.

MallocStackLogging records every memory allocation, release, and reference count change. These logs can be analyzed with the system tool leaks to identify unreleased memory or with the malloc_history tool to translate stack IDs in the logs into readable stack trace information.

Here is an example code for using MallocStackLogging. We can use the enableStackLogging function to enable logging, disableStackLogging to disable logging, and getStackLoggingRecords to retrieve current memory operation details.

In the enableStackLogging function, turn_on_stack_logging is called to enable logging. disableStackLogging calls turn_off_stack_logging to disable logging. getStackLoggingRecords calls mach_stack_logging_enumerate_records and mach_stack_logging_frames_for_uniqued_stack to record the details of current memory operations.

The tools we used earlier, leak and malloc_history for analyzing MallocStackLogging logs, both come from the malloc library. The malloc library provides many tools for debugging memory.

In addition to MallocStackLogging, the system offers many tools for debugging memory, such as Guard Malloc and some environment variables and command-line tools.

The MallocScribble environment variable can detect memory corruption errors.

We’ve talked a lot about how to solve problems when they occur, but is there a way to optimize memory before problems even arise?

In fact, iOS itself evolves to optimize memory management. Especially in iOS, which is designed for mobile devices without swap partitions like desktop systems, it uses the Jetsam mechanism to help developers manage memory proactively when resources are tight.

Additionally, the system provides tools like thread-local storage and mmap(), which are methods that can improve memory efficiency.

Here are a few tips to help reduce unnecessary memory overhead:

  • Take advantage of the copy-on-write principle and avoid frequently modifying large strings.
  • Use value types as much as possible to avoid unnecessary object creation.
  • Make good use of caching and lazy loading.
  • Choose appropriate image formats and control image resolution and file size.

These are some of the optimizations the system does for you, but there are plenty of areas where we can optimize as well.

A slow app launch can be a frustrating experience. We all know that this is a big issue.

App launch actually happens in several stages. The first stage is called Pre-main, which refers to things the system does before the main() function executes, like loading app code, the dynamic linker working, Address Space Layout Randomization (ASLR), and some initialization operations.

After these preparations are done, the app truly starts running and enters the UI rendering stage, where tasks in didFinishLaunchingWithOptions begin executing. These tasks include both the main thread’s work and operations on other threads.

To summarize, app launch is a multi-stage process. From Pre-main to UI rendering, tasks must be properly arranged, and neither the main thread nor background threads should waste resources.

Next, let’s talk about factors affecting launch performance. In the Pre-main stage, the number of dynamic libraries, the number of ObjC classes, the number of C constructors, the number of C++ static objects, and ObjC’s +load methods all directly impact launch speed. Simply put, the fewer, the better.

After the main() function is executed, even more factors can affect the launch time, such as main() execution time, time spent in applicationWillFinishLaunching, view controller loading speed, business logic execution efficiency, the complexity of view hierarchy, number and speed of network requests, size of resource files, usage of locks, thread management, and time-consuming method calls—all of which can slow down the launch.

As you can see, many factors influence launch time, both before and after main(). However, this also means there are many opportunities for optimization.

For large apps, which are often developed by multiple teams, tasks executed at startup can change with each iteration. Therefore, we need an effective way to measure the time consumption of each task during startup to identify the “culprits” slowing down the launch, enabling targeted optimizations and checking the effectiveness of those optimizations.

Common measurement tools include Xcode Instruments’ Time Profiler, MetricKit’s os_signpost, hook initializers, hook objc_msgSend, and LLVM Pass.

Next, I’ll focus on hook objc_msgSend, which can record the execution time of each Objective-C method. For measuring the execution time of Swift functions, you can use LLVM Pass, which I’ll explain in detail when we discuss package size optimization.

By hooking objc_msgSend, we can record method call information, including method names, class names, and parameters. By inserting tracking code before and after method execution, we can calculate the execution time of each method.

The specific approach is to first allocate memory space for jumping, with the jump function being used to record the time. Then, save the register state: the x0 register can obtain the class name, the x1 register gets the method name, and the x2 to x7 registers can be used to get method parameters.

After completing the jump function call, restore the saved registers and use the br instruction to jump back to the original method and continue execution.

Although hook objc_msgSend uses assembly language, it’s not too complicated to write as long as you understand the roles of several registers and how the instructions work.

Next, I will introduce ten very useful startup optimization strategies:

  1. Reduce the use of +load methods.
  2. Reduce static initialization.
  3. Prefer static libraries over dynamic libraries to reduce the number of symbols.
  4. Control the number of dynamic libraries.
  5. Use the all_load compiler option.
  6. Perform binary reordering.

After the main function, we can do a lot more optimization, such as:

  • Optimizing business logic.
  • Using task scheduling frameworks to arrange tasks more efficiently.
  • Leveraging background mechanisms to handle non-essential tasks.
  • Refreshing regularly to fetch server data in a timely manner.

The final important topic is optimizing package size.

Optimizing package size has many benefits. For users, it improves download speed, saves device storage, and reduces resource consumption. For developers, it lowers development and maintenance costs while improving efficiency.

Through static analysis, we can identify some unused resources and code. Today, I will focus on how to discover unused code at runtime, starting with detecting unused classes.

In the meta-class, we can find the class_rw_t structure, which contains a flag that records the state of the class, including whether it has been initialized at runtime.

The code on the right shows how to access this flag and use it to determine whether a class has been initialized.

Next, let’s discuss how to determine which functions haven’t been executed at runtime.

This code shows how to customize an LLVM Pass to instrument each function and track whether they are called. The instrumentation code is written in the runOnFunction or runOnModule functions, where the former handles individual functions, and the latter handles the entire module.

Additionally, LLVM Pass can insert tracking code before and after function execution to record the execution time of each function.

以上就是分享的内容。下面是一些见闻。

KWDC 这次是在一所大学举办的。

这是我、徐驰和 falanke 的合影,会场有个大头照机器,很多人都在这里合影。

iOSConfSG 2025 组织团队负责人 Vina Melody 也来了,我分享结束后跟他们沟通了下明年我去新加坡 iOSConf 分享的内容。

第二天,KWDC团队组织我们在首尔 City walk,第一站是景福宫,我们玩起来 Cosplay。

freddi 是喵神的同事,在福岡。

River 是韩国的一名独立开发者,开发了很有品味的 APP Cherish。她不喜欢 KPOP,但她父母好像是从事表演的。

台湾最知名的 iOS Youtuber Jane 这次也来了。

中午我们吃了鸡肉火锅。

下午去了汉江野餐。晚上我们登上南山,看到了美丽的首尔夜景。

晚上,继续找地方喝酒。韩国晚上街上人依然很多。

giginet 聊了点技术问题,他也是喵神的同事。

二刷 iOS 性能与编译,简单点说

作者 戴铭
2024年9月5日 16:36

本文主要想说说 iOS 的性能问题的原因,如何监控发现问题,以及如何预防和解决这些问题。

为啥要说是二刷呢,因为以前我也写过好几篇性能相关的文章。有性能优化的深入剖析 iOS 性能优化,包体积相关的GMTC 上分享滴滴出行 iOS 端瘦身实践的 Slides用 Swift 编写的工程代码静态分析命令行工具 smck使用Swift3开发了个macOS的程序可以检测出objc项目中无用方法,然后一键全部清理使用 LLVM使用 LLVM 分享的幻灯片。还有启动速度相关的App 启动提速实践和一些想法如何对 iOS 启动阶段耗时进行分析。编译相关的深入剖析 iOS 编译 Clang / LLVM

这次我尽量绕开以前谈的,只简单提提,着重说些以前没提或者说的少的。来个互补吧。也加了些前段时间去深圳给平安做分享的内容。

这次内容也整理进了小册子方便下载后按目录日常查阅,小册子程序本身也是开源的,欢迎 Clone 查看。

由于 iOS 性能问题涉及面很多,我先做个分类,这样好一个一个的说。大概顺序是会先从造成用户体验损失最大的卡顿、内存爆掉来开头,然后说下启动和安装包体积怎么优化,说说性能分析的工具和方案,最后讲讲怎么使用 Bazel 提速编译。

卡顿

先了解下 iOS 视图和图像的显示原理。

介绍

我们了解的 UIKit 和 SwiftUI 都是提供了高层次的管理界面元素的 API。另外还有 ImageView 是专门用来显示图像的类。底层是 Core Graphics,也可以叫做 Quartz,这是 iOS 的 2D 绘图引擎,直接和硬件交互。Core Animation 是处理动画和图像渲染的框架,将图层内容提交到屏幕,并处理图层之间的动画。

底层图形渲染管线 iOS 用的是 Metal。Core Animation 会将要渲染的图层内容转换成 GPU 可以理解的命令,然后让 Metal 渲染到屏幕上。

大图

最容易造成掉帧的原因就是大图。由于大图数据量较大,对应渲染指令就比较多,会影响渲染的时间,造成卡顿。可以在显示大图前,先加载并显示较小尺寸的缩略图,等用户确实需要查看高清版本时,再加载完整图片。

举个例子:

import SwiftUIstruct ThumbnailImageView: View {    let thumbnailImage: UIImage    let fullSizeImageURL: URL        @State private var fullSizeImage: UIImage? = nil    var body: some View {        ZStack {            if let fullSizeImage = fullSizeImage {                Image(uiImage: fullSizeImage)                    .resizable()                    .scaledToFit()            } else {                Image(uiImage: thumbnailImage)                    .resizable()                    .scaledToFit()                    .onAppear(perform: loadFullSizeImage)            }        }    }    private func loadFullSizeImage() {        DispatchQueue.global().async {            if let data = try? Data(contentsOf: fullSizeImageURL),               let image = UIImage(data: data) {                DispatchQueue.main.async {                    self.fullSizeImage = image                }            }        }    }}

在加载大图时使用 CGImageSource 逐步解码图片,在低分辨率时减少内存占用。

import UIKitfunc loadImageWithLowMemoryUsage(url: URL) -> UIImage? {    guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {        return nil    }        let options: [NSString: Any] = [        kCGImageSourceShouldCache: false, // 避免直接缓存到内存        kCGImageSourceShouldAllowFloat: true    ]        return CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary).flatMap {        UIImage(cgImage: $0)    }}

异步绘制

系统资源方面,CPU 主要是计算视图层次结构,布局、文本的绘制、图像解码以及 Core Graphics 绘制。GPU 是处理图层合并、图像渲染、动画和 Metal 绘制。CPU 负责准备数据,GPU 负责渲染这些数据。

因此,CPU 方面需要注意过多的子视图会让 CPU 很累,需要简化视图层次。setNeedsDisplay 或 layoutSubviews 也不易过多调用,这样会让重新绘制不断发生。图像解码也不要放主线程。GPU 方面就是图片不要过大,主要是要合适,保持图片在一定分辨率下清晰就好,另外就是可以采用上面提到的大图优化方式让界面更流畅。

UIView 是界面元素的基础,用于响应用户输入,绘制流程是当视图内容或大小变化时会调用 setNeedsDisplay 或 setNeedsLayout 标记为要更新状态,下个循环会调用 drawRect: 进行绘制。绘制是 Core Graphics,也就是 CPU,显示靠的是 Core Animation,用的是 GPU。异步绘制就是将 Core Graphics 的动作放到主线程外,这样主线程就不会收到绘制计算量的影响。

Core Graphics 的异步绘制是使用 UIGraphicsBeginImageContextWithOptions 函数在后台线程中创建一个 CGContext。使用 GCD 或 NSOperationQueue 来在后台线程中进行绘制操作。完成绘制后,将结果返回主线程以更新 UI。

下面是一个异步绘制的示例代码:

import UIKitclass AsyncDrawingView: UIView {        private var asyncImage: UIImage?        override func draw(_ rect: CGRect) {        super.draw(rect)                // 如果有异步绘制的图片,直接绘制它        asyncImage?.draw(in: rect)    }        func drawAsync() {        Task {            // 创建图形上下文            let size = self.bounds.size            UIGraphicsBeginImageContextWithOptions(size, false, 0.0)            guard let context = UIGraphicsGetCurrentContext() else { return }                        // 进行绘制操作            context.setFillColor(UIColor.blue.cgColor)            context.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))                        // 获取绘制结果            let image = UIGraphicsGetImageFromCurrentImageContext()            UIGraphicsEndImageContext()                        // 更新 UI,回到主线程            await MainActor.run {                self.asyncImage = image                self.setNeedsDisplay() // 触发 draw(_:) 方法重新绘制            }        }    }}

对于复杂的异步绘制,特别是涉及 UIView 的情况下,可以考虑这两个方法。首先是自定义 CALayer 并实现其 draw(in:) 方法来进行异步绘制。其次是使用 UIView 的 draw(:) 方法,在子类中重写 draw(:) 方法,并结合异步操作来更新绘制内容。

import UIKitclass AsyncDrawingLayer: CALayer {        override func draw(in ctx: CGContext) {        super.draw(in: ctx)                Task {            // 在子线程中执行绘制操作            await withCheckedContinuation { continuation in                Task.detached {                    // 执行绘制操作                    ctx.setFillColor(UIColor.red.cgColor)                    ctx.fill(self.bounds)                                        // 完成绘制操作后继续                    continuation.resume()                }            }                        // 回到主线程更新 UI            await MainActor.run {                self.setNeedsDisplay() // 触发 draw(in:) 重新绘制            }        }    }}

离屏渲染也容易掉帧,应该尽量的避免复杂的圆角、阴影效果,或者使用更简单的图形操作。如可能,减少对 layer 的属性设置,尤其是那些可能引起离屏渲染的属性。

运算转移到 GPU

CPU主要负责用户交互的处理,如果能够将运算转移到 GPU 上,就可以给 CPU 减压了。

以下是一些常见的方法和技术,可以在iOS中将计算任务从CPU转移到GPU:

通过Metal的计算管线(Compute Pipeline),可以编写计算着色器(Compute Shaders)在GPU上执行大量并行计算任务,如物理模拟、数据分析等。

// 使用Metal进行简单的计算操作let device = MTLCreateSystemDefaultDevice()let commandQueue = device?.makeCommandQueue()let shaderLibrary = device?.makeDefaultLibrary()let computeFunction = shaderLibrary?.makeFunction(name: "computeShader")let computePipelineState = try? device?.makeComputePipelineState(function: computeFunction!)

Core Image 是一个强大的图像处理框架,内置了许多优化的滤镜(Filters),并能够自动将图像处理任务分配到GPU上执行。

let ciImage = CIImage(image: inputImage)let filter = CIFilter(name: "CISepiaTone")filter?.setValue(ciImage, forKey: kCIInputImageKey)filter?.setValue(0.8, forKey: kCIInputIntensityKey)let outputImage = filter?.outputImage

Core Animation 是iOS的高效动画框架,它会将大部分动画的执行过程自动转移到GPU上。这包括视图的平移、缩放、旋转、淡入淡出等基本动画效果。通过使用CALayer和各种动画属性(如position、transform等),你可以创建平滑的动画,这些动画将在GPU上硬件加速执行。

let layer = CALayer()layer.position = CGPoint(x: 100, y: 100)let animation = CABasicAnimation(keyPath: "position")animation.toValue = CGPoint(x: 200, y: 200)animation.duration = 1.0layer.add(animation, forKey: "positionAnimation")

SpriteKit 和 SceneKit 是两个高层次的框架,分别用于2D和3D游戏开发。它们内部利用GPU进行图形渲染和物理模拟,极大地减少了CPU的负担。

let scene = SKScene(size: CGSize(width: 1024, height: 768))let spriteNode = SKSpriteNode(imageNamed: "Spaceship")spriteNode.position = CGPoint(x: scene.size.width/2, y: scene.size.height/2)scene.addChild(spriteNode)

线程死锁

线程操作稍不留神就会让主线程卡死,比如dispatch_once中同步访问主线程导致的死锁。子线程占用锁资源导致主线程卡死。dyld lock、selector lock和OC runtime lock互相等待。

同步原语(synchronization primitive)会阻塞读写任务执行。iOS 中常用的会阻塞读写任务执行的同步原语有 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、信号量(Dispatch Semaphore)、屏障(Dispatch Barrier)、读写锁(pthread_rwlock_t)、互斥锁(pthread_mutex_t)、@synchronized 指令os_unfair_lock、原子性属性(Atomic Properties)、NSOperationQueue 和 操作依赖(Dependencies)、Actors。

这些同步原语各有优缺点,选择合适的同步机制取决于具体的应用场景。例如,pthread_rwlock_t适用于读多写少的情况,而NSLock或@synchronized则适用于简单的互斥需求。GCD的信号量和屏障则提供了更高层次的并发控制手段。因此在使用同步原语时要特别注意了。检测卡死情况也要重点从同步原语来入手。

IO 过密

磁盘操作通常是阻塞性的,可以将磁盘 IO 操作放到后台线程中执行。

import SwiftUIstruct ContentView: View {    @State private var data: String = "Loading..." // `data` 用于存储从磁盘读取的数据,并在 UI 中显示。        var body: some View {        VStack {            Text(data)                .padding()            Button("Load Data") {                loadData()            }        }    }        func loadData() {        // 通过 `Task` 创建一个并发上下文来运行异步代码块。在这个代码块中执行耗时的磁盘 IO 操作。        Task {            // 在后台执行磁盘 IO 操作            let loadedData = await performDiskIO()            // 在主线程更新 UI            await MainActor.run {                data = loadedData            }        }    }        // 模拟一个磁盘 IO 操作,可能是从文件中读取大数据    func performDiskIO() async -> String {        // 模拟磁盘操作耗时        try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds delay                // 这里可以进行实际的磁盘读取操作        // 例如读取文件内容:        // let fileURL = ...        // let data = try? String(contentsOf: fileURL)                return "Data Loaded Successfully!"    }}@mainstruct DiskIOApp: App {    var body: some Scene {        WindowGroup {            ContentView()        }    }}

跨进程通信导致卡顿

进程间通信(IPC)是一种重要的机制,它允许不同的进程或应用程序之间交换信息。然而,某些系统API的调用可能会导致卡顿或性能问题,特别是在以下几种情况下:

  • CNCopyCurrentNetworkInfo 获取 WiFi 信息
  • 设置系统钥匙串 (Keychain) 中的值
  • NSUserDefaults 调用写操作
  • CLLocationManager 获取当前位置权限状态
  • UIPasteboard 设置和获取值
  • UIApplication 通过 openURL 打开其他应用

在执行以上操作时,心理上是要有预期的。能有替代方案的话那是最好的了。

卡顿监控

监控原理是注册runloop观察者,检测耗时,记录调用栈,上报后台分析。长时间卡顿后,若未进入下一个活跃状态,则标记为卡死崩溃上报。

以下是一个 iOS 卡死监控的代码示例:

#import <Foundation/Foundation.h>#import <UIKit/UIKit.h>#import <execinfo.h>#import <sys/time.h>// 定义 Runloop 模式的枚举typedef enum {    eRunloopDefaultMode,  // 默认模式    eRunloopTrackingMode  // 追踪模式} RunloopMode;// 全局变量,用于记录 Runloop 的活动状态和模式static CFRunLoopActivity g_runLoopActivity;static RunloopMode g_runLoopMode;static BOOL g_bRun = NO;  // 标记 Runloop 是否在运行static struct timeval g_tvRun;  // 记录 Runloop 开始运行的时间// HangMonitor 类,用于监控卡死情况@interface HangMonitor : NSObject@property (nonatomic, assign) CFRunLoopObserverRef runLoopBeginObserver;  // Runloop 开始观察者@property (nonatomic, assign) CFRunLoopObserverRef runLoopEndObserver;    // Runloop 结束观察者@property (nonatomic, strong) dispatch_semaphore_t semaphore;  // 信号量,用于同步@property (nonatomic, assign) NSTimeInterval timeoutInterval;  // 超时时间- (void)addRunLoopObserver;  // 添加 Runloop 观察者的方法- (void)startMonitor;  // 启动监控的方法- (void)logStackTrace;  // 记录调用栈的方法- (void)reportHang;  // 上报卡死的方法@end@implementation HangMonitor// 单例模式,确保 HangMonitor 只有一个实例+ (instancetype)sharedInstance {    static HangMonitor *instance;    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        instance = [[HangMonitor alloc] init];    });    return instance;}// 初始化方法- (instancetype)init {    self = [super init];    if (self) {        _timeoutInterval = 6.0;  // 设置超时时间为6秒        _semaphore = dispatch_semaphore_create(0);  // 创建信号量        [self addRunLoopObserver];  // 添加 Runloop 观察者        [self startMonitor];  // 启动监控    }    return self;}// 添加 Runloop 观察者的方法- (void)addRunLoopObserver {    NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop];  // 获取当前 Runloop    // 创建第一个观察者,监控 Runloop 是否处于运行状态    CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};    CFRunLoopObserverRef beginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &myRunLoopBeginCallback, &context);    CFRetain(beginObserver);  // 保留观察者,防止被释放    self.runLoopBeginObserver = beginObserver;    // 创建第二个观察者,监控 Runloop 是否处于睡眠状态    CFRunLoopObserverRef endObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &myRunLoopEndCallback, &context);    CFRetain(endObserver);  // 保留观察者,防止被释放    self.runLoopEndObserver = endObserver;    // 将观察者添加到当前 Runloop 中    CFRunLoopRef runloop = [curRunLoop getCFRunLoop];    CFRunLoopAddObserver(runloop, beginObserver, kCFRunLoopCommonModes);    CFRunLoopAddObserver(runloop, endObserver, kCFRunLoopCommonModes);}// 第一个观察者的回调函数,监控 Runloop 是否处于运行状态void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {    HangMonitor *monitor = (__bridge HangMonitor *)info;    g_runLoopActivity = activity;  // 更新全局变量,记录当前的 Runloop 活动状态    g_runLoopMode = eRunloopDefaultMode;  // 更新全局变量,记录当前的 Runloop 模式    switch (activity) {        case kCFRunLoopEntry:            g_bRun = YES;  // 标记 Runloop 进入运行状态            break;        case kCFRunLoopBeforeTimers:        case kCFRunLoopBeforeSources:        case kCFRunLoopAfterWaiting:            if (g_bRun == NO) {                gettimeofday(&g_tvRun, NULL);  // 记录 Runloop 开始运行的时间            }            g_bRun = YES;  // 标记 Runloop 处于运行状态            break;        case kCFRunLoopAllActivities:            break;        default:            break;    }    dispatch_semaphore_signal(monitor.semaphore);  // 发送信号量}// 第二个观察者的回调函数,监控 Runloop 是否处于睡眠状态void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {    HangMonitor *monitor = (__bridge HangMonitor *)info;    g_runLoopActivity = activity;  // 更新全局变量,记录当前的 Runloop 活动状态    g_runLoopMode = eRunloopDefaultMode;  // 更新全局变量,记录当前的 Runloop 模式    switch (activity) {        case kCFRunLoopBeforeWaiting:            gettimeofday(&g_tvRun, NULL);  // 记录 Runloop 进入睡眠状态的时间            g_bRun = NO;  // 标记 Runloop 进入睡眠状态            break;        case kCFRunLoopExit:            g_bRun = NO;  // 标记 Runloop 退出运行状态            break;        case kCFRunLoopAllActivities:            break;        default:            break;    }    dispatch_semaphore_signal(monitor.semaphore);  // 发送信号量}// 启动监控的方法- (void)startMonitor {    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{        while (YES) {            long result = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, self.timeoutInterval * NSEC_PER_SEC));            if (result != 0) {                if (g_runLoopActivity == kCFRunLoopBeforeSources || g_runLoopActivity == kCFRunLoopAfterWaiting) {                    [self logStackTrace];  // 记录调用栈                    [self reportHang];  // 上报卡死                }            }        }    });}// 记录调用栈的方法- (void)logStackTrace {    void *callstack[128];    int frames = backtrace(callstack, 128);    char **strs = backtrace_symbols(callstack, frames);    NSMutableString *stackTrace = [NSMutableString stringWithString:@"\n"];    for (int i = 0; i < frames; i++) {        [stackTrace appendFormat:@"%s\n", strs[i]];    }    free(strs);    NSLog(@"%@", stackTrace);}// 上报卡死的方法- (void)reportHang {    // 在这里实现上报后台分析的逻辑    NSLog(@"检测到卡死崩溃,进行上报");}@end// 主函数,程序入口int main(int argc, char * argv[]) {    @autoreleasepool {        HangMonitor *monitor = [HangMonitor sharedInstance];  // 获取 HangMonitor 单例        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));  // 启动应用程序    }}

以上代码中 HangMonitor 类会在主线程的 RunLoop 活动中检测是否有长时间的卡顿,并在检测到卡顿时记录调用栈并上报后台进行分析。超时时间设定为 6 秒,以覆盖大部分用户感知场景并减少性能损耗。

内存

引用计数

iOS 中用引用计数(ARC)来管理对象的生命周期。在ARC之前,开发者需要手动管理对象的内存,通过retain、release、autorelease等方法来控制对象的生命周期。SideTables 是一个包含8个 SideTable 的哈希数组,用于存储对象的引用计数和弱引用信息。每个 SideTable 对应多个对象。SideTable 包含三个主要成员:自旋锁(spinlock_t)、引用计数表(RefcountMap)、弱引用表(weak_table_t)。自旋锁用于防止多线程访问冲突,引用计数表存储对象的引用计数,弱引用表存储对象的弱引用信息。weak_table_t 是一个存储弱引用信息的哈希表,其元素是 weak_entry_t 类型。weak_entry_t 存储了弱引用该对象的指针的指针,即objc_object new_referrer。当对象被销毁时,weak引用的指针会被自动置为nil,防止野指针的出现。

当两个类互相持有对方的强引用时,会导致循环引用问题,导致内存无法正确释放,这会造成内存不断的增多。这类问题通常发生在闭包与类实例之间。为了打破这种循环引用,可以在闭包中使用捕获列表(capture list)将闭包中的引用声明为弱引用或无主引用。

import SwiftUIclass Element {    let title: String    let description: String?        lazy var convertToWeb: () -> String = { [unowned self] in        if let description = self.description {            return "<div class='line'><h2>\(self.title)</h2><p>\(description)</p></div>"        } else {            return "<div class='line'><h2>\(self.title)</h2></div>"        }    }        init(title: String, description: String? = nil) {        self.title = title        self.description = description    }        deinit {        print("\(title) is being deinitialized")    }}struct ContentView: View {    @State private var elm: Element? = Element(title: "Inception", description: "A mind-bending thriller by Christopher Nolan.")        var body: some View {        VStack {            if let html = elm?.convertToWeb() {                Text(html)                    .padding()                    .background(Color.yellow)                    .cornerRadius(10)            }            Button("Clear") {                elm = nil            }            .padding()            .background(Color.red)            .foregroundColor(.white)            .cornerRadius(10)        }        .padding()    }}

在这个示例中,convertToWeb 是一个闭包,使用了 [unowned self] 捕获列表,以避免闭包与 Element 实例之间的强引用循环。

Swift 通常通过引用计数和内存自动管理来保证内存安全,然而在某些高性能或特定底层操作中,开发者可能需要直接操作内存。这时就需要使用到 Swift 的 Unsafe 系列指针类型,例如 UnsafeMutablePointerUnsafePointerUnsafePointer 是一个指向某种类型的指针,它允许只读访问内存地址上的数据。这意味着你可以读取该地址的数据但不能修改它。相反,UnsafeMutablePointer 允许你修改指针指向的内存区域内的数据。使用 UnsafeMutablePointer 修改内存时,必须确保内存已经正确地分配且不会被其他代码同时访问。否则,可能会导致程序崩溃或出现难以调试的问题。Swift 提供的一些辅助工具 withUnsafePointer(to:_:)withUnsafeMutablePointer(to:_:),它们可以在有限的范围内确保内存操作的安全性。这些函数的使用可以帮助开发者避免一些常见的错误,确保指针的生命周期和作用域受到控制。

OOM

内存泄漏,难以监控。内存泄漏是指程序在运行过程中,由于设计错误或者代码实现不当,导致程序未能释放已经不再使用的内存,从而造成系统内存的浪费,严重的会导致程序崩溃。内存泄漏是一个非常严重的问题,因为它会导致程序运行速度变慢,甚至会导致程序崩溃。因此,我们在开发过程中,一定要注意内存泄漏的问题。

OOM(Out Of Memory)指的是iOS设备上应用因内存占用过高被系统强制终止的现象。iOS通过Jetsam机制管理内存资源,当设备内存紧张时,会终止优先级低或内存占用大的进程。分为FOOM(前台OOM)和BOOM(后台OOM),FOOM对用户体验影响更大。

Jetsam日志

包括pageSize(内存页大小)、states(应用状态)、rpages(占用的内存页数)、reason(终止原因)。通过pageSize和rpages可计算出应用崩溃时占用的内存大小。

在现代操作系统中,内存管理是一项关键任务。随着移动设备和桌面系统的复杂性增加,内存资源的高效使用变得更加重要。iOS和macOS通过引入“内存压力”(Memory Pressure)机制来优化内存管理,取代了传统的基于虚拟内存分页的管理方法。

虚拟内存系统允许操作系统将物理内存(RAM)和磁盘存储结合使用,以便在内存不足时将不常用的数据移至磁盘。分页(paging)是虚拟内存管理中的一种技术,它将内存划分为小块(页面),并根据需要将它们从物理内存交换到磁盘。然而,分页存在性能瓶颈,尤其是在存储访问速度远低于内存的情况下。

随着设备硬件的变化和用户体验要求的提高,苹果公司在iOS和macOS中引入了“内存压力”机制。内存压力是一种动态监测内存使用情况的技术,它能够实时评估系统内存的使用状态,并根据不同的压力级别采取相应的措施。

内存压力机制通过系统级别的反馈来管理内存。系统会监测内存的使用情况,并将压力分为四个级别:无压力(No Pressure)、轻度压力(Moderate Pressure)、重度压力(Critical Pressure)和紧急压力(Jetsam)。

压力级别的定义与响应:

  • 无压力(No Pressure):系统内存充足,没有特别的内存管理措施。
  • 轻度压力(Moderate Pressure):系统内存开始紧张,操作系统会建议应用程序释放缓存或非必要的资源。
  • 重度压力(Critical Pressure):系统内存非常紧张,操作系统可能会暂停后台任务或终止不活跃的应用程序。
  • 紧急压力(Jetsam):这是最严重的内存压力状态,系统可能会直接强制关闭占用大量内存的应用程序,以释放资源确保系统的稳定性。

系统对内存压力的应对措施

为了应对不同的内存压力,iOS和macOS系统采取了多种策略,包括:

  • 缓存管理:系统会首先清除可丢弃的缓存数据,以减轻内存负担。
  • 后台任务管理:在压力增加时,操作系统会优先暂停或终止低优先级的后台任务。
  • 应用程序终止:在紧急情况下,系统会选择性地关闭那些占用大量内存且当前不活跃的应用程序,这一过程被称为“Jetsam”。

使用系统提供的工具(如vm_statmemory_pressure等)监测应用程序的内存使用情况。这些工具可以帮助开发者识别内存泄漏、过度的缓存使用等问题。开发者可以通过这些机制感知内存压力的变化。例如,当系统发出UIApplicationDidReceiveMemoryWarningNotification通知时,应用程序应立即释放不必要的资源。

查看内存使用情况

在 iOS 中,可以使用 mach_task_basic_info 结构体来查看应用的实际内存使用情况。mach_task_basic_info 是一个 task_info 结构体的子集,它提供了关于任务(进程)的基本信息,包括内存使用情况。特别地,你可以通过 phys_footprint 字段来获取应用程序实际占用的物理内存量。

import Foundationfunc getMemoryUsage() -> UInt64? {    var info = mach_task_basic_info()    var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4        let kret = withUnsafeMutablePointer(to: &info) { infoPtr in        infoPtr.withMemoryRebound(to: integer_t.self, capacity: 1) { intPtr in            task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), intPtr, &count)        }    }        if kret == KERN_SUCCESS {        return info.phys_footprint    } else {        print("Failed to get task info with error code \(kret)")        return nil    }}// Usageif let memoryUsage = getMemoryUsage() {    print("Memory usage: \(memoryUsage / 1024 / 1024) MB")}

在这个示例中,mach_task_basic_info 结构体用于存储基本信息,task_info() 函数用来填充这些信息,phys_footprint 字段提供了物理内存占用的实际数据。使用这些底层 API 需要适当的权限,有时可能无法在应用程序的沙盒环境中访问所有内存信息。

在 iOS 中,NSProcessInfophysicalMemory 属性可以用来获取设备的总物理内存大小。这个属性返回一个 NSUInteger 类型的值,表示物理内存的大小(以字节为单位)。这个方法在 iOS 9 及更高版本中可用。

import Foundationfunc getPhysicalMemorySize() -> UInt64 {    let physicalMemory = ProcessInfo.processInfo.physicalMemory    return physicalMemory}// Usagelet memorySize = getPhysicalMemorySize()print("Total physical memory: \(memorySize / 1024 / 1024) MB")

vm_statistics_data_t 是一个与虚拟内存相关的数据结构,它提供了关于虚拟内存的统计信息,包括系统的内存使用情况。虽然它不能直接提供应用程序使用的内存,但它可以提供有关整个系统的虚拟内存状态的信息。使用 vm_statistics_data_t 可以获取有关系统内存的更详细的统计数据。

import Foundationimport MachOfunc getVMStatistics() -> (freeMemory: UInt64, usedMemory: UInt64)? {    var vmStats = vm_statistics_data_t()    var count = mach_msg_type_number_t(MemoryLayout<vm_statistics_data_t>.size) / 4    var hostPort: mach_port_t = mach_host_self()        let result = withUnsafeMutablePointer(to: &vmStats) { vmStatsPtr in        vmStatsPtr.withMemoryRebound(to: integer_t.self, capacity: 1) { intPtr in            // 用于获取主机的统计信息。通过指定 `HOST_VM_INFO`,可以获取虚拟内存相关的数据。            host_statistics(hostPort, HOST_VM_INFO, intPtr, &count)        }    }        if result == KERN_SUCCESS {        let pageSize = vm_kernel_page_size // 系统的页面大小(通常为 4096 字节)。        let freeMemory = UInt64(vmStats.free_count) * UInt64(pageSize)        let usedMemory = (UInt64(vmStats.active_count) + UInt64(vmStats.inactive_count) + UInt64(vmStats.wire_count)) * UInt64(pageSize)        return (freeMemory, usedMemory)    } else {        print("Failed to get VM statistics with error code \(result)")        return nil    }}// Usageif let vmStats = getVMStatistics() {    print("Free memory: \(vmStats.freeMemory / 1024 / 1024) MB")    print("Used memory: \(vmStats.usedMemory / 1024 / 1024) MB")}

vm_statistics_data_t 数据结构包含了有关虚拟内存的统计信息,如 free_count(自由页数)、active_count(活跃页数)、inactive_count(非活跃页数)和 wire_count(被锁定的页数)。

获取可用内存的方法如下:

import Foundationimport MachOfunc getAvailableMemory() -> UInt64? {    var vmStats = vm_statistics_data_t()    var count = mach_msg_type_number_t(MemoryLayout<vm_statistics_data_t>.size) / 4    var hostPort: mach_port_t = mach_host_self()        let result = withUnsafeMutablePointer(to: &vmStats) { vmStatsPtr in        vmStatsPtr.withMemoryRebound(to: integer_t.self, capacity: 1) { intPtr in            host_statistics(hostPort, HOST_VM_INFO, intPtr, &count)        }    }        if result == KERN_SUCCESS {        let pageSize = vm_kernel_page_size        let freeMemory = UInt64(vmStats.free_count) * UInt64(pageSize)        let inactiveMemory = UInt64(vmStats.inactive_count) * UInt64(pageSize)        return freeMemory + inactiveMemory    } else {        print("Failed to get VM statistics with error code \(result)")        return nil    }}// Usageif let availableMemory = getAvailableMemory() {    print("Available memory: \(availableMemory / 1024 / 1024) MB")}

free_count 表示系统中未使用的空闲内存页数。inactive_count 表示系统中未使用但可能会重新使用的内存页数。可用内存可以通过将空闲内存和非活跃内存的页数乘以页面大小来计算得到。

造成内存泄漏的常见原因

内存泄漏指的是程序中已动态分配的堆内存由于某些原因未能释放或无法释放,导致系统内存浪费,程序运行速度变慢甚至系统崩溃。

  • 循环引用:对象A强引用对象B,对象B又强引用对象A,或多个对象互相强引用形成闭环。使用Weak-Strong Dance、断开持有关系(如使用__block关键字、将self作为参数传入block)。
  • Block导致的内存泄漏:Block会对其内部的对象强引用,容易形成循环引用。使用Weak-Strong Dance、断开持有关系(如将self作为参数传入block)。
  • NSTimer导致的内存泄漏:NSTimer的target-action机制容易导致self与timer之间的循环引用。在合适的时机销毁NSTimer、使用GCD的定时器、借助中介者(如NSObject对象或NSProxy子类)断开循环引用、使用iOS 10后提供的block方式创建timer。
  • 委托模式中的内存泄漏:UITableView的delegate和dataSource、NSURLSession的delegate。根据具体场景选择使用weak或strong修饰delegate属性,或在请求结束时手动销毁session对象。
  • 非OC对象的内存管理:CoreFoundation框架下的对象(如CI、CG、CF开头的对象)和C语言中的malloc分配的内存。使用完毕后需手动释放(如CFRelease、free)。

Metrics

Metrics 和 XCTest 中的 memgraph 了解和诊断 Xcode 的内存性能问题。

内存泄漏检测工具原理

内存泄漏指的是程序在运行过程中,分配的内存未能及时释放,导致程序占用的内存持续增加。内存泄漏检测工具的基本原理是监控和管理对象的生命周期,检测那些在生命周期结束后仍未被释放的对象。

FBRetainCycleDetector

FBRetainCycleDetector 是由 Facebook 开源的一个用于检测 iOS 应用中的内存泄漏的工具。内存泄漏通常是由于对象之间的强引用循环导致的,FBRetainCycleDetector 的工作原理就是检测对象图中的强引用循环,进而帮助开发者识别和修复这些泄漏。

FBRetainCycleDetector 的核心思想是通过分析对象之间的引用关系来识别可能的循环引用。它通过以下步骤实现这一点:

  • 对象图构建FBRetainCycleDetector 首先会从一个指定的对象开始,递归地遍历该对象的所有属性和关联对象,构建一个引用图。这个图的节点是对象,边是对象之间的强引用。
  • **深度优先搜索 (DFS)**:在构建完对象图之后,FBRetainCycleDetector 会对图进行深度优先搜索,寻找从起始对象到自身的循环路径。换句话说,它会查找路径起始和终止于同一个对象的闭环。
  • 循环检测:当找到一个循环路径时,FBRetainCycleDetector 就会将其标记为潜在的内存泄漏。检测到的循环会以易于理解的方式输出,帮助开发者定位和解决问题。

为了避免不必要的检测,FBRetainCycleDetector 允许开发者定义一些属性过滤规则,忽略一些不会导致泄漏的引用。例如,可以跳过一些不可见的系统属性或自定义的非持有性引用。工具能够识别并忽略弱引用(weakunowned),因为这些引用不会导致内存泄漏。FBRetainCycleDetector 具有较高的灵活性,开发者可以通过扩展和定制对象图的遍历规则,使其适应不同的应用场景和复杂对象结构。由于对象图的遍历和循环检测可能会带来性能开销,FBRetainCycleDetector 主要用于开发和调试阶段,而不建议在生产环境中长期使用。

通常,FBRetainCycleDetector 会在调试时被使用。开发者可以通过简单的代码调用,检测指定对象是否存在循环引用。例如:

FBRetainCycleDetector *detector = [FBRetainCycleDetector new];[detector addCandidate:someObject];NSSet *retainCycles = [detector findRetainCycles];

通过以上代码,可以查找someObject 是否存在循环引用,并返回检测到的循环路径。

在实际应用中,FBRetainCycleDetector 被广泛用于检测复杂的对象之间的引用关系,特别是在自定义控件、大型视图控制器、网络回调等场景下,容易产生强引用循环的问题。通过早期检测和解决这些循环引用,可以大大提高应用的内存管理效率,减少内存泄漏带来的问题。

MLeaksFinder

MLeaksFinder 是一款由腾讯 WeRead 团队开源的 iOS 内存泄漏检测工具,其原理主要基于对象生命周期的监控和延迟检测机制。

MLeaksFinder 通过为基类 NSObject 添加一个 -willDealloc 方法来监控对象的生命周期。当对象应该被释放时(例如,ViewController 被 pop 或 dismiss 后),该方法被调用。在 -willDealloc 方法中,MLeaksFinder 使用一个弱指针(weak pointer)指向待检测的对象,以避免因为对象已经被释放而导致的野指针访问问题。MLeaksFinder 通过检查视图控制器的生命周期来检测内存泄漏。每个 UIViewController 都有一个 viewDidDisappear 方法,这个方法会在视图控制器从屏幕上消失时被调用。MLeaksFinder 通过在 viewDidDisappear 被调用时,检测该视图控制器是否已经被释放,如果没有被释放则认为存在内存泄漏。对于视图 (UIView),MLeaksFinder 会在视图被从其父视图中移除时(即 removeFromSuperview 调用后)检查视图是否已经被释放。如果视图没有被释放,则认为存在内存泄漏。MLeaksFinder 通过扩展 NSObject 的功能(即为 NSObject 添加一个 Category)来追踪对象的生命周期。当对象的 dealloc 方法没有在预期的时间内被调用时,就可以判断该对象是否泄漏。

-willDealloc 方法中,MLeaksFinder 使用 dispatch_after 函数在 GCD(Grand Central Dispatch)的主队列上设置一个延迟(通常是2到3秒)执行的 block。这个 block 在延迟时间后执行,尝试通过之前设置的弱指针访问对象。如果对象已经被释放(即弱指针为 nil),则认为没有内存泄漏;如果对象仍然存活,则认为存在内存泄漏。MLeaksFinder 通过将对象的检测任务加入到下一个 Runloop 中执行,从而避免在当前线程中直接执行检测操作。这种方式确保了不会影响主线程的性能,同时能在适当的时间进行内存泄漏的检测。

如果在延迟时间后对象仍然存活,MLeaksFinder 会执行相应的检测逻辑,并可能通过断言(assertion)中断应用(具体行为可能根据配置和版本有所不同)。MLeaksFinder 会在应用运行时自动检测内存泄漏,不需要开发者手动触发。检测到内存泄漏后,MLeaksFinder 通常会弹出警告框(alert)或通过日志(log)输出相关信息,帮助开发者定位和解决内存泄漏问题。

MLeaksFinder 使用了方法交换技术替换如dismissViewControllerAnimated:completion:等方法,确保释放时触发检测。调用willDealloc方法,设置延时检查对象是否已释放。若未释放,则进入assertNotDealloc方法,中断言提醒开发者。

当 MLeaksFinder 检测到潜在的内存泄漏时,它还可以打印堆栈信息,帮助开发者找出导致对象无法释放的具体代码路径。通过willReleaseChildwillReleaseChildren方法构建子对象的释放堆栈信息。这通常通过递归遍历子对象,并将父对象和子对象的类名组合成视图堆栈(view stack)来实现。

MLeaksFinder 还可能集成了循环引用检测功能,使用如 Facebook 的 FBRetainCycleDetector 这样的工具来找出由 block 等造成的循环引用问题。MLeaksFinder 提供了一种白名单机制,允许开发者将一些特定的对象排除在泄漏检测之外。这在某些对象确实需要持久存在的场景下非常有用。MLeaksFinder 非常轻量,不会显著影响应用的性能。集成简单,自动化检测,极大地方便了开发者发现内存泄漏问题。在某些复杂的情况下,可能会有误报(即认为对象泄漏了,但实际上没有)。

PLeakSniffer

PLeakSniffer是一个用于检测iOS应用程序中内存泄漏的工具。PLeakSniffer的基本工作原理:通过对控制器和视图对象设置弱引用,并使用单例对象周期性地发送ping通知,如果对象在控制器已释放的情况下仍然响应通知,则可能存在内存泄漏。

PLeakSnifferCitizen协议的设计及其在NSObjectUIViewControllerUINavigationControllerUIView中的实现。每个类都通过实现prepareForSniffer方法来挂钩适当的生命周期方法(如viewDidAppearpushViewController等),在适当的时机调用markAlive方法,将代理对象附加到被监测的对象上,以便后续的ping操作能够检测到对象的存活状态。

代理对象PObjectProxy的功能,它主要负责接收ping通知并检查宿主对象是否应当被释放,如果检测到可能的内存泄漏,就会触发警报或打印日志。通过这种方式,PLeakSniffer能够在运行时检测到iOS应用中可能存在的内存泄漏问题。

其他内存泄漏检测工具

hook malloc方法

要在 iOS 上 hook malloc 方法可以监控内存分配。可以使用函数拦截技术。以下是一个示例,展示如何使用 Fishhook 库来 hook malloc 方法。

将 Fishhook 库添加到你的项目中。你可以通过 CocoaPods 或手动添加 Fishhook 源代码。

#import <Foundation/Foundation.h>#import <malloc/malloc.h>#import "fishhook.h"// 原始 malloc 函数指针static void* (*original_malloc)(size_t size);// 自定义 malloc 函数void* custom_malloc(size_t size) {    void *result = original_malloc(size);    NSLog(@"Allocated %zu bytes at %p", size, result);    return result;}// Hook 函数void hookMalloc() {    // 重新绑定 malloc 函数    rebind_symbols((struct rebinding[1]){{"malloc", custom_malloc, (void *)&original_malloc}}, 1);}int main(int argc, const char * argv[]) {    @autoreleasepool {        // Hook malloc        hookMalloc();                // 测试 malloc 和 free        void *ptr = malloc(1024);        free(ptr);    }    return 0;}

在实际项目中使用时,注意性能开销和日志记录的影响。

malloc logger

malloc_logger 是 iOS 和 macOS 中用于内存分配调试的一个工具。它允许开发者设置一个自定义的日志记录器函数,以便在内存分配和释放操作发生时记录相关信息。通过使用 malloc_logger,开发者可以更容易地检测和诊断内存问题,如内存泄漏、过度分配等。

以下是一个使用 Objective-C 实现的示例,展示如何设置和使用 malloc_logger

#import <Foundation/Foundation.h>#import <malloc/malloc.h>// 定义自定义的 malloc logger 函数void custom_malloc_logger(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t result, uintptr_t num_bytes) {    switch (type) {        case MALLOC_LOG_TYPE_ALLOCATE:            NSLog(@"Allocated %lu bytes at %p", (unsigned long)num_bytes, (void *)result);            break;        case MALLOC_LOG_TYPE_DEALLOCATE:            NSLog(@"Deallocated memory at %p", (void *)arg1);            break;        case MALLOC_LOG_TYPE_HAS_ZONE:            NSLog(@"Memory operation with zone at %p", (void *)arg1);            break;        default:            break;    }}// 设置自定义的 malloc loggervoid setCustomMallocLogger() {    malloc_logger = custom_malloc_logger;}int main(int argc, const char * argv[]) {    @autoreleasepool {        // 设置自定义 malloc logger        setCustomMallocLogger();                // 测试 malloc 和 free        void *ptr = malloc(1024);        free(ptr);    }    return 0;}

在这个示例中,我们定义了一个自定义的 malloc_logger 函数 custom_malloc_logger,并在 setCustomMallocLogger 函数中将其设置为当前的 malloc_logger。然后,在 main 函数中,我们测试了内存的分配和释放操作,并通过日志记录器记录这些操作的信息。

通过这种方式,开发者可以在内存分配和释放时记录相关信息,从而更好地理解和优化应用程序的内存使用情况。

内存快照检测方案

扫描进程中所有Dirty内存,建立内存节点之间的引用关系有向图,用于内存问题的分析定位。

在 iOS 中,可以使用 vm_region_recurse_64 函数来获取所有内存区域的信息。

#include <stdio.h>  #include <stdlib.h>  #include <mach/mach.h>  #include <mach/vm_map.h>    int main(int argc, const char * argv[]) {      mach_port_t task = mach_task_self();      vm_address_t address = VM_MIN_ADDRESS;      vm_size_t size = VM_MAX_ADDRESS - VM_MIN_ADDRESS;      vm_region_basic_info_data_64_t info;      mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT_64;      memory_object_name_t object_name;      mach_port_t object_handle;        kern_return_t kr;        while (size > 0) {          kr = vm_region_recurse_64(task, &address, &size, VM_REGION_BASIC_INFO,                                    (vm_region_info_t)&info, &info_count, &object_name,                                    &object_handle);            if (kr != KERN_SUCCESS)              break;            printf("Address: 0x%llx, Size: 0x%llx, Protection: 0x%x, In Use: %s\n",                 (unsigned long long)info.protection,                 (unsigned long long)info.size,                 (unsigned int)info.protection,                 info.is_submap ? "Yes" : "No");            address += info.size;          size -= info.size;      }        if (kr != KERN_SUCCESS) {          char *err = mach_error_string(kr);          fprintf(stderr, "vm_region_recurse_64 failed: %s\n", err);          free(err);      }        return 0;  }

在iOS中,可以使用libmalloc库提供的malloc_get_all_zones函数来获取所有内存区域(zone)的信息。malloc_get_all_zones可以遍历所有的内存区域,并为每个区域执行一个回调函数,从而获取详细的内存分配信息。

以下是一个简单的代码示例,展示如何使用malloc_get_all_zones来获取并打印内存区域的信息:

#import <malloc/malloc.h>#import <mach/mach.h>// 自定义的回调函数,用于处理每个内存区域的块。该函数用于处理每个zone中的内存块,在这个例子中,它简单地打印出每个内存块的地址和大小。void my_zone_enumerator(task_t task, void *context, unsigned type_mask, vm_range_t *ranges, unsigned range_count) {    for (unsigned i = 0; i < range_count; i++) {        printf("Memory range: 0x%llx, Size: %llu\n", ranges[i].address, ranges[i].size);    }}void print_all_zones() {    // 获取当前任务的mach port。用于获取当前任务的Mach端口,这对于与Mach内核通信是必需的。    task_t task = mach_task_self();    unsigned int count;    // 这是`libmalloc`库中的一个结构体,表示内存区域。通过调用其`introspect`属性下的`enumerator`函数,可以遍历该zone中的所有内存块。    malloc_zone_t **zones = NULL;    // 获取所有的内存区域。这个函数返回当前任务的所有内存区域(zone),这些zone通常对应于不同的分配器或内存池。    kern_return_t kr = malloc_get_all_zones(task, NULL, &zones, &count);    if (kr != KERN_SUCCESS) {        fprintf(stderr, "Error: Unable to get all zones\n");        return;    }    // 遍历所有的zone    for (unsigned int i = 0; i < count; i++) {        malloc_zone_t *zone = zones[i];        if (zone != NULL) {            printf("Zone name: %s\n", zone->zone_name);            // 枚举zone中的内存块            zone->introspect->enumerator(task, NULL, MALLOC_PTR_IN_USE_RANGE_TYPE, (vm_address_t)zone, my_zone_enumerator);        }    }}int main(int argc, const char * argv[]) {    print_all_zones();    return 0;}

使用单独的 malloc_zone 管理采集模块的内存使用,减少非法内存访问。遍历进程内所有VM Region(虚拟内存区域),获取Dirty和Swapped内存页数。重点关注libmalloc管理的堆内存,获取存活内存节点的指针和大小。

为内存节点赋予详细的类型名称,如Objective-C/Swift/C++实例类名等。通过运行时信息和mach-o、C++ ABI文档获取C++对象的类型信息。遍历内存节点,搜索并确认节点间的引用关系。对栈内存和Objective-C/Swift堆内存进行特殊处理,获取更详细的引用信息。

后台线程定时检测内存占用,超过设定的危险阈值后触发内存分析。内存分析过程中,对内存节点进行引用关系分析,生成内存节点之间的引用关系有向图。通过图算法,找到内存泄漏的根原因。

libmalloc 内存日志分析

通过代码控制内存日志开关,可以在内存泄漏发生时,输出内存日志。内存日志包括内存分配、释放、引用计数变化等信息,用于分析内存泄漏的原因。

在 iOS 开发中,libmalloc 提供了 turn_on_stack_loggingturn_off_stack_logging 方法,用于启用和禁用堆栈日志记录。这些方法可以帮助开发者在调试和分析内存问题时记录内存分配的堆栈信息。以下是一个使用这些方法的代码示例:

#import <Foundation/Foundation.h>#import <malloc/malloc.h>#import <mach/mach.h>#import <mach/mach_init.h>#import <mach/mach_vm.h>// 启用堆栈日志记录void enableStackLogging() {    turn_on_stack_logging(1);    NSLog(@"Stack logging turned on");}// 禁用堆栈日志记录void disableStackLogging() {    turn_off_stack_logging();    NSLog(@"Stack logging turned off");}// 获取堆栈日志记录void getStackLoggingRecords() {    // 获取当前任务    task_t task = mach_task_self();        // 获取所有堆栈日志记录    mach_vm_address_t *records;    uint32_t count;    kern_return_t kr = __mach_stack_logging_enumerate_records(task, &records, &count);        if (kr != KERN_SUCCESS) {        NSLog(@"Failed to enumerate stack logging records: %s", mach_error_string(kr));        return;    }        for (uint32_t i = 0; i < count; i++) {        mach_vm_address_t record = records[i];        NSLog(@"Record %u: %p", i, (void *)record);                // 定义堆栈帧数组        uint64_t frames[128];        // 获取堆栈帧信息        uint32_t frameCount = __mach_stack_logging_frames_for_uniqued_stack(task, record, frames, 128);                // 遍历堆栈帧,每次循环中,获取当前堆栈帧地址并打印地址信息        for (uint32_t j = 0; j < frameCount; j++) {            NSLog(@"Frame %u: %p", j, (void *)frames[j]);        }    }        // 释放记录数组    vm_deallocate(task, (vm_address_t)records, count * sizeof(mach_vm_address_t));}// 示例函数,分配一些内存void allocateMemory() {    void *ptr1 = malloc(1024);    void *ptr2 = malloc(2048);    free(ptr1);    free(ptr2);}// 主函数int main(int argc, const char * argv[]) {    @autoreleasepool {        // 启用堆栈日志记录        enableStackLogging();                // 分配内存        allocateMemory();                // 获取堆栈日志记录        getStackLoggingRecords();                // 禁用堆栈日志记录        disableStackLogging();    }    return 0;}

在这个示例中,我们首先调用 turn_on_stack_logging 方法来启用堆栈日志记录,然后进行一些内存分配和释放操作。接着,我们调用 __mach_stack_logging_enumerate_records 方法获取所有堆栈日志记录,并使用 __mach_stack_logging_frames_for_uniqued_stack 方法解析每个日志记录以获取堆栈帧信息。最后,我们调用 turn_off_stack_logging 方法来禁用堆栈日志记录。

通过这种方式,开发者可以在需要时启用和禁用堆栈日志记录,并解析这些日志记录以获取详细的堆栈信息。需要注意的是,这些函数在实际项目中使用时,需要确保在合适的时机启用和禁用堆栈日志记录,以避免性能开销和不必要的日志记录。

IO 性能

文件写操作常见但易出错。常见问题包括数据不一致、数据丢失、性能波动等。

读写的 API

文件读写系统调用的 API 有 read()write()read()从文件读取数据到应用内存。write()将数据从应用内存写入文件到内核缓存,但不保证立即写入磁盘。mmap()将文件映射到应用内存,直接访问,但写操作同样先进入内核缓存。fsync()fcntl(F_FULLSYNC) 会强制将文件写入磁盘。c标准库提供的文件读写 API 是 fwrite(buffer, sizeof(char), size, file_pointer)fflush(file_pointer)

iOS 提供了 NSFileManagerreplaceItemAtURL:withItemAtURL:backupItemName:options:resultingItemURL:error: 方法,可以实现原子性操作。

flockfcntl 使用文件锁防止多个进程或线程同时写入同一个文件,避免产生竞争条件,保证数据一致性。

iOS 提供了 NSFileManagerNSData 的封装方法,通常比直接使用 POSIX API 更安全和高效。

测试文件I/O性能时,应通过 fcntl(fd, F_NOCACHE, 1) 禁用统一缓冲缓存(UBC),以避免缓存影响测试结果。

文件缓存

文件缓存可以帮助优化应用性能、减少网络请求和延长电池续航。

iOS 提供了多个文件存储目录,选择合适的目录有助于管理缓存文件的生命周期。包括Caches 目录和tmp 目录。Caches 目录适合存储缓存文件。系统可能会在磁盘空间紧张时清除这个目录下的文件,因此不应存储重要数据。可以通过 NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) 获取路径。tmp 目录适用于临时文件。系统重启或应用未运行时,可能会清除这个目录下的文件。可以通过 NSTemporaryDirectory() 获取路径。

根据数据的重要性和更新频率,制定缓存策略。为缓存数据设置时间戳或过期时间。每次读取缓存时检查数据是否过期,及时更新。实现 LRU 算法,定期清理最久未使用的缓存文件。

为缓存文件生成唯一标识符(如使用哈希值),避免文件名冲突。可以将 URL 的 MD5 或 SHA1 哈希值作为缓存文件名。将缓存文件按类别或特定属性进行分类存储,方便管理。例如,将图片和JSON数据分别存储在不同的子目录中。

对于大型缓存数据,可以在写入文件时使用 GZIP 等压缩技术,减少存储空间占用。iOS 的 NSDataNSFileManager 支持数据的压缩和解压缩。避免在主线程上执行缓存读写操作,使用 Swift Concurrency 将缓存操作移到后台,保持 UI 的流畅性。减少频繁的写入操作,可以将多次写入合并为一次批量操作。

对于敏感数据(如用户信息),应在缓存时进行加密处理。iOS 提供了 Keychain 进行安全存储,也可以使用 CommonCrypto 框架进行自定义加密。

定期清理过期或不再使用的缓存文件,避免占用过多磁盘空间。可以使用 iOS 的 NSURLCache 设置缓存大小限制,自动管理缓存清理。提供手动清理缓存的选项,允许用户在应用内清理缓存数据。根据数据更新频率设置缓存失效时间,确保用户获得最新数据。可以通过 ETag 或 Last-Modified HTTP 头实现增量更新,避免每次都下载完整数据。尽量利用 iOS 自带的缓存机制,例如 NSURLCache,它自动管理 HTTP 请求的缓存,支持内存和磁盘缓存。对于图片缓存,使用 NSCache 或者第三方库,可以在内存和磁盘之间自动管理图片的缓存。

mmap

mmap 是一种内存映射文件的机制,允许用户态的程序像操作内存一样直接操作磁盘文件。通过 mmap,文件的内容被映射到进程的地址空间中,程序可以直接读写这段地址空间,操作系统会在背后处理实际的磁盘读写操作。标准IO(如read/write)涉及系统调用和内存拷贝开销,数据需要在内核态和用户态之间来回拷贝。mmap 避免了这些开销,因为它直接在用户态的内存中操作,操作系统只在需要时(如缺页中断)介入处理磁盘读写。

对于超过物理内存大小的大文件,mmap 可以利用虚拟内存的特性,在有限的物理内存中处理大文件。多个进程可以映射同一个文件到各自的地址空间,实现内存共享,这在动态链接库等场景中非常有用。在某些场景下,mmap 可以提供更好的性能,因为它减少了系统调用和内存拷贝的次数。但具体性能取决于应用场景和操作系统实现。在处理大文件时,mmap 可以避免频繁的内存拷贝和磁盘I/O操作。多个进程可以共享同一个动态链接库,节省内存和磁盘空间。可用于实现高效的内存文件交换,如数据库中的内存映射文件。

mmap 也有些问题需要注意。当访问的页面不在物理内存中时,会发生缺页中断,这会有一定的性能开销。为了维护地址空间与文件的映射关系,内核需要额外的数据结构,这也会带来一定的性能开销。

我们使用 mmap 将文件映射到内存中,并读取文件内容。示例如下:

#import <Foundation/Foundation.h>#import <sys/mman.h>#import <fcntl.h>#import <unistd.h>void mmapExample() {    // 文件路径    NSString *filePath = @"/path/to/your/file.txt";        // 打开文件    int fd = open([filePath UTF8String], O_RDONLY);    if (fd == -1) {        NSLog(@"Failed to open file");        return;    }        // 获取文件大小    off_t fileSize = lseek(fd, 0, SEEK_END);    if (fileSize == -1) {        NSLog(@"Failed to get file size");        close(fd);        return;    }        // 将文件映射到内存    void *mappedFile = mmap(NULL, fileSize, PROT_READ, MAP_PRIVATE, fd, 0);    if (mappedFile == MAP_FAILED) {        NSLog(@"Failed to map file");        close(fd);        return;    }        // 关闭文件描述符    close(fd);        // 读取文件内容    NSData *fileData = [NSData dataWithBytes:mappedFile length:fileSize];    NSString *fileContent = [[NSString alloc] initWithData:fileData encoding:NSUTF8StringEncoding];    NSLog(@"File content: %@", fileContent);        // 解除文件映射    if (munmap(mappedFile, fileSize) == -1) {        NSLog(@"Failed to unmap file");    }}int main(int argc, const char * argv[]) {    @autoreleasepool {        mmapExample();    }    return 0;}

MMKV 是腾讯开源的一个高性能通用键值对存储库,基于 mmap 内存映射机制,它提供了简单易用的接口,支持高效的读写操作,并且支持数据加密。

以下是一个在 iOS 项目中使用 MMKV 的示例代码:

import UIKitimport MMKV@UIApplicationMainclass AppDelegate: UIResponder, UIApplicationDelegate {    var window: UIWindow?    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {        // 初始化 MMKV        MMKV.initialize(rootDir: MMKV.defaultMMKVPath)        return true    }}

使用 MMKV 存储和读取数据

import MMKVfunc mmkvExample() {    // 获取默认的 MMKV 实例    let mmkv = MMKV.default()    // 存储数据    mmkv?.set("Inception", forKey: "movieTitle")    mmkv?.set(8.8, forKey: "movieRating")    // 读取数据    if let movieTitle = mmkv?.string(forKey: "movieTitle") {        print("Movie Title: \(movieTitle)")    }    let movieRating = mmkv?.double(forKey: "movieRating")    print("Movie Rating: \(movieRating ?? 0.0)")}mmkvExample()

NSData 提供了三个与 mmap 相关的读取选项,它们分别是:

  • NSDataReadingUncached:这个选项表示不要缓存数据,如果文件只需要读取一次,使用这个选项可以提高性能。这个选项与 mmap 没有直接关系,因为它不涉及内存映射。
  • NSDataReadingMappedIfSafe:这个选项表示在保证安全的前提下,如果条件允许,则使用 mmap 进行内存映射。这意味着如果文件位于固定磁盘(非可移动磁盘或网络磁盘),则可能会使用 mmap 来优化读取性能。
  • NSDataReadingMappedAlways:这个选项表示总是使用 mmap 进行内存映射,不考虑文件的具体存储位置。但是,在 iOS 上,由于所有应用都运行在沙盒中,对 iOS 而言,NSDataReadingMappedIfSafeNSDataReadingMappedAlways 通常是等价的,因为 iOS 设备上的文件存储通常都是在固定磁盘上。

当你需要读取一个较大的文件,但又不想一次性将整个文件加载到内存中时,可以使用 NSDatadataWithContentsOfFile:options:error: 方法,并传入上述与 mmap 相关的选项之一。以下是一个示例代码,展示了如何使用 NSDataReadingMappedIfSafe 选项来读取文件:

NSError *error = nil;NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];if (data == nil) {    // 处理错误    NSLog(@"Error reading file: %@", error.localizedDescription);} else {    // 成功读取文件,可以处理 data}

在这个例子中,filePath 是你想要读取的文件的路径。通过使用 NSDataReadingMappedIfSafe,系统会在可能的情况下使用 mmap 来映射文件,这样就不需要在内存中为整个文件分配空间,从而减少了内存的使用。然而,需要注意的是,虽然 mmap 减少了物理内存的使用,但它仍然需要消耗虚拟内存地址空间。

在用 mmap 时要注意如果使用 mmap 映射了文件,那么在 NSData 的生命周期内,你不能删除或修改对应的文件,因为这可能会导致内存映射失效,进而引发不可预见的错误。mmap 适用于那些需要频繁读取、但不需要同时读取整个文件内容的场景,如视频加载、大日志文件读取等。mmap 映射的区域大小会占用相应大小的虚拟内存地址空间,因此对于非常大的文件,可能不适合将整个文件映射到内存中。

CPU

CPU 的高占用,会让手机耗电变快。

[NSProcessInfo processInfo].activeProcessorCount 可以获取 CPU 核数。获取 CPU 类型的方法有 sysctl、uname、hw.machine 和 NXArchInfo 几种方法。

怎么获取 CPU 使用率呢?

在 iOS 的 Mach 层中,thread_basic_info 结构体用于提供有关线程的一些基本信息,其中就有线程CPU使用率。这个结构体定义在 <mach/thread_info.h> 头文件中,其包含的字段提供了关于线程运行状态、执行时间和其他统计信息的基本数据。以下是 thread_basic_info 结构体的详细定义及其各字段的解释:

struct thread_basic_info {    time_value_t    user_time;       // 用户模式下线程运行的总时间    time_value_t    system_time;     // 内核模式下线程运行的总时间    integer_t       cpu_usage;       // CPU 使用率,以百分之一为单位    policy_t        policy;          // 调度策略(例如FIFO、Round Robin等)    integer_t       run_state;       // 线程的运行状态    integer_t       flags;           // 线程的标志位(例如是否正在被调度)    integer_t       suspend_count;   // 线程被挂起的次数    integer_t       sleep_time;      // 线程的睡眠时间};

字段解释

  • user_time: 该字段表示线程在用户模式下(即执行用户空间的代码)运行的总时间。time_value_t 是一个结构体,通常表示为秒和微秒。
  • system_time: 该字段表示线程在系统模式下(即执行内核空间的代码)运行的总时间。
  • cpu_usage: 该字段表示线程的 CPU 使用率,以百分之一为单位。例如,如果值为 100,表示线程使用了 1% 的 CPU 时间。
  • policy: 该字段表示线程的调度策略,如固定优先级调度(FIFO)或轮转调度(Round Robin)等。
  • run_state: 该字段表示线程当前的运行状态。可能的值包括:
    • TH_STATE_RUNNING: 正在运行
    • TH_STATE_STOPPED: 已停止
    • TH_STATE_WAITING: 正在等待资源
    • TH_STATE_UNINTERRUPTIBLE: 不可中断的等待
    • TH_STATE_HALTED: 已终止
  • flags: 该字段包含一些线程的标志位,用来表示线程的某些状态特性。例如,线程是否正在被调度等。
  • suspend_count: 该字段表示线程当前被挂起的次数。挂起次数大于 0 时,线程不会被调度执行。
  • sleep_time: 该字段表示线程处于睡眠状态的时间。

这些信息对于性能分析、调试以及获取系统中线程的运行状况非常有用。通过使用 thread_info 函数,可以获取到某个特定线程的 thread_basic_info 结构体实例。

要获取当前应用的 CPU 占用率,可以通过遍历当前应用的所有线程,利用 thread_info 函数获取每个线程的 CPU 使用情况。然后,将所有线程的 CPU 使用率汇总,就能得到整个应用的 CPU 占用率。

下面是一个使用 Objective-C 编写的示例代码,展示了如何获取当前应用的 CPU 占用率:

#import <mach/mach.h>#import <assert.h>float cpu_usage() {    kern_return_t kr;    thread_array_t thread_list;    mach_msg_type_number_t thread_count;    thread_info_data_t thread_info_data;    mach_msg_type_number_t thread_info_count;        // 获取当前任务    task_t task = mach_task_self();        // task_threads 这个函数用于获取当前任务的所有线程。`thread_list` 包含了所有线程的 ID,`thread_count` 是线程的数量。    kr = task_threads(task, &thread_list, &thread_count);    if (kr != KERN_SUCCESS) {        return -1;    }        float total_cpu = 0;        // 遍历所有线程    for (int i = 0; i < thread_count; i++) {        thread_info_count = THREAD_INFO_MAX;                // 通过 thread_info 获取每个线程的 `thread_basic_info`,其中包含了线程的 CPU 使用信息。        kr = thread_info(thread_list[i], THREAD_BASIC_INFO, (thread_info_t)thread_info_data, &thread_info_count);        if (kr != KERN_SUCCESS) {            return -1;        }                thread_basic_info_t thread_info = (thread_basic_info_t)thread_info_data;                if (!(thread_info->flags & TH_FLAGS_IDLE)) {            // 通过 `thread_basic_info` 结构体中的 `cpu_usage` 字段获取每个线程的 CPU 使用率,并将它们相加以得到整个应用的 CPU 使用率。            total_cpu += thread_info->cpu_usage / (float)TH_USAGE_SCALE * 100.0;        }    }        // 用于释放之前分配的线程列表内存。    kr = vm_deallocate(task, (vm_address_t)thread_list, thread_count * sizeof(thread_t));    assert(kr == KERN_SUCCESS);        return total_cpu;}

CPU 占用率是一个瞬时值,通常会波动,因此在实际应用中,可能需要多次采样并取平均值来得到更稳定的结果。这个方法会占用一定的 CPU 资源,尤其是在应用包含大量线程时,所以建议在非主线程或低优先级任务中执行这类操作。

对于总 CPU 占用率,使用 host_statistics 函数获取 host_cpu_load_info 结构体中的 cpu_ticks 值来计算总的 CPU 占用率。cpu_ticks 是一个数组,包含了 CPU 在各种状态(如用户模式、系统模式、空闲、Nice 等)下运行的时钟脉冲数量。通过计算这些脉冲数量的变化,可以得出总的 CPU 占用率。

以下是一个完整的示例代码,展示了如何使用 host_statistics 函数来计算总的 CPU 占用率:

#import <mach/mach.h>#import <stdio.h>float cpu_usage() {    // 获取 host 的 CPU load 信息    host_cpu_load_info_data_t cpuInfo;    mach_msg_type_number_t count = HOST_CPU_LOAD_INFO_COUNT;    // `host_statistics` 这是一个用于获取主机统计信息的函数。通过传递 `HOST_CPU_LOAD_INFO` 作为参数,可以获取 `host_cpu_load_info_data_t` 结构体,该结构体包含了 CPU 在不同状态下的时钟脉冲数。    kern_return_t kr = host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, (host_info_t)&cpuInfo, &count);        if (kr != KERN_SUCCESS) {        return -1;    }    // 获取各个状态下的 CPU 时钟脉冲数。通过将 `cpu_ticks` 数组中的所有值相加,得到 CPU 所有状态下运行的总时钟脉冲数。    unsigned long long totalTicks = 0;    for (int i = 0; i < CPU_STATE_MAX; i++) {        totalTicks += cpuInfo.cpu_ticks[i];    }    // 计算 CPU 占用率    unsigned long long idleTicks = cpuInfo.cpu_ticks[CPU_STATE_IDLE]; // `cpu_ticks[CPU_STATE_IDLE]` 表示 CPU 在空闲状态下的时钟脉冲数。    float cpuUsage = (1.0 - ((float)idleTicks / (float)totalTicks)) * 100.0;    return cpuUsage;}

这种方法计算的是整个系统的 CPU 占用率,而不是某个具体应用的 CPU 占用率。如果需要获取具体应用的 CPU 使用情况,应该使用 thread_info 等方法。

启动优化

移动应用的启动时间是影响用户体验的重要方面。

启动时间

识别启动阶段各个步骤的耗时情况。

启动分为以下三种:

  • Cold Launch:应用完全从零开始加载,最耗时。
  • Warm Launch:应用仍在内存中,但由于系统资源紧张,部分内容可能被清理,需要重新加载。
  • Hot Launch:应用仍在后台,只需快速恢复。

治理主要是针对 Cold Landch。

示例:

import UIKitclass AppDelegate: UIResponder, UIApplicationDelegate {    var window: UIWindow?    var launchTime: CFAbsoluteTime?    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {        // 记录应用启动的时间        launchTime = CFAbsoluteTimeGetCurrent()                // 在主线程完成所有启动任务后,计算应用启动时间        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {            if let launchTime = self.launchTime {                let launchDuration = CFAbsoluteTimeGetCurrent() - launchTime                print("App launch time: \(launchDuration) seconds")            }        }                return true    }}

另外也可获取完整加载使用时间。使用 DispatchQueue.main.asyncAfter 延迟执行,以确保所有启动任务(如 UI 渲染、网络请求等)已经完成。然后再使用 CFAbsoluteTimeGetCurrent() 获取当前时间,与记录的启动时间相减,得到启动耗时。

使用 mach_absolute_time() 来计算时间:

static uint64_t startTime;static uint64_t endTime = -1;static mach_timebase_info_data_t timebaseInfo;static inline NSTimeInterval MachTimeToSeconds(uint64_t machTime) {    return ((machTime / 1e9) * timebaseInfo.numer) / timebaseInfo.denom;}@implementation DurationTracker+ (void)load {    startTime = mach_absolute_time();    mach_timebase_info(&timebaseInfo);        @autoreleasepool {        __block id<NSObject> observer;        observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification                                                                object:nil queue:nil                                                            usingBlock:^(NSNotification *note) {            dispatch_async(dispatch_get_main_queue(), ^{                endTime = mach_absolute_time();                NSLog(@"StartupMeasurer: it took %f seconds until the app could respond to user interaction.", MachTimeToSeconds(endTime - startTime));            });            [[NSNotificationCenter defaultCenter] removeObserver:observer];        }];    }}

启动治理思路

减少初始加载的工作量主要有延迟初始化、按需加载数据和优化依赖注入。减少不必要的资源加载的方式有移除未使用的资源和使用延迟加载。减少动态库的数量,避免在启动时过度使用复杂的泛型或协议扩展,因为这些特性可能会增加编译器在运行时的解析开销。使用 Swift Concurrency 将耗时操作异步化,以并行处理更多任务,减少主线程的压力。减少初始界面上的复杂视图层次结构,优先加载并显示关键内容,延迟非关键内容的加载。在启动时尽量减少复杂的动画过渡,以提升首屏的渲染速度。

打法上:

  • 删:出最小集,减任务
  • 延:按需,延到首页后
  • 并:统一管理,编排,充分利用多核
  • 快:减 I/O,少并发,少计算(缓存)

经验:

  • 动态库转静态库
  • 不链用不到的系统库
  • 懒加载动态库,动态取类,dlopen 动态库
  • +load 里任务挪地
  • 减少视图数,少层级,懒加载
  • 主线程等待的子线程设高优先级
  • 子线程预加载
  • 文件大拆小,碎合并
  • 统计高频调用方法
  • 警惕隐藏的全局锁

包体积

影响和手段

包体积优化的必要性:

  • 下载转化率下降:每增加6M,应用下载转化率下降1%。
  • App Store限制:超过200MB的包,iOS 13以下用户无法通过蜂窝数据下载,iOS 13及以上用户需手动设置。
  • 磁盘占用:大包体积占用更多存储空间,影响低存储用户。
  • 用户下载意愿:大包体积减少用户下载意愿,尤其在蜂窝数据低数据模式下。
  • 性能影响:包体积大增加启动时间和SIGKILL风险,降低基础体验。

技术方案主要是以下几种:

  • 资源优化:优化大块资源、无用配置文件和重复资源。
  • 工程架构优化:建立体积检测流水线,控制体积增长。
  • 图片优化:无用图片优化、Asset Catalog优化、HEIC和WebP压缩优化、TinyPng压缩。
  • 编译器优化:使用LLVM编译选项,进行OC、C++、Swift等语言的编译优化。
  • 代码优化:无用类、方法、模块瘦身,精简重复代码,AB实验固化。

效果上讲,工程方向优化大于资源优化,资源优化大于代码优化。

系统提供的方式有

  • App Thinning:利用Apple提供的App Thinning功能,根据用户的设备自动下载适合该设备的资源包,有助于减少初装包的大小。
  • 按需下载资源:使用On-Demand Resources来按需下载资源,只下载用户实际需要的部分,从而减小初始安装包的大小。

包分析

iOS端安装包组成部分有:

  • Mach-O文件:iOS系统上的可执行文件。
  • Watch APP:带有小组件功能的WatchApp。
  • 自定义动态库:动态库推迟到运行时加载,节省代码段空间。
  • Swift系统库:高版本iOS系统自带,低版本需iPA包中自带。
  • Assets资源:Assets.car文件,包含图片资源。
  • 根目录下图片资源:直接添加进工程的图片文件。
  • bundle资源:管理图片和其他配置文件。
  • 其他配置文件:如plist、js、css、json等。

Mach-O是Mach Object文件格式的缩写,用于记录Mac及iOS系统上的可执行文件、目标代码、动态库和内存转储。使用MachOView和otool命令查看Mach-O文件信息,以及通过file和lipo命令查看文件格式和架构。Mach-O文件有Header、LoadCommands和Data部分,特别是LoadCommands中的关键cmd类型如LC_SEGMENT_64,及其段(__PAGEZERO、__TEXT、__DATA、__LINKEDIT)

APPAnalyze 是一款用于分析iOS ipa包的脚本工具,能够自动扫描并发现可修复的包体积问题,同时生成包体积数据用于查看。

资源优化

资源优化方案有图片压缩、资源清理、动态加载资源、使用 Assets.xcassets 等。

Asset Catalog是Xcode提供的资源管理工具,用于集中管理项目中的图片等资源。通过Xcode自带工具actool生成Assets.car文件,可使用assetutil工具分析文件内容。开发者在图片放入Asset Catalog前不要做无损压缩,因为actool会重新进行压缩处理。

Asset Catalog 的优点有:

  • 包体积瘦身:根据不同设备下载匹配的图片资源,减少下载包大小。
  • 统一的图片无损压缩:采用Apple Deep Pixel Image Compression技术,提高压缩比。
  • 便利的资源管理:将图片资源统一压缩成Assets.car文件,便于管理。
  • 高效的I/O操作:图片加载耗时减少两个数量级,提升应用性能。

代码优化

方案有:

  • 移除未使用的代码:查找并删除未使用的类、方法、变量等。审查业务逻辑,删除不再使用或已被废弃的代码模块。
  • 重构代码:对重复的代码进行重构,使用函数、类等方法来减少代码冗余。优化数据结构,减少内存占用和CPU消耗。
  • 编译策略调整:修改编译策略,如启用LTO(链接时优化)来优化跨模块调用代码。剥离符号表(Strip Linked Product),删除未引用的C/C++/Swift代码。精简编译产物,只保留必要的符号和导出信息。
  • 代码组件化:将常用代码文件打包成静态库,切断不同业务代码之间的依赖,减少每次编译的代码量。
  • 减少文件引用:能使用@class就使用@class,尽量减少文件之间的直接引用关系。
  • 减少Storyboard和XIB文件的使用:尽量使用代码布局,减少Storyboard和XIB文件的使用,这些文件在编译时会增加包体积。
  • 清理未使用的资源:清理项目中未使用的图片、音频等资源文件,以及未使用的类和合并重复功能的类。
  • 模块化设计:将App拆分成多个模块,每个模块独立编译和打包,可以根据需要动态加载或更新模块,减少主包的体积。
  • 依赖管理:合理使用CocoaPods、Carthage等依赖管理工具,管理项目的第三方库依赖,避免不必要的库被包含进最终的包中。

Periphery 是一个用于识别 Swift 项目中未使用代码的工具。Periphery 能够清除的无用代码种类有未使用的函数和方法,变量和常量,类或结构体,协议,枚举,全局和静态变量,导入语句和扩展。

需要注意的是,Periphery 可能会因为项目的特殊配置或动态特性(如反射、运行时类型检查等)而错过一些实际上在使用中的代码。

Periphery 不能自动清除或处理的代码有被间接引用的代码,未来可能使用的代码,跨项目共享的代码,特定构建配置下的使用,编译器特性或优化相关的代码。

Periphery 主要使用静态代码分析技术来识别 Swift 项目中未使用的代码。这种技术允许它在不实际运行代码的情况下,通过扫描代码库来查找潜在的问题,如未使用的变量、废弃的函数等。

Periphery 首先使用 xcodebuild 构建指定的 Xcode 工作区或项目,并通过 --schemes--targets 选项指定要构建的方案和目标。它索引这些目标中所有文件的声明和引用,生成一个包含这些信息的图形。在图形构建完成后,Periphery 对其执行大量的变异操作,并通过分析这些变异来识别未使用的声明。这些声明可能包括类、结构体、协议、函数、属性、构造函数、枚举、类型别名或关联类型等。Periphery 能够执行更高级的分析,例如识别协议函数中未使用的参数,但这需要在所有实现中也未使用时才会报告。类似地,重写函数的参数也只有在基函数和所有重写函数中也未使用时才会被报告为未使用。允许用户通过 YAML 配置文件来自定义排除规则,以避免误报。用户可以根据项目的需求,设置特定的排除路径或模式。可以与各种 CI/CD 工具集成,如 GitHub Actions、Jenkins 和 GitLab CI/CD,实现持续集成中的静态代码分析。通过自动运行代码扫描,Periphery 可以帮助团队在每次提交或拉取请求时发现和解决潜在的问题。Periphery 提供了两种扫描命令:scanscan-syntaxscan-syntax 命令只执行语法分析,因此速度更快,但可能无法提供与 scan 命令相同水平的准确性。用户可以根据项目的具体需求选择合适的命令。

Swift 代码静态分析的开源项目还有 SwiftLint 和 SourceKitten。

接下来具体说下运行时无用类检测方案。

静态检测,通过分析Mach-O文件中的__DATA __objc_classlist__DATA __objc_classrefs段,获取未使用的类信息。但存在无法检测反射调用类及方法的缺点。

动态检测的方法。在Objective-C(OC)中,每个类结构体内部都含有一个名为isa的指针,这个指针非常关键,因为它指向了该类对应的元类(meta-class)。元类本身也是一个类,用于存储类方法的实现等信息。

通过对元类(meta-class)的结构体进行深入分析,我们可以找到class_rw_t这样一个结构体,它是元类内部结构的一部分。在class_rw_t中,存在一个flag标志位,这个标志位用于记录类的各种状态信息。

通过检查这个flag标志位,我们可以进行一系列的计算或判断,从而得知当前类在运行时(runtime)环境中是否已经被初始化过。这种机制是Objective-C运行时系统的一个重要特性,它允许开发者在运行时动态地获取类的信息,包括类的初始化状态等。

也就是通过isa指针找到元类,再分析元类中的class_rw_t结构体中的flag标志位,我们可以得知OC中某个类是否已被初始化。

// class is initialized#define RW_INITIALIZED        (1<<29)struct objc_class : objc_object {    bool isInitialized() {    return getMeta()->data()->flags & RW_INITIALIZED;    }};

在Objective-C的运行时(runtime)机制中,类的内部结构和状态通常是由Objective-C运行时库管理的,而不是直接暴露给开发者在应用程序代码中调用的。不过,你可以通过Objective-C的runtime API来间接地获取这些信息。

关于类是否已被初始化的问题,通常不是直接通过objc_class结构体中的某个函数来判断的,因为objc_class结构体(及其元类)的细节和具体实现是私有的,并且不推荐开发者直接操作。然而,Objective-C运行时确实提供了一些工具和API来检查类的状态和行为。

为了检查一个类是否在当前应用程序的生命周期中被使用过(即“被初始化过”),开发者可能会采用一些间接的方法,而不是直接操作类结构体的内部函数。以下是一个简化的说明:

由于不能直接访问类的内部结构,开发者可能会通过其他方式来跟踪类的使用情况。例如,可以在类的初始化方法中设置一个静态标志位或计数器,以记录类是否已被初始化或实例化的次数。虽然不能直接调用objc_class结构体中的函数,但开发者可以使用Objective-C的runtime API(如objc_getClassclass_getInstanceSize等)来获取类的元信息和执行其他操作。然而,对于直接检查类是否“被初始化过”的需求,这些API可能并不直接提供所需的功能。在实际应用中,可能并不需要直接检查类是否“被初始化过”,而是可以通过检查该类的实例是否存在、类的某个特定方法是否被调用过等间接方式来判断。自定义与系统类相同的结构体并实现isInitialized()函数可能是一种模拟或抽象的方式。然而,在实际Objective-C开发中,这样的做法是不必要的,因为直接操作类的内部结构是违反封装原则且容易出错的。相反,开发者应该利用Objective-C提供的runtime API和其他设计模式来达成目标。提到通过赋值转换获取meta-class中的数据,这通常指的是利用Objective-C的runtime机制来查询类的元类信息。然而,直接“判断指定类是否在当前生命周期中是否被初始化过”并不是通过简单地查询元类数据就能实现的,因为这需要跟踪类的实例化过程,而不是仅仅查看元类的结构。

获取类结构体里面的数据

struct mock_objc_class : lazyFake_objc_object {    mock_objc_class* metaClass() {        #if __LP64__            return (mock_objc_class *)((long long)isa & ISA_MASK);        #else            return (mock_objc_class *)((long long)isa);        #endif    }    bool isInitialized() {        return metaClass()->data()->flags & RW_INITIALIZED;    }};

所有 OC 自定义类

Dl_info info;dladdr(&_mh_execute_header, &info);classes = objc_copyClassNamesForImage(info.dli_fname, &classCount);

是否初始化

struct mock_objc_class *objectClass = (__bridge struct mock_objc_class *)cls;BOOL isInitial = objectClass->isInitialized();

最后通过无用类占比指标(无用类数量/总类数量*100%)快速识别不再被使用的模块。对于无用类占比高的模块,进行下线或迁移处理,减少组件数量。

更细粒度无用方法检测方案有:

编译器优化

Xcode 14的编译器可能通过更智能的分析,识别并消除不必要的Retain和Release调用。这些调用在内存管理中是必要的,但在某些情况下,它们可能是多余的,因为对象的生命周期管理可以通过其他方式更有效地实现。在Objective-C的运行时层面,Xcode 14可能引入了更高效的内存管理策略。这些策略可能包括更快的对象引用计数更新、更智能的对象生命周期预测等,从而减少了Retain和Release操作的执行次数和开销。剥离了未使用的代码和库,包括那些与Retain和Release操作相关的部分。这种优化可以减少最终生成的二进制文件的大小。

一些配置对包体积的优化:

  • Generate Debug Symbols:在Levels选项内,将Generate Debug Symbols设置为NO,这可以减小安装包体积,但需要注意,这样设置后无法在断点处停下。
  • 舍弃老旧架构:舍弃不再支持的架构,如armv7,以减小安装包体积。
  • 编译优化选项:在Build Settings中,将Optimization Level设置为Fastest, Smallest [-Os],这个选项会开启那些不增加代码大小的全部优化,并让可执行文件尽可能小。同时,将Strip Debug Symbols During Copy和Symbols Hidden by Default在release版本设为yes,可以去除不必要的调试符号。
  • 预编译头文件:将Precompile Prefix Header设置为YES,预编译头文件可以加快编译速度,但需要注意,一旦PCH文件和引用的头文件内容发生变化,所有引用到PCH的源文件都需要重新编译。
  • 仅编译当前架构:在Debug模式下,将Build Active Architecture Only设置为YES,这样只编译当前架构的版本,可以加快编译速度。但在Release模式下,需要设置为NO以确保兼容性。
  • Debug Information Format:设置为DWARF,减少dSYM文件的生成,从而减少包体积。
  • Enable Index-While-Building Functionality:设置为NO,关闭Xcode在编译时建立代码索引的功能,以加快编译速度。

另外

还可以使用 -why_load 链接器标志来减少 iOS 应用程序的二进制文件大小, -why_load 标志的作用:它可以帮助开发者识别最终二进制文件中包含的不必要符号。

在 iOS 开发中,链接器负责将代码、库和资源结合成一个最终的可执行文件。在此过程中,可能会有一些不必要的代码被包含进去,例如未使用的库、重复的符号或模块。这些多余的代码会导致应用程序的二进制文件增大,进而影响应用的下载速度、安装时间以及设备的存储空间。

-ObjC 标志,它通常用于强制链接所有 Objective-C 代码到最终的二进制文件中。这在某些情况下是必要的,例如使用了某些需要反射的 Objective-C 代码时,但是它也会导致未使用的代码被包含进去。通过 -why_load,开发者可以识别出哪些代码是多余的,并通过删除 -ObjC 标志来减少文件大小。

性能分析

有些开源的工具可以直接用于性能分析。

  • XCTest XCTest 是 Apple 官方的单元测试框架,支持性能测试。开发者可以通过 measure 方法来衡量代码块的执行时间,从而发现性能瓶颈。适合需要在单元测试中添加性能测试的场景。
  • KSCrash KSCrash 是一个强大的崩溃报告框架,它不仅能够捕获崩溃信息,还能提供应用程序的性能数据,例如内存使用和 CPU 使用情况。适合需要深入了解崩溃原因并监控相关性能数据的场景。
  • GT (GDT, GodEye) GodEye 是一个开源的 iOS 性能监控工具包,提供了多种监控功能,包括 FPS、内存使用、CPU 使用率、网络请求、崩溃日志等。它有一个方便的 UI,可以实时显示性能数据。适合在开发过程中嵌入应用进行实时性能监控。
  • libimobiledevice libimobiledevice 是一个开源的库,提供了与 iOS 设备交互的 API,可以用来监控设备状态和性能,特别是对非越狱设备进行操作。

常用的 In-app Debug 工具有:

  • Flex 是一个功能强大的 In-app Debug 工具,允许开发者在应用内实时查看和修改视图层次结构、网络请求、用户默认设置等。它还支持动态调整 UI 以及调试其他 app 内部逻辑。无需重新编译代码即可直接调试;可以修改内存中的值来观察变化。
  • Chisel 是 Facebook 开发的一组 LLDB 命令集,专门用于在调试时提供更方便的操作。它能帮助开发者快速检查视图层次结构、查看控件信息等。与 Xcode LLDB 无缝集成,通过命令行调试视图、打印出布局相关信息等。
  • Reveal 是一个图形化的 In-app Debug 工具,它允许开发者在运行中的应用中实时查看和编辑视图层次结构,支持 2D 和 3D 的视图展示。提供直观的 UI 调试界面,可以轻松地查看和修改视图属性;支持 iOS 和 tvOS。
  • Lookin 是一个开源的 iOS 视觉调试工具,专门用于分析和检查 iOS 应用的界面结构。它提供类似于 Xcode 的 View Debugging 功能,但更加灵活和强大,尤其是在复杂 UI 布局的分析上。通过 Lookin,你可以轻松地获取 iOS 应用中的界面层级、布局信息,并进行实时的 UI 调试和调整。可以称之为开源版的 Reveal。

Bazel

介绍

Polyrepo(多仓库)在代码量不断增加,开发团队扩大后,会觉得不合适,比如配置 CI 工具的繁琐,容易出现冗余代码,构建出现问题滞后等。Monorepo 指的是将多个模块化的 package 或 library 放在一个高度模块化且可管理的单一代码仓库中。谷歌的 Blaze、Bazel,以及微软的 Lage 和 Rush 等工具都是典型的 Monorepo 工具。Bazel 是一个现代化的多语言构建和测试工具。

你可以理解为是现代化的 Make 工具,但更加强大。

Bazel 通过缓存和增量构建机制,可以有效减少重复构建时间。支持并行构建,能够利用多核处理器提高构建速度。这两个点应该就是最吸引人的地方了。

另外它还允许用户定义自己的构建规则。因此,Bazel 是很适合大型的项目,还有容器化的应用。

接下来我就详细的说下 Bazel 是怎么使用的。

Bazel 组织 iOS 工程结构的方式具有高度的模块化和可管理性。

  • WORKSPACE 文件:根目录的核心文件。每个使用 Bazel 的项目都会在项目根目录中包含一个 WORKSPACE 文件,这个文件定义了项目的整体环境和依赖项。它类似于项目的“入口点”,Bazel 通过它知道如何构建整个项目。
  • BUILD 文件:模块的定义。在 Bazel 中,每个独立的模块(如一个应用、库、测试等)都需要一个 BUILD 文件,这个文件定义了该模块的构建规则。通过 BUILD 文件,开发者可以指定模块的依赖项、构建方式(如编译源代码、生成静态库等),以及测试配置。
  • Targets(目标):构建单元。BUILD 文件中定义的每个构建任务被称为“Target”(目标),可以是一个 iOS 应用程序、一个静态库、或单元测试等。目标可以依赖其他目标,这样可以构建出复杂的依赖图,确保模块间的依赖关系被正确处理。
  • 模块化组织:模块隔离与复用。Bazel 鼓励将代码分解成多个模块,每个模块都可以独立构建和测试。这种模块化结构提高了代码的可复用性,也简化了依赖管理。
  • 依赖管理:声明式依赖。Bazel 使用声明式依赖管理,即通过 BUILD 文件明确指定每个模块依赖哪些其他模块。这种方式有助于避免传统 iOS 项目中常见的依赖冲突和版本管理问题。
  • 跨语言支持:对于使用多种编程语言的项目,Bazel 提供了原生支持。对于 iOS 工程,Bazel 既支持 Objective-C 和 Swift 的构建,也支持与其他语言(如 C++、Java)的集成。
  • 并行构建与缓存:增量构建和缓存。Bazel 的构建系统支持并行构建和缓存。它能够有效地重用已经构建的模块,避免重复构建,从而大幅缩短构建时间。
  • Xcode 集成:与 Xcode 协作。虽然 Bazel 可以独立执行构建任务,但它也提供了与 Xcode 的集成,开发者可以在 Xcode 中进行代码编辑和调试,同时使用 Bazel 进行构建和测试。

WORKSPACE 文件

WORKSPACE 文件是定义项目根目录的关键文件,它告诉 Bazel 项目依赖了哪些外部库和资源,并为整个构建过程提供了基础配置。下面是一个典型的 WORKSPACE 文件的结构和示例代码:

一个典型的 WORKSPACE 文件包括以下部分:

  • 加载 Bazel 提供的 iOS 相关规则集,如 rules_applerules_swift
  • 声明项目中使用的第三方库,通常使用 http_archivegit_repository 来加载外部依赖。
  • 配置目标平台、构建工具链等。
# WORKSPACE 文件的开头,定义需要加载的规则集# 引入苹果生态系统的 Bazel 规则load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")# 加载苹果的构建规则 (rules_apple)http_archive(    name = "build_bazel_rules_apple",    url = "https://github.com/bazelbuild/rules_apple/releases/download/1.0.0/rules_apple.1.0.0.tar.gz",    strip_prefix = "rules_apple-1.0.0",)# 加载 Swift 的构建规则 (rules_swift)http_archive(    name = "build_bazel_rules_swift",    url = "https://github.com/bazelbuild/rules_swift/releases/download/0.24.0/rules_swift.0.24.0.tar.gz",    strip_prefix = "rules_swift-0.24.0",)# 使用 rules_apple 提供的默认设置load("@build_bazel_rules_apple//apple:repositories.bzl", "apple_rules_dependencies")apple_rules_dependencies()# 使用 rules_swift 提供的默认设置load("@build_bazel_rules_swift//swift:repositories.bzl", "swift_rules_dependencies")swift_rules_dependencies()# 加载 CocoaPods 规则(如果项目中使用了 CocoaPods)http_archive(    name = "bazel_pod_rules",    url = "https://github.com/pinterest/PodToBUILD/releases/download/0.1.0/PodToBUILD.tar.gz",    strip_prefix = "PodToBUILD-0.1.0",)# 声明 Xcode 版本和 SDK 的目标设置(可选)load("@build_bazel_rules_apple//apple:config.bzl", "apple_common")apple_common.xcode_config(    name = "xcode_config",    default_ios_sdk_version = "14.5",    default_macos_sdk_version = "11.3",    default_watchos_sdk_version = "7.4",    default_tvos_sdk_version = "14.5",)# 声明项目中使用的第三方库(例如使用 gRPC 或其他库)load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")git_repository(    name = "com_github_grpc_grpc",    commit = "your_commit_hash",    remote = "https://github.com/grpc/grpc.git",)# 声明额外的外部依赖(例如 Swift Package Manager 包)load("@build_bazel_rules_swift//swift:repositories.bzl", "swift_package")swift_package(    name = "swift_lib_example",    repository = "https://github.com/apple/swift-argument-parser",    revision = "0.4.4",)# 配置 BUILD.bazel 文件所在目录中的第三方依赖load("@bazel_pod_rules//:defs.bzl", "new_pod_repository")new_pod_repository(    name = "AFNetworking",    url = "https://github.com/AFNetworking/AFNetworking.git",    tag = "4.0.1",)

rules_applerules_swift 是 Bazel 提供的官方规则集,用于构建 iOS 和 Swift 项目。通过 http_archive 你可以指定需要的规则集版本。http_archivegit_repository 用于加载第三方库或工具集成。new_pod_repository 是专门为 CocoaPods 提供的规则,用于管理 iOS 项目中的 CocoaPods 依赖。apple_common.xcode_config 用于指定 iOS SDK 版本、Xcode 版本等,可以确保项目在正确的环境下构建。

BUILD 文件

编写 iOS 程序的 BUILD 文件时,需要使用 Bazel 提供的专门规则来构建 iOS 应用、库和测试。这些规则可以帮助你定义目标、依赖项和其他构建配置。

基本概念

  • ios_application: 用于定义一个 iOS 应用的目标。
  • objc_library: 用于定义一个 Objective-C 或 Swift 库。
  • ios_unit_testios_ui_test: 用于定义 iOS 的单元测试和 UI 测试目标。
  • apple_binary: 用于定义一个包含所有依赖的 iOS 可执行文件,通常与 ios_application 一起使用。

假设我们有一个简单的 iOS 项目,它包含一个应用和一个静态库,项目结构如下:

项目结构

my_ios_project/├── WORKSPACE├── BUILD├── App/│   ├── BUILD│   ├── AppDelegate.swift│   ├── ViewController.swift│   ├── Assets.xcassets│   └── Main.storyboard└── Libs/    ├── BUILD    ├── MyLib.swift    └── MyLib.h

Libs/BUILD 文件

首先,定义一个 Objective-C/Swift 库,这个库将在应用中使用:

# 用于定义一个 Objective-C 或 Swift 的库。objc_library(    name = "MyLib", # 库目标的名称。    srcs = ["MyLib.swift"], # 源文件列表(包括 Swift 和 Objective-C 文件)。    hdrs = ["MyLib.h"], // 头文件列表(如果有 Objective-C 文件)。    visibility = ["//visibility:public"],  # 公开可见,以供其他目标使用)

接下来,定义 iOS 应用目标,并指定它依赖于上面定义的库:

# 用于定义一个 iOS 应用目标。ios_application(    name = "MyApp", # 应用目标的名称。    bundle_id = "com.example.MyApp", # 应用的唯一标识符。    families = ["iphone", "ipad"], # 目标设备类型(如 iPhone 和 iPad)。    infoplists = ["Info.plist"], # 应用的 `Info.plist` 文件。    srcs = ["AppDelegate.swift", "ViewController.swift"], # 应用的源文件列表(Swift 和 Objective-C)。    storyboards = ["Main.storyboard"],     resources = glob(["Assets.xcassets/**/*"]), # 应用的资源文件,如图像、音效等,使用 `glob` 语法可以方便地将多个资源文件包含在 `BUILD` 文件中。    deps = ["//Libs:MyLib"],  # 依赖于 MyLib 库。 `deps` 参数用来定义该目标依赖的其他库或目标,Bazel 会自动处理这些依赖关系并确保它们的构建顺序正确。)

通常在项目的根目录也会有一个 BUILD 文件来聚合或定义一些全局目标,或仅作为入口文件:

# 设置包的默认可见性,这里设置为对所有目标公开可见。package(default_visibility = ["//visibility:public"]) # 创建别名,方便从顶层访问应用目标。alias(    name = "app",    actual = "//App:MyApp",)

Starlark 语言

Starlark 是一种由 Bazel 使用的嵌入式编程语言,用于定义构建规则和操作构建文件。它类似于 Python,专门设计用于 Bazel 的构建系统,允许用户扩展 Bazel 的功能。在 iOS 工程构建中,Starlark 主要用于编写自定义的规则、宏和函数。

Starlark 基础语法

Starlark 的语法类似 Python,包括变量、函数、条件、循环等基本结构。

变量与函数

# 定义变量message = "Hello, Starlark!"# 定义函数def greet(name):    return "Hello, " + name + "!"

条件与循环

# 条件语句def is_even(x):    if x % 2 == 0:        return True    else:        return False# 循环语句def sum_of_evens(limit):    sum = 0    for i in range(limit):        if is_even(i):            sum += i    return sum

使用 Starlark 自定义 iOS 构建

假设你想要定义一个自定义的 iOS 静态库规则,它能够简化库的定义并统一管理依赖。

项目结构

my_ios_project/├── WORKSPACE├── BUILD├── app/│   ├── BUILD│   ├── AppDelegate.swift│   └── ViewController.swift└── libs/    ├── BUILD    ├── mylib.swift    └── lib.bzl

编写 lib.bzl 文件

libs/ 目录下创建一个 lib.bzl 文件,定义自定义的 iOS 静态库规则。

# 这是一个宏,用于简化 `objc_library` 规则的定义。通过这种方式,你可以统一管理 ARC 选项、依赖等设置。def ios_static_library(name, srcs, hdrs = [], deps = []):    objc_library(        name = name,        srcs = srcs,        hdrs = hdrs,        deps = deps,        copts = ["-fobjc-arc"],  # 指定编译选项,如在此处启用 ARC。    )

使用 lib.bzl 文件中的宏

libs/BUILD 文件中使用上面定义的宏来创建一个 iOS 静态库。

# 用于加载 Starlark 文件中的宏或函数。在此例中,`//libs:lib.bzl` 表示加载 `libs` 目录中的 `lib.bzl` 文件。load("//libs:lib.bzl", "ios_static_library")# `ios_static_library` 宏会被调用来定义一个名为 `mylib` 的 iOS 静态库。ios_static_library(    name = "mylib",    srcs = ["mylib.swift"],)

app/BUILD 文件中,定义一个 iOS 应用目标,并依赖于上述的静态库:

ios_application(    name = "MyApp",    bundle_id = "com.example.MyApp",    families = ["iphone", "ipad"],    infoplists = ["Info.plist"],    srcs = ["AppDelegate.swift", "ViewController.swift"],    deps = ["//libs:mylib"],)

自定义 iOS Framework 构建的示例

你可以使用 Starlark 编写更复杂的规则,例如为 iOS 定制一个 Framework 的构建规则:

# 这是一个 Bazel 的内置规则,用于创建 iOS Framework。自定义的 `ios_framework` 宏将静态库打包成一个 Framework,简化了应用与库之间的集成。def ios_framework(name, srcs, hdrs = [], deps = [], bundle_id = None):    objc_library(        name = name + "_lib",        srcs = srcs,        hdrs = hdrs,        deps = deps,    )    apple_framework(        name = name,        bundle_id = bundle_id,        infoplists = ["Info.plist"],        deps = [":" + name + "_lib"],    )

运行

在终端中运行以下命令来构建 iOS 应用。

构建应用

bazel build //App:MyApp

运行应用

bazel run //App:MyApp

测试应用

bazel test //App:MyAppTests

rules_xcodeproj 生成 Xcode 工程

rules_xcodeproj 是一个用于生成 Xcode 工程文件 (.xcodeproj) 的 Bazel 插件。它允许你在使用 Bazel 构建系统的同时,仍然能够使用 Xcode 进行开发和调试。它目前支持两种主要的构建模式:BwB (Build with Bazel) 和 **BwX (Build with Xcode)**。
BwB 模式是将 Bazel 作为主要的构建工具,Xcode 项目仅用于 IDE 支持,而实际的构建过程完全由 Bazel 管理。BwX 模式官方后续支持会变弱,不建议使用。

首先,在你的 WORKSPACE 文件中添加 rules_xcodeproj 规则的依赖项。

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")http_archive(    name = "build_bazel_rules_xcodeproj",    sha256 = "<SHA256>",    url = "https://github.com/buildbuddy-io/rules_xcodeproj/releases/download/<version>/rules_xcodeproj-<version>.tar.gz",)load("@build_bazel_rules_xcodeproj//:workspace_setup.bzl", "rules_xcodeproj_workspace_setup")rules_xcodeproj_workspace_setup()

你需要替换 <SHA256><version> 为相应的值,可以从 rules_xcodeproj 的发布页面 获取。

在项目的 BUILD.bazel 文件中,使用 xcodeproj 规则生成 .xcodeproj 文件。例如:

load("@build_bazel_rules_xcodeproj//:defs.bzl", "xcodeproj")xcodeproj(    name = "MyApp_xcodeproj", # 定义生成的 `.xcodeproj` 的目标名称。    project_name = "MyApp", # 定义 Xcode 工程的名称。    targets = ["//app:MyApp"], # 指定 Bazel 中需要包含在 Xcode 工程中的目标。)

在命令行中,运行以下命令生成 Xcode 工程文件:

bazel run //:MyApp_xcodeproj

这将生成一个名为 MyApp.xcodeproj 的文件,位于你运行命令的目录中。你可以用 Xcode 打开这个工程文件,并在 Xcode 中调试和开发你的应用。

rules_xcodeproj 提供了多种配置选项,你可以根据需要进行自定义。例如,可以配置生成的 Xcode 工程中的编译设置、构建配置等。以下是一些常用的配置:

xcodeproj(    name = "MyApp_xcodeproj",    project_name = "MyApp",    targets = ["//app:MyApp"],    build_settings = {        "SWIFT_VERSION": "5.0",        "CODE_SIGN_IDENTITY": "",    }, # 指定 Xcode 工程的编译设置,例如 Swift 版本、代码签名等。    extra_generated_files = ["//path/to/resource"], #指定额外的生成文件,可能包括资源文件等。)

Build with Proxy 模式

rules_xcodeproj 新推出的 Build with Proxy 模式,是一种新的构建模式。在 “Build with Proxy” 模式下,Bazel 通过 XCBBuildServiceProxy 完全接管了整个构建过程。Xcode 在这个模式下只作为一个前端界面,所有的构建逻辑和执行都由 Bazel 来完成。在 “Build with Bazel” 模式下,Xcode 依然是主导构建过程的工具,但它在构建的关键步骤(如编译和链接)上调用 Bazel 来完成实际的工作。Xcode 会生成编译任务并将其委托给 Bazel,同时保持对构建过程的部分控制权。

流程是,当开发者在 Xcode 中触发构建时,XCBBuildServiceProxy 拦截 Xcode 的构建请求。构建请求被重定向到 Bazel,由 Bazel 完全控制构建过程,包括依赖管理、编译、链接等。构建结果通过 XCBBuildServiceProxy 返回给 Xcode,Xcode 仅作为显示界面。

Bazel 完全控制构建过程,提供更高效的构建性能和更一致的结果。由于 Xcode 不再控制构建过程,调试和查看构建日志可能需要适应 Bazel 的方式,还有更高的初始配置成本。

首先,你需要在 Bazel 的 WORKSPACE 文件中引入 rules_xcodeproj

http_archive(    name = "build_bazel_rules_xcodeproj",    url = "https://github.com/buildbuddy-io/rules_xcodeproj/releases/download/{version}/release.tar.gz",    sha256 = "{sha256}",)load("@build_bazel_rules_xcodeproj//xcodeproj:workspace.bzl", "xcodeproj_dependencies")xcodeproj_dependencies()

接着,在你的 BUILD 文件中配置 Xcode 项目生成规则,并启用 “Build with Proxy” 模式:

load("@build_bazel_rules_xcodeproj//xcodeproj:xcodeproj.bzl", "xcodeproj")xcodeproj(    name = "MyAppProject",    targets = ["//App:MyApp"],    build_mode = "build_with_proxy",  # 启用 "Build with Proxy" 模式    minimum_xcode_version = "14.0",    # 其他配置...)

生成 Xcode 项目文件:

bazel run //:MyAppProject

生成的 .xcodeproj 文件将会配置为使用 Bazel 进行构建。

XCBBuildServiceProxy 是核心代理组件,它通过拦截 Xcode 的构建请求并将其转发给 Bazel 进行处理。在 “Build with Proxy” 模式下,Xcode 的构建流程大致如下:

# 当你在 Xcode 中点击“构建”时,Xcode 会调用 XCBBuildServiceProxy。# XCBBuildServiceProxy 会将构建请求转发给 Bazel。bazel build //App:MyApp# Bazel 处理所有构建任务,包括编译、链接等。# 构建完成后,Bazel 将结果返回给 XCBBuildServiceProxy。# XCBBuildServiceProxy 将结果反馈给 Xcode,Xcode 显示构建输出。

为了确保 Xcode 在构建时使用 Bazel,你需要配置项目的 Scheme。在生成的 .xcodeproj 文件中,确保构建 Scheme 设置为使用 XCBBuildServiceProxy 调用 Bazel。

生成 IPA 包的过程

当你运行 bazel build //App:MyApp 这条命令时,Bazel 会从指定的目标 //App:MyApp 开始,递归解析其依赖树,执行构建过程,最终生成一个 IPA 文件。

//App:MyApp 是一个 Bazel 目标,它指向一个定义在 App/BUILD.bazel 文件中的构建规则。Bazel 首先会解析这个目标并确定其直接依赖项。

假设在 App/BUILD.bazel 文件中定义了一个 ios_application 规则:

ios_application(    name = "MyApp",    bundle_id = "com.example.myapp",    families = ["iphone", "ipad"],    infoplists = ["Info.plist"],    entitlements = "MyApp.entitlements",    provisioning_profile = "//:MyAppProfile",    app_icon = "AppIcon",    launch_images = ["LaunchImage"],    deps = [        "//App/Core:core_lib",        "//App/UI:ui_lib",    ],)

在这个例子中,MyApp 依赖于两个库 core_libui_lib

Bazel 会递归地解析 deps 字段中的依赖项,从而构建整个依赖树。在上面的例子中,Bazel 会进一步解析 //App/Core:core_lib//App/UI:ui_libBUILD.bazel 文件。

假设 core_libui_lib 是通过 objc_library 规则定义的:

# App/Core/BUILD.bazelobjc_library(    name = "core_lib",    srcs = ["CoreLib.m"],    hdrs = ["CoreLib.h"],    deps = [        "//third_party/some_lib:some_lib",    ],)
# App/UI/BUILD.bazelobjc_library(    name = "ui_lib",    srcs = ["UILib.m"],    hdrs = ["UILib.h"],    deps = [        "//App/Core:core_lib",    ],)

在这里,ui_lib 依赖于 core_lib,而 core_lib 依赖于一个第三方库 some_lib

在解析完依赖树后,Bazel 开始实际的构建过程。这包括编译源文件、链接目标文件、处理资源文件,并最终打包为一个 IPA 文件。

Bazel 会首先编译 objc_library 目标。比如,将 CoreLib.mUILib.m 文件编译为 .o 对象文件,并处理相应的头文件。之后,Bazel 将链接这些编译后的对象文件,生成静态库或可执行文件。Bazel 将所有编译结果(如可执行文件、静态库)、资源文件(如 Info.plist、图标)打包为一个 .app 目录。最后,Bazel 使用 ios_application 规则的配置,将 .app 目录压缩并签名为一个 IPA 文件。

Bazel 通过其强大的缓存和增量构建机制,只重新构建那些发生变化的目标。例如,如果只修改了 UILib.m 文件,那么 Bazel 只会重新编译 ui_lib 相关的目标,而不需要重新构建整个应用。

生成的 IPA 文件通常会保存在 bazel-bin 目录中,路径类似于 bazel-bin/App/MyApp.ipa

依赖分析

Bazel 的依赖分析(dependency analysis)是其构建系统中关键的一部分,用于决定哪些文件或目标需要重新构建,以及哪些可以重用之前的构建结果。这一过程高度依赖于 Bazel 的增量构建和缓存机制。

Bazel 依赖分析的核心步骤

  • 目标(Target)定义与依赖图:Bazel 使用 BUILD 文件定义构建目标(如库、应用、测试等)以及这些目标之间的依赖关系。这些依赖关系形成了一个有向无环图(DAG),用于描述项目的依赖结构。
  • 文件和目标的输入输出(Input/Output)追踪:Bazel 追踪每个目标的输入(源文件、依赖项)和输出(编译后的二进制文件、对象文件等)。任何影响输入的更改都会触发相应目标的重新构建。
  • 哈希校验与缓存:Bazel 对每个目标的输入文件进行哈希校验(如 MD5 或 SHA-256),并将其存储在缓存中。如果同一目标的输入哈希值未发生变化,则 Bazel 直接使用缓存中的构建结果,而不需要重新构建。
  • 增量构建:当 Bazel 发现输入文件发生了变化,它会自动标记该目标以及依赖于该目标的所有下游目标为“脏”(dirty),这些目标将在下一次构建时重新编译。
  • 依赖分析的递归性:Bazel 的依赖分析是递归进行的。如果一个目标的依赖发生变化,Bazel 将递归地检查其所有上游目标是否需要重建。

以下是一个简单的 Bazel 项目结构示例,展示了 Bazel 的依赖分析过程:

项目结构

my_project/├── WORKSPACE├── BUILD├── main/│   ├── BUILD│   ├── main.m│   └── AppDelegate.m└── libs/    ├── BUILD    ├── libA.m    ├── libA.h    ├── libB.m    └── libB.h

项目根目录的 BUILD 文件:

# 根目录下的 BUILD 文件ios_application(    name = "MyApp",    srcs = ["main/main.m", "main/AppDelegate.m"],    deps = [        "//libs:libA",        "//libs:libB",    ],)

libs/ 目录的 BUILD 文件:

# libs 目录下的 BUILD 文件objc_library(    name = "libA",    srcs = ["libA.m"],    hdrs = ["libA.h"],)objc_library(    name = "libB",    srcs = ["libB.m"],    hdrs = ["libB.h"],    deps = [":libA"],  # libB 依赖于 libA)

Bazel 的依赖分析过程

  • 依赖图的生成:MyApp 依赖于 libAlibB,而 libB 又依赖于 libA。Bazel 会根据这些依赖关系生成一个依赖图。
  • 输入输出追踪与哈希校验:在每次构建时,Bazel 会对 libA.mlibB.mmain.m 等输入文件进行哈希校验,并将结果与上次构建时的哈希值进行比较。例如,如果 libA.m 发生了变化,Bazel 会检测到其哈希值发生了变化,从而标记 libA 及依赖于它的 libBMyApp 为“脏”。
  • 增量构建:由于 libA.m 发生了变化,Bazel 将重新构建 libA,然后递归地重新构建依赖它的 libB,最终重新构建 MyApp
  • 缓存与重用:如果 libB.mmain.m 没有变化,Bazel 可以重用它们之前的编译结果(缓存),只需要重新构建那些受影响的目标。
  • 输出结果:最终,Bazel 生成一个新的 MyApp 二进制文件,包含了最新的代码改动,并保证所有依赖关系都得到了正确的处理。

Bazel 使用哈希校验来精确判断哪些输入文件发生了变化。只有当输入文件的哈希值变化时,才会触发相应目标的重新构建,这样可以最大程度地重用已有的构建结果,减少不必要的编译时间。Bazel 的依赖分析是递归的,这意味着任何下游依赖的变化都会向上递归地影响依赖它的所有目标。这确保了每次构建的结果都是一致且正确的。由于 Bazel 精确地追踪了目标的依赖关系和输入输出变化,它能够有效地执行增量构建,只重新编译那些受影响的模块。

不会影响依赖分析缓存的代码改动有哪些呢?

在 Bazel 中,构建系统的性能很大程度上依赖于其增量构建和缓存机制。Bazel 使用依赖分析(dependency analysis)来决定哪些部分的代码需要重新构建,哪些部分可以使用缓存结果。

以下是一些不会影响依赖分析缓存的代码改动类型,这些改动不会导致 Bazel 重新构建依赖的目标,因为它们不会改变编译输出或依赖图:

  • 注释的更改:添加、删除或修改代码中的注释不会影响构建输出,因为注释不参与代码编译。
  • 代码格式化:仅涉及代码格式(如缩进、空格、换行)的改动不会影响构建结果,格式化不会改变编译后的二进制文件。
  • 无实际影响的变量命名更改:在局部范围内(如函数内部)修改变量名称(而不影响函数签名)不会影响依赖分析缓存。
  • 无效或未使用代码的添加:添加从未使用的代码(如未调用的函数)在某些情况下不会触发 Bazel 的重构建,特别是在这些代码片段与已构建目标无关时。
  • 函数内部的逻辑更改:在某些情况下,对函数内部进行的改动可能不会影响其他模块的构建,具体取决于目标间的依赖关系和可见性(例如,私有函数内部的更改)。

以下是一个具体的代码示例,展示了不会影响 Bazel 依赖分析缓存的几种改动:

# 示例 BUILD 文件# 定义一个简单的 iOS 应用程序目标ios_application(    name = "MyApp",    srcs = ["main.m", "AppDelegate.m"],    deps = [":MyLibrary"],)objc_library(    name = "MyLibrary",    srcs = ["MyLibrary.m"],    hdrs = ["MyLibrary.h"],)

假设我们有以下 Objective-C 代码:

// MyLibrary.m#import "MyLibrary.h"// 1. 注释的改动// 添加一些注释,不会影响 Bazel 的依赖分析缓存// 例如:以下注释不会触发重新构建// This is a utility function@implementation MyLibrary// 2. 变量名更改(局部范围)。在函数内部修改变量名称不会影响其他目标或模块的编译结果,只要变量名的改变不影响接口或其他模块的依赖。- (void)performTask {    int localVar = 5;  // 如果将 localVar 改为 anotherVar,这不会触发重新构建    NSLog(@"Task performed");}// 3. 代码格式改动。如添加空行、调整缩进或更改代码对齐方式等纯粹的格式改动,不会改变源代码的语义,因此不会触发重新编译。- (void)doSomething {    int a = 10;    int b = 20;  // 对齐方式或空格的改变不会触发重新构建    NSLog(@"Sum: %d", a + b);}// 4. 添加未使用的代码。如果添加的代码从未被调用或引用,Bazel 可能不会重新构建该模块,尤其是在该代码片段没有影响编译输出时。- (void)unusedFunction {    NSLog(@"This function is never called.");}@end

在 Bazel 的构建过程中,操作图(Action Graph)是一个关键的概念,它定义了构建任务之间的依赖关系,并确保这些任务能够按照正确的顺序并行执行。Baziel 使用操作图来确定哪些任务可以并行执行,哪些任务需要依赖其他任务的结果。

操作图是一个有向无环图(DAG),其中每个节点代表一个操作(Action),每个边代表操作之间的依赖关系。操作可能包括编译源文件、链接对象文件、打包资源文件等。

操作图中的节点和边的关系如下:

  • 节点(Action):一个构建任务,如编译、链接或打包。
  • 边(Dependency): 表示一个操作依赖于另一个操作的输出。

Bazel 从指定的构建目标(如 bazel build //App:MyApp)开始,递归地解析 BUILD 文件中定义的目标和依赖关系,生成操作图。具体步骤如下:

  1. Bazel 解析 BUILD 文件,找到指定目标和其依赖项。
  2. 每个构建规则(如 objc_library, ios_application)会生成一组操作。这些操作可能包括编译源文件、链接目标文件等。
  3. Bazel 将生成的操作按照依赖关系连接起来,形成操作图。

Bazel 确保操作图中的操作按正确的顺序并行运行,遵循以下原则:

  • 一个操作只能在它所有的依赖操作完成后才能运行。
  • Bazel 会并行执行那些没有依赖关系或者依赖已经满足的操作。

假设我们有一个简单的项目,其中包含两个库和一个应用程序。每个库都有自己的源文件和头文件,应用程序依赖于这两个库。以下是 BUILD 文件的定义:

# App/Core/BUILD.bazelobjc_library(    name = "core_lib",    srcs = ["CoreLib.m"],    hdrs = ["CoreLib.h"],)# App/UI/BUILD.bazelobjc_library(    name = "ui_lib",    srcs = ["UILib.m"],    hdrs = ["UILib.h"],    deps = [        "//App/Core:core_lib",    ],)# App/BUILD.bazelios_application(    name = "MyApp",    bundle_id = "com.example.myapp",    families = ["iphone", "ipad"],    infoplists = ["Info.plist"],    deps = [        "//App/Core:core_lib",        "//App/UI:ui_lib",    ],)

对于上述项目,Bazel 会生成如下操作图:

  1. 编译操作:

    • CoreLib.m -> CoreLib.ocore_lib 的编译操作)
    • UILib.m -> UILib.oui_lib 的编译操作)
  2. 链接操作:

    • core_lib 编译完成后,可以立即编译 ui_lib,因为 ui_lib 依赖于 core_lib
    • core_libui_lib 都编译完成后,可以将它们链接到一起,生成 MyApp 的可执行文件。
  3. 打包操作:

    • 在所有链接操作完成后,将生成的二进制文件与资源文件(如 Info.plist)打包为 .app 目录,然后进一步打包为 IPA 文件。

在这个操作图中,CoreLib.oUILib.o 的编译操作可以并行执行,因为它们没有依赖关系。链接操作则需要等待所有编译操作完成后才能执行。

Bazel 在内部使用操作图来调度这些任务。通过分析操作图,Bazel 能够确定哪些任务可以并行执行,哪些任务需要等待依赖完成,从而最大化利用多核 CPU 的能力,加速构建过程。

query指令找依赖关系

Bazel 的 query 命令是一种强大的工具,用于在 Monorepo(单体代码库)中查找和分析目标之间的依赖关系。通过 query,你可以获取关于构建目标的详细信息,包括它们的依赖关系、反向依赖、测试等。

bazel query 命令的一般语法如下:

bazel query '<expression>'

<expression> 是你想要查询的表达式。Bazel 提供了一系列表达式来帮助你查找所需的信息。

以下是常见的 Bazel Query 表达式

列出工作区中所有可用的构建目标:

bazel query '//...'

//... 表示从当前工作区的根目录开始递归查找所有目标。

查找某个目标的所有直接和间接依赖:

bazel query 'deps(<target>)'

例如,查找 //app:main 目标的所有依赖:

bazel query 'deps(//app:main)'

查找哪些目标依赖于某个特定目标(即反向依赖):

bazel query 'rdeps(<scope>, <target>)'

例如,查找工作区中哪些目标依赖于 //lib:my_library

bazel query 'rdeps(//..., //lib:my_library)'

例如,列出所有的测试目标:

bazel query 'kind(test, //...)'

kind(test, //...) 将查找工作区中的所有测试目标。

如果只想查找目标的直接依赖而非递归依赖,可以使用:

bazel query 'deps(<target>, 1)'

例如:

bazel query 'deps(//app:main, 1)'

使用 attr 过滤带有特定属性的目标。例如,查找所有带有特定标签的目标:

bazel query 'attr(tags, "my_tag", //...)'

假设你有以下项目结构:

workspace/├── app/│   ├── BUILD│   ├── main.swift│   └── AppDelegate.swift├── lib/│   ├── BUILD│   ├── util.swift│   └── helper.swift└── third_party/    ├── BUILD    └── external_lib.swift

app/BUILD 文件中,你定义了一个 ios_application 目标:

ios_application(    name = "MyApp",    bundle_id = "com.example.MyApp",    srcs = ["main.swift", "AppDelegate.swift"],    deps = ["//lib:util"],)

lib/BUILD 文件中定义了一个 swift_library 目标:

swift_library(    name = "util",    srcs = ["util.swift", "helper.swift"],    deps = ["//third_party:external_lib"],)

你可以运行以下命令来查找 MyApp 的所有直接和间接依赖:

bazel query 'deps(//app:MyApp)'

这将输出:

//app:MyApp//lib:util//third_party:external_lib

查找依赖于 external_lib 的所有目标

你可以使用以下命令来查找反向依赖:

bazel query 'rdeps(//..., //third_party:external_lib)'

这将列出所有依赖于 external_lib 的目标,比如 //lib:util

你还可以生成图形化的依赖关系图,使用 dot 格式输出:

bazel query 'deps(//app:MyApp)' --output graph > graph.dot

然后使用 Graphviz 等工具将 graph.dot 文件转换为图形文件。

query 指令是理解和管理 Monorepo 中依赖关系的关键工具。它提供了多种强大的表达式,帮助你轻松地查找目标的依赖关系、反向依赖、过滤目标等。在大型代码库中,使用 query 可以大大简化依赖关系的管理,并且可以帮助你识别不必要的依赖或者循环依赖。

远程缓存

Bazel 的远程缓存功能允许你在不同的开发环境、构建机器或 CI 系统之间共享构建产物。这可以显著加快构建速度,因为已经构建好的产物可以被重复使用,而不需要重新编译。

Bazel 的远程缓存功能可以将构建产物(如编译后的二进制文件、对象文件等)存储在一个远程存储系统中。当你在不同环境或机器上构建同一个项目时,Bazel 会检查远程缓存,并下载已存在的构建产物,而不必重新构建。

Bazel 支持多种远程缓存后端,包括:

  • HTTP/HTTPS 服务器:可以使用支持 HTTP 的远程服务器作为缓存。
  • 云存储:如 Google Cloud Storage (GCS) 或 Amazon S3。
  • gRPC 缓存服务:可以通过 gRPC 接口进行缓存和检索。

在你的项目中,可以通过 ~/.bazelrc 文件或项目级别的 .bazelrc 文件来配置远程缓存。以下是如何配置不同类型远程缓存的示例。

配置 HTTP 远程缓存

build --remote_cache=http://my-cache-server.com/cache/

如果你使用 Google Cloud Storage (GCS) 作为远程缓存,你可以这样配置:

build --remote_cache=grpc://gcs.example.com/bucket-namebuild --google_credentials=/path/to/credentials.json

在这个例子中,grpc://gcs.example.com/bucket-name 是 GCS 的地址,/path/to/credentials.json 是你的 GCS 凭证文件。

配置 gRPC 远程缓存

build --remote_cache=grpc://my-grpc-cache-server.com

你可以使用 gRPC 缓存服务器,如 BuildBarn 或 BuildGrid 来搭建自己的 gRPC 远程缓存服务。

有些远程缓存服务需要身份认证,如 GCS 或 Amazon S3。对于 GCS,你可以配置 google_credentials 选项,或者使用 gcloud auth 命令登录:

gcloud auth application-default login

对于需要 AWS 认证的服务,你可以配置 AWS CLI,然后通过环境变量传递认证信息:

export AWS_ACCESS_KEY_ID="your-access-key-id"export AWS_SECRET_ACCESS_KEY="your-secret-access-key"

配置完成后,Bazel 会自动使用远程缓存。在运行构建命令时,如:

bazel build //App:MyApp

Bazel 会:

  1. 首先检查远程缓存,是否有匹配当前源代码和构建配置的缓存。
  2. 如果找到匹配的缓存,直接下载使用,而不重新编译。
  3. 如果没有找到匹配的缓存,正常编译并将结果上传到远程缓存,以便下次使用。

注意远程缓存和远程执行是不同的概念。远程缓存仅共享构建产物,而远程执行允许你在远程机器上执行整个构建过程。你可以根据需要选择合适的方案。

以下是一个项目级别的 .bazelrc 文件示例,它配置了远程缓存到一个 HTTP 服务器:

# .bazelrcbuild --remote_cache=http://cache.example.com/cache/build --disk_cache=/path/to/local/cachebuild --google_default_credentials

远程执行配置

Bazel 的远程执行功能允许你在远程服务器或集群上分布式执行构建任务,而不是在本地机器上执行。这种能力特别适用于大规模的项目,可以显著缩短构建时间,因为它利用了多台机器的计算资源。

远程执行让 Bazel 在远程执行环境中运行构建任务,例如编译、链接、测试等。Bazel 将构建任务分发到一个或多个远程执行节点,这些节点并行处理任务并将结果返回给本地 Bazel 客户端。

一个典型的远程执行环境由以下组件组成:

  • 远程执行服务器:处理来自 Bazel 的任务,并将它们分发给执行节点。
  • 远程工作节点:这些节点执行实际的构建任务。
  • Remote Cache(远程缓存):存储构建产物以便重复使用,避免重新执行相同任务。

要启用 Bazel 的远程执行功能,你需要配置 Bazel 来连接远程执行服务。配置通常在 .bazelrc 文件中完成。

假设你有一个远程执行服务器,它的地址是 remotebuild.example.com。你可以通过以下配置启用远程执行:

# .bazelrcbuild --remote_executor=grpc://remotebuild.example.com:443build --remote_cache=grpc://remotebuild.example.com:443build --remote_timeout=300build --spawn_strategy=remotebuild --strategy=Javac=remotebuild --strategy=CppCompile=remotebuild --strategy=Objc=remote
  • --remote_executor:指定远程执行服务器的地址。
  • --remote_cache:配置远程缓存的地址,这里可以和远程执行服务器一致。
  • --remote_timeout:设置远程执行的超时时间。
  • --spawn_strategy=remote:告诉 Bazel 使用远程策略执行所有构建任务。
  • --strategy=Javac=remote 等:为特定类型的任务指定使用远程执行。

如果远程执行服务器需要身份验证,你可能需要配置凭据。对于 Google Cloud Remote Build Execution (RBE) 服务,典型的配置如下:

build --google_credentials=/path/to/credentials.json

使用 gcloud 工具登录:

gcloud auth application-default login

设置远程执行服务(如 BuildFarm、BuildGrid 或 Google 的 Remote Build Execution (RBE))通常涉及以下步骤:

  1. 安装和配置 Remote Execution Server:这包括配置服务器的计算资源、执行策略等。
  2. 配置 Remote Workers:确保工作节点能够连接到服务器,并具备执行构建任务所需的环境和依赖。
  3. 配置 Remote Cache:搭建和配置远程缓存,以便存储和共享构建产物。

配置完成后,你可以运行 Bazel 命令进行远程执行,例如:

bazel build //App:MyApp

在这个过程中,Bazel 会:

  1. 将构建请求发送到远程执行服务器。
  2. 服务器将任务分发到远程工作节点,并行执行。
  3. 远程节点完成任务后,将结果和构建产物返回到本地。
  4. 本地 Bazel 客户端将最终产物(如可执行文件或 IPA 文件)生成。

使用远程执行的好处

  • 通过分布式构建,可以显著缩短构建时间。
  • 充分利用远程集群的计算资源,而不是依赖本地机器的性能。
  • 确保所有开发人员、CI/CD 系统在相同的环境中执行构建,减少“在我机器上正常”的问题。

假设你有一个项目 App,其中包括一个 BUILD 文件。以下是如何在远程执行环境中构建这个项目的完整配置。

.bazelrc 文件:

build --remote_executor=grpc://remotebuild.example.com:443build --remote_cache=grpc://remotebuild.example.com:443build --google_credentials=/path/to/credentials.jsonbuild --spawn_strategy=remotebuild --strategy=CppCompile=remotebuild --strategy=Javac=remotebuild --strategy=Objc=remote

然后你可以执行以下命令:

bazel build //App:MyApp

自定义构建规则

Bazel 的可扩展性是其强大功能之一,它允许开发者为尚未支持的编程语言或构建工具创建自定义的构建规则。通过编写自定义规则,你可以让 Bazel 识别、编译、链接特定语言的代码,并将它们集成到现有的 Bazel 构建系统中。

在自定义规则中,你可以指定输入、输出、依赖关系以及构建过程中的具体操作。

一个自定义的 Bazel 构建规则通常包括以下部分:

  • 规则定义:描述构建过程的逻辑和依赖关系。
  • 构建步骤:实际执行的命令,比如编译或链接操作。
  • 规则调用:在 BUILD 文件中调用自定义规则来应用于实际项目。

假设我们要为一个尚未被官方支持的编程语言 MyLang 创建一个简单的构建规则,该规则能够将 .mylang 源文件编译为可执行文件。

首先,在项目的根目录下创建一个 mylang_rules.bzl 文件,用于定义 MyLang 的构建规则。

# mylang_rules.bzldef _mylang_binary_impl(ctx):    # 输入文件    source = ctx.file.src        # 输出文件 (可执行文件)    output = ctx.actions.declare_file(ctx.label.name)        # 编译命令    ctx.actions.run(        inputs=[source],        outputs=[output],        arguments=[source.path, "-o", output.path],        executable="path/to/mylang_compiler",    )    return DefaultInfo(        executable=output,    )# 定义 mylang_binary 规则mylang_binary = rule(    implementation=_mylang_binary_impl,    attrs={        "src": attr.label(allow_single_file=True),  # 单个源文件    },    executable=True,  # 生成可执行文件)

_mylang_binary_impl 实现了 mylang_binary 规则的逻辑,它使用 Bazel 的 ctx.actions.run 来定义编译过程。mylang_binary定义了一个新的构建规则,允许我们在 BUILD 文件中使用 mylang_binary 规则来处理 MyLang 源文件。

在你的项目中,使用自定义的 mylang_binary 规则。比如,在 my_project/BUILD 文件中:

# my_project/BUILDload("//:mylang_rules.bzl", "mylang_binary")mylang_binary(    name = "my_program",    src = "main.mylang",)

这个 BUILD 文件表示使用 mylang_binary 规则编译 main.mylang 文件,并生成一个名为 my_program 的可执行文件。

你可以通过 Bazel 构建这个项目:

bazel build //my_project:my_program

这将使用 MyLang 编译器将 main.mylang 编译为 my_program 可执行文件。

自定义规则的功能可以进一步扩展。例如,你可以添加支持多个源文件、库依赖、资源文件等。如果你希望 mylang_binary 支持多个源文件,可以修改规则定义:

# mylang_rules.bzldef _mylang_binary_impl(ctx):    sources = ctx.files.srcs    output = ctx.actions.declare_file(ctx.label.name)    # 假设 mylang_compiler 能够接受多个源文件    args = [source.path for source in sources] + ["-o", output.path]    ctx.actions.run(        inputs=sources,        outputs=[output],        arguments=args,        executable="path/to/mylang_compiler",    )    return DefaultInfo(        executable=output,    )mylang_binary = rule(    implementation=_mylang_binary_impl,    attrs={        "srcs": attr.label_list(allow_files=True),  # 支持多个源文件    },    executable=True,)

BUILD 文件中:

# my_project/BUILDmylang_binary(    name = "my_program",    srcs = ["main.mylang", "utils.mylang"],)

通过创建自定义规则,你可以将 MyLang 与 Bazel 的其他功能(如远程缓存、远程执行、增量构建等)集成在一起。你还可以通过将规则打包为 Bazel 模块,供其他项目复用。

使用 SwiftUI 开发 RSS 阅读器

作者 戴铭
2023年4月24日 15:51

在 Apple 加速器活动和字节内分享了使用 SwiftUI 做 RSS 阅读器的一点心得。可能你还不知道什么是 RSS 阅读器,简单来说 RSS 是一些博客和新闻网站,甚至是播客和视频平台发布他们的内容更新的一种 XML 格式,阅读器就是通过请求这个 XML 以获取他们内容更新的客户端。

这就有了接下来几个问题:

目前已有 Reeder 和 NetNewsWire 等 RSS 阅读器,那么为什么还要再开发一个呢,早在14年我曾做过一个,陆续也更新过,后来还是以 Reeder 作为主力,feedly 作为服务,后来 feedly 有些不稳定,我又改成本地获取 feed 的方式,但是改成本地模式后设备同步又成了问题。正好最近几年苹果在界面、数据流和存储上都做了很大的功能加强。于是我打算将以前 objc、rac和 FMDB 替换成 SwiftUI 和 CoreData 技术,同时补上以前缺少的一些功能,比如添加管理feed,不同设备同步订阅 feed、文章已读状态和收藏信息等功能。

先说下怎么订阅 RSS。

如上图所示先通过链接获取待解析的数据,以及 mimeType,通过 mimeType 看里面是否包含如下描述:

application/atom+xmlapplication/rss+xmlapplication/jsonapplication/feed+jsontext/xmlapplication/xml

包含的话就可以判断是 RSS。

如果不是的话就需要手动从网页里获取 RSS 的链接,方法如下:

mime.contains("text/html")SwiftSoup.parse(homepageHTML)htmlDom.select("link[rel=alternate]")

其中 SwiftSoup 是一个专门用来将 HTML 解析成 DOM 对象的库。一般 RSS 的链接会在属性键值是 rel 和 alternate 的 Link 这个标签里。但是很多网站并没有遵循这个规范,那么就需要在链接后直接通过添加以下文件名来查找哪个是它的 RSS 链接:

["feed.xml","rss.xml","atom.xml","feed","feed.rss","rss","index.xml"]

找到了 RSS 的链接就可以获取到它的数据,接下来就是对数据的处理,根据 RSS 的规范,RSS 的数据主要是以下三种。

对应的结构体如下:

RSS 的图标的获取方式有两种

对处理好的数据需要进行本地的存储,目前不管是 Apple 还是三方库主要都是基于 SQLite 的封装。估计是因为 SQLite 开销小,支持大多数 SQL 92 标准语法,采用标准的 ANSI-C 代码,很容易在多个平台运行,同时 SQLite 还支持所有 SQL 用来保障数据安全和完整性的事务属性,比如原子性、一致性、隔离性和持久性。以下是 iOS 上一些主要基于 SQLite 封装库:

我选择的是 Core Data,首先是 Core Data 的 API 很强,将复杂数据建模和操作的 SQL 语句都做成了可视化和对象模式操作。多个数据对象之间的关联关系也做了很多自动处理。Core Data 还使用了惰性加载的方式,只有在需要时才从存储区域获取数据,以节省内存,提高执行效率。

Core Data 的使用需要对数据库进行设置。

在读取实体存储时可以设置 Core Spotlight 以及进行一些调试测试工作。

Core Data 对数据的增删改和检索操作都是在 NSManagerObjectContext 中完成的。

如果要支持 CloudKit,NSManagerObjectContext 初始化时需要在合并策略做一些设置。context 的数据操作都是基于对象操作的方式,比如增加一个 feed 就是在 context 中创建一个 feed 的对象,然后对其字段对应的属性进行设置即可。

删除就是用 context 的 delete 方法将对要删除数据对应的对象进行删除即可。

修改就是对读取的对象进行设置。

检索有两种方式,一种是创建一个 Controller,使用 lazy 来修饰检索检索结果,惰性加载以节省内存。数据变化会在 NSFetchedResultsController 代理里进行回调,在回调里可以更新 @Published 属性包装的属性以及时同步展示更新的数据。

另一种检索方式是使用 @FetchRequest 属性包装,写法更加简洁。

下面是 RSS 数据操作对应的代码。

添加 Feed 的代码

let newFeed = WebFeedMO(context: stack.context)newFeed.id = UUID()newFeed.createAt = Date.nownewFeed.homePageURL = inputURLstack.save()await handleAFeed(webFeed: newFeed) // 文章

删除 Feed

for a in webFeed.allElements {    stack.context.delete(a)}stack.deleteWebFeed(webFeed)

检索 Feed 列表

let fetch = WebFeedMO.fetchRequest()let sortDescriptorUnreadCount = NSSortDescriptor(key: "unreadCount", ascending: false)let sortDescriptorCreateAt = NSSortDescriptor(key: "createAt", ascending: false)fetch.sortDescriptors = [sortDescriptorUnreadCount, sortDescriptorCreateAt]let controller = NSFetchedResultsController(fetchRequest: fetch, managedObjectContext: stack.context, sectionNameKeyPath: nil, cacheName: "webFeeds")controller.delegate = selftry? controller.performFetch()

Feed 里文章的列表检索

let fetch = ArticleMO.fetchRequest()let sortDescriptor = NSSortDescriptor(key: "datePublished", ascending: false)fetch.sortDescriptors = [sortDescriptor]let controller = NSFetchedResultsController(fetchRequest: fetch, managedObjectContext: stack.context, sectionNameKeyPath: nil, cacheName: "newArticles")controller.delegate = selftry? controller.performFetch()

标记已读

@Published var selectedArticle: ArticleMO? {    willSet(newValue) {        newValue?.read = true        selectedWebFeed?.countUnreadArticles()    }}

全部标记已读

let countElement = selectedWebFeed?.allElements.count ?? 0var index = 0for a in selectedWebFeed?.allElements ?? [] {    index += 1    if a.read == false {        a.read = true    }    if countElement > 1000 && index > 1000 && a.favourite == false {        stack.context.delete(a)    }}// 最后重置未读总数selectedWebFeed?.countUnreadArticles()

收藏状态的切换直接对布尔属性 favourite 执行 toggle 方法。

selectedArticle?.favourite.toggle()selectedArticle?.dateModified = Date.now

工具栏中的分享功能可以直接使用 SwiftUI 内置的 ShareLink 视图。Item 的 placement 对于不同平台的位置会有不同。

.toolbar {    ToolbarItemGroup(placement: .primaryAction) {        Menu {            Button { ... } label: {                Label("拷贝链接", systemImage: "doc.on.doc")            }            Divider()            ShareLink("分享", item: link)        } label: {            Image(systemName: "square.and.arrow.up")        }    }    ToolbarItemGroup(placement: .automatic) {        Button { ... } label: {            Label("收藏", systemImage: "star")        }        Button { ... } label: {            Label("浏览器", systemImage: "safari")        }    }} // end toolbar

由于网站提供的 RSS 是静态的,因此每次获取数据时需要进行和本地存储的数据进行比对去重。

Core Data 提供了一种通过简单配置约束就可以去重的方法。具体方法如上图所示。

但是如果要支持 iCloud 就没法使用唯一约束这个功能。因此只能回到老办法,手动比对。

为了提升大量数据添加的效率,可以使用 NSBatchInsertRequest。正常情况下,在使用 Core Data 进行大量数据插入时,应用程序需要为每个插入操作都创建上下文和执行请求。这样会导致上下文过度膨胀和查询操作的重复,并且会对内存和 CPU 带来负担。而 NSBatchInsertRequest 则能够通过批量插入的方式一次性将多条数据插入到 Core Data 中,并且执行速度要比逐条插入要快得多。NSBatchInsertRequest 实际上是在底层利用 SQLite 数据库的 INSERT INTO 语法来执行批量插入操作。这种方式通过一次性将数据提交给 SQLite,可以减少插入操作所需的检查、协调和锁定操作,从而提高插入操作的效率和性能。当使用 NSBatchInsertRequest 执行批量插入时,Core Data 会首先创建一个临时表,然后将待插入的数据全部插入到该临时表中。接着,Core Data 会使用关联操作将临时表中的数据一次性插入到实际的数据库表中,从而进一步提高了数据插入的效率。NSBatchInsertRequest 还提供了一些可用的参数设置选项,开发者可以根据具体的需求进行灵活配置。例如,通过设置 batchSize 参数,可以控制批量插入时每个批次所包含的最大行数,以避免内存的过度消耗;通过设置 propertiesToUpdate 参数,可以在批量插入后更新指定的属性值,从而避免对整个对象进行额外的查询和更新操作。

Core Data 里的数据可以通过 iCloud 实现多设备的同步,比如我在 macOS 上订阅、阅读和收藏的信息能够无缝切换到手机和 iPad 上。未来支持 iCloud 可以进行如下的设置:

支持 iCloud 也会有一些限制,对于我目前来说最大限制就是不支持唯一约束,另外数据表结构更改后老版本的兼容也是需要注意的,这是由于 iCloud 是云端数据统一传输,并不会兼容多版本。

通过以下方法可以让兼容合并更安全。

应用支持 iCloud 后会有 cloudd 这个后台进程对 iCloud 服务的同步和管理,定期检查 iCloud 上数据是否需要同步到本地设备,或者本地数据是否需要传到 iCloud。 apsd 进程会将数据的更新以通知的方式推送到其他设备,dasd 进程会对 iCloud 的数据进行处理然后交给应用进程。对这个流程的调试就是基于上面提到的这四个进程进行日志记录。

另外 Core Data 还支持一些调试参数,除了 iCloud 还可以支持多线程、SQL、合并等信息的日志打印。

为了节省 iCloud 空间大小,对于文章内容这样数据量大的数据就不用支持 iCloud 了,方法是如下:

另外,Core Data 里的数据还能够很容易的支持 spotlight 索引,方便在应用外能够被检索。

界面使用的是 NavigationSplitView。代码如下:

struct HomeThreeColumnView: View {    @EnvironmentObject var webFeedController: WebFeedController    var body: some View {        NavigationSplitView {            SidebarView() // 左侧频道列表        } content: {            AWebFeedArticlesView() // 文章列表        } detail: {            ArticleWebView() // 文章内容        }    } // end body}

NavigationSplitView 可以同时显示主视图和辅助视图。实现了 iOS 系统中常见的 iPad 多窗口布局模式,允许用户同时操作两个视图,提高了应用程序的多任务处理能力和用户体验。NavigationSplitView 提供了一组简洁易用的 API,开发者可以通过少量的代码实现大部分常见的多窗口布局需求。例如,只需要设置主视图和辅助视图的内容即可快速创建一个 NavigationSplitView,而无需手动管理视图控制器的层次结构。NavigationSplitView 还支持自定义视图拆分行为、边缘滑动手势等功能。

数据处理,包括 Core Data 的初始化配置和增删改和检索等我都放在了 Controller 里,Controller 的关键代码如下:

final class WebFeedController: NSObject ,ObservableObject {    @Published var selectedWebFeed: WebFeedMO?    @Published var selectedArticle: ArticleMO?        @Published private(set) var webFeeds: [WebFeedMO] = []    @Published private(set) var newArticles: [ArticleMO] = [] // 最新文章    @Published private(set) var favoriteArticles: [ArticleMO] = [] // 收藏的文章    var stack: NRCDStack        init(stack: NRCDStack) {        ...        webFeeds = fetchedResults.fetchedObjects ?? []        newArticles = fetchedNewArticlesResults.fetchedObjects ?? []        favoriteArticles = fetchFavoriteArticlesResults.fetchedObjects ?? []    }        // 获取所有 feed 源    lazy var fetchedResults: NSFetchedResultsController<WebFeedMO> = { ... }()    // 获取最新 article    lazy var fetchedNewArticlesResults: NSFetchedResultsController<ArticleMO> = { ... }()    // 获取收集 article    lazy var fetchFavoriteArticlesResults: NSFetchedResultsController<ArticleMO> = { ... }()}// MARK: - NSFetchedResultsControllerDelegate// 跟踪变化,在回调中处理。extension WebFeedController: NSFetchedResultsControllerDelegate {    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {        webFeeds = fetchedResults.fetchedObjects ?? []        newArticles = fetchedNewArticlesResults.fetchedObjects ?? []        favoriteArticles = fetchFavoriteArticlesResults.fetchedObjects ?? []    }}// MARK: - 数据 CRUD 操作extension WebFeedController {    // 更新全部 Feed    func updateAllFeeds() async { ... }        // MARK: - Feed 的操作    // 添加 Feed    @discardableResult    func createFeed(inputURL: String, nameForDisplay: String = "") -> WebFeedMO { ... }    // 删除操作,删掉一个 Feed    func deleteWebFeed(_ webFeed: WebFeedMO) {        stack.deleteWebFeed(webFeed)    }        // 更新    func updateFeedByModel(for webFeed: WebFeedMO, model: FeedModel) { ... }        // MARK: - 文章的操作    // 收藏的文章    func fetchFavoriteArticles() {        favoriteArticles = fetchFavoriteArticlesResults.fetchedObjects ?? []    }        // 最新文章    func fetchNewArticles() {        newArticles = fetchedNewArticlesResults.fetchedObjects ?? []    }        // 收藏    func favoriteArticle() {        selectedArticle?.favourite.toggle()        selectedArticle?.dateModified = Date.now    }        // 清空所选 feed 下所有文章    func deleteAll() { ... }        // 标记全部已读    func markAllAsRead() { ... }        // 新增文章    func createArticleByModel(for webFeed: WebFeedMO, model: ArticleModel) async { ... } }

应用最终效果如下图:

使用 LLVM 分享的幻灯片

作者 戴铭
2022年7月8日 13:01

我在快手中学出品,T沙龙主办的活动做了场直播分享。录播地址 T Chat | 戴铭:我在快手做移动端 Part 1:使用 LLVMT Chat | 戴铭:我在快手做移动端 Part 2:戴老师的斜杠人生

讲的内容我先前在清华大学和美团讲过。清华大学王继良教授给他的本科生开了门移动开发的课程,并邀请我给他们讲了一堂课。记得在18年 @Swift 大会我分享完了,现场有人开玩笑说要是以前大学老师能这么讲编译就好了,没想到我还实现了一次这个玩笑。

美团是他们 TC 委员会邀请的。当天小青还到了现场,完后他还给我推荐了南京大学《软件分析》课程,因为他觉得这门课是静态分析最好的入门课程,他说静态分析有个重要的点 sound/soundness,要保证分析的结果是可靠的/safe 的,我们说代码都不能删是无用结论,说都能删是错误结论。在这个过程寻找最合适的答案,不过考虑到运行的时候,那就是理论上 unsound 了(毕竟输入的参数不能证明是完整的)。小青说这个和我在分享开头说的差不多(那天时间比较充裕,我说的稍多了些)。小青最近也写了篇《超棒的课程推荐:南大《软件分析》(附我的完整学习路线)》,可以观摩学习学习。

这次 Apple 教育团队负责人还拉了高校同学来看,快手客户端校招简历投递信箱是 cuijia03@kuaishou.com 。下面是这次分享内容的幻灯片:

幻灯片原图我也上传了网盘,链接: https://pan.baidu.com/s/1Bip4mK5_B4cofnCAj7gpwg?pwd=4s7m 提取码: 4s7m

示例代码可参看文章《使用 LLVM》

昨天有个推荐资料的问题,我还有个要补充下,每年老司机技术周刊在小专栏出品的 WWDC 内参,尤其是今年《WWDC22 内参》 ,用浙剑跟我说的“今年我们做的很用心,每篇文章都保证了至少两个审核来保证质量。一个审核从专业性角度看内容是否正确,另外一位审核从读者视角看只是是否正确引导。目前发布的每一篇都质量非常高,大部分内容都已经超越视频原本的内容了。”

买买买吧。

使用 SwiftUI 开发 RSS 阅读器

作者 戴铭
2023年4月24日 15:51

在 Apple 加速器活动和字节内分享了使用 SwiftUI 做 RSS 阅读器的一点心得。可能你还不知道什么是 RSS 阅读器,简单来说 RSS 是一些博客和新闻网站,甚至是播客和视频平台发布他们的内容更新的一种 XML 格式,阅读器就是通过请求这个 XML 以获取他们内容更新的客户端。

这就有了接下来几个问题:

目前已有 Reeder 和 NetNewsWire 等 RSS 阅读器,那么为什么还要再开发一个呢,早在14年我曾做过一个,陆续也更新过,后来还是以 Reeder 作为主力,feedly 作为服务,后来 feedly 有些不稳定,我又改成本地获取 feed 的方式,但是改成本地模式后设备同步又成了问题。正好最近几年苹果在界面、数据流和存储上都做了很大的功能加强。于是我打算将以前 objc、rac和 FMDB 替换成 SwiftUI 和 CoreData 技术,同时补上以前缺少的一些功能,比如添加管理feed,不同设备同步订阅 feed、文章已读状态和收藏信息等功能。

先说下怎么订阅 RSS。

如上图所示先通过链接获取待解析的数据,以及 mimeType,通过 mimeType 看里面是否包含如下描述:

application/atom+xmlapplication/rss+xmlapplication/jsonapplication/feed+jsontext/xmlapplication/xml

包含的话就可以判断是 RSS。

如果不是的话就需要手动从网页里获取 RSS 的链接,方法如下:

mime.contains("text/html")SwiftSoup.parse(homepageHTML)htmlDom.select("link[rel=alternate]")

其中 SwiftSoup 是一个专门用来将 HTML 解析成 DOM 对象的库。一般 RSS 的链接会在属性键值是 rel 和 alternate 的 Link 这个标签里。但是很多网站并没有遵循这个规范,那么就需要在链接后直接通过添加以下文件名来查找哪个是它的 RSS 链接:

["feed.xml","rss.xml","atom.xml","feed","feed.rss","rss","index.xml"]

找到了 RSS 的链接就可以获取到它的数据,接下来就是对数据的处理,根据 RSS 的规范,RSS 的数据主要是以下三种。

对应的结构体如下:

RSS 的图标的获取方式有两种

对处理好的数据需要进行本地的存储,目前不管是 Apple 还是三方库主要都是基于 SQLite 的封装。估计是因为 SQLite 开销小,支持大多数 SQL 92 标准语法,采用标准的 ANSI-C 代码,很容易在多个平台运行,同时 SQLite 还支持所有 SQL 用来保障数据安全和完整性的事务属性,比如原子性、一致性、隔离性和持久性。以下是 iOS 上一些主要基于 SQLite 封装库:

我选择的是 Core Data,首先是 Core Data 的 API 很强,将复杂数据建模和操作的 SQL 语句都做成了可视化和对象模式操作。多个数据对象之间的关联关系也做了很多自动处理。Core Data 还使用了惰性加载的方式,只有在需要时才从存储区域获取数据,以节省内存,提高执行效率。

Core Data 的使用需要对数据库进行设置。

在读取实体存储时可以设置 Core Spotlight 以及进行一些调试测试工作。

Core Data 对数据的增删改和检索操作都是在 NSManagerObjectContext 中完成的。

如果要支持 CloudKit,NSManagerObjectContext 初始化时需要在合并策略做一些设置。context 的数据操作都是基于对象操作的方式,比如增加一个 feed 就是在 context 中创建一个 feed 的对象,然后对其字段对应的属性进行设置即可。

删除就是用 context 的 delete 方法将对要删除数据对应的对象进行删除即可。

修改就是对读取的对象进行设置。

检索有两种方式,一种是创建一个 Controller,使用 lazy 来修饰检索检索结果,惰性加载以节省内存。数据变化会在 NSFetchedResultsController 代理里进行回调,在回调里可以更新 @Published 属性包装的属性以及时同步展示更新的数据。

另一种检索方式是使用 @FetchRequest 属性包装,写法更加简洁。

下面是 RSS 数据操作对应的代码。

添加 Feed 的代码

let newFeed = WebFeedMO(context: stack.context)newFeed.id = UUID()newFeed.createAt = Date.nownewFeed.homePageURL = inputURLstack.save()await handleAFeed(webFeed: newFeed) // 文章

删除 Feed

for a in webFeed.allElements {    stack.context.delete(a)}stack.deleteWebFeed(webFeed)

检索 Feed 列表

let fetch = WebFeedMO.fetchRequest()let sortDescriptorUnreadCount = NSSortDescriptor(key: "unreadCount", ascending: false)let sortDescriptorCreateAt = NSSortDescriptor(key: "createAt", ascending: false)fetch.sortDescriptors = [sortDescriptorUnreadCount, sortDescriptorCreateAt]let controller = NSFetchedResultsController(fetchRequest: fetch, managedObjectContext: stack.context, sectionNameKeyPath: nil, cacheName: "webFeeds")controller.delegate = selftry? controller.performFetch()

Feed 里文章的列表检索

let fetch = ArticleMO.fetchRequest()let sortDescriptor = NSSortDescriptor(key: "datePublished", ascending: false)fetch.sortDescriptors = [sortDescriptor]let controller = NSFetchedResultsController(fetchRequest: fetch, managedObjectContext: stack.context, sectionNameKeyPath: nil, cacheName: "newArticles")controller.delegate = selftry? controller.performFetch()

标记已读

@Published var selectedArticle: ArticleMO? {    willSet(newValue) {        newValue?.read = true        selectedWebFeed?.countUnreadArticles()    }}

全部标记已读

let countElement = selectedWebFeed?.allElements.count ?? 0var index = 0for a in selectedWebFeed?.allElements ?? [] {    index += 1    if a.read == false {        a.read = true    }    if countElement > 1000 && index > 1000 && a.favourite == false {        stack.context.delete(a)    }}// 最后重置未读总数selectedWebFeed?.countUnreadArticles()

收藏状态的切换直接对布尔属性 favourite 执行 toggle 方法。

selectedArticle?.favourite.toggle()selectedArticle?.dateModified = Date.now

工具栏中的分享功能可以直接使用 SwiftUI 内置的 ShareLink 视图。Item 的 placement 对于不同平台的位置会有不同。

.toolbar {    ToolbarItemGroup(placement: .primaryAction) {        Menu {            Button { ... } label: {                Label("拷贝链接", systemImage: "doc.on.doc")            }            Divider()            ShareLink("分享", item: link)        } label: {            Image(systemName: "square.and.arrow.up")        }    }    ToolbarItemGroup(placement: .automatic) {        Button { ... } label: {            Label("收藏", systemImage: "star")        }        Button { ... } label: {            Label("浏览器", systemImage: "safari")        }    }} // end toolbar

由于网站提供的 RSS 是静态的,因此每次获取数据时需要进行和本地存储的数据进行比对去重。

Core Data 提供了一种通过简单配置约束就可以去重的方法。具体方法如上图所示。

但是如果要支持 iCloud 就没法使用唯一约束这个功能。因此只能回到老办法,手动比对。

为了提升大量数据添加的效率,可以使用 NSBatchInsertRequest。正常情况下,在使用 Core Data 进行大量数据插入时,应用程序需要为每个插入操作都创建上下文和执行请求。这样会导致上下文过度膨胀和查询操作的重复,并且会对内存和 CPU 带来负担。而 NSBatchInsertRequest 则能够通过批量插入的方式一次性将多条数据插入到 Core Data 中,并且执行速度要比逐条插入要快得多。NSBatchInsertRequest 实际上是在底层利用 SQLite 数据库的 INSERT INTO 语法来执行批量插入操作。这种方式通过一次性将数据提交给 SQLite,可以减少插入操作所需的检查、协调和锁定操作,从而提高插入操作的效率和性能。当使用 NSBatchInsertRequest 执行批量插入时,Core Data 会首先创建一个临时表,然后将待插入的数据全部插入到该临时表中。接着,Core Data 会使用关联操作将临时表中的数据一次性插入到实际的数据库表中,从而进一步提高了数据插入的效率。NSBatchInsertRequest 还提供了一些可用的参数设置选项,开发者可以根据具体的需求进行灵活配置。例如,通过设置 batchSize 参数,可以控制批量插入时每个批次所包含的最大行数,以避免内存的过度消耗;通过设置 propertiesToUpdate 参数,可以在批量插入后更新指定的属性值,从而避免对整个对象进行额外的查询和更新操作。

Core Data 里的数据可以通过 iCloud 实现多设备的同步,比如我在 macOS 上订阅、阅读和收藏的信息能够无缝切换到手机和 iPad 上。未来支持 iCloud 可以进行如下的设置:

支持 iCloud 也会有一些限制,对于我目前来说最大限制就是不支持唯一约束,另外数据表结构更改后老版本的兼容也是需要注意的,这是由于 iCloud 是云端数据统一传输,并不会兼容多版本。

通过以下方法可以让兼容合并更安全。

应用支持 iCloud 后会有 cloudd 这个后台进程对 iCloud 服务的同步和管理,定期检查 iCloud 上数据是否需要同步到本地设备,或者本地数据是否需要传到 iCloud。 apsd 进程会将数据的更新以通知的方式推送到其他设备,dasd 进程会对 iCloud 的数据进行处理然后交给应用进程。对这个流程的调试就是基于上面提到的这四个进程进行日志记录。

另外 Core Data 还支持一些调试参数,除了 iCloud 还可以支持多线程、SQL、合并等信息的日志打印。

为了节省 iCloud 空间大小,对于文章内容这样数据量大的数据就不用支持 iCloud 了,方法是如下:

另外,Core Data 里的数据还能够很容易的支持 spotlight 索引,方便在应用外能够被检索。

界面使用的是 NavigationSplitView。代码如下:

struct HomeThreeColumnView: View {    @EnvironmentObject var webFeedController: WebFeedController    var body: some View {        NavigationSplitView {            SidebarView() // 左侧频道列表        } content: {            AWebFeedArticlesView() // 文章列表        } detail: {            ArticleWebView() // 文章内容        }    } // end body}

NavigationSplitView 可以同时显示主视图和辅助视图。实现了 iOS 系统中常见的 iPad 多窗口布局模式,允许用户同时操作两个视图,提高了应用程序的多任务处理能力和用户体验。NavigationSplitView 提供了一组简洁易用的 API,开发者可以通过少量的代码实现大部分常见的多窗口布局需求。例如,只需要设置主视图和辅助视图的内容即可快速创建一个 NavigationSplitView,而无需手动管理视图控制器的层次结构。NavigationSplitView 还支持自定义视图拆分行为、边缘滑动手势等功能。

数据处理,包括 Core Data 的初始化配置和增删改和检索等我都放在了 Controller 里,Controller 的关键代码如下:

final class WebFeedController: NSObject ,ObservableObject {    @Published var selectedWebFeed: WebFeedMO?    @Published var selectedArticle: ArticleMO?        @Published private(set) var webFeeds: [WebFeedMO] = []    @Published private(set) var newArticles: [ArticleMO] = [] // 最新文章    @Published private(set) var favoriteArticles: [ArticleMO] = [] // 收藏的文章    var stack: NRCDStack        init(stack: NRCDStack) {        ...        webFeeds = fetchedResults.fetchedObjects ?? []        newArticles = fetchedNewArticlesResults.fetchedObjects ?? []        favoriteArticles = fetchFavoriteArticlesResults.fetchedObjects ?? []    }        // 获取所有 feed 源    lazy var fetchedResults: NSFetchedResultsController<WebFeedMO> = { ... }()    // 获取最新 article    lazy var fetchedNewArticlesResults: NSFetchedResultsController<ArticleMO> = { ... }()    // 获取收集 article    lazy var fetchFavoriteArticlesResults: NSFetchedResultsController<ArticleMO> = { ... }()}// MARK: - NSFetchedResultsControllerDelegate// 跟踪变化,在回调中处理。extension WebFeedController: NSFetchedResultsControllerDelegate {    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {        webFeeds = fetchedResults.fetchedObjects ?? []        newArticles = fetchedNewArticlesResults.fetchedObjects ?? []        favoriteArticles = fetchFavoriteArticlesResults.fetchedObjects ?? []    }}// MARK: - 数据 CRUD 操作extension WebFeedController {    // 更新全部 Feed    func updateAllFeeds() async { ... }        // MARK: - Feed 的操作    // 添加 Feed    @discardableResult    func createFeed(inputURL: String, nameForDisplay: String = "") -> WebFeedMO { ... }    // 删除操作,删掉一个 Feed    func deleteWebFeed(_ webFeed: WebFeedMO) {        stack.deleteWebFeed(webFeed)    }        // 更新    func updateFeedByModel(for webFeed: WebFeedMO, model: FeedModel) { ... }        // MARK: - 文章的操作    // 收藏的文章    func fetchFavoriteArticles() {        favoriteArticles = fetchFavoriteArticlesResults.fetchedObjects ?? []    }        // 最新文章    func fetchNewArticles() {        newArticles = fetchedNewArticlesResults.fetchedObjects ?? []    }        // 收藏    func favoriteArticle() {        selectedArticle?.favourite.toggle()        selectedArticle?.dateModified = Date.now    }        // 清空所选 feed 下所有文章    func deleteAll() { ... }        // 标记全部已读    func markAllAsRead() { ... }        // 新增文章    func createArticleByModel(for webFeed: WebFeedMO, model: ArticleModel) async { ... } }

应用最终效果如下图:

使用 LLVM 分享的幻灯片

作者 戴铭
2022年7月8日 13:01

我在快手中学出品,T沙龙主办的活动做了场直播分享。录播地址 T Chat | 戴铭:我在快手做移动端 Part 1:使用 LLVMT Chat | 戴铭:我在快手做移动端 Part 2:戴老师的斜杠人生

讲的内容我先前在清华大学和美团讲过。清华大学王继良教授给他的本科生开了门移动开发的课程,并邀请我给他们讲了一堂课。记得在18年 @Swift 大会我分享完了,现场有人开玩笑说要是以前大学老师能这么讲编译就好了,没想到我还实现了一次这个玩笑。

美团是他们 TC 委员会邀请的。当天小青还到了现场,完后他还给我推荐了南京大学《软件分析》课程,因为他觉得这门课是静态分析最好的入门课程,他说静态分析有个重要的点 sound/soundness,要保证分析的结果是可靠的/safe 的,我们说代码都不能删是无用结论,说都能删是错误结论。在这个过程寻找最合适的答案,不过考虑到运行的时候,那就是理论上 unsound 了(毕竟输入的参数不能证明是完整的)。小青说这个和我在分享开头说的差不多(那天时间比较充裕,我说的稍多了些)。小青最近也写了篇《超棒的课程推荐:南大《软件分析》(附我的完整学习路线)》,可以观摩学习学习。

这次 Apple 教育团队负责人还拉了高校同学来看,快手客户端校招简历投递信箱是 cuijia03@kuaishou.com 。下面是这次分享内容的幻灯片:

幻灯片原图我也上传了网盘,链接: https://pan.baidu.com/s/1Bip4mK5_B4cofnCAj7gpwg?pwd=4s7m 提取码: 4s7m

示例代码可参看文章《使用 LLVM》

昨天有个推荐资料的问题,我还有个要补充下,每年老司机技术周刊在小专栏出品的 WWDC 内参,尤其是今年《WWDC22 内参》 ,用浙剑跟我说的“今年我们做的很用心,每篇文章都保证了至少两个审核来保证质量。一个审核从专业性角度看内容是否正确,另外一位审核从读者视角看只是是否正确引导。目前发布的每一篇都质量非常高,大部分内容都已经超越视频原本的内容了。”

买买买吧。

WWDC22 笔记

作者 戴铭
2022年6月10日 12:13

第一天

今年是 WWDC 的第39个年头了。今年的 WWDC.playground 活动()是 SwiftGG、T 沙龙和老司机技术一起会和社区开发者们一起聊聊这次 WWDC。WWDC.playground 活动在节日期间每天都会有直播,我会和 61、13 他们参加 6月11日晚上8点那场直播。现在那场直播的录播已经放了出来,地址是 WWDC22.playground - Day 5:回顾 WWDC22

下面我整理了一份今年 WWDC 的指南,也算提供个方便的入口吧。

  1. WWDC22 直播地址微博直播WWDC22 YouTube 地址
  2. Apple WWDC22 页面
  3. Apple WWDC22 指南
  4. Apple Developer app 观看 Session 的 Apple 出的 App。
  5. Session 网页版
  6. Digital Lounge 注册感兴趣的主题,到时候就可以和 Apple 工程师在 Slack 上一起看 Session,交流。
  7. Labs 可以获得和 Apple 专家一对一指导。6号 keynote 完后就可以开始预约。
  8. Beyond WWDC22 和去年一样,这里是 Apple 制作的世界各地的社区活动。
  9. weak self Discord WWDC22 Keynote Watch Party 全球最多听众的 iOS 中文 Podcast 之一 weak self 的活动。
  10. Swiftly Rush WWDC22
  11. iOS Feeds 的 WWDC 2022 新闻聚合
  12. WWWDC.io App 社区的看 Session 的 App。
  13. Keynote 后的 Platforms State of the Union 这个主题是对后面一周 Session 的总结,开发者可以重点关注下。
  14. WWDC Notes 汇聚了大家的 Session 笔记,可以快速看到各个 Session 的重点。
  15. Technologies 这里是 Apple 框架 API 分类地址,看完 Session 可以直接在这里找对应 API 的更新。还有个网站 Apple Platform SDK API Differences 会列出新 SDK 里有哪些框架更新了。
  16. Apple Design Awards 提名作品

Apple Design Awards 提名作品,我先列几个我喜欢的:

  1. procreate
  2. Wylde Flowers
  3. 笼中窥梦
  4. Gibbon: Beyond the Trees
  5. Vectornator: Design Software
  6. Wylde Flowers
  7. Behind the Frame
  8. MD Clock - Live in the present
  9. 专注面条
  10. Townscaper

第二天

今天最让我印象深刻是 M2、Lock screen widgets、Stage manager、Swift Charts、WeatherKit、SwiftUI Navigation API、只要一个 1024x1024 App Icon、Sticky headers on Xcode scrolling、Xcode View Debugger 可以用于 SwiftUI 了,还有 iOS 16 原生的支持 Nintendo Switch Pro 手柄了。

后面我将更多内容使用点对点的分发,可以用 Planet 关注,我的 IPNS 是:k51qzi5uqu5dlorvgrleqaphsd1suegn8w40xwhxl0bgsyxw3zerivt59xbk74

Keynote 要点:

  • iOS 16
    • new lock screen
    • live activities
    • extend focus to lock screen
    • forcus filter for apps
    • dictation improvements
    • live text in video
    • visual lookup
    • maps
      • multistop routing
      • transit(add card to wallet)
      • new details
      • lookaround api
    • iCloud shared photo library
    • persanalized spatial audio
    • quick notes on iPhone
    • fitness app without watch
    • messages
      • edit messages
      • delete messages
      • mark as unread
      • share play
    • pay
      • tap to pay on iPhone
      • order tracking
    • carplay
      • widgets
      • more personalization
      • multi-screen
    • safety check
      • quickly remove access for others
    • home
      • introduce matter as new standard
      • redesign of app
  • M2
    • 15.8 trillion operations per seconds
    • 10-core GPU
    • macbook air and macbook pro 13”
    • better and faster
    • silent design
    • fast charge
    • new colors
    • magsafe
    • audio jack
  • macOS Ventura
    • improved spotlight
    • undo send and more
    • shared tab groups
    • passkeys
    • desk view
    • stage manager
    • continuity for facetime
    • use iPhone as camera on macbook
  • iPadOS 16
    • weather app
    • WeatherKit
    • collaborations api
    • freeform board
    • stage manager
  • WatchOS 9
    • four new watch faces
    • new ShareKit api
    • improved metrics for running
    • heart rate zones
    • create custom workouts

重要的几个信息:

大赞的库:

好用的功能和组件:

一些方便上手的例子:

一些感兴趣的 Session:

第三天


WWDC.playground 很精彩,怎么感觉昨天的 WWDC.playground 像是听了一期枫言枫语呢。预感 11 号可能会变成为一期 weak self 呢。

昨天老司机还整理了份 WWDC22 Session 观看介绍的列表

Apple 出的内容看不够的话,可使用 Follow WWDC 2022 News! 来看最新的 WWDC 相关的社区文章。

下面是我今天的一些记录。

Xcode

代码补全的更新。以前多个可选参数的体验很差,这次输入参数比如 frame 里的 maxWidth,会只显示当前要补全的参数。而且速度快了很多。

以前是编完源码再生成 module,然后 link编好的文件,最后再 link。现在整个过程改成并行执行,同时 link 还快了两倍。结果是比以前快了25%,核越多效果越明显。还有可可视化整个过程。

多平台以前是多个 tagets,现在是在一个 target 里管理。

Hangs 是官方线上主线程被卡了的检查工具,在 Organizer 里查看对应问题堆栈也很方便。

当然最爱的还是 sticky headers,秒杀其它编辑器 (虽然我还是觉得 Emacs 最好,由于会暴露年龄,一般我都不说)。

还有内存也好了很多,总体来说,这次 Xcode 更新很棒。

完整 Xcode release notes

WidgetKit

WidgetKit 将 WatchOS 上的 Circular、Rectangle 还有 Inline 带到了 iOS 和其他平台。

WeatherKit

安全方便获得用户位置信息,只用于天气。

VisionKit

Live Text API,感觉这类库都是为了以后出眼镜做铺垫的。

macOS

macOS 支持window,menuBar也支持了。

Swift

distrubuted actor 更安全,还可以在设备间(本地设备<->本地设备本地设备<->服务器)进行通信保护。

泛型新语法 some 和 any 关键字写起来真的简化了很多。

Swift 的更新了什么,除了 Session 外,还可以参看 Paul Hudson 这篇文章 What’s new in Swift 5.7 ,还有 Donny Wals 的这篇 What’s the difference between any and some in Swift 5.7?

SwiftUI

SwiftUI里没有用属性包装的属性也能够和视图变化绑定了。

关于 SwiftUI 的更新,Paul Hudson 写了很多例子 What’s new in SwiftUI for iOS 16

Reda Lemeden 整理了 WWDC22 SwiftUI 的所有相关内容 SwiftUI @ WWDC 2022 。可见社区对 SwiftUI 热情依然是最高的。

SPM

Swift Package Plugin,本来用其他语言,比如 ruby 、python 或 shell 做的事情,现在可以通过 Swift 语言来完成了,写的 plugin 还可以方便的在 Xcode 中使用。

虚机

使用 Virtualization 框架,享受 Rosetta 2 的优势,运行 x86-64 Linux 系统。

Apple 出虚机可运行 Linux 系统这点可以看得出 Apple 对开源的拥抱,原因还有一点是 Swift 也可以用在 Linux 服务器上了,Apple 用心良苦,也是想让开发者用本打算买其它硬件的钱来买 Apple 的硬件吧,更好的榨干 Apple 硬件过于优秀的性能,如同新出 Stage Manager 通过投到大屏来榨干 M1 的 iPad 性能。 不光是这样,还有文件,也就是存储设备也只需要一份了,更方便,还有苹果特有的 Trackpad 和 Magic mouse 也能够用于 Linux 系统中。

虚机运行 Linux 和 macOS 的区别是,启动 Linux 使用的是 EFI Boot Loader 来加载 Linux 文件,VirtioGraphicDevice 进行 Linux 系统图形界面的设置和渲染。使用Rosetta 运行 Linux 系统,运行 Linux 就是比其它虚机要快。

介绍的 session Create macOS or Linux virtual machines ,代码说明 Running GUI Linux in a virtual machine on a Mac,相关主题 Virtualization

第四天

今晚五神会现身 WWDC.playground 。内容涉及 SwiftUI 和 AR,不要错过。

今日零散记录

从 Apple 推出 WeatherKit 可以看出,Apple 喜欢把关键和有想象空间盈利价值的技术掌握在自己手上,WeatherKit 提供大量数据,包括分钟、小时、每日预报,还有提前警报,这些信息的商业价值本就很大。

今天看了 WeatherKit、Swift Chart 还有 SwiftUI 的 Layout,感觉 Apple 的接口设计能力很值得学习,可能具备了这些能力才能更好地沟通。

swift-algorithms 可以使用 .indexed() 来替代 zip。

Federico Zanetello 对 Platforms State of the Union 这个 Session 做的笔记

应用层面,今天还有好多 Swift Chart 的介绍。

Layout

Grid、Layout、ViewThatFits、AnyLayout,特别是 Grid 还统一了 HStack 和 VStack。这些布局方式,让先前复杂的要借助 GeometryReader,且容易出错的布局有了更易的写法。Layout 协议可以为 layout 创建自定义属性,另外布局计算也会被缓存。

Link

Link fast: Improve build and launch time 详细讲了 Apple 今年怎么改进了 link,思路很棒,很值得学习。

Static linking 和 Dynamic linking ,也就是静态链接和动态链接。

静态链接就是链接各个编译好的源文件以及链接源文件和编译好的库文件,通过将函数名放到符号表,链接新文件时确定先前是否有包含的 undefined 符号,给函数的数据指令分配地址,最后生成一个有 TEXT、DATA、LINKEDIT 段的可执行文件。

今年 Apple 通过利用多核优势让静态链接快了两倍。

具体做法是,并行的拷贝文件内容。并行构建 LINKEDIT 段的各个不同部分。并行改变 UUID 计算和 codesigning 哈希。然后是提高 exports-trie 构建器的算法。使用最新的 Crypto 库利用硬件加速的优势加速 UUID 计算。提高其它静态库处理算法库,debug-notes 生成也更快了。

Apple 推荐静态库最佳实践是:

使用 -all_load-force_load 可以让 .a 文件像 .o 文件那样并行处理,不过开启这个选项需要先处理重复的符号。另外一个副作用是会将一些被判断无用的代码也被链接进来,使包体变大,因此开启之前可以先使用静态分析工具分析处理,这个过程定期做就行,不用放到每次编译过程中。演讲者推荐使用 -dead_strip 选项,但是这样做并没有真实去掉费代码,以后这些代码还是会被编译分析,如果只是暂时不用,可以先注释掉。

使用 -no_exported_symbols 选项。链接器生成的 LINKEDIT 段的一部分是 exports trie,这是一个前缀树,对所有导出的符号名称、地址和标志进行编码。动态库 是会导出符号的,但运行的二进制文件其实是不用这些符号的,因此可以用 -no_exported_symbols 选项来跳过 LINKEDIT 中 trie 数据结构的创建,这样链接起来就快多了。如果程序导出符号是一百万个,这个选项就可以减少 2 到 3 秒的时间。但需要注意的是,如果要加载插件链接回主程序就需要所有的导出的 trie 数据,无法用这个选项。

另外一个是 -no_deduplicate 选项。先前 Apple 给链接器加了个 pass 用来合并函数的指令相同,函数名不相同,这个 pass 会对每个函数的指令进行递归散列,用这种方式来找重复指令,这样做比较费 CPU,由于调试时其实是不需要关注包大小,因此可以加上 -no_deduplicate 选项来跳过这个 pass。

这些选项在 Xcode 的 Other Linker Flags 里进行设置即可。

动态库也就是 dylib,其它平台就是 DSO 或 DLL。 动态链接器不是将代码从库里考到主二进制里,而是记录某种承诺,记录从动态库中使用符号名称,还有库路径。这样做好处就是好复用动态库,不用拷贝多份。虚拟内存看到多进程使用相同动态库,就会重新给这个动态库用相同的物理内存页。

动态库好处是构建快了,启动加载慢了,多个动态库不光要加载,还要在启动时链接。也就是把链接成本从本地构建换到了用户启动时。动态库还有个缺点是基于动态库的程序会有更多的 dirty 页,因为静态链接时会把全局数据放到主程序同一个 DATA 页中,动态库的话,每个都在自己的 DATA 页中。

动态库工作的原理是,可执行的二进制会有不同权限的段,至少会有 TEXT、DATA 和 LINKEDIT。分段总是操作系统页大小的倍数。TEXT 段有执行的权限,CPU 可以将页上的字节当做机器代码指令。运行时,dyld 会根据每个段权限将可执行文件 mmap() 到内存,这些段是页大小和页对齐的,虚拟内存系统可以直接将程序或动态库文件设置为 VM 范围的备份存储。在这些页的内存访问前是不会被加载到 RAM 里,就会触发一个页 fault,导致 VM 去读取文件的子范围,将内存填充到需要 RAM 页中。光映射不够,还要用某种方式“wired up”或绑到动态库上。比如要调用动态库上的某个函数,会转换成调用 site,调用 site 成为一个在相同 TEXT 段合成的 sub 的调用,相对地址在构建时就知道了,就意味着可以正确的形成 BL 指令。这样做的好处是,stub 从 DATA 加载一个指针并跳到对应的位置,不用在运行时修改 TEXT 段,dyld 只在运行时改 DATA 段。dyld 所进行的修改很简单,就是在 DATA 段里设置了一个指针而已。

当 dyld 或应用程序的指针指向自己时要 rebase,ASLR 使 dyld 以随机地址加载动态库,内部指针不能在构建时设置,dyld 在启动时 rebase 这些指针,磁盘上,如果动态库在地址零出被加载,这些指针包含它们的目标地址。LINKEDIT 需要记录的就是每个重定位的位置。然后,dyld 只需将动态库的实际加载地址添加到每个 rebase 位置。还有种修改方式是绑定,绑定就是符号引用,符号存储在 LINKEDIT 中,dyld 在动态库的 exports tire 中找实际地址,然后 dyld 将该值存储在绑定指定的位置。

今年 Apple 发布了一个新的修改方式 chained fixups。较前面两种的优势就是可以使 LINKEDIT 更小。新格式只存储每个 DATA 页中第一个 fixup 位置和一个导入的符号列表。其它信息编码到 DATA 段。iOS 13.4 就开始支持了。

下面先说下 dyld 原理介绍。

dyld 从主可执行文件开始,解析 mach-o 找依赖动态库,对动态库进行 mmap()。然后对每个动态库进行遍历并解析 mach-o 结构,根据需要加载其它动态库。加载完毕,dyld 会查找所有需要绑定符号,并在修改时使用这些地址。最后修改完,dyld 自下而上运行初始化程序。先前做的优化是只要程序和动态库,dyld 很多步骤都可以在首次启动时被缓存。

今年 Apple 做了更多的优化,这个优化叫 page-in linking,就是 dyld 在启动时做的 DATA 页面修改放到 page-in 时,也可以理解为懒修改。以前,在 mmap() 区域的某些页面中第一次使用某些地址会触发内核读入该页面。现在如果它是一个数据页,内核会应用改页需要的修改。这种机制减少了 dirty 内存和启动时间。意味着 DATA_CONST 也是干净的,可以像 TEXT 页一样被 evicted 和重新创建,以减少内存压力。需要注意的是 page-in linking 只用于启动,dlopen() 不支持。你看,Apple 优化启动的思路也是按需加载。

Apple 还提供了追踪 dyld 运行情况的 dyld_usage 工具。检查磁盘和 dyld 缓存中的二进制文件的 dyld_info 工具。

今日推荐 Session

除了 link 外,还有 Meet distributed actors in Swift 也是比看的,Mike Ash 和 Doug Gregor 一年的心血就在这了。

第五天

性能

性能的 Improve app size and runtime performance Session 值得一看。

今年苹果通过更有效的检查 Swift 协议,使 OC 消息发送调用更小,使 autorelease elision 更快更小这几个个方面来让 App 体积更小,性能更高。

Swift 协议检查。

一个协议通过 as 操作符检查传递值是否符合协议,这种检查会在编译器的构建时间被优化掉,所以往往需要在运行时借助之前计算协议检查元数据来看对象是否真的符合了协议。一些元数据是在编译时建的,但还有很多元数据只能在启动时建立,特别是使用泛型时。协议多了,会增加耗时,差不多会多一半启动时间。

今年 Apple 推出新的 Swift 运行时,可以提前计算 Swift 协议元数据,作为 App 可执行文件和它在启动时使用的任何动态库的 dyld 闭包的一部分。这个是在系统上的,因此,只要是使用了今年最新系统的 App 都会享受这个优化,可以理解为,新系统上启动老 App 也会快些。

消息发送。

Xcode 14 中新的编译器和链接器已经将 ARM64 的消息发送调用从 12 字节减少到 8 字节。因此如果你的 App 都是 OC 代码的话,使用 Xcode 14 编出来的二进制文件可以少 2%。老系统也有效。

使用 objc_stubs_small 选项可以只优化大小,获得最大的大小优化。objc_msgSend 调动有 8 个字节指令,也就是2个指令是专门用来准备 selector 的,对于任何特定的 selector,总是相同的代码,由于始终是相同的代码,那么就可以对其共享,每个 selector 只 emit 一次,而不是每次发送消息时都 emit。共享这段代码地方是一个叫 selector stub 的函数。

ARC 会在编译器插入大量的 c 的 retain/release 函数调用。这些调用遵守平台应用二进制接口(ABI)所定义的 c 语言 call convention。也就意味着我们要更多代码来完成这些调用,用来传递正确寄存器的指针。Apple 今年推出了自定义的 call convention 根据指针位置,适时使用正确变量而不用移动它,从而摆脱了调用里的多余代码。Apple 果然是坚持用户体验优先,为了更好体验不惜修改 c 的 ABI。

autorelease elision 。

App 今年对 objc 运行时进行了修改,使 autorelease elision 更小更快。deployment target 为 iOS 16 今年新系统时才可享用哦。

Apple 怎么做的呢?

ARC 在调用方插入一个 retain,在被调用的函数中插入一个 release。当我们返回我们的临时对象时,我们需要在函数中先释放它,因为它要离开 scope。在它还没有任何其它引用时还不能这么做,不然返回前他就会被销毁。Apple 现在使用一个新的 convention ,让其可以返回临时对象。做法是当返回一个自动释放值,编译器会发出一个特殊标记,这个标记会告诉运行时这是符合自动释放条件的。它的后面是 retain,我们会在后面执行。获取返回地址,也就是一个指针,将它先保存起来,然后离开运行时的自动释放调用。在运行时,可以将保留时得到的指正和先前做自动释放时保存的指针进行比较,这样标记指令不再是数据之间的比较,比较指针内存访问少。比较成功就可以省去 autorelease/retain。

autorelease elision 的优化同样也可以减少 2% 大小。感谢 Apple 为了用户和开发者 OKR 的付出。

SwiftUI

new navigation api,看完感觉我做的小册子还有幻灯应用要花些时间好好改改了。

接下来,有活干了。

WWDC.playground

明天的 WWDC.playground 嘉宾有谜底科技和 weak self,欢迎来捧场。

下面是按分类做的记录:

Swift

String Index 大升级 String Index Overhaul

参考

Regex

标准库多了个 Regex<Output> 类型,Regex 语法与 Perl、Python、Ruby、Java、NSRegularExpression 和许多其他语言兼容。可以用 let regex = try! Regex("a[bc]+")let regex = /a[bc]+/ 写法来使用。SE-0350 Regex Type and Overview 引入 Regex 类型。SE-0351 Regex builder DSL 使用 result builder 来构建正则表达式的 DSL。SE-0354 Regex Literals 简化的正则表达式。SE-0357 Regex-powered string processing algorithms 提案里有基于正则表达式的新字符串处理算法。

RegexBuilder 文档

session Meet Swift RegexSwift Regex: Beyond the basics

Regex 示例代码如下:

let s1 = "I am not a good painter"print(s1.ranges(of: /good/))do {    let regGood = try Regex("[a-z]ood")    print(s1.replacing(regGood, with: "bad"))} catch {    print(error)}print(s1.trimmingPrefix(/i am /.ignoresCase()))let reg1 = /(.+?) read (\d+) books./let reg2 = /(?<name>.+?) read (?<books>\d+) books./let s2 = "Jack read 3 books."do {    if let r1 = try reg1.wholeMatch(in: s2) {        print(r1.1)        print(r1.2)    }    if let r2 = try reg2.wholeMatch(in: s2) {        print("name:" + r2.name)        print("books:" + r2.books)    }} catch {    print(error)}

使用 regex builders 的官方示例:

// Text to parse:// CREDIT  03/02/2022  Payroll from employer     $200.23// CREDIT  03/03/2022  Suspect A           $2,000,000.00// DEBIT   03/03/2022  Ted's Pet Rock Sanctuary    $2,000,000.00// DEBIT   03/05/2022  Doug's Dugout Dogs      $33.27import RegexBuilderlet fieldSeparator = /\s{2,}|\t/let transactionMatcher = Regex {  /CREDIT|DEBIT/  fieldSeparator  One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) // 👈🏻 we define which data locale/timezone we want to use  fieldSeparator  OneOrMore {    NegativeLookahead { fieldSeparator } // 👈🏻 we stop as soon as we see one field separator    CharacterClass.any  }  fieldSeparator  One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US")))}

在正则表达式中捕获数据,使用 Capture:

let fieldSeparator = /\s{2,}|\t/let transactionMatcher = Regex {  Capture { /CREDIT|DEBIT/ } // 👈🏻  fieldSeparator  Capture { One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) } // 👈🏻  fieldSeparator  Capture { // 👈🏻    OneOrMore {      NegativeLookahead { fieldSeparator }      CharacterClass.any    }  }  fieldSeparator  Capture { One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US"))) } // 👈🏻}// transactionMatcher: Regex<(Substring, Substring, Date, Substring, Decimal)>

泛型与协议

session Embrace Swift genericsDesign protocol interfaces in Swift

swift 5.6 和之前编写泛型接口如下:

func feed<A>(_ animal: A) where A: Animal// 👆🏻👇🏻 Equivalentsfunc feed<A: Animal>(_ animal: A)

swift 5.7 可以这样写:

func feed(_ animal: some Animal)

some 关键字可以用于参数和结构类型。some 会保证类型关系,而 any 会持有任意具体类型,删除类型关系。

SE-0347 Type inference from default expressions 扩展 Swift 泛型参数类型的默认值能力。如下代码示例:

func suffledArray<T: Sequence>(from options: T = 1...100) -> [T.Element] {    Array(options.shuffled())}print(suffledArray())print(suffledArray(from: ["one", "two", "three"]))

SE-0341 Opaque Parameter Declarations 使用 some 参数简化泛型参数声明。SE-0328 Structural opaque result types 扩大不透明结果返回类型可以使用的范围。SE-0360 Opaque result types with limited availability 可用性有限的不透明结果类型,比如 if #available(macOS 13.0, *) {} 就可以根据系统不同版本返回不同类型,新版本出现新类型的 View 就可以和以前的 View 类型区别开。

SE-0309 Unlock existentials for all protocols 改进了 existentials 和 泛型的交互。这样就可以更方便的检查 Any 类型的两个值是否相等

any 关键字充当的是类型擦除的助手,是通过告知编译器你使用 existential 作为类型,此语法可兼容以前系统。

SE-0346 Lightweight same-type requirements for primary associated types 引入一种新语法,用于符合泛型参数并通过相同类型要求约束关联类型。SE-0358 Primary Associated Types in the Standard Library 引入主要关联类型概念,并将其带入了标准库。这些关联类型很像泛型,允许开发者将给定关联类型的类型指定为通用约束。

SE-0353 Constrained Existential Types 基于 SE-0309 和 SE-0346 提案,在 existential 类型的上下文中重用轻量关联类型的约束。

SE-0352 Implicitly Opened Existentials 允许 Swift 在很多情况下使用协议调用泛型函数。

Swift 论坛上一个对 any 和 some 关键字语法使用场景的讨论,Do any and some help with “Protocol Oriented Testing” at all?

Swift Concurrency

session Eliminate data races using Swift ConcurrencyVisualize and optimize Swift concurrencyMeet Swift Async Algorithms

表示持续时间有了新的放来来表达,对应提案是 SE-0329 Clock, Instant, and Duration ,continuous clock 是在系统睡眠状态还会增加时间,suspending clock 在系统睡眠状态不会增加时间。Instants 表示一个确定的时间。Duration 表示两个时间经历了多久。

新增 SE-0338 Clarify the Execution of Non-Actor-Isolated Async Functions 通过收紧可发送性检查的规则来避免潜在的数据竞争。

SE-0343 Concurrency in Top-level Code 这个提案主要是更好地支持命令行工具的开发,可以直接将 concurrency 代码写到 main.swift 文件里。

SE-0340 Unavailable From Async Attribute 提供 noasync 语法以允许我们将类型和函数标记为在异步上下文不可用。

Task 是按顺序执行的,是异步的,在 await 时可以暂停任意次数。task 是自包含的,有自己的资源,可以独立于任何其他 task 独立运行。task 通过在 body 末尾返回一个值来传递对象,值类型没问题,如果是引用类型有可能出现数据竞争。

通过 Sendable 协议 Swift 可以帮助告诉我们什么时候 task 之间共享数据是安全的。Sendable 描述的类型可以跨隔离 domain,不会有数据竞争,Swift 编译器会在构建时检查数据竞争。task 的返回类型要符合 Sendable。

引用类型只能在很少的情况下符合 Sendable。比如 final class 只有不可变的存储。对于自己内部同步的引用类型,比如锁,可以用 @unchecked Sendable

class ConcurrentCache<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {  var lock: NSLock  var storage: [Key: Value]  // ...}

Actor 提供了一种隔离状态的方法可以消除数据竞争。使用 task 来执行 actor 定义的代码。一次只能在一个 actor 上执行一个 task。actor 也是依赖 Sendable。actor 是引用类型,但隔离了他们所有属性和代码来防止并发访问。@MainActor 表示的是主线程,你要在应用中更新 UI 时来用它。

@MainActor func updateView() {}Task { @MainActor in  // update UI here}

@MainActor 也可以用于类,类的属性和方法只能在主 main actor 上访问,除非标记为 nonisolated

@MainActorclass ChickenValley: Sendable {  var flock: [Chicken]  var food: [Pineapple]  func advanceTime() {    for chicken in flock {      chicken.eat(from: &food)    }  }}

Distributed Actors

actor 具有分布式形式工作能力,也就是可以 RPC 通过网络读取和写入属性或者调用方法。设计为保护在跨多个进程中的低级别数据竞争。Distributed actors 可以在两个进程间建立通道,隔离它们状态,并在它们之间异步通信。每个 distributed actors 在 actor 初始化时分配一个不可以手动创建的 id,在它所属整个 distributed actor 系统中唯一标识所指 actor,这样无论 distributed actors 在哪,都可以以相同的方式与之交互。

session Meet distributed actors in Swift 。这里有个 distributed actors 的代码示例 TicTacFish: Implementing a game using distributed actors

SE-0336 Distributed Actor IsolationSE-0344 Distributed Actor Runtime 是两个 Distributed Actors 的相关提案。

Apple 提供了一个参考的服务端 cluster actor 系统实现示例,cluster actor system implementation

Optional

SE-0345 if let shorthand for shadowing an existing optional variable 引入的新语法,用于 unwrapping optinal。

let s1: String? = "hey"let s2: String? = "u"if let s1 {    print(s1)}guard let s1, let s2 else { return }print(s1 + " " + s2)

类型推断

SE-0326 提高了 Swift 对闭包使用参数和类型推断的能力。如下代码:

let a = [1,2,3]let r = a.map { i in    if i >= 2 {        return "\(i) 大于等于2"    } else {        return "\(i) 小于2"    }}print(r)

Result Builders

SE-0348 buildPartialBlock for result builders 简化了实现复杂 result buiders 所需的重载。

Swift-DocC

现在支持 Swift、OC 和 C,文档标记一样。.doccarchive 包含可部署的网站内容,兼容大多数托管服务,比如 Github pages。部署到在线服务上可参考 Generating Documentation for Hosting OnlinePublishing to GitHub Pages 文档。

和 SPM 集成参看 SwiftDocCPlugin

session 有 What’s new in Swift -DocCImprove the discoverability of your Swift-DocC content

SE-0356 Swift Snippets 代码片段用于示例文档的提案。

调试

session Debug Swift debugging with LLDB

编译器编译 swift 文件生成 .o 文件会有 __debug_info 段,其中有可以映射到源文件和行号的地址。debug 信息可以链接到 .dSYM 包。debug 信息链接器叫 dsymutil,dsymutil 可以为每个动态库、framework 或 dylib 和可执行文件打包一个 debug 信息存档(.dSYM 包)。

image 和路径怎么重映射。使用 image list nameOfFramework 来检查 LLDB 是否找到了我们应用程序里嵌入的第三方框架的 debug dSYM。使用 image lookup 0xMemoryAddressHere 获取当前地址更多信息。要重新映射源文件 .dSYM 路径,使用 settings set target.source-map old/path new/path。每个 .dSYM 都有一个 UUID.plist,我们可以在其中设置 DBGSourcePathRemapping 这个字典。

Xcode 14 新增 swift-healthcheck 命令,这个命令可以了解 module 为何导入失败。

LLDB 怎么找到 Swift module?每个 .dSYM 包都可以包含二级制 swift module,其中可能包含桥头文件、swift 接口文件 .swiftinterface,还有 debug 信息。静态存档不是由链接器生成的,需要向链接器注册 swift module,使用 ld ... -add-ast-path /path/to/My.swiftmodule ,动态库和可执行文件的话,Xcode 会自动完成此操作。可以使用 dsymutil 来 dump 你可执行文件的符号表,并用 grep 找 swiftmodule,命令是 dsymutil -s MyApp | grep .swiftmodule

内存管理

相关提案包括 SE-0349 Unaligned Loads and Stores from Raw MemorySE-0334 Pointer API Usability ImprovementsSE-0333 Expand usability of withMemoryRebound

Set 使用新的 Temporary Buffers 功能,让 intersect 速度提升了 4 到 6 倍。

SwiftUI

介绍

Kuba Suder 做了一个 SwiftUI Index/Changelog ,从官方文档中提取版本信息,一目了然 SwiftUI 每个版本 view,modifier 还有属性做了哪些增加和改变。当然也包括这次 SwiftUI 4 的更新。还有份对今年更新整理的 cheat sheet What’s New In SwiftUI for iOS Cheat Sheet - WWDC22

SwiftUI 4 做了大量细节更新,比如添加了后台任务函数 backgroundTask(_:action:) 。List 改用 UICollectionView。AnyLayout 让 HStack 和 VStack 之间可以自由切换。scrollDismissesKeyboard() modifier 可以让键盘在滚动时自动 dismiss。scrollIndicators() modifier 可以隐藏 ScrollView 和 List 等视图的滚动指示。defersSystemGestures() modifier 允许我们的手势优先于系统的内置手势。颜色的 .gradient 可以获得很简单的渐变,Rectangle().fill(.red.gradient),还有 .shadow 用来创建投影 Rectangle().fill(.red.shadow(.drop(color: .black, radius: 10))),还有 .inner 内阴影。lineLimit() modifier 支持范围设置。还有一些 modifier 支持 toggle 参数,比如 .bold().italic() 等,这样利于运行时进行调整。

参考

session:

社区整理的和 SwiftUI 的 digital lounges 内容:

Navigation 接口

控制导航启动状态、管理 size class 之间的 transition 和响应 deep link。

Navigation bar 有新的默认行为,如果没有提供标题,导航栏默认为 inline title 显示模式。使用 navigationBarTitleDisplayMode(_:) 改变显示模式。如果 navigation bar 没有标题、工具栏项或搜索内容,它就会自动隐藏。使用 .toolbar(.visible) modifier 显示一个空 navigation bar。

参考:

NavigationStack 的示例:

struct PNavigationStack: View {    @State private var a = [1, 3, 9] // 深层链接    var body: some View {        NavigationStack(path: $a) {            List(1..<10) { i in                NavigationLink(value: i) {                    Label("第 \(i) 行", systemImage: "\(i).circle")                }            }            .navigationDestination(for: Int.self) { i in                Text("第 \(i) 行内容")            }            .navigationTitle("NavigationStack Demo")        }    }}

这里的 path 设置了 stack 的深度路径。

NavigationSplitView 两栏的例子:

struct PNavigationSplitViewTwoColumn: View {    @State private var a = ["one", "two", "three"]    @State private var choice: String?        var body: some View {        NavigationSplitView {            List(a, id: \.self, selection: $choice, rowContent: Text.init)        } detail: {            Text(choice ?? "选一个")        }    }}

NavigationSplitView 三栏的例子:

struct PNavigationSplitViewThreeColumn: View {    struct Group: Identifiable, Hashable {        let id = UUID()        var title: String        var subs: [String]    }        @State private var gps = [        Group(title: "One", subs: ["o1", "o2", "o3"]),        Group(title: "Two", subs: ["t1", "t2", "t3"])    ]        @State private var choiceGroup: Group?    @State private var choiceSub: String?        @State private var cv = NavigationSplitViewVisibility.automatic        var body: some View {        NavigationSplitView(columnVisibility: $cv) {            List(gps, selection: $choiceGroup) { g in                Text(g.title).tag(g)            }            .navigationSplitViewColumnWidth(250)        } content: {            List(choiceGroup?.subs ?? [], id: \.self, selection: $choiceSub) { s in                Text(s)            }        } detail: {            Text(choiceSub ?? "选一个")            Button("点击") {                cv = .all            }        }        .navigationSplitViewStyle(.prominentDetail)    }}

navigationSplitViewColumnWidth() 是用来自定义宽的,navigationSplitViewStyle 设置为 .prominentDetail 是让 detail 的视图尽量保持其大小。

SwiftUI 新加了个功能可以配置是否隐藏 Tabbar,这样在从主页进入下一级时就可以选择不显示底部标签栏了,示例代码如下:

ContentView().toolbar(.hidden, in: .tabBar)

相比较以前 NavigationView 增强的是 destination 可以根据值的不同类型展示不同的目的页面,示例代码如下:

struct PNavigationStackDestination: View {    var body: some View {        NavigationStack {            List {                NavigationLink(value: "字符串") {                    Text("字符串")                }                NavigationLink(value: Color.red) {                    Text("红色")                }            }            .navigationTitle("不同类型 Destination")            .navigationDestination(for: Color.self) { c in                c.clipShape(Circle())            }            .navigationDestination(for: String.self) { s in                Text("\(s) 的 detail")            }        }    }}

Swift Charts

可视化数据,使用 SwiftUI 语法来创建。还可以使用 ChartRenderer 接口将图标渲染成图。

官方文档 Swift Charts

入门参看 Hello Swift Charts

Apple 文章 Creating a chart using Swift Charts

高级定制和创建更精细图表,可以看这个 session Swift Charts: Raise the bar 这个 session 也会提到如何在图表中进行交互。这里是 session 对应的代码示例 Visualizing your app’s data

图表设计的 session,Design an effective chartDesign app experiences with charts

下面是一个简单的代码示例:

import Chartsstruct PChartModel: Hashable {    var day: String    var amount: Int = .random(in: 1..<100)}extension PChartModel {    static var data: [PChartModel] {        let calendar = Calendar(identifier: .gregorian)        let days = calendar.shortWeekdaySymbols        return days.map { day in            PChartModel(day: day)        }    }}struct PlayCharts: View {    var body: some View {        Chart(PChartModel.data, id: \.self) { v in            BarMark(x: .value("天", v.day), y: .value("数量", v.amount))                    }        .padding()    }}struct PSwiftCharts: View {    struct CData: Identifiable {        let id = UUID()        let i: Int        let v: Double    }        @State private var a: [CData] = [        .init(i: 0, v: 2),        .init(i: 1, v: 20),        .init(i: 2, v: 3),        .init(i: 3, v: 30),        .init(i: 4, v: 8),        .init(i: 5, v: 80)    ]        var body: some View {        Chart(a) { i in            LineMark(x: .value("Index", i.i), y: .value("Value", i.v))            BarMark(x: .value("Index", i.i), yStart: .value("开始", 0), yEnd: .value("结束", i.v))                .foregroundStyle(by: .value("Value", i.v))        } // end Chart    } // end body}

BarMark 用于创建条形图,LineMark 用于创建折线图。SwiftUI Charts 框架还提供 PointMark、AxisMarks、AreaMark、RectangularMark 和 RuleMark 用于创建不同类型的图表。注释使用 .annotation modifier,修改颜色可以使用 .foregroundStyle modifier。.lineStyle modifier 可以修改线宽。

AxisMarks 的示例如下:

struct MonthlySalesChart: View {    var body: some View {        Chart(data, id: \.month) {            BarMark(                x: .value("Month", $0.month, unit: .month),                y: .value("Sales", $0.sales)            )        }        .chartXAxis {            AxisMarks(values: .stride(by: .month)) { value in                if value.as(Date.self)!.isFirstMonthOfQuarter {                    AxisGridLine().foregroundStyle(.black)                    AxisTick().foregroundStyle(.black)                    AxisValueLabel(                        format: .dateTime.month(.narrow)                    )                } else {                    AxisGridLine()                }            }        }    }}

可交互图表示例如下:

struct InteractiveBrushingChart: View {    @State var range: (Date, Date)? = nil        var body: some View {        Chart {            ForEach(data, id: \.day) {                LineMark(                    x: .value("Month", $0.day, unit: .day),                    y: .value("Sales", $0.sales)                )                .interpolationMethod(.catmullRom)                .symbol(Circle().strokeBorder(lineWidth: 2))            }            if let (start, end) = range {                RectangleMark(                    xStart: .value("Selection Start", start),                    xEnd: .value("Selection End", end)                )                .foregroundStyle(.gray.opacity(0.2))            }        }        .chartOverlay { proxy in            GeometryReader { nthGeoItem in                Rectangle().fill(.clear).contentShape(Rectangle())                    .gesture(DragGesture()                        .onChanged { value in                            // Find the x-coordinates in the chart’s plot area.                            let xStart = value.startLocation.x - nthGeoItem[proxy.plotAreaFrame].origin.x                            let xCurrent = value.location.x - nthGeoItem[proxy.plotAreaFrame].origin.x                            // Find the date values at the x-coordinates.                            if let dateStart: Date = proxy.value(atX: xStart),                               let dateCurrent: Date = proxy.value(atX: xCurrent) {                                range = (dateStart, dateCurrent)                            }                        }                        .onEnded { _ in range = nil } // Clear the state on gesture end.                    )            }        }    }}

社区做的更多 Swift Charts 范例 Swift Charts Examples

Advanced layout control

session Compose custom layouts with SwiftUI

提供了新的 Grid 视图来同时满足 VStack 和 HStack。还有一个更低级别 Layout 接口,可以完全控制构建应用所需的布局。另外还有 ViewThatFits 可以自动选择填充可用空间的方式。

Grid 示例代码如下:

Grid {    GridRow {        Text("One")        Text("One")        Text("One")    }    GridRow {        Text("Two")        Text("Two")    }    Divider()    GridRow {        Text("Three")        Text("Three")            .gridCellColumns(2)    }}

gridCellColumns() modifier 可以让一个单元格跨多列。

ViewThatFits 的新视图,允许根据适合的大小放视图。ViewThatFits 会自动选择对于当前屏幕大小合适的子视图进行显示。Ryan Lintott 的示例效果 ,对应示例代码 LayoutThatFits.swift

新的 Layout 协议可以观看 Swift Talk 第 308 期 The Layout Protocol

通过符合 Layout 协议,我们可以自定义一个自定义的布局容器,直接参与 SwiftUI 的布局过程。新的 ProposedViewSize 结构,它是容器视图提供的大小。 Layout.Subviews 是布局视图的子视图代理集合,我们可以在其中为每个子视图请求各种布局属性。

public protocol Layout: Animatable {  static var layoutProperties: LayoutProperties { get }  associatedtype Cache = Void  typealias Subviews = LayoutSubviews  func updateCache(_ cache: inout Self.Cache, subviews: Self.Subviews)  func spacing(subviews: Self.Subviews, cache: inout Self.Cache) -> ViewSpacing  /// We return our view size here, use the passed parameters for computing the  /// layout.  func sizeThatFits(    proposal: ProposedViewSize,     subviews: Self.Subviews,     cache: inout Self.Cache // 👈🏻 use this for calculated data shared among Layout methods  ) -> CGSize    /// Use this to tell your subviews where to appear.  func placeSubviews(    in bounds: CGRect, // 👈🏻 region where we need to place our subviews into, origin might not be .zero    proposal: ProposedViewSize,     subviews: Self.Subviews,     cache: inout Self.Cache  )    // ... there are more a couple more optional methods}

下面例子是一个自定义的水平 stack 视图,为其所有子视图提供其最大子视图的宽度:

struct MyEqualWidthHStack: Layout {  /// Returns a size that the layout container needs to arrange its subviews.  /// - Tag: sizeThatFitsHorizontal  func sizeThatFits(    proposal: ProposedViewSize,    subviews: Subviews,    cache: inout Void  ) -> CGSize {    guard !subviews.isEmpty else { return .zero }    let maxSize = maxSize(subviews: subviews)    let spacing = spacing(subviews: subviews)    let totalSpacing = spacing.reduce(0) { $0 + $1 }    return CGSize(      width: maxSize.width * CGFloat(subviews.count) + totalSpacing,      height: maxSize.height)  }  /// Places the stack's subviews.  /// - Tag: placeSubviewsHorizontal  func placeSubviews(    in bounds: CGRect,    proposal: ProposedViewSize,    subviews: Subviews,    cache: inout Void  ) {    guard !subviews.isEmpty else { return }    let maxSize = maxSize(subviews: subviews)    let spacing = spacing(subviews: subviews)    let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height)    var nextX = bounds.minX + maxSize.width / 2    for index in subviews.indices {      subviews[index].place(        at: CGPoint(x: nextX, y: bounds.midY),        anchor: .center,        proposal: placementProposal)      nextX += maxSize.width + spacing[index]    }  }  /// Finds the largest ideal size of the subviews.  private func maxSize(subviews: Subviews) -> CGSize {    let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }    let maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize in      CGSize(        width: max(currentMax.width, subviewSize.width),        height: max(currentMax.height, subviewSize.height))    }    return maxSize  }  /// Gets an array of preferred spacing sizes between subviews in the  /// horizontal dimension.  private func spacing(subviews: Subviews) -> [CGFloat] {    subviews.indices.map { index in      guard index < subviews.count - 1 else { return 0 }      return subviews[index].spacing.distance(        to: subviews[index + 1].spacing,        along: .horizontal)    }  }}

自定义 layout 只能访问子视图代理 Layout.Subviews ,而不是视图或数据模型。我们可以通过 LayoutValueKey 在每个子视图上存储自定义值,通过 layoutValue(key:value:) modifier 设置。

private struct Rank: LayoutValueKey {  static let defaultValue: Int = 1}extension View {  func rank(_ value: Int) -> some View { // 👈🏻 convenience method    layoutValue(key: Rank.self, value: value) // 👈🏻 the new modifier  }}

然后,我们就可以通过 Layout 方法中的 Layout.Subviews 代理读取自定义 LayoutValueKey 值:

func placeSubviews(  in bounds: CGRect,  proposal: ProposedViewSize,  subviews: Subviews,  cache: inout Void) {  let ranks = subviews.map { subview in    subview[Rank.self] // 👈🏻  }  // ...}

要在布局之间变化使用动画,需要用 AnyLayout,代码示例如下:

struct PAnyLayout: View {    @State private var isVertical = false    var body: some View {        let layout = isVertical ? AnyLayout(VStack()) : AnyLayout(HStack())        layout {            Image(systemName: "star").foregroundColor(.yellow)            Text("Starming.com")            Text("戴铭")        }        Button("Click") {            withAnimation {                isVertical.toggle()            }        } // end button    } // end body}

同时 Text 和图片也支持了样式布局变化,代码示例如下:

struct PTextTransitionsView: View {    @State private var expandMessage = true    private let mintWithShadow: AnyShapeStyle = AnyShapeStyle(Color.mint.shadow(.drop(radius: 2)))    private let primaryWithoutShadow: AnyShapeStyle = AnyShapeStyle(Color.primary.shadow(.drop(radius: 0)))    var body: some View {        Text("Dai Ming Swift Pamphlet")            .font(expandMessage ? .largeTitle.weight(.heavy) : .body)            .foregroundStyle(expandMessage ? mintWithShadow : primaryWithoutShadow)            .onTapGesture { withAnimation { expandMessage.toggle() }}            .frame(maxWidth: expandMessage ? 150 : 250)            .drawingGroup()            .padding(20)            .background(.cyan.opacity(0.3), in: RoundedRectangle(cornerRadius: 6))    }}

分享接口

Transferable 协议使数据可以用于剪切板、拖放和 Share Sheet。

可以在自己应用程序之间或你的应用和其他应用之间发送或接受可传输项目。

支持 SwiftUI 来使用。

官方文档 Core Transferable

session Meet Transferable

新增一个专门用来接受 Transferable 的按钮视图 PasteButton,使用示例如下:

struct PPasteButton: View {    @State private var s = "戴铭"    var body: some View {        TextField("输入", text: $s)            .textFieldStyle(.roundedBorder)        PasteButton(payloadType: String.self) { str in            guard let first = str.first else { return }            s = first        }    }}

ShareLink

ShareLink 视图可以让你轻松共享数据。示例代码如下:

struct PShareLink: View {    let url = URL(string: "https://ming1016.github.io/")!    var body: some View {        ShareLink(item: url, message: Text("戴铭的博客"))        ShareLink("戴铭的博客", item: url)        ShareLink(item: url) {            Label("戴铭的博客", systemImage: "swift")        }    }}

锁屏的 Widget

和 WatchOS 一样,可以瞟一眼就获取信息。

官方指南 Creating Lock Screen Widgets and Watch Complications

Bottom Sheet

SwiftUI 新推出的 presentationDetents() modifier 可以创建一个可以定制的 bottom sheet。示例代码如下:

struct PSheet: View {    @State private var isShow = false    var body: some View {        Button("显示 Sheet") {            isShow.toggle()        }        .sheet(isPresented: $isShow) {            Text("这里是 Sheet 的内容")                .presentationDetents([.medium, .large])        }    }}

detent 默认值是 .large。也可以提供一个百分比,比如 .presentationDetents([.fraction(0.7)]),或者直接指定高度 .presentationDetents([.height(100)])

presentationDragIndicator modifier 可以用来显示隐藏拖动标识。

List

list 支持 Section footer。

list 分隔符可以自定义,使用 HorizontalEdge.leadingHorizontalEdge.trailing

list 不使用 UITableView 了。

今年 list 还新增了一个 EditOperation 可以自动生成移动和删除,新增了 edits 参数,传入 [.delete, .move] 数组即可。这也是一个演示如何更好扩展和配置功能的方式。

ScrollView

新增 modifier

ScrollView {    ForEach(0..<300) { i in        Text("\(i)")            .id(i)    }}.scrollDisabled(false).scrollDismissesKeyboard(.interactively).scrollIndicators(.visible)

TextField

支持多行,使用 Axis.vertical 以允许多行。TextField 超过行限制可以变成滚动视图。

今年 TextField 可以嵌到 .alert 里了。

Search

.searchable 支持 token 和 scope,示例如下:

struct PSearchTokensAndScopes: View {    enum AttendanceScope {        case inPerson, online    }    @State private var queryText: String    @State private var queryTokens: [InvitationToken]    @State private var scope: AttendanceScope        var body: some View {        invitationCountView()            .searchable(text: $queryText, tokens: $queryTokens, scope: $scope) { token in                Label(token.diplayName, systemImage: token.systemImage)            } scopes: {                Text("In Person").tag(AttendanceScope.inPerson)                Text("Online").tag(AttendanceScope.online)            }    }}

Gauge

SwiftUI 引入一个新显示进度的视图 Gauge。

简单示例如下:

struct PGauge: View {    @State private var progress = 0.45    var body: some View {        Gauge(value: progress) {            Text("进度")        } currentValueLabel: {            Text(progress.formatted(.percent))        } minimumValueLabel: {            Text(0.formatted(.percent))        } maximumValueLabel: {            Text(100.formatted(.percent))        }                Gauge(value: progress) {                    } currentValueLabel: {            Text(progress.formatted(.percent))                .font(.footnote)        }        .gaugeStyle(.accessoryCircularCapacity)        .tint(.cyan)    }}

Group Form

Form 今年也得到了增强,示例如下:

Form {    Section {        LabeledContent("Location") {            AddressView(location)        }        DatePicker("Date", selection: $date)        TextField("Description", text: $eventDescription, axis: .vertical)            .lineLimit(3, reservesSpace: true)    }        Section("Vibe") {        Picker("Accent color", selection: $accent) {            ForEach(Theme.allCases) { accent in                Text(accent.rawValue.capitalized).tag(accent)            }        }        Picker("Color scheme", selection: $scheme) {            Text("Light").tag(ColorScheme.light)            Text("Dark").tag(ColorScheme.dark)        }#if os(macOS)        .pickerStyle(.inline)#endif        Toggle(isOn: $extraGuests) {            Text("Allow extra guests")            Text("The more the merrier!")        }        if extraGuests {            Stepper("Guests limit", value: $spacesCount, format: .number)        }    }        Section("Decorations") {        Section {            List(selection: $selectedDecorations) {                DisclosureGroup {                    HStack {                        Toggle("Balloons 🎈", isOn: $includeBalloons)                        Spacer()                        decorationThemes[.balloon].map { $0.swatch }                    }                    .tag(Decoration.balloon)                                        HStack {                        Toggle("Confetti 🎊", isOn: $includeConfetti)                        Spacer()                        decorationThemes[.confetti].map { $0.swatch }                    }                    .tag(Decoration.confetti)                                        HStack {                        Toggle("Inflatables 🪅", isOn: $includeInflatables)                        Spacer()                        decorationThemes[.inflatables].map { $0.swatch }                    }                    .tag(Decoration.inflatables)                                        HStack {                        Toggle("Party Horns 🥳", isOn: $includeBlowers)                        Spacer()                        decorationThemes[.noisemakers].map { $0.swatch }                    }                    .tag(Decoration.noisemakers)                } label: {                    Toggle("All Decorations", isOn: [                        $includeBalloons, $includeConfetti,                        $includeInflatables, $includeBlowers                    ])                    .tag(Decoration.all)                }#if os(macOS)                .toggleStyle(.checkbox)#endif            }                        Picker("Decoration theme", selection: themes) {                Text("Blue").tag(Theme.blue)                Text("Black").tag(Theme.black)                Text("Gold").tag(Theme.gold)                Text("White").tag(Theme.white)            }#if os(macOS)            .pickerStyle(.radioGroup)#endif        }    }    }.formStyle(.grouped)

Button

.buttonStyle 可组合,示例如下:

struct PButtonStyleComposition: View {    @State private var isT = false    var body: some View {        Section("标签") {            VStack(alignment: .leading) {                HStack {                    Toggle("Swift", isOn: $isT)                    Toggle("SwiftUI", isOn: $isT)                }                HStack {                    Toggle("Swift Chart", isOn: $isT)                    Toggle("Navigation API", isOn: $isT)                }            }            .toggleStyle(.button)            .buttonStyle(.bordered)        }    }}

Tap Location

可以获取点击的位置,示例代码如下:

Rectangle()    .fill(.green)    .frame(width: 50, height: 50)    .onTapGesture(coordinateSpace: .global) { location in        print("Tap in \(location)")    }

其中 coordinateSpace 指定为 .global 表示位置是相对屏幕左上角,默认是相对当前视图的左上角的位置。

选择多个日期

MultiDatePicker 视图会显示一个日历,用户可以选择多个日期,可以设置选择范围。示例如下:

struct PMultiDatePicker: View {    @Environment(\.calendar) var cal    @State var dates: Set<DateComponents> = []    var body: some View {        MultiDatePicker("选择个日子", selection: $dates, in: Date.now...)        Text(s)    }    var s: String {        dates.compactMap { c in            cal.date(from:c)?.formatted(date: .long, time: .omitted)        }        .formatted()    }}

PhotosPick

支持图片选择,示例代码如下:

import PhotosUIimport CoreTransferablestruct ContentView: View {    @ObservedObject var viewModel: FilterModel = .shared        var body: some View {        NavigationStack {            Gallery()                .navigationTitle("Birthday Filter")                .toolbar {                    PhotosPicker(                        selection: $viewModel.imageSelection,                        matching: .images                    ) {                        Label("Pick a photo", systemImage: "plus.app")                    }                    Button {                        viewModel.applyFilter()                    } label: {                        Label("Apply Filter", systemImage: "camera.filters")                    }                }        }    }}

Table

今年 iOS 和 iPadOS 也可以使用去年只能在 macOS 上使用的 Table了,据 digital lounges 里说,iOS table 的性能和 list 差不多,table 默认为 plian list。我想 iOS 上加上 table 只是为了兼容 macOS 代码吧。

table 使用示例如下:

Table(attendeeStore.attendees) {    TableColumn("Name") { attendee in        AttendeeRow(attendee)    }    TableColumn("City", value: \.city)    TableColumn("Status") { attendee in        StatusRow(attendee)    }}.contextMenu(forSelectionType: Attendee.ID.self) { selection in    if selection.isEmpty {        Button("New Invitation") { addInvitation() }    } else if selection.count == 1 {        Button("Mark as VIP") { markVIPs(selection) }    } else {        Button("Mark as VIPs") { markVIPs(selection) }    }}

Toolbar

对 toolbar 的自定义,示例如下:

.toolbar(id: "toolbar") {    ToolbarItem(id: "new", placement: .secondaryAction) {        Button(action: {}) {            Label("New Invitation", systemImage: "envelope")        }    }}.toolbarRole(.editor)

SF Symbol

SF Symbol 支持变量值,可以通过设置 variableValue 来填充不同部分,比如 wifi 图标,不同值会亮不同部分,Image(systemName: "wifi", variableValue: 0.5)

Gradient 和 Shadow

下面是个简单示例:

struct PGradientAndShadow: View {    var body: some View {        Image(systemName: "bird")            .frame(width: 150, height: 150)            .background(in: Rectangle())            .backgroundStyle(.cyan.gradient)            .foregroundStyle(.white.shadow(.drop(radius: 1, y: 3.0)))            .font(.system(size: 60))    }}

Paul Hudson 使用 Core Motion 做了一个阴影随设备倾斜而变化的效果,非常棒,How to use inner shadows to simulate depth with SwiftUI and Core Motion

嵌入 UIKit

示例如下:

cell.contentConfiguration = UIHostingConfiguration {    VStack {        Image(systemName: "wand.and.stars")            .font(.title)        Text("Like magic!")            .font(.title2).bold()    }    .foregroundStyle(Color.purple)}

macOS

支持了 window,可以控制位置和大小。官方代码示例 Bringing multiple windows to your SwiftUI app

openWindow 代码示例如下:

struct PartyPlanner: App {    var body: some Scene {        WindowGroup("Party Planner") {            PartyPlannerHome()        }        Window("Party Budget", id: "budget") {            Text("Budget View")        }        .keyboardShortcut("0")        .defaultPosition(.topLeading)        .defaultSize(width: 220, height: 250)    }}struct DetailView: View {    @Environment(\.openWindow) var openWindow    var body: some View {        Text("Detail View")            .toolbar {                Button {                    openWindow(id: "budget")                } label: {                    Image(systemName: "dollarsign")                }            }    }}

session Bring multiple windows to your SwiftUI app 两个新 Scene 类型。WindowGroup 允许多 window。MenuBarExtra。可编程方式打开新 window 和 document。

MenuBarExtra 代码示例如下:

struct PartyPlanner: App {    var body: some Scene {        Window("Party Budget", id: "budget") {            Text("Budget View")        }        MenuBarExtra("Bulletin Board", systemImage: "quote.bubble") {            BulletinBoard()        }        .menuBarExtraStyle(.window)    }}

讲和 AppKit 混编的 session Use SwiftUI with AppKit

The craft of SwiftUI API design: Progressive disclosure 使用 windows 还有 MenuBarExtra,使用 modifier 来自定义应用程序 window 的 presentation 和行为。

使用 .dropDestination 来支持拖动。示例如下:

.dropDestination(payloadType: Image.self) { receivedImages, location in        guard let image = receivedImages.first else {            return false        }        viewModel.imageState = .success(image)        return true    }

今年有新的 FormStyle ,示例如下:

Form {    Picker("Notify Me About:", selection: $notifyMeAbout) {        Text("Direct Messages").tag(NotifyMeAboutType.directMessages)        Text("Mentions").tag(NotifyMeAboutType.mentions)        Text("Anything").tag(NotifyMeAboutType.anything)    }    Toggle("Play notification sounds", isOn: $playNotificationSounds)    Toggle("Send read receipts", isOn: $sendReadReceipts)    Picker("Profile Image Size:", selection: $profileImageSize) {        Text("Large").tag(ProfileImageSize.large)        Text("Medium").tag(ProfileImageSize.medium)        Text("Small").tag(ProfileImageSize.small)    }    .pickerStyle(.inline)}.formStyle(.columns)

Apple 自身在 macOS 系统中使用了多少 SwiftUI 呢?邮件、iWork 和 Keychain Access 的部分视图使用了,笔记、照片 和 Xcode 部分功能及新增功能的完整界面都是用的 SwiftUI,另外控制中心、字体册和系统设置的大部分都是用 SwiftUI 开发了。

ImageRenderer

可以将 SwiftUI 的 View 生成图片。

官方参考文档 ImageRenderer

后台任务

session Efficiency awaits: Background tasks in SwiftUI 了解如何使用 SwiftUI 后台任务 API 简洁地处理任务。展示如何使用 Swift Concurrency 来处理网络响应、后台刷新等——同时保持性能和功率。

Xcode 14

Xcode 14 里有新的 Swift 5.7,其中对泛型和协议有很大的改进。

参考

通用

编出来的二进制小 30%。

改进了并行性,构建提速 25%。

改进了在 iOS 设备上调试 Swift 程序的性能。

提供单一图标大小,Xcode 完成剩下的。

更智能的代码完成,滚动时置顶类、结构体和函数名。错误消息在重新处理时会变暗。

Xcode 搜索和替换栏中可以使用正则表达式。相信以后社区会出现很多好用的正则表达式分享。

Xcode Organizer 中新增 Hang 报告,用来提供主线程上发生挂起的调用堆栈信息,以及提供设备和 iOS 版本信息等统计信息。

Xcode 14 现在支持为 iPadOS 开发 DriverKit 驱动程序。

创建新 C++ 项目,Clang 默认使用 C++20。已经实现了几篇 C++20 和 C++2b 论文。

iOS、tvOS 和 watchOS 的构建默认不再包含 bitcode。

legacy 构建系统被删除,LLVM 14 也不再支持 legacy。

Xcode 中的 Swift-DocC 现支持 OC 和 C 的 API 构建文档。生成的 Swift-DocC 文档网站包括一个新的导航侧边栏,用于浏览和过滤文档。可将 Swift-DocC 部署到 GitHub Pages。

性能问题修复

代码完成不再自动导入模块。

提高了复杂表达式 SwiftUI 中代码完成的速度和准确性。

修复了包含大量错误或警告的文件时导致性能下降的问题。

修复了 minimap 在长文件时性能问题。

源码编辑器

滚动编辑器时,Xcode 会将代码结构的元素固定到编辑器顶部。

支持了 Regex 表达式语法高亮。Editor > Refactoring > Convert to Regex Builder 可以将正则文本转成等效 Regex builder。

可以输入匹配参数来选择代码完成中默认参数的任意组合。

Swift 中代码完成提供基于变量名的 map、filter 和 contains 的 snippet。

提高 Swift 代码完成的准确性。

SwiftUI 的代码完成,现在有了 List 和 ForEach 的 snippet。

Xcode 14 还要很多贴心代码完成改进,比如写 struct 的 init 可以自动完成。Codable 的 encode 也可以自动完成。

Xcode Preview

Preview 增强,默认是交互式的。

创建新项目会自动 resume。大量编辑时也不会暂停。会动态调整更新频率。

Swift Packages

引入新参数 moduleAliases 来为冲突的模块定义唯一名称,并以新名称构建而不用改代码。注意的是起别名的模块要是纯 Swift 模块。

允许使用 Swift Package command plugins。Xcode 为 Swift Package plugins 提供了 XcodeProjectPlugin 接口,这个接口扩展了 Swift Package Manager 的 PackagePlugin 接口。用这个接口可以获得 Xcode 项目结构的简化描述。

session 有 Meet Swift Package pluginsCreate Swift Package plugins

Instrument

Hang Tracing 工具,可以显示应用程序的主线程什么时候无法长时间处理传入事件,从而导致 UI 卡住。

Runloop 工具,显示 runloop 的使用和单独的迭代,视觉上区分了进程中所有 runloop 的 runloop sleep 和 busy interval。

Instrument 新模板更方便调试 distributed actors 和其它 Swift concurrency 特性。

memory graph 调试器可以显示 memory graph 的所有传入和传出引用。

Instrument 现有一个新的 Swift Concurrency 模板,用于跟踪 swift concurrency 的使用。这个模板包括 Swift Tasks 工具,可显示随时间变化的 task 的状态,总结 task 状态,提供详细的 task 描述,task 关系和 task 创建 callstacks 的调用树结构。还有 Swift Actors 工具,可以跟踪 actor 之间的 task 行为,显示每个 actor 的 task 队列,并帮助诊断 actor-isolated 代码等问题。

Instrument 里的代码查看更好显示包含了性能数据。Interleave 模式,可以同时查看源码和关联的反汇编。源码查看现在会在源码和反汇编判断显示 CPU 计数器,PMC 事件和动态公式。

修复了很多 Swift 相关显示不友好的问题。

多端

官方例子 Configuring a multiplatform app 。一个示例了 NavigationSplitView、Layout、Chart 和 WeatherKit 的运用的官方例子 Food Truck: Building a SwiftUI multiplatform app

Session 笔记

https://www.wwdcnotes.com/notes/wwdc22/110371/

下面是 App Intents、WidgetKit 相关内容,这些都属于 App Services,WWDC22 专门整理了 App Service 专题 。新系统服务比如 Messages collaboration、网络、CloudKit 的 System Service 主题

Widget

iOS 16 和 WatchOS 9 可以使用同一套代码编写 widget。iOS 新增场景是锁屏和 Live Activities(晚些时候推出)。

利用 Smart Stack,让 widget 出现到栈顶,可以使用 TimelineEntryRelevance

官方参考:

介绍怎么将 widgets 添加到 lock screen 的 session Complications and widgets: Reloaded 。对应的实例代码 Adding widgets to the Lock Screen and watch faces

App Intents

打通 App Shortcuts,从 Shortcuts 应用、Spotlight 和 Siri 运行你的 App 特定任务。

对应 Session

文档 App Intents

官方几篇 App Intents 文章:

对于 Shortcut 的使用少数派有篇很棒的文章 《iOS 快捷指令搭配 Notion API,更快速地编辑内容》 。

WeatherKit

Apple 收购 Dark Sky 后带来了 WeatherKit 和 WeatherKit REST API。有着易用的 Swift 接口,还有配套的 REST API。WeatherKit 内置了 async/await 支持。

WeatherKit 指南
WeatherKit 文档

session Meet WeatherKit 。一个 Apple 提供的天气代码示例 Fetching weather forecasts with WeatherKit

HealthKit

提供了更详细的睡眠和锻炼数据。session 介绍 What’s new in HealthKit

Vision

更新介绍 session What’s new in Vision

VisionKit 现在有一个结合 AVCapture 和 Vision 的数据扫描仪进行实时捕捉。 session Capture machine-readable codes and text with VisionKit

Live Text 接口

视觉库的应用接口。可以从照片和暂停视频中获取文本。

官方参考:

ScreenCaptureKit

creenCaptureKit 框架可以给你的 macOS 程序添加对高性能屏幕录制的支持。文档地址:ScreenCaptureKit

App Store

内购

可以将 App Store Connect 内购产品同步到 Xcode。

新测试功能,比如在沙盒和 Xcode 里请求测试通知和测试其它应用内购买场景。

官方参考:

这里有个 Kevin 开源的微信支付 SDK wechatpay-swift

全球化

session Build global apps: Localization by example

request review

你可以用 requestReview 这个 environment 键提示用户对你的 App 进行评论。示例代码如下:

struct PRequestReview: View {    @Environment(\.requestReview) var rr    var body: some View {        Button("来评论吧") {            rr()        }    }}

Apple 的最佳实践例子 Requesting App Store Reviews

参考

审核

这次审核,规则 4.2.3 中取消二进制要有启动时足够的内容,这可能是因为 Background Assets 的推出可以让用户更快更聪明的下载。另外 5.3.3 放宽了彩排等限制。

性能

Apple 除了做编译优化体积外,还提供了一个 Background Assets 在应用安装后、应用更新时以及应用保留在设备上时定期在后台下载资源,看起来类似 ODR。Background Assets 的 session Meet Background Assets

官方参考:

硬件和虚机

官方参考:

session 有:

虚机的应用可见 insidegui/VirtualBuddy 这个开源项目。

网络

session Reduce networking delays for a more responsive appBuild device-to-device interactions with Network Framework

Metal 3

利用多核优势,高分辨率图形渲染更快,资源加载更快。使用 GPU 训练机器学习网络。WWDC22 期间社区有个给背景添加雨水效果有些流行,作者放出了代码,介绍了如何将 Metal 引入 SwiftUI 工作流,Atmos

官方参考:

RoomPlan

ARKit 支持的新 Swift 接口。使用摄像头和 LiDAR 创建 3D 平面图。另外还有一个视觉库的代码例子很有趣,就是从视频中检测人物行为,Detecting Human Actions in a Live Video Feed

官方参考:

session Create parametric 3D room scans with RoomPlan 。官方示例代码 Create a 3D model of an interior room by guiding the user through an AR experience

Passkeys

身份验证,使用行业标准。

官方参考:

交互设计

Apple 的人机界面交互指南 Human Interface Guidelines 。内容超级详细,涉及程序界面方方面面。

官方参考:

资料

戴铭的 Swift 小册子 5.0

作者 戴铭
2021年11月23日 16:28

update:内容已更新到 5.0 版本。新增 WWDC22 内容。

新dyld源码透出近期苹果出新系统必然,依苹果 taste,势必要用好技术抛落后技术。漫漫长假我完善了Swift手册内容,字数达到十五万字,内容已压缩压缩再压缩,求全存简,more big, so small 。满满诚意,望有用、值得收藏、求转发。

come on and learn (੭*ˊᵕˋ)੭

– 戴·代码之使徒·画终结者·被光选中的人·铭

背景说明

越来越多同学打算开始用 Swift 来开发了,可很多人以前都没接触过 Swift。这篇和我以前文章不同的是,本篇只是面向 Swift 零基础的同学,内容主要是一些直接可用的小例子,例子可以直接在工程中用或自己调试着看。

记得以前 PHP 有个 chm 的手册,写的很简单,但很全,每个知识点都有例子,社区版每个知识点下面还有留言互动。因此,我弄了个 Swift 的手册,是个 macOS 程序。建议使用我开发的这个 macOS 程序来浏览。源码地址:ming1016/SwiftPamphletApp,直接下载 dmg 地址:戴铭的小册子5.0.dmg.zip

这个程序是Swift写的,按照声明式UI,响应式编程范式开发的,源码也可以看看。与其讲一堆,不如调着试。

下面是文本内容。注:代码中简化变量名是为了能更快速关注到语言用法。

语法速查

基础

变量 let, var

变量是可变的,使用 var 修饰,常量是不可变的,使用 let 修饰。类、结构体和枚举里的变量是属性。

var v1:String = "hi" // 标注类型var v2 = "类型推导"let l1 = "标题" // 常量class a {    let p1 = 3    var p2: Int {        p1 * 3    }}

属性没有 set 可以省略 get,如果有 set 需加 get。变量设置前通过 willSet 访问到,变量设置后通过 didSet 访问。

打印 print(“”)

控制台打印值

print("hi")let i = 14print(i)print("9月\(i)是小柠檬的生日")for i in 1...3{    print(i)}// output:// 1// 2// 3// 使用terminator使循环打印更整洁for i in 1...3 {    print("\(i) ", terminator: "")}// output:// 1 2 3

注释 //

// 单行注释/*多行注释第一行。多行注释第二行。*/ // MARK: 会在 minimap 上展示// TODO: 待做// FIXME: 待修复

可选 ?, !

可能会是 nil 的变量就是可选变量。当变量为 nil 通过??操作符可以提供一个默认值。

var o: Int? = nillet i = o ?? 0

闭包

闭包也可以叫做 lambda,是匿名函数,对应 OC 的 block。

let a1 = [1,3,2].sorted(by: { (l: Int, r: Int) -> Bool in    return l < r})// 如果闭包是唯一的参数并在表达式最后可以使用结尾闭包语法,写法简化为let a2 = [1,3,2].sorted { (l: Int, r: Int) -> Bool in    return l < r}// 已知类型可以省略let a3 = [1,3,2].sorted { l, r in    return l < r}// 通过位置来使用闭包的参数,最后简化如下:let a4 = [1,3,2].sorted { $0 < $1 }

函数也是闭包的一种,函数的参数也可以是闭包。@escaping 表示逃逸闭包,逃逸闭包是可以在函数返回之后继续调用的。@autoclosure 表示自动闭包,可以用来省略花括号。

函数 func

函数可以作为另一个函数的参数,也可以作为另一个函数的返回。函数是特殊的闭包,在类、结构体和枚举中是方法。

// 为参数设置默认值func f1(p: String = "p") -> String {    "p is \(p)"}// 函数作为参数func f2(fn: (String) -> String, p: String) -> String {    return fn(p)}print(f2(fn:f1, p: "d")) // p is d// 函数作为返回值func f3(p: String) -> (String) -> String {    return f1}print(f3(p: "yes")("no")) // p is no

函数可以返回多个值,函数是可以嵌套的,也就是函数里内可以定义函数,函数内定义的函数可以访问自己作用域外函数内的变量。inout 表示的是输入输出参数,函数可以在函数内改变输入输出参数。defer 标识的代码块会在函数返回之前执行。

函数在 Swift 5.4 时开始有了使用多个变量参数的能力,使用方法如下:

func f4(s: String..., i: Int...) {    print(s)    print(i)}f4(s: "one", "two", "three", i: 1, 2, 3)/// ["one", "two", "three"]/// [1, 2, 3]

嵌套函数可以重载,嵌套函数可以在声明函数之前调用他。

func f5() {    nf5()    func nf5() {        print("this is nested function")    }}f5() // this is nested function

访问控制

在 Xcode 里的 target 就是模块,使用 import 可导入模块。模块内包含源文件,每个源文件里可以有多个类、结构体、枚举和函数等多种类型。访问级别可以通过一些关键字描述,分为如下几种:

  • open:在模块外可以调用和继承。
  • public:在模块外可调用不可继承,open 只适用类和类成员。
  • internal:默认级别,模块内可跨源文件调用,模块外不可调用。
  • fileprivate:只能在源文件内访问。
  • private:只能在所在的作用域内访问。

重写继承类的成员,可以设置成员比父类的这个成员更高的访问级别。Setter 的级别可以低于对应的 Getter 的级别,比如设置 Setter 访问级别为 private,可以在属性使用 private(set) 来修饰。

基础类型

数字 Int, Float

数字的类型有 Int、Float 和 Double

// Intlet i1 = 100let i2 = 22print(i1 / i2) // 向下取整得 4// Floatlet f1: Float = 100.0let f2: Float = 22.0print(f1 / f2) // 4.5454545let f3: Float16 = 5.0 // macOS 还不能用let f4: Float32 = 5.0let f5: Float64 = 5.0let f6: Float80 = 5.0print(f4, f5, f6) // 5.0 5.0 5.0// Doublelet d1: Double = 100.0let d2: Double = 22.0print(d1 / d2) // 4.545454545454546// 字面量print(Int(0b10101)) // 0b 开头是二进制 print(Int(0x00afff)) // 0x 开头是十六进制print(2.5e4) // 2.5x10^4 十进制用 eprint(0xAp2) // 10*2^2  十六进制用 pprint(2_000_000) // 2000000// isMultiple(of:) 方法检查一个数字是否是另一个数字的倍数let i3 = 36print(i3.isMultiple(of: 9)) // true

处理数字有 floor、ceil、round。floor 是向下取整,只取整数部分;cell 是向上取整,只要有不为零的小数,整数就加1;round 是四舍五入。

布尔数 Bool

布尔数有 true 和 false 两种值,还有一个能够切换这两个值的 toggle 方法。

var b = falseb.toggle() // trueb.toggle() // false

元组 (a, b, c)

元组里的值类型可以是不同的。元组可以看成是匿名的结构体。

let t1 = (p1: 1, p2: "two", p3: [1,2,3])print(t1.p1)print(t1.p3)// 类型推导let t2 = (1, "two", [1,2,3])// 通过下标访问print(t2.1) // two// 分解元组let (dp1, dp2, _) = t2print(dp1)print(dp2)

字符串

let s1 = "Hi! This is a string. Cool?"/// 转义符 \n 表示换行。/// 其它转义字符有 \0 空字符)、\t 水平制表符 、\n 换行符、\r 回车符let s2 = "Hi!\nThis is a string. Cool?"// 多行let s3 = """Hi!This is a string.Cool?"""// 长度print(s3.count)print(s3.isEmpty)// 拼接print(s3 + "\nSure!")// 字符串中插入变量let i = 1print("Today is good day, double \(i)\(i)!")/// 遍历字符串/// 输出:/// o/// n/// efor c in "one" {    print(c)}// 查找print(s3.lowercased().contains("cool")) // true// 替换let s4 = "one is two"let newS4 = s4.replacingOccurrences(of: "two", with: "one")print(newS4)// 删除空格和换行let s5 = " Simple line. \n\n  "print(s5.trimmingCharacters(in: .whitespacesAndNewlines))// 切割成数组let s6 = "one/two/three"let a1 = s6.components(separatedBy: "/") // 继承自 NSString 的接口print(a1) // ["one", "two", "three"]let a2 = s6.split(separator: "/")print(a2) // ["one", "two", "three"] 属于切片,性能较 components 更好// 判断是否是某种类型let c1: Character = "🤔"print(c1.isASCII) // falseprint(c1.isSymbol) // trueprint(c1.isLetter) // falseprint(c1.isNumber) // falseprint(c1.isUppercase) // false// 字符串和 Data 互转let data = Data("hi".utf8)let s7 = String(decoding: data, as: UTF8.self)print(s7) // hi// 字符串可以当作集合来用。let revered = s7.reversed()print(String(revered))

Unicode、Character 和 SubString 等内容参见官方字符串文档:Strings and Characters — The Swift Programming Language (Swift 5.1)

字符串字面符号可以参看《String literals in Swift》。

原始字符串

// 原始字符串在字符串前加上一个或多个#符号。里面的双引号和转义符号将不再起作用了,如果想让转义符起作用,需要在转义符后面加上#符号。let s8 = #"\(s7)\#(s7) "one" and "two"\n. \#nThe second line."#print(s8)/// \(s7)hi "one" and "two"\n./// The second line.// 原始字符串在正则使用效果更佳,反斜杠更少了。let s9 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"let s10 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#print(s9) // \\[A-Z]+[A-Za-z]+\.[a-z]+print(s10) // \\[A-Z]+[A-Za-z]+\.[a-z]+

枚举

Swift的枚举有类的一些特性,比如计算属性、实例方法、扩展、遵循协议等等。

enum E1:String, CaseIterable {    case e1, e2 = "12"}// 关联值enum E2 {    case e1([String])    case e2(Int)}let e1 = E2.e1(["one","two"])let e2 = E2.e2(3)switch e1 {case .e1(let array):    print(array)case .e2(let int):    print(int)}print(e2)// 原始值print(E1.e1.rawValue)// 遵循 CaseIterable 协议可迭代for ie in E1.allCases {    print("show \(ie)")}// 递归枚举enum RE {    case v(String)    indirect case node(l:RE, r:RE)}let lNode = RE.v("left")let rNode = RE.v("right")let pNode = RE.node(l: lNode, r: rNode)switch pNode {case .v(let string):    print(string)case .node(let l, let r):    print(l,r)    switch l {    case .v(let string):        print(string)    case .node(let l, let r):        print(l, r)    }    switch r {    case .v(let string):        print(string)    case .node(let l, let r):        print(l, r)    }}

@unknown 用来区分固定的枚举和可能改变的枚举的能力。@unknown 用于防止未来新增枚举属性会进行提醒提示完善每个 case 的处理。

// @unknownenum E3 {    case e1, e2, e3}func fe1(e: E3) {    switch e {    case .e1:        print("e1 ok")    case .e2:        print("e2 ok")    case .e3:        print("e3 ok")    @unknown default:        print("not ok")    }}

符合 Comparable 协议的枚举可以进行比较。

// Comparable 枚举比较enum E4: Comparable {    case e1, e2    case e3(i: Int)    case e4}let e3 = E4.e4let e4 = E4.e3(i: 3)let e5 = E4.e3(i: 2)let e6 = E4.e1print(e3 > e4) // truelet a1 = [e3, e4, e5, e6]let a2 = a1.sorted()for i in a2 {    print(i.self)}/// e1/// e3(i: 2)/// e3(i: 3)/// e4

泛型

泛型可以减少重复代码,是一种抽象的表达方式。where 关键字可以对泛型做约束。

func fn<T>(p: T) -> [T] {    var r = [T]()    r.append(p)    return r}print(fn(p: "one"))// 结构体struct S1<T> {    var arr = [T]()    mutating func add(_ p: T) {        arr.append(p)    }}var s1 = S1(arr: ["zero"])s1.add("one")s1.add("two")print(s1.arr) // ["zero", "one", "two"]

关联类型

protocol pc {    associatedtype T    mutating func add(_ p: T)}struct S2: pc {    typealias T = String // 类型推导,可省略    var strs = [String]()    mutating func add(_ p: String) {        strs.append(p)    }}

泛型适用于嵌套类型

struct S3<T> {    struct S4 {        var p: T    }        var p1: T    var p2: S4}let s2 = S3(p1: 1, p2: S3.S4(p: 3))let s3 = S3(p1: "one", p2: S3.S4(p: "three"))print(s2,s3)

不透明类型

不透明类型会隐藏类型,让使用者更关注功能。不透明类型和协议很类似,不同的是不透明比协议限定的要多,协议能够对应更多类型。

protocol P {    func f() -> String}struct S1: P {    func f() -> String {        return "one\n"    }}struct S2<T: P>: P {    var p: T    func f() -> String {        return p.f() + "two\n"    }}struct S3<T1: P, T2: P>: P {    var p1: T1    var p2: T2    func f() -> String {        return p1.f() + p2.f() + "three\n"    }}func someP() -> some P {    return S3(p1: S1(), p2: S2(p: S1()))}let r = someP()print(r.f())

函数调用者决定返回什么类型是泛型,函数自身决定返回什么类型使用不透明返回类型。

Result

Result 类型用来处理错误,特别适用异步接口的错误处理。

extension URLSession {    func dataTaskWithResult(        with url: URL,        handler: @escaping (Result<Data, Error>) -> Void    ) -> URLSessionDataTask {        dataTask(with: url) { data, _, err in            if let err = err {                handler(.failure(err))            } else {                handler(.success(data ?? Data()))            }        }    }}let url = URL(string: "https://ming1016.github.io/")!// 以前网络请求let t1 = URLSession.shared.dataTask(with: url) {    data, _, error in    if let err = error {        print(err)    } else if let data = data {        print(String(decoding: data, as: UTF8.self))    }}t1.resume()// 使用 Result 网络请求let t2 = URLSession.shared.dataTaskWithResult(with: url) { result in    switch result {    case .success(let data):        print(String(decoding: data, as: UTF8.self))    case .failure(let err):        print(err)    }}t2.resume()

类型转换

使用 is 关键字进行类型判断, 使用as 关键字来转换成子类。

class S0 {}class S1: S0 {}class S2: S0 {}var a = [S0]()a.append(S1())a.append(S2())for e in a {    // 类型判断    if e is S1 {        print("Type is S1")    } else if e is S2 {        print("Type is S2")    }    // 使用 as 关键字转换成子类    if let s1 = e as? S1 {        print("As S1 \(s1)")    } else if let s2 = e as? S2 {        print("As S2 \(s2)")    }}

类和结构体

类可以定义属性、方法、构造器、下标操作。类使用扩展来扩展功能,遵循协议。类还以继承,运行时检查实例类型。

class C {    var p: String    init(_ p: String) {        self.p = p    }        // 下标操作    subscript(s: String) -> String {        get {            return p + s        }        set {            p = s + newValue        }    }}let c = C("hi")print(c.p)print(c[" ming"])c["k"] = "v"print(c.p)

结构体

结构体是值类型,可以定义属性、方法、构造器、下标操作。结构体使用扩展来扩展功能,遵循协议。

struct S {    var p1: String = ""    var p2: Int}extension S {    func f() -> String {        return p1 + String(p2)    }}var s = S(p2: 1)s.p1 = "1"print(s.f()) // 11

属性

类、结构体或枚举里的变量常量就是他们的属性。

struct S {    static let sp = "类型属性" // 类型属性通过类型本身访问,非实例访问    var p1: String = ""    var p2: Int = 1    // cp 是计算属性    var cp: Int {        get {            return p2 * 2        }        set {            p2 = newValue + 2        }    }    // 只有 getter 的是只读计算属性    var rcp: Int {        p2 * 4    }}print(S.sp)print(S().cp) // 2var s = S()s.cp = 3print(s.p2) // 5print(S().rcp) // 4

willSet 和 didSet 是属性观察器,可以在属性值设置前后插入自己的逻辑处理。

键路径表达式作为函数

struct S2 {    let p1: String    let p2: Int}let s2 = S2(p1: "one", p2: 1)let s3 = S2(p1: "two", p2: 2)let a1 = [s2, s3]let a2 = a1.map(\.p1)print(a2) // ["one", "two"]

方法

enum E: String {    case one, two, three    func showRawValue() {        print(rawValue)    }}let e = E.threee.showRawValue() // three// 可变的实例方法,使用 mutating 标记struct S {    var p: String    mutating func addFullStopForP() {        p += "."    }}var s = S(p: "hi")s.addFullStopForP()print(s.p)// 类方法class C {    class func cf() {        print("类方法")    }}

static和class关键字修饰的方法类似 OC 的类方法。static 可以修饰存储属性,而 class 不能;class 修饰的方法可以继承,而 static 不能。在协议中需用 static 来修饰。

静态下标方法

// 静态下标struct S2 {    static var sp = [String: Int]()        static subscript(_ s: String, d: Int = 10) -> Int {        get {            return sp[s] ?? d        }        set {            sp[s] = newValue        }    }}S2["key1"] = 1S2["key2"] = 2print(S2["key2"]) // 2print(S2["key3"]) // 10

自定义类型中实现了 callAsFunction() 的话,该类型的值就可以直接调用。

// callAsFunction()struct S3 {    var p1: String        func callAsFunction() -> String {        return "show \(p1)"    }}let s2 = S3(p1: "hi")print(s2()) // show hi

继承

类能继承另一个类,继承它的方法、属性等。

// 类继承class C1 {    var p1: String    var cp1: String {        get {            return p1 + " like ATM"        }        set {            p1 = p1 + newValue        }    }    init(p1: String) {        self.p1 = p1    }    func sayHi() {        print("Hi! \(p1)")    }}class C2: C1 {    var p2: String    init(p2: String) {        self.p2 = p2        super.init(p1: p2 + "'s father")    }}C2(p2: "Lemon").sayHi() // Hi! Lemon's father// 重写父类方法class C3: C2 {    override func sayHi() {        print("Hi! \(p2)")    }}C3(p2: "Lemon").sayHi() // Hi! Lemon// 重写计算属性class C4: C1 {    override var cp1: String {        get {            return p1 + " like Out of the blade"        }        set {            p1 = p1 + newValue        }    }}print(C1(p1: "Lemon").cp1) // Lemon like ATMprint(C4(p1: "Lemon").cp1) // Lemon like Out of the blade

通过 final 关键字可以防止类被继承,final 还可以用于属性和方法。使用 super 关键字指代父类。

函数式

map

map 可以依次处理数组中元素,并返回一个处理后的新数组。

let a1 = ["a", "b", "c"]let a2 = a1.map {    "\($0)2"}print(a2) // ["a2", "b2", "c2"]

使用 compactMap 可以过滤 nil 的元素。flatMap 会将多个数组合成一个数组返回。

filter

根据指定条件返回

let a1 = ["a", "b", "c", "call my name"]let a2 = a1.filter {    $0.prefix(1) == "c"}print(a2) // ["c", "call my name"]

reduce

reduce 可以将迭代中返回的结果用于下个迭代中,并,还能让你设个初始值。

let a1 = ["a", "b", "c", "call my name.", "get it?"]let a2 = a1.reduce("Hey u,", { partialResult, s in    // partialResult 是前面返回的值,s 是遍历到当前的值    partialResult + " \(s)"})print(a2) // Hey u, a b c call my name. get it?

sorted

排序

// 类型遵循 Comparablelet a1 = ["a", "b", "c", "call my name.", "get it?"]let a2 = a1.sorted()let a3 = a1.sorted(by: >)let a4 = a1.sorted(by: <)print(a2) // Hey u, a b c call my name. get it?print(a3) // ["get it?", "call my name.", "c", "b", "a"]print(a4) // ["a", "b", "c", "call my name.", "get it?"]// 类型不遵循 Comparablestruct S {    var s: String    var i: Int}let a5 = [S(s: "a", i: 0), S(s: "b", i: 1), S(s: "c", i: 2)]let a6 = a5    .sorted { l, r in        l.i > r.i    }    .map {        $0.i    }print(a6) // [2, 1, 0]

控制流

If • If let • If case let

// iflet s = "hi"if s.isEmpty {    print("String is Empty")} else {    print("String is \(s)")}// 三元条件s.isEmpty ? print("String is Empty again") : print("String is \(s) again")// if let-elsefunc f(s: String?) {    if let s1 = s {        print("s1 is \(s1)")    } else {        print("s1 is nothing")    }    // nil-coalescing    let s2 = s ?? "nothing"    print("s2 is \(s2)")}f(s: "something")f(s: nil)// if case letenum E {    case c1(String)    case c2([String])        func des() {        switch self {        case .c1(let string):            print(string)        case .c2(let array):            print(array)        }    }}E.c1("enum c1").des()E.c2(["one", "two", "three"]).des()

Guard guard, guard let

更好地处理异常情况

// guardfunc f1(p: String) -> String {    guard p.isEmpty != true else {        return "Empty string."    }    return "String \(p) is not empty."}print(f1(p: "")) // Empty string.print(f1(p: "lemon")) // String lemon is not empty.// guard letfunc f2(p1: String?) -> String {    guard let p2 = p1 else {        return "Nil."    }    return "String \(p2) is not nil."}print(f2(p1: nil)) // Nil.print(f2(p1: "lemon")) // String lemon is not nil.

遍历 For-in

let a = ["one", "two", "three"]for str in a {    print(str)}// 使用下标范围for i in 0..<10 {    print(i)}// 使用 enumeratedfor (i, str) in a.enumerated() {    print("第\(i + 1)个是:\(str)")}// for in wherefor str in a where str.prefix(1) == "t" {    print(str)}// 字典 for in,遍历是无序的let dic = [    "one": 1,    "two": 2,    "three": 3]for (k, v) in dic {    print("key is \(k), value is \(v)")}// stridefor i in stride(from: 10, through: 0, by: -2) {    print(i)}/* 10 8 6 4 2 0 */

While while, repeat-while

// whilevar i1 = 10while i1 > 0 {    print("positive even number \(i1)")    i1 -= 2}// repeat whilevar i2 = 10repeat {    print("positive even number \(i2)")    i2 -= 2} while i2 > 0

使用 break 结束遍历,使用 continue 跳过当前作用域,继续下个循环

Switch

func f1(pa: String, t:(String, Int)) {    var p1 = 0    var p2 = 10    switch pa {    case "one":        p1 = 1    case "two":        p1 = 2        fallthrough // 继续到下个 case 中    default:        p2 = 0    }    print("p1 is \(p1)")    print("p2 is \(p2)")        // 元组    switch t {    case ("0", 0):        print("zero")    case ("1", 1):        print("one")    default:        print("no")    }}f1(pa: "two", t:("1", 1))/* p1 is 2 p2 is 0 one */// 枚举enum E {    case one, two, three, unknown(String)}func f2(pa: E) {    var p: String    switch pa {    case .one:        p = "1"    case .two:        p = "2"    case .three:        p = "3"    case let .unknown(u) where Int(u) ?? 0 > 0 : // 枚举关联值,使用 where 增加条件        p = u    case .unknown(_):        p = "negative number"    }    print(p)}f2(pa: E.one) // 1f2(pa: E.unknown("10")) // 10f2(pa: E.unknown("-10")) // negative number

集合

数组 [1, 2, 3]

数组是有序集合

var a0: [Int] = [1, 10]a0.append(2)a0.remove(at: 0)print(a0) // [10, 2]let a1 = ["one", "two", "three"]let a2 = ["three", "four"]// 找两个集合的不同let dif = a1.difference(from: a2) // swift的 diffing 算法在这 http://www.xmailserver.org/diff2.pdf swift实现在  swift/stdlib/public/core/Diffing.swiftfor c in dif {    switch c {    case .remove(let o, let e, let a):        print("offset:\(o), element:\(e), associatedWith:\(String(describing: a))")    case .insert(let o, let e, let a):        print("offset:\(o), element:\(e), associatedWith:\(String(describing: a))")    }}/* remove offset:1, element:four, associatedWith:nil insert offset:0, element:one, associatedWith:nil insert offset:1, element:two, associatedWith:nil */let a3 = a2.applying(dif) ?? [] // 可以用于添加删除动画print(a3) // ["one", "two", "three"]

dif 有第三个 case 值 .insert(let offset, let element, let associatedWith) 可以跟踪成对的变化,用于高级动画。

从数组中随机取一个元素

print(a0.randomElement() ?? 0)

数组排序

// 排序struct S1 {    let n: Int    var b = true}let a4 = [    S1(n: 1),    S1(n: 10),    S1(n: 3),    S1(n: 2)]let a5 = a4.sorted { i1, i2 in    i1.n < i2.n}for n in a5 {    print(n)}/// S1(n: 1)/// S1(n: 2)/// S1(n: 3)/// S1(n: 10)let a6 = [1,10,4,7,2]print(a6.sorted(by: >)) // [10, 7, 4, 2, 1]

可以加到数组扩展中,通过扩展约束能够指定特定元素类型的排序,代码如下:

extension Array where Element == Int {    // 升序    func intSortedASC() -> [Int] {        return self.sorted(by: <)    }    // 降序    func intSortedDESC() -> [Int] {        return self.sorted(by: <)    }}print(a6.intSortedASC()) // 使用扩展增加自定义排序能力

在数组中检索满足条件的元素,代码如下:

// 第一个满足条件了就返回let a7 = a4.first {    $0.n == 10}print(a7?.n ?? 0)// 是否都满足了条件print(a4.allSatisfy { $0.n == 1 }) // falseprint(a4.allSatisfy(\.b)) // true// 找出最大的那个print(a4.max(by: { e1, e2 in    e1.n < e2.n}) ?? S1(n: 0))// S1(n: 10, b: true)// 看看是否包含某个元素print(a4.contains(where: {    $0.n == 7}))// false

一些切割数组的方法。

// 切片// 取前3个,并不是直接复制,对于大的数组有性能优势。print(a6[..<3]) // [1, 10, 4] 需要做越界检查print(a6.prefix(30)) // [1, 10, 4, 7, 2] 不需要做越界检查,也是切片,性能一样// 去掉前3个print(a6.dropFirst(3)) // [7, 2]

prefix(while:) 和 drop(while:) 方法,顺序遍历执行闭包里的逻辑判断,满足条件就返回,遇到不匹配就会停止遍历。prefix 返回满足条件的元素集合,drop 返回停止遍历之后那些元素集合。

let a8 = [8, 9, 20, 1, 35, 3]let a9 = a8.prefix {    $0 < 30}print(a9) // [8, 9, 20, 1]let a10 = a8.drop {    $0 < 30}print(a10) // [35, 3]

比 filter 更高效的删除元素的方法 removeAll

// 删除所有不满足条件的元素var a11 = [1, 3, 5, 12, 25]a11.removeAll { $0 < 10 }print(a11) // [4, 3, 1, 3, 3] 随机// 创建未初始化的数组let a12 = (0...4).map { _ in    Int.random(in: 0...5)}print(a12) // [0, 3, 3, 2, 5] 随机

#if 用于后缀表达式

// #if 用于后缀表达式let a13 = a11#if os(iOS)    .count#else    .reduce(0, +)#endifprint(a13) //37

Sets Set<Int>

Set 是无序集合,元素唯一

let s0: Set<Int> = [2, 4]let s1: Set = [2, 10, 6, 4, 8]let s2: Set = [7, 3, 5, 1, 9, 10]let s3 = s1.union(s2) // 合集let s4 = s1.intersection(s2) // 交集let s5 = s1.subtracting(s2) // 非交集部分let s6 = s1.symmetricDifference(s2) // 非交集的合集print(s3) // [4, 2, 1, 7, 3, 10, 8, 9, 6, 5]print(s4) // [10]print(s5) // [8, 4, 2, 6]print(s6) // [9, 1, 3, 4, 5, 2, 6, 8, 7]// s0 是否被 s1 包含print(s0.isSubset(of: s1)) // true// s1 是否包含了 s0print(s1.isSuperset(of: s0)) // truelet s7: Set = [3, 5]// s0 和 s7 是否有交集print(s0.isDisjoint(with: s7)) // true// 可变 Setvar s8: Set = ["one", "two"]s8.insert("three")s8.remove("one")print(s8) // ["two", "three"]

字典 [:]

字典是无序集合,键值对应。

var d1 = [    "k1": "v1",    "k2": "v2"]d1["k3"] = "v3"d1["k4"] = nilprint(d1) // ["k2": "v2", "k3": "v3", "k1": "v1"]for (k, v) in d1 {    print("key is \(k), value is \(v)")}/* key is k1, value is v1 key is k2, value is v2 key is k3, value is v3 */ if d1.isEmpty == false {    print(d1.count) // 3}// mapValueslet d2 = d1.mapValues {    $0 + "_new"}print(d2) // ["k2": "v2_new", "k3": "v3_new", "k1": "v1_new"]// 对字典的值或键进行分组let d3 = Dictionary(grouping: d1.values) {    $0.count}print(d3) // [2: ["v1", "v2", "v3"]]// 从字典中取值,如果键对应无值,则使用通过 default 指定的默认值d1["k5", default: "whatever"] += "."print(d1["k5"] ?? "") // whatever.let v1 = d1["k3", default: "whatever"]print(v1) // v3// compactMapValues() 对字典值进行转换和解包。可以解可选类型,并去掉 nil 值let d4 = [    "k1": 1,    "k2": 2,    "k3": nil]let d5 = d4.mapValues { $0 }let d6 = d4.compactMapValues{ $0 }print(d5)// ["k3": nil, "k1": Optional(1), "k2": Optional(2)]print(d6)// ["k1": 1, "k2": 2]

操作符

赋值 =, +=. -=, *=, /=

let i1 = 1var i2 = i1i2 = 2print(i2) // 2i2 += 1print(i2) // 3i2 -= 2print(i2) // 1i2 *= 10print(i2) // 10i2 /= 2print(i2) // 5

计算符 +, -, *, /, %

let i1 = 1let i2 = i1print((i1 + i2 - 1) * 10 / 2 % 3) // 2print("i" + "1") // i1// 一元运算符print(-i1) // -1

比较运算符 ==, >

遵循 Equatable 协议可以使用 == 和 != 来判断是否相等

print(1 > 2) // falsestruct S: Equatable {    var p1: String    var p2: Int}let s1 = S(p1: "one", p2: 1)let s2 = S(p1: "two", p2: 2)let s3 = S(p1: "one", p2: 2)let s4 = S(p1: "one", p2: 1)print(s1 == s2) // falseprint(s1 == s3) // falseprint(s1 == s4) // true

类需要实现 == 函数

class C: Equatable {    var p1: String    var p2: Int    init(p1: String, p2: Int) {        self.p1 = p1        self.p2 = p2    }        static func == (l: C, r: C) -> Bool {        return l.p1 == r.p1 && l.p2 == r.p2    }}let c1 = C(p1: "one", p2: 1)let c2 = C(p1: "one", p2: 1)print(c1 == c2)
// 元组比较// 会先比较第一个数,第一个无法比较才会比较第二个数// 字符串比较和字母大小还有长度有关。先比较字母大小,在比较长度("apple", 1) < ("apple", 2) // true("applf", 1) < ("apple", 2) // false("appl", 2) < ("apple", 1) // true("appm", 2) < ("apple", 1) // false

三元 _ ? _ : _

简化 if else 写法

// if elsefunc f1(p: Int) {    if p > 0 {        print("positive number")    } else {        print("negative number")    }}// 三元func f2(p: Int) {    p > 0 ? print("positive number") : print("negative number")}f1(p: 1)f2(p: 1)

Nil-coalescing ??

简化 if let else 写法

// if elsefunc f1(p: Int?) {    if let i = p {        print("p have value is \(i)")    } else {        print("p is nil, use defalut value")    }}// 使用 ??func f2(p: Int?) {    let i = p ?? 0    print("p is \(i)")}

范围 a…b

简化的值范围表达方式。

// 封闭范围for i in 0...10 {    print(i)}// 半开范围for i in 0..<10 {    print(i)}
// 单侧区间let nums = [5,6,7,8]print(nums[2...]) // 7 8

逻辑 !, &&, ||

let i1 = -1let i2 = 2if i1 != i2 && (i1 < 0 || i2 < 0) {    print("i1 and i2 not equal, and one of them is negative number.")}

恒等 ===, !==

恒等返回是否引用了相同实例。

class C {    var p: String    init(p: String) {        self.p = p    }}let c1 = C(p: "one")let c2 = C(p: "one")let c3 = c1print(c1 === c2) // falseprint(c1 === c3) // trueprint(c1 !== c2) // true

运算符

位运算符

let i1: UInt8 = 0b00001111let i2 = ~i1 // Bitwise NOT Operator(按位取反运算符),取反let i3: UInt8 = 0b00111111let i4 = i1 & i3 // Bitwise AND Operator(按位与运算符),都为1才是1let i5 = i1 | i3 // Bitwise OR Operator(按位或运算符),有一个1就是1let i6 = i1 ^ i3 // Bitwise XOR Operator(按位异或运算符),不同为1,相同为0print(i1,i2,i3,i4,i5,i6)// << 按位左移,>> 按位右移let i7 = i1 << 1let i8 = i1 >> 2print(i7,i8)

溢出运算符,有 &+、&- 和 &*

var i1 = Int.maxprint(i1) // 9223372036854775807i1 = i1 &+ 1print(i1) // -9223372036854775808i1 = i1 &+ 10print(i1) // -9223372036854775798var i2 = UInt.maxi2 = i2 &+ 1print(i2) // 0

运算符函数包括前缀运算符、后缀运算符、复合赋值运算符以及等价运算符。另,还可以自定义运算符,新的运算符要用 operator 关键字进行定义,同时要指定 prefix、infix 或者 postfix 修饰符。

基础库

时间

Date 的基本用法如下:

let now = Date()// Date 转 时间戳let interval = now.timeIntervalSince1970 // 时间戳let df = DateFormatter()df.dateFormat = "yyyy 年 MM 月 dd 日 HH:mm:ss"print("时间戳:\(Int(interval))") // 时间戳:1642399901print("格式化的时间:" + df.string(from: now)) // 格式化的时间:2022 年 01 月 17 日 14:11:41df.dateStyle = .shortprint("short 样式时间:" + df.string(from: now)) // short 样式时间:2022/1/17df.locale = Locale(identifier: "zh_Hans_CN")df.dateStyle = .fullprint("full 样式时间:" + df.string(from: now)) // full 样式时间:2022年1月17日 星期一// 时间戳转 Datelet date = Date(timeIntervalSince1970: interval)print(date) // 2022-01-17 06:11:41 +0000

复杂的时间操作,比如说 GitHub 接口使用的是 ISO 标准,RSS 输出的是 RSS 标准字符串,不同标准对应不同时区的时间计算处理,可以使用开源库 SwiftDate 来完成。示例代码如下:

import SwiftDate// 使用 SwiftDate 库let cn = Region(zone: Zones.asiaShanghai, locale: Locales.chineseChina)SwiftDate.defaultRegion = cnprint("2008-02-14 23:12:14".toDate()?.year ?? "") // 2008let d1 = "2022-01-17T23:20:35".toISODate(region: cn)guard let d1 = d1 else {    return}print(d1.minute) // 20let d2 = d1 + 1.minutesprint(d2.minute)// 两个 DateInRegion 相差时间 intervallet i1 = DateInRegion(Date(), region: cn) - d1let s1 = i1.toString {    $0.maximumUnitCount = 4    $0.allowedUnits = [.day, .hour, .minute]    $0.collapsesLargestUnit = true    $0.unitsStyle = .abbreviated    $0.locale = Locales.chineseChina}print(s1) // 9小时45分钟

格式化

使用标准库的格式来描述不同场景的情况可以不用去考虑由于不同地区的区别,这些在标准库里就可以自动完成了。

描述两个时间之间相差多长时间

// 计算两个时间之间相差多少时间,支持多种语言字符串let d1 = Date().timeIntervalSince1970 - 60 * 60 * 24let f1 = RelativeDateTimeFormatter()f1.dateTimeStyle = .namedf1.formattingContext = .beginningOfSentencef1.locale = Locale(identifier: "zh_Hans_CN")let str = f1.localizedString(for: Date(timeIntervalSince1970: d1), relativeTo: Date())print(str) // 昨天// 简写let str2 = Date.now.addingTimeInterval(-(60 * 60 * 24))    .formatted(.relative(presentation: .named))print(str2) // yesterday

描述多个事物

// 描述多个事物let s1 = ListFormatter.localizedString(byJoining: ["冬天","春天","夏天","秋天"])print(s1)

描述名字

// 名字let f2 = PersonNameComponentsFormatter()var nc1 = PersonNameComponents()nc1.familyName = "戴"nc1.givenName = "铭"nc1.nickname = "铭哥"print(f2.string(from: nc1)) // 戴铭f2.style = .shortprint(f2.string(from: nc1)) // 铭哥f2.style = .abbreviatedprint(f2.string(from: nc1)) // 戴var nc2 = PersonNameComponents()nc2.familyName = "Dai"nc2.givenName = "Ming"nc2.nickname = "Starming"f2.style = .defaultprint(f2.string(from: nc2)) // Ming Daif2.style = .shortprint(f2.string(from: nc2)) // Starmingf2.style = .abbreviatedprint(f2.string(from: nc2)) // MD// 取出名let componets = f2.personNameComponents(from: "戴铭")print(componets?.givenName ?? "") // 铭

描述数字

// 数字let f3 = NumberFormatter()f3.locale = Locale(identifier: "zh_Hans_CN")f3.numberStyle = .currencyprint(f3.string(from: 123456) ?? "") // ¥123,456.00f3.numberStyle = .percentprint(f3.string(from: 123456) ?? "") // 12,345,600%let n1 = 1.23456let n1Str = n1.formatted(.number.precision(.fractionLength(3)).rounded())print(n1Str) // 1.235

描述地址

// 地址import Contactslet f4 = CNPostalAddressFormatter()let address = CNMutablePostalAddress()address.street = "海淀区王庄路XX号院X号楼X门XXX"address.postalCode = "100083"address.city = "北京"address.country = "中国"print(f4.string(from: address))/// 海淀区王庄路XX号院X号楼X门XXX/// 北京 100083/// 中国

度量值

标准库里的物理量,在这个文档里有详细列出,包括角度、平方米等。

// 参考:https://developer.apple.com/documentation/foundation/nsdimensionlet m1 = Measurement(value: 1, unit: UnitLength.kilometers)let m2 = m1.converted(to: .meters) // 千米转米print(m2) // 1000.0 m// 度量值转为本地化的值let mf = MeasurementFormatter()mf.locale = Locale(identifier: "zh_Hans_CN")print(mf.string(from: m1)) // 1公里

一些物理公式供参考:

面积 = 长度 × 长度体积 = 长度 × 长度 × 长度 = 面积 × 长度速度=长度/时间加速度=速度/时间力 = 质量 × 加速度扭矩 = 力 × 长度压力 = 力 / 面积密度=质量 / 体积能量 = 功率 × 时间电阻 = 电压 / 电流

Data

数据压缩和解压

// 对数据的压缩let d1 = "看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?".data(using: .utf8)! as NSDataprint("ori \(d1.count) bytes")do {    /// 压缩算法    /// * lz4    /// * lzma    /// * zlib    /// * lzfse    let compressed = try d1.compressed(using: .zlib)    print("comp \(compressed.count) bytes")        // 对数据解压    let decomressed = try compressed.decompressed(using: .zlib)    let deStr = String(data: decomressed as Data, encoding: .utf8)    print(deStr ?? "")} catch {}/// ori 297 bytes/// comp 37 bytes

文件

文件的一些基本操作的代码如下:

let path1 = "/Users/mingdai/Downloads/1.html"let path2 = "/Users/mingdai/Documents/GitHub/"let u1 = URL(string: path1)do {    // 写入    let url1 = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: u1, create: true) // 保证原子性安全保存    print(url1)    // 读取    let s1 = try String(contentsOfFile: path1, encoding: .utf8)    print(s1)} catch {}// 检查路径是否可用let u2 = URL(fileURLWithPath:path2)do {    let values = try u2.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])    if let capacity = values.volumeAvailableCapacityForImportantUsage {        print("可用: \(capacity)")    } else {        print("不可用")    }} catch {    print("错误: \(error.localizedDescription)")}

怎么遍历多级目录结构中的文件呢?看下面的代码的实现:

// 遍历路径下所有目录let u3 = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)let fm = FileManager.defaultfm.enumerator(atPath: u3.path)?.forEach({ path in    guard let path = path as? String else {        return    }    let url = URL(fileURLWithPath: path, relativeTo: u3)    print(url.lastPathComponent)})

可以使用 FileWrapper 来创建文件夹和文件。举个例子:

// FileWrapper 的使用// 创建文件let f1 = FileWrapper(regularFileWithContents: Data("# 第 n 个文件\n ## 标题".utf8))f1.fileAttributes[FileAttributeKey.creationDate.rawValue] = Date()f1.fileAttributes[FileAttributeKey.modificationDate.rawValue] = Date()// 创建文件夹let folder1 = FileWrapper(directoryWithFileWrappers: [    "file1.md": f1])folder1.fileAttributes[FileAttributeKey.creationDate.rawValue] = Date()folder1.fileAttributes[FileAttributeKey.modificationDate.rawValue] = Date()do {    try folder1.write(        to: URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent("NewFolder"),        options: .atomic,        originalContentsURL: nil    )} catch {}print(FileManager.default.currentDirectoryPath)

上面代码写起来比较繁琐,对 FileWrapper 更好的封装可以参考这篇文章《 A Type-Safe FileWrapper | Heberti Almeida 》。

文件读写处理完整能力可以参看这个库 GitHub - JohnSundell/Files: A nicer way to handle files & folders in Swift

本地或者网络上,比如网盘和FTP的文件发生变化时,怎样知道能够观察到呢?

通过 HTTPHeader 里的 If-Modified-Since、Last-Modified、If-None-Match 和 Etag 等字段来判断文件的变化,本地则是使用 DispatchSource.makeFileSystemObjectSource 来进行的文件变化监听。可以参考 KZFileWatchers 库的做法。

Scanner

let s1 = """one1,two2,three3."""let sn1 = Scanner(string: s1)while !sn1.isAtEnd {    if let r1 = sn1.scanUpToCharacters(from: .newlines) {        print(r1 as String)    }}/// one1,/// two2,/// three3.// 找出数字let sn2 = Scanner(string: s1)sn2.charactersToBeSkipped = CharacterSet.decimalDigits.inverted // 不是数字的就跳过var p: Int = 0while !sn2.isAtEnd {    if sn2.scanInt(&p) {        print(p)    }}/// 1/// 2/// 3

上面的代码还不是那么 Swifty,可以通过用AnySequence和AnyIterator来包装下,将序列中的元素推迟到实际需要时再来处理,这样性能也会更好些。具体实现可以参看《 String parsing in Swift 》这篇文章。

AttributeString

效果如下:

代码如下:

var aStrs = [AttributedString]()var aStr1 = AttributedString("""标题正文内容,具体查看链接。这里摘出第一个重点,还要强调的内容。""")// 标题let title = aStr1.range(of: "标题")guard let title = title else {    return aStrs}var c1 = AttributeContainer() // 可复用容器c1.inlinePresentationIntent = .stronglyEmphasizedc1.font = .largeTitleaStr1[title].setAttributes(c1)// 链接let link = aStr1.range(of: "链接")guard let link = link else {    return aStrs}var c2 = AttributeContainer() // 链接c2.strokeColor = .bluec2.link = URL(string: "https://ming1016.github.io/")aStr1[link].setAttributes(c2.merging(c1)) // 合并 AttributeContainer// Runslet i1 = aStr1.range(of: "重点")let i2 = aStr1.range(of: "强调")guard let i1 = i1, let i2 = i2 else {    return aStrs}var c3 = AttributeContainer()c3.foregroundColor = .yellowc3.inlinePresentationIntent = .stronglyEmphasizedaStr1[i1].setAttributes(c3)aStr1[i2].setAttributes(c3)for r in aStr1.runs {    print("-------------")    print(r.attributes)}aStrs.append(aStr1)// Markdowndo {    let aStr2 = try AttributedString(markdown: """    内容[链接](https://ming1016.github.io/)。需要**强调**的内容。    """)        aStrs.append(aStr2)    } catch {}

SwiftUI 的 Text 可以直接读取 AttributedString 来进行显示。

随机

用法:

let ri = Int.random(in: 0..<10)print(ri) // 0到10随机数let a = [0, 1, 2, 3, 4, 5]print(a.randomElement() ?? 0) // 数组中随机取个数print(a.shuffled()) // 随机打乱数组顺序

UserDefaults

使用方法如下:

enum UDKey {    static let k1 = "token"}let ud = UserDefaults.standardud.set("xxxxxx", forKey: UDKey.k1)let tk = ud.string(forKey: UDKey.k1)print(tk ?? "")

模式

单例

struct S {    static let shared = S()    private init() {        // 防止实例初始化    }}

系统及设备

系统判断

#if os(tvOS)     // do something in tvOS#elseif os(iOS)     // do somthing in iOS#elseif os(macOS)    // do somthing in macOS#endif

版本兼容

// 版本@available(iOS 15, *)func f() {}// 版本检查if #available(iOS 15, macOS 12, *) {    f()} else {    // nothing happen}

canImport 判断库是否可使用

#if canImport(SpriteKit)   // iOS 等苹果系统执行#else   // 非苹果系统#endif

targetEnvironment 环境的判断

#if targetEnvironment(simulator)   // 模拟器#else   // 真机#endif

自带属性包装

@resultBuilder

结果生成器(Result builders),通过传递序列创建新值,SwiftUI就是使用的结果生成器将多个视图生成一个视图

@resultBuilderstruct RBS {    // 基本闭包支持    static func buildBlock(_ components: Int...) -> Int {        components.reduce(0) { partialResult, i in            partialResult + i        }    }    // 支持条件判断    static func buildEither(first component: Int) -> Int {        component    }    static func buildEither(second component: Int) -> Int {        component    }    // 支持循环    static func buildArray(_ components: [Int]) -> Int {        components.reduce(0) { partialResult, i in            partialResult + i        }    }}let a = RBS.buildBlock(    1,    2,    3)print(a) // 6// 应用到函数中@RBS func f1() -> Int {    1    2    3}print(f1()) // 6// 设置了 buildEither 就可以在闭包中进行条件判断。@RBS func f2(stopAtThree: Bool) -> Int {    1    2    3    if stopAtThree == true {        0    } else {        4        5        6    }}print(f2(stopAtThree: false)) // 21// 设置了 buildArray 就可以在闭包内使用循环了@RBS func f3() -> Int {    for i in 1...3 {        i * 2    }}print(f3()) // 12

@dynamicMemberLookup 动态成员查询

@dynamicMemberLookup 指示访问属性时调用一个已实现的处理动态查找的下标方法 subscript(dynamicMemeber:),通过指定属性字符串名返回值。使用方法如下:

@dynamicMemberLookupstruct D {    // 找字符串    subscript(dynamicMember m: String) -> String {        let p = ["one": "first", "two": "second"]        return p[m, default: ""]    }    // 找整型    subscript(dynamicMember m: String) -> Int {        let p = ["one": 1, "two": 2]        return p[m, default: 0]    }    // 找闭包    subscript(dynamicMember m: String) -> (_ s: String) -> Void {        return {            print("show \($0)")        }    }    // 静态数组成员    var p = ["This is a member"]    // 动态数组成员    subscript(dynamicMember m: String) -> [String] {        return ["This is a dynamic member"]    }}let d = D()let s1: String = d.oneprint(s1) // firstlet i1: Int = d.oneprint(i1) // 1d.show("something") // show somethingprint(d.p) // ["This is a member"]let dynamicP:[String] = d.dpprint(dynamicP) // ["This is a dynamic member"]

类使用 @dynamicMemberLookup,继承的类也会自动加上 @dynamicMemberLookup。协议上定义 @dynamicMemberLookup,通过扩展可以默认实现 subscript(dynamicMember:) 方法。

@dynamicCallable 动态可调用类型

@dynamicCallable 动态可调用类型。通过实现 dynamicallyCall 方法来定义变参的处理。

@dynamicCallablestruct D {    // 带参数说明    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Int {        let firstArg = args.first?.value ?? 0        return firstArg * 2    }        // 无参数说明    func dynamicallyCall(withArguments args: [String]) -> String {        var firstArg = ""        if args.count > 0 {            firstArg = args[0]        }        return "show \(firstArg)"    }}let d = D()let i = d(numberIs: 2)print(i) // 4let s = d("hi")print(s) // show hi

自带协议

Hashable

struct H: Hashable {    var p1: String    var p2: Int        // 提供随机 seed    func hash(into hasher: inout Hasher) {        hasher.combine(p1)    }}let h1 = H(p1: "one", p2: 1)let h2 = H(p1: "two", p2: 2)var hs1 = Hasher()hs1.combine(h1)hs1.combine(h2)print(h1.hashValue) // 7417088153212460033 随机值print(h2.hashValue) // -6972912482785541972 随机值print(hs1.finalize()) // 7955861102637572758 随机值print(h1.hashValue) // 7417088153212460033 和前面 h1 一样let h3 = H(p1: "one", p2: 1)print(h3.hashValue) // 7417088153212460033 和前面 h1 一样var hs2 = Hasher()hs2.combine(h3)hs2.combine(h2)print(hs2.finalize()) // 7955861102637572758 和前面 hs1 一样

应用生命周期内,调用 combine() 添加相同属性哈希值相同,由于 Hasher 每次都会使用随机的 seed,因此不同应用生命周期,也就是下次启动的哈希值,就会和上次的哈希值不同。

Codable

JSON 没有 id 字段

如果SwiftUI要求数据Model都是遵循Identifiable协议的,而有的json没有id这个字段,可以使用扩展struct的方式解决:

struct CommitModel: Decodable, Hashable {  var sha: String  var author: AuthorModel  var commit: CommitModel}extension CommitModel: Identifiable {  var id: String {    return sha  }}

网络

网络状态检查

通过 Network 库的 NWPathMonitor 来检查

import Combineimport Network// 网络状态检查 network state checkfinal class Nsck: ObservableObject {    static let shared = Nsck()    private(set) lazy var pb = mkpb()    @Published private(set) var pt: NWPath        private let monitor: NWPathMonitor    private lazy var sj = CurrentValueSubject<NWPath, Never>(monitor.currentPath)    private var sb: AnyCancellable?        init() {        monitor = NWPathMonitor()        pt = monitor.currentPath        monitor.pathUpdateHandler = { [weak self] path in            self?.pt = path            self?.sj.send(path)        }        monitor.start(queue: DispatchQueue.global())    }        deinit {        monitor.cancel()        sj.send(completion: .finished)    }        private func mkpb() -> AnyPublisher<NWPath, Never> {        return sj.eraseToAnyPublisher()    }}

使用方法

var sb = Set<AnyCancellable>()var alertMsg = ""Nsck.shared.pb    .sink { _ in        //    } receiveValue: { path in        alertMsg = path.debugDescription        switch path.status {        case .satisfied:            alertMsg = ""        case .unsatisfied:            alertMsg = "😱"        case .requiresConnection:            alertMsg = "🥱"        @unknown default:            alertMsg = "🤔"        }        if path.status == .unsatisfied {            switch path.unsatisfiedReason {            case .notAvailable:                alertMsg += "网络不可用"            case .cellularDenied:                alertMsg += "蜂窝网不可用"            case .wifiDenied:                alertMsg += "Wifi不可用"            case .localNetworkDenied:                alertMsg += "网线不可用"            @unknown default:                alertMsg += "网络不可用"            }        }    }    .store(in: &sb)

动画

布局动画

import SwiftUIstruct AnimateLayout: View { @State var changeLayout: Bool = true @Namespace var namespace var body: some View {  VStack(spacing: 30) {   if changeLayout {    HStack { items }   } else {    VStack { items }   }   Button("切换布局") {    withAnimation { changeLayout.toggle() }   }  }  .padding() } @ViewBuilder var items: some View {  Text("one")   .matchedGeometryEffect(id: "one", in: namespace)  Text("Two")   .matchedGeometryEffect(id: "Two", in: namespace)  Text("Three")   .matchedGeometryEffect(id: "Three", in: namespace) }}

安全

Keychain

使用方法:

let d1 = Data("keyChain github token".utf8)let service = "access-token"let account = "github"let q1 = [    kSecValueData: d1,    kSecClass: kSecClassGenericPassword,    kSecAttrService: service,    kSecAttrAccount: account] as CFDictionary// 添加一个 keychainlet status = SecItemAdd(q1, nil)// 如果已经添加过会抛出 -25299 错误代码,需要调用 SecItemUpdate 来进行更新if status == errSecDuplicateItem {    let q2 = [        kSecClass: kSecClassGenericPassword,        kSecAttrService: service,        kSecAttrAccount: account    ] as CFDictionary    let q3 = [        kSecValueData: d1    ] as CFDictionary    SecItemUpdate(q2, q3)}// 读取let q4 = [    kSecAttrService: service,    kSecAttrAccount: account,    kSecClass: kSecClassGenericPassword,    kSecReturnData: true] as CFDictionaryvar re: AnyObject?SecItemCopyMatching(q4, &re)guard let reData = re as? Data else { return }print(String(decoding: reData, as: UTF8.self)) // keyChain github token// 删除let q5 = [    kSecAttrService: service,    kSecAttrAccount: account,    kSecClass: kSecClassGenericPassword,] as CFDictionarySecItemDelete(q5)

工程

程序入口点

Swift 允许全局编写 Swift 代码,实际上 clang 会自动将代码包进一个模拟 C 的函数中。Swift 也能够指定入口点,比如 @UIApplicationMain 或 @NSApplicationMain,UIKit 启动后生命周期管理是 AppDelegate 和 SceneDelegate,《 Understanding the iOS 13 Scene Delegate 》这篇有详细介绍。

@UIApplicationMain 和 @NSApplicationMain 会自动生成入口点。这些入口点都是平台相关的,Swift 发展来看是多平台的,这样在 Swift 5.3 时引入了 @main,可以方便的指定入口点。代码如下:

@main // 要定义个静态的 main 函数struct M {  static func main() {    print("let's begin")  }}

ArgumentParser 库,Swift 官方开源的一个开发命令行工具的库,也支持 @main。使用方法如下:

import ArgumentParser@mainstruct C: ParsableCommand {  @Argument(help: "Start")  var phrase: String     func run() throws {    for _ in 1...5 {      print(phrase)    }  }}

专题

Swift 那些事

语法速查

基础

变量 let, var

变量是可变的,使用 var 修饰,常量是不可变的,使用 let 修饰。类、结构体和枚举里的变量是属性。

var v1:String = "hi" // 标注类型var v2 = "类型推导"let l1 = "标题" // 常量class a {    let p1 = 3    var p2: Int {        p1 * 3    }}

属性没有 set 可以省略 get,如果有 set 需加 get。变量设置前通过 willSet 访问到,变量设置后通过 didSet 访问。

打印 print(“”)

控制台打印值

print("hi")let i = 14print(i)print("9月\(i)是小柠檬的生日")for i in 1...3{    print(i)}// output:// 1// 2// 3// 使用terminator使循环打印更整洁for i in 1...3 {    print("\(i) ", terminator: "")}// output:// 1 2 3

注释 //

// 单行注释/*多行注释第一行。多行注释第二行。*/ // MARK: 会在 minimap 上展示// TODO: 待做// FIXME: 待修复

可选 ?, !

可能会是 nil 的变量就是可选变量。当变量为 nil 通过??操作符可以提供一个默认值。

var o: Int? = nillet i = o ?? 0

SE-0345 if let shorthand for shadowing an existing optional variable 引入的新语法,用于 unwrapping optinal。

let s1: String? = "hey"let s2: String? = "u"if let s1 {    print(s1)}guard let s1, let s2 else { return }print(s1 + " " + s2)

闭包

闭包也可以叫做 lambda,是匿名函数,对应 OC 的 block。

let a1 = [1,3,2].sorted(by: { (l: Int, r: Int) -> Bool in    return l < r})// 如果闭包是唯一的参数并在表达式最后可以使用结尾闭包语法,写法简化为let a2 = [1,3,2].sorted { (l: Int, r: Int) -> Bool in    return l < r}// 已知类型可以省略let a3 = [1,3,2].sorted { l, r in    return l < r}// 通过位置来使用闭包的参数,最后简化如下:let a4 = [1,3,2].sorted { $0 < $1 }

函数也是闭包的一种,函数的参数也可以是闭包。@escaping 表示逃逸闭包,逃逸闭包是可以在函数返回之后继续调用的。@autoclosure 表示自动闭包,可以用来省略花括号。

SE-0326 提高了 Swift 对闭包使用参数和类型推断的能力。如下代码:

let a = [1,2,3]let r = a.map { i in    if i >= 2 {        return "\(i) 大于等于2"    } else {        return "\(i) 小于2"    }}print(r)

函数 func

函数可以作为另一个函数的参数,也可以作为另一个函数的返回。函数是特殊的闭包,在类、结构体和枚举中是方法。

// 为参数设置默认值func f1(p: String = "p") -> String {    "p is \(p)"}// 函数作为参数func f2(fn: (String) -> String, p: String) -> String {    return fn(p)}print(f2(fn:f1, p: "d")) // p is d// 函数作为返回值func f3(p: String) -> (String) -> String {    return f1}print(f3(p: "yes")("no")) // p is no

函数可以返回多个值,函数是可以嵌套的,也就是函数里内可以定义函数,函数内定义的函数可以访问自己作用域外函数内的变量。inout 表示的是输入输出参数,函数可以在函数内改变输入输出参数。defer 标识的代码块会在函数返回之前执行。

函数在 Swift 5.4 时开始有了使用多个变量参数的能力,使用方法如下:

func f4(s: String..., i: Int...) {    print(s)    print(i)}f4(s: "one", "two", "three", i: 1, 2, 3)/// ["one", "two", "three"]/// [1, 2, 3]

嵌套函数可以重载,嵌套函数可以在声明函数之前调用他。

func f5() {    nf5()    func nf5() {        print("this is nested function")    }}f5() // this is nested function

访问控制

在 Xcode 里的 target 就是模块,使用 import 可导入模块。模块内包含源文件,每个源文件里可以有多个类、结构体、枚举和函数等多种类型。访问级别可以通过一些关键字描述,分为如下几种:

  • open:在模块外可以调用和继承。
  • public:在模块外可调用不可继承,open 只适用类和类成员。
  • internal:默认级别,模块内可跨源文件调用,模块外不可调用。
  • fileprivate:只能在源文件内访问。
  • private:只能在所在的作用域内访问。

重写继承类的成员,可以设置成员比父类的这个成员更高的访问级别。Setter 的级别可以低于对应的 Getter 的级别,比如设置 Setter 访问级别为 private,可以在属性使用 private(set) 来修饰。

Regex

标准库多了个 Regex<Output> 类型,Regex 语法与 Perl、Python、Ruby、Java、NSRegularExpression 和许多其他语言兼容。可以用 let regex = try! Regex("a[bc]+")let regex = /a[bc]+/ 写法来使用。SE-0350 Regex Type and Overview 引入 Regex 类型。SE-0351 Regex builder DSL 使用 result builder 来构建正则表达式的 DSL。SE-0354 Regex Literals 简化的正则表达式。SE-0357 Regex-powered string processing algorithms 提案里有基于正则表达式的新字符串处理算法。

RegexBuilder 文档

session Meet Swift RegexSwift Regex: Beyond the basics

Regex 示例代码如下:

let s1 = "I am not a good painter"print(s1.ranges(of: /good/))do {    let regGood = try Regex("[a-z]ood")    print(s1.replacing(regGood, with: "bad"))} catch {    print(error)}print(s1.trimmingPrefix(/i am /.ignoresCase()))let reg1 = /(.+?) read (\d+) books./let reg2 = /(?<name>.+?) read (?<books>\d+) books./let s2 = "Jack read 3 books."do {    if let r1 = try reg1.wholeMatch(in: s2) {        print(r1.1)        print(r1.2)    }    if let r2 = try reg2.wholeMatch(in: s2) {        print("name:" + r2.name)        print("books:" + r2.books)    }} catch {    print(error)}

使用 regex builders 的官方示例:

// Text to parse:// CREDIT  03/02/2022  Payroll from employer     $200.23// CREDIT  03/03/2022  Suspect A           $2,000,000.00// DEBIT   03/03/2022  Ted's Pet Rock Sanctuary    $2,000,000.00// DEBIT   03/05/2022  Doug's Dugout Dogs      $33.27import RegexBuilderlet fieldSeparator = /\s{2,}|\t/let transactionMatcher = Regex {  /CREDIT|DEBIT/  fieldSeparator  One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) // 👈🏻 we define which data locale/timezone we want to use  fieldSeparator  OneOrMore {    NegativeLookahead { fieldSeparator } // 👈🏻 we stop as soon as we see one field separator    CharacterClass.any  }  fieldSeparator  One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US")))}

在正则表达式中捕获数据,使用 Capture:

let fieldSeparator = /\s{2,}|\t/let transactionMatcher = Regex {  Capture { /CREDIT|DEBIT/ } // 👈🏻  fieldSeparator  Capture { One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) } // 👈🏻  fieldSeparator  Capture { // 👈🏻    OneOrMore {      NegativeLookahead { fieldSeparator }      CharacterClass.any    }  }  fieldSeparator  Capture { One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US"))) } // 👈🏻}// transactionMatcher: Regex<(Substring, Substring, Date, Substring, Decimal)>

基础类型

数字 Int, Float

数字的类型有 Int、Float 和 Double

// Intlet i1 = 100let i2 = 22print(i1 / i2) // 向下取整得 4// Floatlet f1: Float = 100.0let f2: Float = 22.0print(f1 / f2) // 4.5454545let f3: Float16 = 5.0 // macOS 还不能用let f4: Float32 = 5.0let f5: Float64 = 5.0let f6: Float80 = 5.0print(f4, f5, f6) // 5.0 5.0 5.0// Doublelet d1: Double = 100.0let d2: Double = 22.0print(d1 / d2) // 4.545454545454546// 字面量print(Int(0b10101)) // 0b 开头是二进制 print(Int(0x00afff)) // 0x 开头是十六进制print(2.5e4) // 2.5x10^4 十进制用 eprint(0xAp2) // 10*2^2  十六进制用 pprint(2_000_000) // 2000000// isMultiple(of:) 方法检查一个数字是否是另一个数字的倍数let i3 = 36print(i3.isMultiple(of: 9)) // true

处理数字有 floor、ceil、round。floor 是向下取整,只取整数部分;cell 是向上取整,只要有不为零的小数,整数就加1;round 是四舍五入。

布尔数 Bool

布尔数有 true 和 false 两种值,还有一个能够切换这两个值的 toggle 方法。

var b = falseb.toggle() // trueb.toggle() // false

元组 (a, b, c)

元组里的值类型可以是不同的。元组可以看成是匿名的结构体。

let t1 = (p1: 1, p2: "two", p3: [1,2,3])print(t1.p1)print(t1.p3)// 类型推导let t2 = (1, "two", [1,2,3])// 通过下标访问print(t2.1) // two// 分解元组let (dp1, dp2, _) = t2print(dp1)print(dp2)

字符串

let s1 = "Hi! This is a string. Cool?"/// 转义符 \n 表示换行。/// 其它转义字符有 \0 空字符)、\t 水平制表符 、\n 换行符、\r 回车符let s2 = "Hi!\nThis is a string. Cool?"// 多行let s3 = """Hi!This is a string.Cool?"""// 长度print(s3.count)print(s3.isEmpty)// 拼接print(s3 + "\nSure!")// 字符串中插入变量let i = 1print("Today is good day, double \(i)\(i)!")/// 遍历字符串/// 输出:/// o/// n/// efor c in "one" {    print(c)}// 查找print(s3.lowercased().contains("cool")) // true// 替换let s4 = "one is two"let newS4 = s4.replacingOccurrences(of: "two", with: "one")print(newS4)// 删除空格和换行let s5 = " Simple line. \n\n  "print(s5.trimmingCharacters(in: .whitespacesAndNewlines))// 切割成数组let s6 = "one/two/three"let a1 = s6.components(separatedBy: "/") // 继承自 NSString 的接口print(a1) // ["one", "two", "three"]let a2 = s6.split(separator: "/")print(a2) // ["one", "two", "three"] 属于切片,性能较 components 更好// 判断是否是某种类型let c1: Character = "🤔"print(c1.isASCII) // falseprint(c1.isSymbol) // trueprint(c1.isLetter) // falseprint(c1.isNumber) // falseprint(c1.isUppercase) // false// 字符串和 Data 互转let data = Data("hi".utf8)let s7 = String(decoding: data, as: UTF8.self)print(s7) // hi// 字符串可以当作集合来用。let revered = s7.reversed()print(String(revered))

Unicode、Character 和 SubString 等内容参见官方字符串文档:Strings and Characters — The Swift Programming Language (Swift 5.1)

字符串字面符号可以参看《String literals in Swift》。

原始字符串

// 原始字符串在字符串前加上一个或多个#符号。里面的双引号和转义符号将不再起作用了,如果想让转义符起作用,需要在转义符后面加上#符号。let s8 = #"\(s7)\#(s7) "one" and "two"\n. \#nThe second line."#print(s8)/// \(s7)hi "one" and "two"\n./// The second line.// 原始字符串在正则使用效果更佳,反斜杠更少了。let s9 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"let s10 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#print(s9) // \\[A-Z]+[A-Za-z]+\.[a-z]+print(s10) // \\[A-Z]+[A-Za-z]+\.[a-z]+

Swift5.7 String Index 大升级 String Index Overhaul

枚举

Swift的枚举有类的一些特性,比如计算属性、实例方法、扩展、遵循协议等等。

enum E1:String, CaseIterable {    case e1, e2 = "12"}// 关联值enum E2 {    case e1([String])    case e2(Int)}let e1 = E2.e1(["one","two"])let e2 = E2.e2(3)switch e1 {case .e1(let array):    print(array)case .e2(let int):    print(int)}print(e2)// 原始值print(E1.e1.rawValue)// 遵循 CaseIterable 协议可迭代for ie in E1.allCases {    print("show \(ie)")}// 递归枚举enum RE {    case v(String)    indirect case node(l:RE, r:RE)}let lNode = RE.v("left")let rNode = RE.v("right")let pNode = RE.node(l: lNode, r: rNode)switch pNode {case .v(let string):    print(string)case .node(let l, let r):    print(l,r)    switch l {    case .v(let string):        print(string)    case .node(let l, let r):        print(l, r)    }    switch r {    case .v(let string):        print(string)    case .node(let l, let r):        print(l, r)    }}

@unknown 用来区分固定的枚举和可能改变的枚举的能力。@unknown 用于防止未来新增枚举属性会进行提醒提示完善每个 case 的处理。

// @unknownenum E3 {    case e1, e2, e3}func fe1(e: E3) {    switch e {    case .e1:        print("e1 ok")    case .e2:        print("e2 ok")    case .e3:        print("e3 ok")    @unknown default:        print("not ok")    }}

符合 Comparable 协议的枚举可以进行比较。

// Comparable 枚举比较enum E4: Comparable {    case e1, e2    case e3(i: Int)    case e4}let e3 = E4.e4let e4 = E4.e3(i: 3)let e5 = E4.e3(i: 2)let e6 = E4.e1print(e3 > e4) // truelet a1 = [e3, e4, e5, e6]let a2 = a1.sorted()for i in a2 {    print(i.self)}/// e1/// e3(i: 2)/// e3(i: 3)/// e4

泛型和协议

泛型可以减少重复代码,是一种抽象的表达方式。where 关键字可以对泛型做约束。

func fn<T>(p: T) -> [T] {    var r = [T]()    r.append(p)    return r}print(fn(p: "one"))// 结构体struct S1<T> {    var arr = [T]()    mutating func add(_ p: T) {        arr.append(p)    }}var s1 = S1(arr: ["zero"])s1.add("one")s1.add("two")print(s1.arr) // ["zero", "one", "two"]

关联类型

protocol pc {    associatedtype T    mutating func add(_ p: T)}struct S2: pc {    typealias T = String // 类型推导,可省略    var strs = [String]()    mutating func add(_ p: String) {        strs.append(p)    }}

泛型适用于嵌套类型

struct S3<T> {    struct S4 {        var p: T    }        var p1: T    var p2: S4}let s2 = S3(p1: 1, p2: S3.S4(p: 3))let s3 = S3(p1: "one", p2: S3.S4(p: "three"))print(s2,s3)

session Embrace Swift genericsDesign protocol interfaces in Swift

swift 5.6 和之前编写泛型接口如下:

func feed<A>(_ animal: A) where A: Animal// 👆🏻👇🏻 Equivalentsfunc feed<A: Animal>(_ animal: A)

swift 5.7 可以这样写:

func feed(_ animal: some Animal)

some 关键字可以用于参数和结构类型。some 会保证类型关系,而 any 会持有任意具体类型,删除类型关系。

SE-0347 Type inference from default expressions 扩展 Swift 泛型参数类型的默认值能力。如下代码示例:

func suffledArray<T: Sequence>(from options: T = 1...100) -> [T.Element] {    Array(options.shuffled())}print(suffledArray())print(suffledArray(from: ["one", "two", "three"]))

SE-0341 Opaque Parameter Declarations 使用 some 参数简化泛型参数声明。SE-0328 Structural opaque result types 扩大不透明结果返回类型可以使用的范围。SE-0360 Opaque result types with limited availability 可用性有限的不透明结果类型,比如 if #available(macOS 13.0, *) {} 就可以根据系统不同版本返回不同类型,新版本出现新类型的 View 就可以和以前的 View 类型区别开。

SE-0309 Unlock existentials for all protocols 改进了 existentials 和 泛型的交互。这样就可以更方便的检查 Any 类型的两个值是否相等

any 关键字充当的是类型擦除的助手,是通过告知编译器你使用 existential 作为类型,此语法可兼容以前系统。

SE-0346 Lightweight same-type requirements for primary associated types 引入一种新语法,用于符合泛型参数并通过相同类型要求约束关联类型。SE-0358 Primary Associated Types in the Standard Library 引入主要关联类型概念,并将其带入了标准库。这些关联类型很像泛型,允许开发者将给定关联类型的类型指定为通用约束。

SE-0353 Constrained Existential Types 基于 SE-0309 和 SE-0346 提案,在 existential 类型的上下文中重用轻量关联类型的约束。

SE-0352 Implicitly Opened Existentials 允许 Swift 在很多情况下使用协议调用泛型函数。

Swift 论坛上一个对 any 和 some 关键字语法使用场景的讨论,Do any and some help with “Protocol Oriented Testing” at all?

不透明类型

不透明类型会隐藏类型,让使用者更关注功能。不透明类型和协议很类似,不同的是不透明比协议限定的要多,协议能够对应更多类型。

protocol P {    func f() -> String}struct S1: P {    func f() -> String {        return "one\n"    }}struct S2<T: P>: P {    var p: T    func f() -> String {        return p.f() + "two\n"    }}struct S3<T1: P, T2: P>: P {    var p1: T1    var p2: T2    func f() -> String {        return p1.f() + p2.f() + "three\n"    }}func someP() -> some P {    return S3(p1: S1(), p2: S2(p: S1()))}let r = someP()print(r.f())

函数调用者决定返回什么类型是泛型,函数自身决定返回什么类型使用不透明返回类型。

Result

Result 类型用来处理错误,特别适用异步接口的错误处理。

extension URLSession {    func dataTaskWithResult(        with url: URL,        handler: @escaping (Result<Data, Error>) -> Void    ) -> URLSessionDataTask {        dataTask(with: url) { data, _, err in            if let err = err {                handler(.failure(err))            } else {                handler(.success(data ?? Data()))            }        }    }}let url = URL(string: "https://ming1016.github.io/")!// 以前网络请求let t1 = URLSession.shared.dataTask(with: url) {    data, _, error in    if let err = error {        print(err)    } else if let data = data {        print(String(decoding: data, as: UTF8.self))    }}t1.resume()// 使用 Result 网络请求let t2 = URLSession.shared.dataTaskWithResult(with: url) { result in    switch result {    case .success(let data):        print(String(decoding: data, as: UTF8.self))    case .failure(let err):        print(err)    }}t2.resume()

类型转换

使用 is 关键字进行类型判断, 使用as 关键字来转换成子类。

class S0 {}class S1: S0 {}class S2: S0 {}var a = [S0]()a.append(S1())a.append(S2())for e in a {    // 类型判断    if e is S1 {        print("Type is S1")    } else if e is S2 {        print("Type is S2")    }    // 使用 as 关键字转换成子类    if let s1 = e as? S1 {        print("As S1 \(s1)")    } else if let s2 = e as? S2 {        print("As S2 \(s2)")    }}

类和结构体

类可以定义属性、方法、构造器、下标操作。类使用扩展来扩展功能,遵循协议。类还以继承,运行时检查实例类型。

class C {    var p: String    init(_ p: String) {        self.p = p    }        // 下标操作    subscript(s: String) -> String {        get {            return p + s        }        set {            p = s + newValue        }    }}let c = C("hi")print(c.p)print(c[" ming"])c["k"] = "v"print(c.p)

结构体

结构体是值类型,可以定义属性、方法、构造器、下标操作。结构体使用扩展来扩展功能,遵循协议。

struct S {    var p1: String = ""    var p2: Int}extension S {    func f() -> String {        return p1 + String(p2)    }}var s = S(p2: 1)s.p1 = "1"print(s.f()) // 11

属性

类、结构体或枚举里的变量常量就是他们的属性。

struct S {    static let sp = "类型属性" // 类型属性通过类型本身访问,非实例访问    var p1: String = ""    var p2: Int = 1    // cp 是计算属性    var cp: Int {        get {            return p2 * 2        }        set {            p2 = newValue + 2        }    }    // 只有 getter 的是只读计算属性    var rcp: Int {        p2 * 4    }}print(S.sp)print(S().cp) // 2var s = S()s.cp = 3print(s.p2) // 5print(S().rcp) // 4

willSet 和 didSet 是属性观察器,可以在属性值设置前后插入自己的逻辑处理。

键路径表达式作为函数

struct S2 {    let p1: String    let p2: Int}let s2 = S2(p1: "one", p2: 1)let s3 = S2(p1: "two", p2: 2)let a1 = [s2, s3]let a2 = a1.map(\.p1)print(a2) // ["one", "two"]

方法

enum E: String {    case one, two, three    func showRawValue() {        print(rawValue)    }}let e = E.threee.showRawValue() // three// 可变的实例方法,使用 mutating 标记struct S {    var p: String    mutating func addFullStopForP() {        p += "."    }}var s = S(p: "hi")s.addFullStopForP()print(s.p)// 类方法class C {    class func cf() {        print("类方法")    }}

static和class关键字修饰的方法类似 OC 的类方法。static 可以修饰存储属性,而 class 不能;class 修饰的方法可以继承,而 static 不能。在协议中需用 static 来修饰。

静态下标方法

// 静态下标struct S2 {    static var sp = [String: Int]()        static subscript(_ s: String, d: Int = 10) -> Int {        get {            return sp[s] ?? d        }        set {            sp[s] = newValue        }    }}S2["key1"] = 1S2["key2"] = 2print(S2["key2"]) // 2print(S2["key3"]) // 10

自定义类型中实现了 callAsFunction() 的话,该类型的值就可以直接调用。

// callAsFunction()struct S3 {    var p1: String        func callAsFunction() -> String {        return "show \(p1)"    }}let s2 = S3(p1: "hi")print(s2()) // show hi

继承

类能继承另一个类,继承它的方法、属性等。

// 类继承class C1 {    var p1: String    var cp1: String {        get {            return p1 + " like ATM"        }        set {            p1 = p1 + newValue        }    }    init(p1: String) {        self.p1 = p1    }    func sayHi() {        print("Hi! \(p1)")    }}class C2: C1 {    var p2: String    init(p2: String) {        self.p2 = p2        super.init(p1: p2 + "'s father")    }}C2(p2: "Lemon").sayHi() // Hi! Lemon's father// 重写父类方法class C3: C2 {    override func sayHi() {        print("Hi! \(p2)")    }}C3(p2: "Lemon").sayHi() // Hi! Lemon// 重写计算属性class C4: C1 {    override var cp1: String {        get {            return p1 + " like Out of the blade"        }        set {            p1 = p1 + newValue        }    }}print(C1(p1: "Lemon").cp1) // Lemon like ATMprint(C4(p1: "Lemon").cp1) // Lemon like Out of the blade

通过 final 关键字可以防止类被继承,final 还可以用于属性和方法。使用 super 关键字指代父类。

函数式

map

map 可以依次处理数组中元素,并返回一个处理后的新数组。

let a1 = ["a", "b", "c"]let a2 = a1.map {    "\($0)2"}print(a2) // ["a2", "b2", "c2"]

使用 compactMap 可以过滤 nil 的元素。flatMap 会将多个数组合成一个数组返回。

filter

根据指定条件返回

let a1 = ["a", "b", "c", "call my name"]let a2 = a1.filter {    $0.prefix(1) == "c"}print(a2) // ["c", "call my name"]

reduce

reduce 可以将迭代中返回的结果用于下个迭代中,并,还能让你设个初始值。

let a1 = ["a", "b", "c", "call my name.", "get it?"]let a2 = a1.reduce("Hey u,", { partialResult, s in    // partialResult 是前面返回的值,s 是遍历到当前的值    partialResult + " \(s)"})print(a2) // Hey u, a b c call my name. get it?

sorted

排序

// 类型遵循 Comparablelet a1 = ["a", "b", "c", "call my name.", "get it?"]let a2 = a1.sorted()let a3 = a1.sorted(by: >)let a4 = a1.sorted(by: <)print(a2) // Hey u, a b c call my name. get it?print(a3) // ["get it?", "call my name.", "c", "b", "a"]print(a4) // ["a", "b", "c", "call my name.", "get it?"]// 类型不遵循 Comparablestruct S {    var s: String    var i: Int}let a5 = [S(s: "a", i: 0), S(s: "b", i: 1), S(s: "c", i: 2)]let a6 = a5    .sorted { l, r in        l.i > r.i    }    .map {        $0.i    }print(a6) // [2, 1, 0]

控制流

If • If let • If case let

// iflet s = "hi"if s.isEmpty {    print("String is Empty")} else {    print("String is \(s)")}// 三元条件s.isEmpty ? print("String is Empty again") : print("String is \(s) again")// if let-elsefunc f(s: String?) {    if let s1 = s {        print("s1 is \(s1)")    } else {        print("s1 is nothing")    }    // nil-coalescing    let s2 = s ?? "nothing"    print("s2 is \(s2)")}f(s: "something")f(s: nil)// if case letenum E {    case c1(String)    case c2([String])        func des() {        switch self {        case .c1(let string):            print(string)        case .c2(let array):            print(array)        }    }}E.c1("enum c1").des()E.c2(["one", "two", "three"]).des()

Guard guard, guard let

更好地处理异常情况

// guardfunc f1(p: String) -> String {    guard p.isEmpty != true else {        return "Empty string."    }    return "String \(p) is not empty."}print(f1(p: "")) // Empty string.print(f1(p: "lemon")) // String lemon is not empty.// guard letfunc f2(p1: String?) -> String {    guard let p2 = p1 else {        return "Nil."    }    return "String \(p2) is not nil."}print(f2(p1: nil)) // Nil.print(f2(p1: "lemon")) // String lemon is not nil.

遍历 For-in

let a = ["one", "two", "three"]for str in a {    print(str)}// 使用下标范围for i in 0..<10 {    print(i)}// 使用 enumeratedfor (i, str) in a.enumerated() {    print("第\(i + 1)个是:\(str)")}// for in wherefor str in a where str.prefix(1) == "t" {    print(str)}// 字典 for in,遍历是无序的let dic = [    "one": 1,    "two": 2,    "three": 3]for (k, v) in dic {    print("key is \(k), value is \(v)")}// stridefor i in stride(from: 10, through: 0, by: -2) {    print(i)}/* 10 8 6 4 2 0 */

While while, repeat-while

// whilevar i1 = 10while i1 > 0 {    print("positive even number \(i1)")    i1 -= 2}// repeat whilevar i2 = 10repeat {    print("positive even number \(i2)")    i2 -= 2} while i2 > 0

使用 break 结束遍历,使用 continue 跳过当前作用域,继续下个循环

Switch

func f1(pa: String, t:(String, Int)) {    var p1 = 0    var p2 = 10    switch pa {    case "one":        p1 = 1    case "two":        p1 = 2        fallthrough // 继续到下个 case 中    default:        p2 = 0    }    print("p1 is \(p1)")    print("p2 is \(p2)")        // 元组    switch t {    case ("0", 0):        print("zero")    case ("1", 1):        print("one")    default:        print("no")    }}f1(pa: "two", t:("1", 1))/* p1 is 2 p2 is 0 one */// 枚举enum E {    case one, two, three, unknown(String)}func f2(pa: E) {    var p: String    switch pa {    case .one:        p = "1"    case .two:        p = "2"    case .three:        p = "3"    case let .unknown(u) where Int(u) ?? 0 > 0 : // 枚举关联值,使用 where 增加条件        p = u    case .unknown(_):        p = "negative number"    }    print(p)}f2(pa: E.one) // 1f2(pa: E.unknown("10")) // 10f2(pa: E.unknown("-10")) // negative number

集合

数组 [1, 2, 3]

数组是有序集合

var a0: [Int] = [1, 10]a0.append(2)a0.remove(at: 0)print(a0) // [10, 2]let a1 = ["one", "two", "three"]let a2 = ["three", "four"]// 找两个集合的不同let dif = a1.difference(from: a2) // swift的 diffing 算法在这 http://www.xmailserver.org/diff2.pdf swift实现在  swift/stdlib/public/core/Diffing.swiftfor c in dif {    switch c {    case .remove(let o, let e, let a):        print("offset:\(o), element:\(e), associatedWith:\(String(describing: a))")    case .insert(let o, let e, let a):        print("offset:\(o), element:\(e), associatedWith:\(String(describing: a))")    }}/* remove offset:1, element:four, associatedWith:nil insert offset:0, element:one, associatedWith:nil insert offset:1, element:two, associatedWith:nil */let a3 = a2.applying(dif) ?? [] // 可以用于添加删除动画print(a3) // ["one", "two", "three"]

dif 有第三个 case 值 .insert(let offset, let element, let associatedWith) 可以跟踪成对的变化,用于高级动画。

从数组中随机取一个元素

print(a0.randomElement() ?? 0)

数组排序

// 排序struct S1 {    let n: Int    var b = true}let a4 = [    S1(n: 1),    S1(n: 10),    S1(n: 3),    S1(n: 2)]let a5 = a4.sorted { i1, i2 in    i1.n < i2.n}for n in a5 {    print(n)}/// S1(n: 1)/// S1(n: 2)/// S1(n: 3)/// S1(n: 10)let a6 = [1,10,4,7,2]print(a6.sorted(by: >)) // [10, 7, 4, 2, 1]

可以加到数组扩展中,通过扩展约束能够指定特定元素类型的排序,代码如下:

extension Array where Element == Int {    // 升序    func intSortedASC() -> [Int] {        return self.sorted(by: <)    }    // 降序    func intSortedDESC() -> [Int] {        return self.sorted(by: <)    }}print(a6.intSortedASC()) // 使用扩展增加自定义排序能力

在数组中检索满足条件的元素,代码如下:

// 第一个满足条件了就返回let a7 = a4.first {    $0.n == 10}print(a7?.n ?? 0)// 是否都满足了条件print(a4.allSatisfy { $0.n == 1 }) // falseprint(a4.allSatisfy(\.b)) // true// 找出最大的那个print(a4.max(by: { e1, e2 in    e1.n < e2.n}) ?? S1(n: 0))// S1(n: 10, b: true)// 看看是否包含某个元素print(a4.contains(where: {    $0.n == 7}))// false

一些切割数组的方法。

// 切片// 取前3个,并不是直接复制,对于大的数组有性能优势。print(a6[..<3]) // [1, 10, 4] 需要做越界检查print(a6.prefix(30)) // [1, 10, 4, 7, 2] 不需要做越界检查,也是切片,性能一样// 去掉前3个print(a6.dropFirst(3)) // [7, 2]

prefix(while:) 和 drop(while:) 方法,顺序遍历执行闭包里的逻辑判断,满足条件就返回,遇到不匹配就会停止遍历。prefix 返回满足条件的元素集合,drop 返回停止遍历之后那些元素集合。

let a8 = [8, 9, 20, 1, 35, 3]let a9 = a8.prefix {    $0 < 30}print(a9) // [8, 9, 20, 1]let a10 = a8.drop {    $0 < 30}print(a10) // [35, 3]

比 filter 更高效的删除元素的方法 removeAll

// 删除所有不满足条件的元素var a11 = [1, 3, 5, 12, 25]a11.removeAll { $0 < 10 }print(a11) // [4, 3, 1, 3, 3] 随机// 创建未初始化的数组let a12 = (0...4).map { _ in    Int.random(in: 0...5)}print(a12) // [0, 3, 3, 2, 5] 随机

#if 用于后缀表达式

// #if 用于后缀表达式let a13 = a11#if os(iOS)    .count#else    .reduce(0, +)#endifprint(a13) //37

Sets Set

Set 是无序集合,元素唯一

let s0: Set<Int> = [2, 4]let s1: Set = [2, 10, 6, 4, 8]let s2: Set = [7, 3, 5, 1, 9, 10]let s3 = s1.union(s2) // 合集let s4 = s1.intersection(s2) // 交集let s5 = s1.subtracting(s2) // 非交集部分let s6 = s1.symmetricDifference(s2) // 非交集的合集print(s3) // [4, 2, 1, 7, 3, 10, 8, 9, 6, 5]print(s4) // [10]print(s5) // [8, 4, 2, 6]print(s6) // [9, 1, 3, 4, 5, 2, 6, 8, 7]// s0 是否被 s1 包含print(s0.isSubset(of: s1)) // true// s1 是否包含了 s0print(s1.isSuperset(of: s0)) // truelet s7: Set = [3, 5]// s0 和 s7 是否有交集print(s0.isDisjoint(with: s7)) // true// 可变 Setvar s8: Set = ["one", "two"]s8.insert("three")s8.remove("one")print(s8) // ["two", "three"]

字典 [:]

字典是无序集合,键值对应。

var d1 = [    "k1": "v1",    "k2": "v2"]d1["k3"] = "v3"d1["k4"] = nilprint(d1) // ["k2": "v2", "k3": "v3", "k1": "v1"]for (k, v) in d1 {    print("key is \(k), value is \(v)")}/* key is k1, value is v1 key is k2, value is v2 key is k3, value is v3 */ if d1.isEmpty == false {    print(d1.count) // 3}// mapValueslet d2 = d1.mapValues {    $0 + "_new"}print(d2) // ["k2": "v2_new", "k3": "v3_new", "k1": "v1_new"]// 对字典的值或键进行分组let d3 = Dictionary(grouping: d1.values) {    $0.count}print(d3) // [2: ["v1", "v2", "v3"]]// 从字典中取值,如果键对应无值,则使用通过 default 指定的默认值d1["k5", default: "whatever"] += "."print(d1["k5"] ?? "") // whatever.let v1 = d1["k3", default: "whatever"]print(v1) // v3// compactMapValues() 对字典值进行转换和解包。可以解可选类型,并去掉 nil 值let d4 = [    "k1": 1,    "k2": 2,    "k3": nil]let d5 = d4.mapValues { $0 }let d6 = d4.compactMapValues{ $0 }print(d5)// ["k3": nil, "k1": Optional(1), "k2": Optional(2)]print(d6)// ["k1": 1, "k2": 2]

操作符

赋值 =, +=. -=, *=, /=

let i1 = 1var i2 = i1i2 = 2print(i2) // 2i2 += 1print(i2) // 3i2 -= 2print(i2) // 1i2 *= 10print(i2) // 10i2 /= 2print(i2) // 5

计算符 +, -, *, /, %

let i1 = 1let i2 = i1print((i1 + i2 - 1) * 10 / 2 % 3) // 2print("i" + "1") // i1// 一元运算符print(-i1) // -1

比较运算符 ==, >

遵循 Equatable 协议可以使用 == 和 != 来判断是否相等

print(1 > 2) // falsestruct S: Equatable {    var p1: String    var p2: Int}let s1 = S(p1: "one", p2: 1)let s2 = S(p1: "two", p2: 2)let s3 = S(p1: "one", p2: 2)let s4 = S(p1: "one", p2: 1)print(s1 == s2) // falseprint(s1 == s3) // falseprint(s1 == s4) // true

类需要实现 == 函数

class C: Equatable {    var p1: String    var p2: Int    init(p1: String, p2: Int) {        self.p1 = p1        self.p2 = p2    }        static func == (l: C, r: C) -> Bool {        return l.p1 == r.p1 && l.p2 == r.p2    }}let c1 = C(p1: "one", p2: 1)let c2 = C(p1: "one", p2: 1)print(c1 == c2)
// 元组比较// 会先比较第一个数,第一个无法比较才会比较第二个数// 字符串比较和字母大小还有长度有关。先比较字母大小,在比较长度("apple", 1) < ("apple", 2) // true("applf", 1) < ("apple", 2) // false("appl", 2) < ("apple", 1) // true("appm", 2) < ("apple", 1) // false

三元 _ ? _ : _

简化 if else 写法

// if elsefunc f1(p: Int) {    if p > 0 {        print("positive number")    } else {        print("negative number")    }}// 三元func f2(p: Int) {    p > 0 ? print("positive number") : print("negative number")}f1(p: 1)f2(p: 1)

Nil-coalescing ??

简化 if let else 写法

// if elsefunc f1(p: Int?) {    if let i = p {        print("p have value is \(i)")    } else {        print("p is nil, use defalut value")    }}// 使用 ??func f2(p: Int?) {    let i = p ?? 0    print("p is \(i)")}

范围 a…b

简化的值范围表达方式。

// 封闭范围for i in 0...10 {    print(i)}// 半开范围for i in 0..<10 {    print(i)}
// 单侧区间let nums = [5,6,7,8]print(nums[2...]) // 7 8

逻辑 !, &&, ||

let i1 = -1let i2 = 2if i1 != i2 && (i1 < 0 || i2 < 0) {    print("i1 and i2 not equal, and one of them is negative number.")}

恒等 ===, !==

恒等返回是否引用了相同实例。

class C {    var p: String    init(p: String) {        self.p = p    }}let c1 = C(p: "one")let c2 = C(p: "one")let c3 = c1print(c1 === c2) // falseprint(c1 === c3) // trueprint(c1 !== c2) // true

运算符

位运算符

let i1: UInt8 = 0b00001111let i2 = ~i1 // Bitwise NOT Operator(按位取反运算符),取反let i3: UInt8 = 0b00111111let i4 = i1 & i3 // Bitwise AND Operator(按位与运算符),都为1才是1let i5 = i1 | i3 // Bitwise OR Operator(按位或运算符),有一个1就是1let i6 = i1 ^ i3 // Bitwise XOR Operator(按位异或运算符),不同为1,相同为0print(i1,i2,i3,i4,i5,i6)// << 按位左移,>> 按位右移let i7 = i1 << 1let i8 = i1 >> 2print(i7,i8)

溢出运算符,有 &+、&- 和 &*

var i1 = Int.maxprint(i1) // 9223372036854775807i1 = i1 &+ 1print(i1) // -9223372036854775808i1 = i1 &+ 10print(i1) // -9223372036854775798var i2 = UInt.maxi2 = i2 &+ 1print(i2) // 0

运算符函数包括前缀运算符、后缀运算符、复合赋值运算符以及等价运算符。另,还可以自定义运算符,新的运算符要用 operator 关键字进行定义,同时要指定 prefix、infix 或者 postfix 修饰符。

基础库

时间

Date 的基本用法如下:

let now = Date()// Date 转 时间戳let interval = now.timeIntervalSince1970 // 时间戳let df = DateFormatter()df.dateFormat = "yyyy 年 MM 月 dd 日 HH:mm:ss"print("时间戳:\(Int(interval))") // 时间戳:1642399901print("格式化的时间:" + df.string(from: now)) // 格式化的时间:2022 年 01 月 17 日 14:11:41df.dateStyle = .shortprint("short 样式时间:" + df.string(from: now)) // short 样式时间:2022/1/17df.locale = Locale(identifier: "zh_Hans_CN")df.dateStyle = .fullprint("full 样式时间:" + df.string(from: now)) // full 样式时间:2022年1月17日 星期一// 时间戳转 Datelet date = Date(timeIntervalSince1970: interval)print(date) // 2022-01-17 06:11:41 +0000

复杂的时间操作,比如说 GitHub 接口使用的是 ISO 标准,RSS 输出的是 RSS 标准字符串,不同标准对应不同时区的时间计算处理,可以使用开源库 SwiftDate 来完成。示例代码如下:

import SwiftDate// 使用 SwiftDate 库let cn = Region(zone: Zones.asiaShanghai, locale: Locales.chineseChina)SwiftDate.defaultRegion = cnprint("2008-02-14 23:12:14".toDate()?.year ?? "") // 2008let d1 = "2022-01-17T23:20:35".toISODate(region: cn)guard let d1 = d1 else {    return}print(d1.minute) // 20let d2 = d1 + 1.minutesprint(d2.minute)// 两个 DateInRegion 相差时间 intervallet i1 = DateInRegion(Date(), region: cn) - d1let s1 = i1.toString {    $0.maximumUnitCount = 4    $0.allowedUnits = [.day, .hour, .minute]    $0.collapsesLargestUnit = true    $0.unitsStyle = .abbreviated    $0.locale = Locales.chineseChina}print(s1) // 9小时45分钟

格式化

使用标准库的格式来描述不同场景的情况可以不用去考虑由于不同地区的区别,这些在标准库里就可以自动完成了。

描述两个时间之间相差多长时间

// 计算两个时间之间相差多少时间,支持多种语言字符串let d1 = Date().timeIntervalSince1970 - 60 * 60 * 24let f1 = RelativeDateTimeFormatter()f1.dateTimeStyle = .namedf1.formattingContext = .beginningOfSentencef1.locale = Locale(identifier: "zh_Hans_CN")let str = f1.localizedString(for: Date(timeIntervalSince1970: d1), relativeTo: Date())print(str) // 昨天// 简写let str2 = Date.now.addingTimeInterval(-(60 * 60 * 24))    .formatted(.relative(presentation: .named))print(str2) // yesterday

描述多个事物

// 描述多个事物let s1 = ListFormatter.localizedString(byJoining: ["冬天","春天","夏天","秋天"])print(s1)

描述名字

// 名字let f2 = PersonNameComponentsFormatter()var nc1 = PersonNameComponents()nc1.familyName = "戴"nc1.givenName = "铭"nc1.nickname = "铭哥"print(f2.string(from: nc1)) // 戴铭f2.style = .shortprint(f2.string(from: nc1)) // 铭哥f2.style = .abbreviatedprint(f2.string(from: nc1)) // 戴var nc2 = PersonNameComponents()nc2.familyName = "Dai"nc2.givenName = "Ming"nc2.nickname = "Starming"f2.style = .defaultprint(f2.string(from: nc2)) // Ming Daif2.style = .shortprint(f2.string(from: nc2)) // Starmingf2.style = .abbreviatedprint(f2.string(from: nc2)) // MD// 取出名let componets = f2.personNameComponents(from: "戴铭")print(componets?.givenName ?? "") // 铭

描述数字

// 数字let f3 = NumberFormatter()f3.locale = Locale(identifier: "zh_Hans_CN")f3.numberStyle = .currencyprint(f3.string(from: 123456) ?? "") // ¥123,456.00f3.numberStyle = .percentprint(f3.string(from: 123456) ?? "") // 12,345,600%let n1 = 1.23456let n1Str = n1.formatted(.number.precision(.fractionLength(3)).rounded())print(n1Str) // 1.235

描述地址

// 地址import Contactslet f4 = CNPostalAddressFormatter()let address = CNMutablePostalAddress()address.street = "海淀区王庄路XX号院X号楼X门XXX"address.postalCode = "100083"address.city = "北京"address.country = "中国"print(f4.string(from: address))/// 海淀区王庄路XX号院X号楼X门XXX/// 北京 100083/// 中国

度量值

标准库里的物理量,在这个文档里有详细列出,包括角度、平方米等。

// 参考:https://developer.apple.com/documentation/foundation/nsdimensionlet m1 = Measurement(value: 1, unit: UnitLength.kilometers)let m2 = m1.converted(to: .meters) // 千米转米print(m2) // 1000.0 m// 度量值转为本地化的值let mf = MeasurementFormatter()mf.locale = Locale(identifier: "zh_Hans_CN")print(mf.string(from: m1)) // 1公里

一些物理公式供参考:

面积 = 长度 × 长度体积 = 长度 × 长度 × 长度 = 面积 × 长度速度=长度/时间加速度=速度/时间力 = 质量 × 加速度扭矩 = 力 × 长度压力 = 力 / 面积密度=质量 / 体积能量 = 功率 × 时间电阻 = 电压 / 电流

Data

数据压缩和解压

// 对数据的压缩let d1 = "看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?".data(using: .utf8)! as NSDataprint("ori \(d1.count) bytes")do {    /// 压缩算法    /// * lz4    /// * lzma    /// * zlib    /// * lzfse    let compressed = try d1.compressed(using: .zlib)    print("comp \(compressed.count) bytes")        // 对数据解压    let decomressed = try compressed.decompressed(using: .zlib)    let deStr = String(data: decomressed as Data, encoding: .utf8)    print(deStr ?? "")} catch {}/// ori 297 bytes/// comp 37 bytes

文件

文件的一些基本操作的代码如下:

let path1 = "/Users/mingdai/Downloads/1.html"let path2 = "/Users/mingdai/Documents/GitHub/"let u1 = URL(string: path1)do {    // 写入    let url1 = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: u1, create: true) // 保证原子性安全保存    print(url1)    // 读取    let s1 = try String(contentsOfFile: path1, encoding: .utf8)    print(s1)} catch {}// 检查路径是否可用let u2 = URL(fileURLWithPath:path2)do {    let values = try u2.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])    if let capacity = values.volumeAvailableCapacityForImportantUsage {        print("可用: \(capacity)")    } else {        print("不可用")    }} catch {    print("错误: \(error.localizedDescription)")}

怎么遍历多级目录结构中的文件呢?看下面的代码的实现:

// 遍历路径下所有目录let u3 = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)let fm = FileManager.defaultfm.enumerator(atPath: u3.path)?.forEach({ path in    guard let path = path as? String else {        return    }    let url = URL(fileURLWithPath: path, relativeTo: u3)    print(url.lastPathComponent)})

可以使用 FileWrapper 来创建文件夹和文件。举个例子:

// FileWrapper 的使用// 创建文件let f1 = FileWrapper(regularFileWithContents: Data("# 第 n 个文件\n ## 标题".utf8))f1.fileAttributes[FileAttributeKey.creationDate.rawValue] = Date()f1.fileAttributes[FileAttributeKey.modificationDate.rawValue] = Date()// 创建文件夹let folder1 = FileWrapper(directoryWithFileWrappers: [    "file1.md": f1])folder1.fileAttributes[FileAttributeKey.creationDate.rawValue] = Date()folder1.fileAttributes[FileAttributeKey.modificationDate.rawValue] = Date()do {    try folder1.write(        to: URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent("NewFolder"),        options: .atomic,        originalContentsURL: nil    )} catch {}print(FileManager.default.currentDirectoryPath)

上面代码写起来比较繁琐,对 FileWrapper 更好的封装可以参考这篇文章《 A Type-Safe FileWrapper | Heberti Almeida 》。

文件读写处理完整能力可以参看这个库 GitHub - JohnSundell/Files: A nicer way to handle files & folders in Swift

本地或者网络上,比如网盘和FTP的文件发生变化时,怎样知道能够观察到呢?

通过 HTTPHeader 里的 If-Modified-Since、Last-Modified、If-None-Match 和 Etag 等字段来判断文件的变化,本地则是使用 DispatchSource.makeFileSystemObjectSource 来进行的文件变化监听。可以参考 KZFileWatchers 库的做法。

Scanner

let s1 = """one1,two2,three3."""let sn1 = Scanner(string: s1)while !sn1.isAtEnd {    if let r1 = sn1.scanUpToCharacters(from: .newlines) {        print(r1 as String)    }}/// one1,/// two2,/// three3.// 找出数字let sn2 = Scanner(string: s1)sn2.charactersToBeSkipped = CharacterSet.decimalDigits.inverted // 不是数字的就跳过var p: Int = 0while !sn2.isAtEnd {    if sn2.scanInt(&p) {        print(p)    }}/// 1/// 2/// 3

上面的代码还不是那么 Swifty,可以通过用AnySequence和AnyIterator来包装下,将序列中的元素推迟到实际需要时再来处理,这样性能也会更好些。具体实现可以参看《 String parsing in Swift 》这篇文章。

AttributeString

效果如下:

代码如下:

var aStrs = [AttributedString]()var aStr1 = AttributedString("""标题正文内容,具体查看链接。这里摘出第一个重点,还要强调的内容。""")// 标题let title = aStr1.range(of: "标题")guard let title = title else {    return aStrs}var c1 = AttributeContainer() // 可复用容器c1.inlinePresentationIntent = .stronglyEmphasizedc1.font = .largeTitleaStr1[title].setAttributes(c1)// 链接let link = aStr1.range(of: "链接")guard let link = link else {    return aStrs}var c2 = AttributeContainer() // 链接c2.strokeColor = .bluec2.link = URL(string: "https://ming1016.github.io/")aStr1[link].setAttributes(c2.merging(c1)) // 合并 AttributeContainer// Runslet i1 = aStr1.range(of: "重点")let i2 = aStr1.range(of: "强调")guard let i1 = i1, let i2 = i2 else {    return aStrs}var c3 = AttributeContainer()c3.foregroundColor = .yellowc3.inlinePresentationIntent = .stronglyEmphasizedaStr1[i1].setAttributes(c3)aStr1[i2].setAttributes(c3)for r in aStr1.runs {    print("-------------")    print(r.attributes)}aStrs.append(aStr1)// Markdowndo {    let aStr2 = try AttributedString(markdown: """    内容[链接](https://ming1016.github.io/)。需要**强调**的内容。    """)        aStrs.append(aStr2)    } catch {}

SwiftUI 的 Text 可以直接读取 AttributedString 来进行显示。

随机

用法:

let ri = Int.random(in: 0..<10)print(ri) // 0到10随机数let a = [0, 1, 2, 3, 4, 5]print(a.randomElement() ?? 0) // 数组中随机取个数print(a.shuffled()) // 随机打乱数组顺序

UserDefaults

使用方法如下:

enum UDKey {    static let k1 = "token"}let ud = UserDefaults.standardud.set("xxxxxx", forKey: UDKey.k1)let tk = ud.string(forKey: UDKey.k1)print(tk ?? "")

模式

单例

struct S {    static let shared = S()    private init() {        // 防止实例初始化    }}

系统及设备

系统判断

#if os(tvOS)     // do something in tvOS#elseif os(iOS)     // do somthing in iOS#elseif os(macOS)    // do somthing in macOS#endif

版本兼容

// 版本@available(iOS 15, *)func f() {}// 版本检查if #available(iOS 15, macOS 12, *) {    f()} else {    // nothing happen}

canImport 判断库是否可使用

#if canImport(SpriteKit)   // iOS 等苹果系统执行#else   // 非苹果系统#endif

targetEnvironment 环境的判断

#if targetEnvironment(simulator)   // 模拟器#else   // 真机#endif

自带属性包装

@resultBuilder

结果生成器(Result builders),通过传递序列创建新值,SwiftUI就是使用的结果生成器将多个视图生成一个视图

@resultBuilderstruct RBS {    // 基本闭包支持    static func buildBlock(_ components: Int...) -> Int {        components.reduce(0) { partialResult, i in            partialResult + i        }    }    // 支持条件判断    static func buildEither(first component: Int) -> Int {        component    }    static func buildEither(second component: Int) -> Int {        component    }    // 支持循环    static func buildArray(_ components: [Int]) -> Int {        components.reduce(0) { partialResult, i in            partialResult + i        }    }}let a = RBS.buildBlock(    1,    2,    3)print(a) // 6// 应用到函数中@RBS func f1() -> Int {    1    2    3}print(f1()) // 6// 设置了 buildEither 就可以在闭包中进行条件判断。@RBS func f2(stopAtThree: Bool) -> Int {    1    2    3    if stopAtThree == true {        0    } else {        4        5        6    }}print(f2(stopAtThree: false)) // 21// 设置了 buildArray 就可以在闭包内使用循环了@RBS func f3() -> Int {    for i in 1...3 {        i * 2    }}print(f3()) // 12

SE-0348 buildPartialBlock for result builders 简化了实现复杂 result buiders 所需的重载。

@dynamicMemberLookup 动态成员查询

@dynamicMemberLookup 指示访问属性时调用一个已实现的处理动态查找的下标方法 subscript(dynamicMemeber:),通过指定属性字符串名返回值。使用方法如下:

@dynamicMemberLookupstruct D {    // 找字符串    subscript(dynamicMember m: String) -> String {        let p = ["one": "first", "two": "second"]        return p[m, default: ""]    }    // 找整型    subscript(dynamicMember m: String) -> Int {        let p = ["one": 1, "two": 2]        return p[m, default: 0]    }    // 找闭包    subscript(dynamicMember m: String) -> (_ s: String) -> Void {        return {            print("show \($0)")        }    }    // 静态数组成员    var p = ["This is a member"]    // 动态数组成员    subscript(dynamicMember m: String) -> [String] {        return ["This is a dynamic member"]    }}let d = D()let s1: String = d.oneprint(s1) // firstlet i1: Int = d.oneprint(i1) // 1d.show("something") // show somethingprint(d.p) // ["This is a member"]let dynamicP:[String] = d.dpprint(dynamicP) // ["This is a dynamic member"]

类使用 @dynamicMemberLookup,继承的类也会自动加上 @dynamicMemberLookup。协议上定义 @dynamicMemberLookup,通过扩展可以默认实现 subscript(dynamicMember:) 方法。

@dynamicCallable 动态可调用类型

@dynamicCallable 动态可调用类型。通过实现 dynamicallyCall 方法来定义变参的处理。

@dynamicCallablestruct D {    // 带参数说明    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Int {        let firstArg = args.first?.value ?? 0        return firstArg * 2    }        // 无参数说明    func dynamicallyCall(withArguments args: [String]) -> String {        var firstArg = ""        if args.count > 0 {            firstArg = args[0]        }        return "show \(firstArg)"    }}let d = D()let i = d(numberIs: 2)print(i) // 4let s = d("hi")print(s) // show hi

自带协议

Hashable

struct H: Hashable {    var p1: String    var p2: Int        // 提供随机 seed    func hash(into hasher: inout Hasher) {        hasher.combine(p1)    }}let h1 = H(p1: "one", p2: 1)let h2 = H(p1: "two", p2: 2)var hs1 = Hasher()hs1.combine(h1)hs1.combine(h2)print(h1.hashValue) // 7417088153212460033 随机值print(h2.hashValue) // -6972912482785541972 随机值print(hs1.finalize()) // 7955861102637572758 随机值print(h1.hashValue) // 7417088153212460033 和前面 h1 一样let h3 = H(p1: "one", p2: 1)print(h3.hashValue) // 7417088153212460033 和前面 h1 一样var hs2 = Hasher()hs2.combine(h3)hs2.combine(h2)print(hs2.finalize()) // 7955861102637572758 和前面 hs1 一样

应用生命周期内,调用 combine() 添加相同属性哈希值相同,由于 Hasher 每次都会使用随机的 seed,因此不同应用生命周期,也就是下次启动的哈希值,就会和上次的哈希值不同。

Codable

JSON 没有 id 字段

如果SwiftUI要求数据Model都是遵循Identifiable协议的,而有的json没有id这个字段,可以使用扩展struct的方式解决:

struct CommitModel: Decodable, Hashable {  var sha: String  var author: AuthorModel  var commit: CommitModel}extension CommitModel: Identifiable {  var id: String {    return sha  }}

网络

网络状态检查

通过 Network 库的 NWPathMonitor 来检查

import Combineimport Network// 网络状态检查 network state checkfinal class Nsck: ObservableObject {    static let shared = Nsck()    private(set) lazy var pb = mkpb()    @Published private(set) var pt: NWPath        private let monitor: NWPathMonitor    private lazy var sj = CurrentValueSubject<NWPath, Never>(monitor.currentPath)    private var sb: AnyCancellable?        init() {        monitor = NWPathMonitor()        pt = monitor.currentPath        monitor.pathUpdateHandler = { [weak self] path in            self?.pt = path            self?.sj.send(path)        }        monitor.start(queue: DispatchQueue.global())    }        deinit {        monitor.cancel()        sj.send(completion: .finished)    }        private func mkpb() -> AnyPublisher<NWPath, Never> {        return sj.eraseToAnyPublisher()    }}

使用方法

var sb = Set<AnyCancellable>()var alertMsg = ""Nsck.shared.pb    .sink { _ in        //    } receiveValue: { path in        alertMsg = path.debugDescription        switch path.status {        case .satisfied:            alertMsg = ""        case .unsatisfied:            alertMsg = "😱"        case .requiresConnection:            alertMsg = "🥱"        @unknown default:            alertMsg = "🤔"        }        if path.status == .unsatisfied {            switch path.unsatisfiedReason {            case .notAvailable:                alertMsg += "网络不可用"            case .cellularDenied:                alertMsg += "蜂窝网不可用"            case .wifiDenied:                alertMsg += "Wifi不可用"            case .localNetworkDenied:                alertMsg += "网线不可用"            @unknown default:                alertMsg += "网络不可用"            }        }    }    .store(in: &sb)

动画

布局动画

import SwiftUIstruct AnimateLayout: View { @State var changeLayout: Bool = true @Namespace var namespace var body: some View {  VStack(spacing: 30) {   if changeLayout {    HStack { items }   } else {    VStack { items }   }   Button("切换布局") {    withAnimation { changeLayout.toggle() }   }  }  .padding() } @ViewBuilder var items: some View {  Text("one")   .matchedGeometryEffect(id: "one", in: namespace)  Text("Two")   .matchedGeometryEffect(id: "Two", in: namespace)  Text("Three")   .matchedGeometryEffect(id: "Three", in: namespace) }}

安全

Keychain

使用方法:

let d1 = Data("keyChain github token".utf8)let service = "access-token"let account = "github"let q1 = [    kSecValueData: d1,    kSecClass: kSecClassGenericPassword,    kSecAttrService: service,    kSecAttrAccount: account] as CFDictionary// 添加一个 keychainlet status = SecItemAdd(q1, nil)// 如果已经添加过会抛出 -25299 错误代码,需要调用 SecItemUpdate 来进行更新if status == errSecDuplicateItem {    let q2 = [        kSecClass: kSecClassGenericPassword,        kSecAttrService: service,        kSecAttrAccount: account    ] as CFDictionary    let q3 = [        kSecValueData: d1    ] as CFDictionary    SecItemUpdate(q2, q3)}// 读取let q4 = [    kSecAttrService: service,    kSecAttrAccount: account,    kSecClass: kSecClassGenericPassword,    kSecReturnData: true] as CFDictionaryvar re: AnyObject?SecItemCopyMatching(q4, &re)guard let reData = re as? Data else { return }print(String(decoding: reData, as: UTF8.self)) // keyChain github token// 删除let q5 = [    kSecAttrService: service,    kSecAttrAccount: account,    kSecClass: kSecClassGenericPassword,] as CFDictionarySecItemDelete(q5)

工程

程序入口点

Swift 允许全局编写 Swift 代码,实际上 clang 会自动将代码包进一个模拟 C 的函数中。Swift 也能够指定入口点,比如 @UIApplicationMain 或 @NSApplicationMain,UIKit 启动后生命周期管理是 AppDelegate 和 SceneDelegate,《 Understanding the iOS 13 Scene Delegate 》这篇有详细介绍。

@UIApplicationMain 和 @NSApplicationMain 会自动生成入口点。这些入口点都是平台相关的,Swift 发展来看是多平台的,这样在 Swift 5.3 时引入了 @main,可以方便的指定入口点。代码如下:

@main // 要定义个静态的 main 函数struct M {  static func main() {    print("let's begin")  }}

ArgumentParser 库,Swift 官方开源的一个开发命令行工具的库,也支持 @main。使用方法如下:

import ArgumentParser@mainstruct C: ParsableCommand {  @Argument(help: "Start")  var phrase: String     func run() throws {    for _ in 1...5 {      print(phrase)    }  }}

工具

Swift-DocC

现在支持 Swift、OC 和 C,文档标记一样。.doccarchive 包含可部署的网站内容,兼容大多数托管服务,比如 Github pages。部署到在线服务上可参考 Generating Documentation for Hosting OnlinePublishing to GitHub Pages 文档。

和 SPM 集成参看 SwiftDocCPlugin

session 有 What’s new in Swift -DocCImprove the discoverability of your Swift-DocC content

SE-0356 Swift Snippets 代码片段用于示例文档的提案。

专题

Swift 那些事

Swift 各版本演进

Swift 1.1
  • countElements() 改成了 count()。
  • @NSApplicationMain 可以在 macOS 上使用。
Swift 1.2
  • 引入 Set 类型。
  • if let 可以放到一起,使用逗号分隔。
  • 新增 zip() 和 flatMap()。
  • 类增加静态方法和静态属性,使用 static 关键字描述。
  • as! 用于类型强转,失败会崩溃。
  • @noescape 用于描述作为参数闭包,用来告诉 Swift 闭包将在函数返回前使用。
  • 常量可以延后初始化。
Swift 2.0
  • 增加 guard 关键字,用于解可选项值。
  • defer 关键字用来延迟执行,即使抛出错误了都会在最后执行。
  • ErrorType 协议,以及 throws、do、try 和 catch 的引入用来处理错误。
  • characters 加上 count,用来替代 count()。
  • #available 用来检查系统版本。
Swift 2.1
  • 字符串插值可以包含字符串字面符号。

Swift 2.2

官方博客介绍:Swift 2.2 Released!New Features in Swift 2.2Swift 2.2 Release Process

  • FILE, LINEFUNCTION 换成 #file,#line 和 #function。
  • 废弃 ++ 和 – 操作符。
  • C 语言风格 for 循环废弃。
  • 废弃变量参数,因为变量参数容易和 inout 搞混。
  • 废弃字符串化的选择器,选择器不再能写成字符串了。
  • 元组可直接比较是否相等。
Swift 3.0

官方博客介绍:Swift 3.0 Released!Swift 3.0 Preview 1 Released!Swift 3.0 Release Process

  • 规范动词和名词来命名。
  • 去掉 NS 前缀。
  • 方法名描述参数部分变为参数名。
  • 省略没必要的单词,命名做了简化呢。比如 stringByTrimmingCharactersInSet 就换成了 trimmingCharacters。
  • 枚举的属性使用小写开头。
  • 引入 C 函数的属性。
Swift 3.1

官方博客介绍:Swift 3.1 Released!Swift 3.1 Release Process

  • 序列新增 prefix(while:) 和 drop(while:) 方法,顺序遍历执行闭包里的逻辑判断,满足条件就返回,遇到不匹配就会停止遍历。prefix 返回满足条件的元素集合,drop 返回停止遍历之后那些元素集合。
  • 泛型适用于嵌套类型。
  • 类型的扩展可以使用约束条件,比如扩展数组时,加上元素为整数的约束,这样的扩展就只会对元素为整数的数组有效。
Swift 4.0

官方博客介绍:Swift 4.0 Released!Swift 4 Release Process

Swift 4.1

官方博客介绍:Swift 4.1 Released!Swift 4.1 Release Process

Swift 4.2

官方博客介绍:Swift 4.2 Released!Swift 4.2 Release Process

Swift 5.0

官方博客介绍:Swift 5 Released!Swift 5.0 Release Process

Swift 5.1

官方博客介绍:Swift 5.1 Released!Swift 5.1 Release Process

Swift 5.2

官方博客介绍:Swift 5.2 Released!Swift 5.2 Release Process

Swift 5.3

官方博客介绍:Swift 5.3 released!Swift 5.3 Release Process

Swift 5.4

官方博客介绍:Swift 5.4 Released!

Swift 5.5

官方博客介绍:Swift 5.5 Released!

Swift 5.6

5.6 官方博客介绍

Package Manage 的一些提案

Swift 5.7

规范

注意事项

参考:

多用静态特性。swift 在编译期间所做的优化比 OC 要多,这是由于他的静态派发、泛型特化、写时复制这些静态特性决定的。另外通过 final 和 private 这样的表示可将动态特性转化为静态方式,编译开启 WMO 可以自动推导出哪些动态派发可转化为静态派发。

如何避免崩溃?

  • 字典:用结构体替代
  • Any:可用泛型或关联关联类型替代
  • as? :少用 AnyObject,多用泛型或不透明类型
  • !:要少用

好的实践?

  • 少用继承,多用 protocol
  • 多用 extension 对自己代码进行管理

资料推荐

书单

  • 《Thinking in SwiftUI》
  • 《Swift 进阶》
  • 《函数式Swift》
  • 《深入解析Mac OS X & iOS操作系统》
  • 《LLVM Techniques, Tips, and Best Practices Clang and Middle-End Libraries》
  • 《Learn LLVM 12》
  • 《Crafting Interpreters》
  • 《TCP/IP Illustrated》
  • 《松本行弘的程序世界》
  • 《现代操作系统》
  • 《深入理解计算机系统》
  • 《程序员的自我修养》
  • 《Head First 设计模式》

三方库使用

SQLite.swift 的使用

下面是 SQLite.swift 库的使用介绍,包括了数据库创建,表创建,表的添加、更新、删除、查找等处理方法

import SQLitestruct DB {    static let shared = DB()    static let path = NSSearchPathForDirectoriesInDomains(        .applicationSupportDirectory, .userDomainMask, true    ).first!    let BBDB: Connection?    private init() {        do {            print(DB.path)            BBDB = try Connection("\(DB.path)/github.sqlite3")                    } catch {            BBDB = nil        }        /// Swift 类型和 SQLite 类型对标如下:        /// Int64 = INTEGER        /// Double = REAL        /// String = TEXT        /// nil = NULL        /// SQLite.Blob = BLOB            }        // 创建表    func cTbs() throws {        do {            try ReposNotiDataHelper.createTable()            try DevsNotiDataHelper.createTable()        } catch {            throw DBError.connectionErr        }    }    }enum DBError: Error {    case connectionErr, insertErr, deleteErr, searchErr, updateErr, nilInData}protocol DataHelperProtocol {    associatedtype T    static func createTable() throws -> Void    static func insert(i: T) throws -> Int64    static func delete(i: T) throws -> Void    static func findAll() throws -> [T]?}// MARK: 开发者更新提醒typealias DBDevNoti = (    login: String,    lastReadId: String,    unRead: Int)struct DevsNotiDataHelper: DataHelperProtocol {    static let table = Table("devsNoti")    static let login = Expression<String>("login")    static let lastReadId = Expression<String>("lastReadId")    static let unRead = Expression<Int>("unRead")    typealias T = DBDevNoti        static func createTable() throws {        guard let db = DB.shared.BBDB else {            throw DBError.connectionErr        }        do {            let _ = try db.run(table.create(ifNotExists: true) { t in                t.column(login, unique: true)                t.column(lastReadId, defaultValue: "")                t.column(unRead, defaultValue: 0)            })        } catch _ {            throw DBError.connectionErr        }    } // end createTable        static func insert(i: DBDevNoti) throws -> Int64 {        guard let db = DB.shared.BBDB else {            throw DBError.connectionErr        }        let insert = table.insert(login <- i.login, lastReadId <- i.lastReadId, unRead <- i.unRead)        do {            let rowId = try db.run(insert)            guard rowId > 0 else {                throw DBError.insertErr            }            return rowId        } catch {            throw DBError.insertErr        }    } // end insert        static func delete(i: DBDevNoti) throws {        guard let db = DB.shared.BBDB else {            throw DBError.connectionErr        }        let query = table.filter(login == i.login)        do {            let tmp = try db.run(query.delete())            guard tmp == 1 else {                throw DBError.deleteErr            }        } catch {            throw DBError.deleteErr        }    } // end delete        static func find(sLogin: String) throws -> DBDevNoti? {        guard let db = DB.shared.BBDB else {            throw DBError.connectionErr        }        let query = table.filter(login == sLogin)        let items = try db.prepare(query)        for i in items {            return DBDevNoti(login: i[login], lastReadId: i[lastReadId], unRead: i[unRead])        }        return nil    } // end find        static func update(i: DBDevNoti) throws {        guard let db = DB.shared.BBDB else {            throw DBError.connectionErr        }        let query = table.filter(login == i.login)        do {            if try db.run(query.update(lastReadId <- i.lastReadId, unRead <- i.unRead)) > 0 {                            } else {                throw DBError.updateErr            }        } catch {            throw DBError.updateErr        }    } // end update        static func findAll() throws -> [DBDevNoti]? {        guard let db = DB.shared.BBDB else {            throw DBError.connectionErr        }        var arr = [DBDevNoti]()        let items = try db.prepare(table)        for i in items {            arr.append(DBDevNoti(login: i[login], lastReadId: i[lastReadId], unRead: i[unRead]))        }        return arr    } // end find all    }

使用时,可以在初始化时这么做:

// MARK: 初始化数据库et db = DB.shareddo {    try db.cTbs() // 创建表} catch {    }

使用的操作示例如下:

do {    if let fd = try ReposNotiDataHelper.find(sFullName: r.id) {        reposDic[fd.fullName] = fd.unRead    } else {        do {            let _ = try ReposNotiDataHelper.insert(i: DBRepoNoti(fullName: r.id, lastReadCommitSha: "", unRead: 0))            reposDic[r.id] = 0        } catch {            return reposDic        }    }} catch {    return reposDic}

macOS

范例

三栏结构

三栏结构架子搭建,代码如下:

import SwiftUIstruct SwiftPamphletApp: View {    var body: some View {        NavigationView {            SPSidebar()            Text("第二栏")            Text("第三栏")        }        .navigationTitle("Swift 小册子")        .toolbar {            ToolbarItem(placement: ToolbarItemPlacement.navigation) {                Button {                    NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)                } label: {                    Label("Sidebar", systemImage: "sidebar.left")                }            }        }    }}struct SPSidebar: View {    var body: some View {        List {            Section("第一组") {                NavigationLink("第一项", destination: SPList(title: "列表1"))                    .badge(3)                NavigationLink("第二项", destination: SPList(title: "列表2"))            }            Section("第二组") {                NavigationLink("第三项", destination: SPList(title: "列表3"))                NavigationLink("第四项", destination: SPList(title: "列表4"))            }        }        .listStyle(SidebarListStyle())        .frame(minWidth: 160)        .toolbar {            ToolbarItem {                Menu {                    Text("1")                    Text("2")                } label: {                    Label("Label", systemImage: "slider.horizontal.3")                }            }        }    }}struct SPList: View {    var title: String    @State var searchText: String = ""    var body: some View {        List(0..<3) { i in            Text("内容\(i)")        }        .toolbar(content: {            Button {                //            } label: {                Label("Add", systemImage: "plus")            }        })        .navigationTitle(title)        .navigationSubtitle("副标题")        .searchable(text: $searchText)    }}

显示效果如下:

全屏模式

将 NSSplitView 里的其中一个 NSView 设置为全屏和退出全屏的函数如下:

// MARK: - 获取 NSSplitViewControllerfunc splitVC() -> NSSplitViewController {    return ((NSApp.keyWindow?.contentView?.subviews.first?.subviews.first?.subviews.first as? NSSplitView)?.delegate as? NSSplitViewController)!}// MARK: - 全屏func fullScreen(isEnter: Bool) {    if isEnter == true {        // 进入全屏        let presOptions:        NSApplication.PresentationOptions = ([.autoHideDock,.autoHideMenuBar])        let optionsDictionary = [NSView.FullScreenModeOptionKey.fullScreenModeApplicationPresentationOptions : NSNumber(value: presOptions.rawValue)]                let v = splitVC().splitViewItems[2].viewController.view        v.enterFullScreenMode(NSScreen.main!, withOptions: optionsDictionary)        v.wantsLayer = true    } else {        // 退出全屏        NSApp.keyWindow?.contentView?.exitFullScreenMode()    } // end if}

使用方法

struct V: View {    @StateObject var appVM = AppVM()    @State var isEnterFullScreen: Bool = false // 全屏控制    var body: some View {        Button {            isEnterFullScreen.toggle()            appVM.fullScreen(isEnter: isEnterFullScreen)        } label: {            Image(systemName: isEnterFullScreen == true ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right")        }    }}

共享菜单

struct ShareView: View {    var s: String    var body: some View {        Menu {            Button {                let p = NSPasteboard.general                p.declareTypes([.string], owner: nil)                p.setString(s, forType: .string)            } label: {                Text("拷贝链接")                Image(systemName: "doc.on.doc")            }            Divider()            ForEach(NSSharingService.sharingServices(forItems: [""]), id: \.title) { item in                Button {                    item.perform(withItems: [s])                } label: {                    Text(item.title)                    Image(nsImage: item.image)                }            }        } label: {            Text("分享")            Image(systemName: "square.and.arrow.up")        }    }}

剪贴板

添加和读取剪贴板的方法如下:

// 读取剪贴板内容let s = NSPasteboard.general.string(forType: .string)guard let s = s else {    return}print(s)// 设置剪贴板内容let p = NSPasteboard.generalp.declareTypes([.string], owner: nil)p.setString(s, forType: .string)

性能和构建

调试

session Debug Swift debugging with LLDB

编译器编译 swift 文件生成 .o 文件会有 __debug_info 段,其中有可以映射到源文件和行号的地址。debug 信息可以链接到 .dSYM 包。debug 信息链接器叫 dsymutil,dsymutil 可以为每个动态库、framework 或 dylib 和可执行文件打包一个 debug 信息存档(.dSYM 包)。

image 和路径怎么重映射。使用 image list nameOfFramework 来检查 LLDB 是否找到了我们应用程序里嵌入的第三方框架的 debug dSYM。使用 image lookup 0xMemoryAddressHere 获取当前地址更多信息。要重新映射源文件 .dSYM 路径,使用 settings set target.source-map old/path new/path。每个 .dSYM 都有一个 UUID.plist,我们可以在其中设置 DBGSourcePathRemapping 这个字典。

Xcode 14 新增 swift-healthcheck 命令,这个命令可以了解 module 为何导入失败。

LLDB 怎么找到 Swift module?每个 .dSYM 包都可以包含二级制 swift module,其中可能包含桥头文件、swift 接口文件 .swiftinterface,还有 debug 信息。静态存档不是由链接器生成的,需要向链接器注册 swift module,使用 ld ... -add-ast-path /path/to/My.swiftmodule ,动态库和可执行文件的话,Xcode 会自动完成此操作。可以使用 dsymutil 来 dump 你可执行文件的符号表,并用 grep 找 swiftmodule,命令是 dsymutil -s MyApp | grep .swiftmodule

内存管理

相关提案包括 SE-0349 Unaligned Loads and Stores from Raw MemorySE-0334 Pointer API Usability ImprovementsSE-0333 Expand usability of withMemoryRebound

Set 使用新的 Temporary Buffers 功能,让 intersect 速度提升了 4 到 6 倍。

链接器

Link fast: Improve build and launch time 详细讲了 Apple 今年怎么改进了 link,思路很棒,很值得学习。

Static linking 和 Dynamic linking ,也就是静态链接和动态链接。

静态链接就是链接各个编译好的源文件以及链接源文件和编译好的库文件,通过将函数名放到符号表,链接新文件时确定先前是否有包含的 undefined 符号,给函数的数据指令分配地址,最后生成一个有 TEXT、DATA、LINKEDIT 段的可执行文件。

今年 Apple 通过利用多核优势让静态链接快了两倍。

具体做法是,并行的拷贝文件内容。并行构建 LINKEDIT 段的各个不同部分。并行改变 UUID 计算和 codesigning 哈希。然后是提高 exports-trie 构建器的算法。使用最新的 Crypto 库利用硬件加速的优势加速 UUID 计算。提高其它静态库处理算法库,debug-notes 生成也更快了。

Apple 推荐静态库最佳实践是:

使用 -all_load-force_load 可以让 .a 文件像 .o 文件那样并行处理,不过开启这个选项需要先处理重复的符号。另外一个副作用是会将一些被判断无用的代码也被链接进来,使包体变大,因此开启之前可以先使用静态分析工具分析处理,这个过程定期做就行,不用放到每次编译过程中。演讲者推荐使用 -dead_strip 选项,但是这样做并没有真实去掉费代码,以后这些代码还是会被编译分析,如果只是暂时不用,可以先注释掉。

使用 -no_exported_symbols 选项。链接器生成的 LINKEDIT 段的一部分是 exports trie,这是一个前缀树,对所有导出的符号名称、地址和标志进行编码。动态库 是会导出符号的,但运行的二进制文件其实是不用这些符号的,因此可以用 -no_exported_symbols 选项来跳过 LINKEDIT 中 trie 数据结构的创建,这样链接起来就快多了。如果程序导出符号是一百万个,这个选项就可以减少 2 到 3 秒的时间。但需要注意的是,如果要加载插件链接回主程序就需要所有的导出的 trie 数据,无法用这个选项。

另外一个是 -no_deduplicate 选项。先前 Apple 给链接器加了个 pass 用来合并函数的指令相同,函数名不相同,这个 pass 会对每个函数的指令进行递归散列,用这种方式来找重复指令,这样做比较费 CPU,由于调试时其实是不需要关注包大小,因此可以加上 -no_deduplicate 选项来跳过这个 pass。

这些选项在 Xcode 的 Other Linker Flags 里进行设置即可。

动态库也就是 dylib,其它平台就是 DSO 或 DLL。 动态链接器不是将代码从库里考到主二进制里,而是记录某种承诺,记录从动态库中使用符号名称,还有库路径。这样做好处就是好复用动态库,不用拷贝多份。虚拟内存看到多进程使用相同动态库,就会重新给这个动态库用相同的物理内存页。

动态库好处是构建快了,启动加载慢了,多个动态库不光要加载,还要在启动时链接。也就是把链接成本从本地构建换到了用户启动时。动态库还有个缺点是基于动态库的程序会有更多的 dirty 页,因为静态链接时会把全局数据放到主程序同一个 DATA 页中,动态库的话,每个都在自己的 DATA 页中。

动态库工作的原理是,可执行的二进制会有不同权限的段,至少会有 TEXT、DATA 和 LINKEDIT。分段总是操作系统页大小的倍数。TEXT 段有执行的权限,CPU 可以将页上的字节当做机器代码指令。运行时,dyld 会根据每个段权限将可执行文件 mmap() 到内存,这些段是页大小和页对齐的,虚拟内存系统可以直接将程序或动态库文件设置为 VM 范围的备份存储。在这些页的内存访问前是不会被加载到 RAM 里,就会触发一个页 fault,导致 VM 去读取文件的子范围,将内存填充到需要 RAM 页中。光映射不够,还要用某种方式“wired up”或绑到动态库上。比如要调用动态库上的某个函数,会转换成调用 site,调用 site 成为一个在相同 TEXT 段合成的 sub 的调用,相对地址在构建时就知道了,就意味着可以正确的形成 BL 指令。这样做的好处是,stub 从 DATA 加载一个指针并跳到对应的位置,不用在运行时修改 TEXT 段,dyld 只在运行时改 DATA 段。dyld 所进行的修改很简单,就是在 DATA 段里设置了一个指针而已。

当 dyld 或应用程序的指针指向自己时要 rebase,ASLR 使 dyld 以随机地址加载动态库,内部指针不能在构建时设置,dyld 在启动时 rebase 这些指针,磁盘上,如果动态库在地址零出被加载,这些指针包含它们的目标地址。LINKEDIT 需要记录的就是每个重定位的位置。然后,dyld 只需将动态库的实际加载地址添加到每个 rebase 位置。还有种修改方式是绑定,绑定就是符号引用,符号存储在 LINKEDIT 中,dyld 在动态库的 exports tire 中找实际地址,然后 dyld 将该值存储在绑定指定的位置。

今年 Apple 发布了一个新的修改方式 chained fixups。较前面两种的优势就是可以使 LINKEDIT 更小。新格式只存储每个 DATA 页中第一个 fixup 位置和一个导入的符号列表。其它信息编码到 DATA 段。iOS 13.4 就开始支持了。

下面先说下 dyld 原理介绍。

dyld 从主可执行文件开始,解析 mach-o 找依赖动态库,对动态库进行 mmap()。然后对每个动态库进行遍历并解析 mach-o 结构,根据需要加载其它动态库。加载完毕,dyld 会查找所有需要绑定符号,并在修改时使用这些地址。最后修改完,dyld 自下而上运行初始化程序。先前做的优化是只要程序和动态库,dyld 很多步骤都可以在首次启动时被缓存。

今年 Apple 做了更多的优化,这个优化叫 page-in linking,就是 dyld 在启动时做的 DATA 页面修改放到 page-in 时,也可以理解为懒修改。以前,在 mmap() 区域的某些页面中第一次使用某些地址会触发内核读入该页面。现在如果它是一个数据页,内核会应用改页需要的修改。这种机制减少了 dirty 内存和启动时间。意味着 DATA_CONST 也是干净的,可以像 TEXT 页一样被 evicted 和重新创建,以减少内存压力。需要注意的是 page-in linking 只用于启动,dlopen() 不支持。你看,Apple 优化启动的思路也是按需加载。

Apple 还提供了追踪 dyld 运行情况的 dyld_usage 工具。检查磁盘和 dyld 缓存中的二进制文件的 dyld_info 工具。

2022年 Apple 性能更新

Improve app size and runtime performance

今年苹果通过更有效的检查 Swift 协议,使 OC 消息发送调用更小,使 autorelease elision 更快更小这几个个方面来让 App 体积更小,性能更高。

Swift 协议检查。

一个协议通过 as 操作符检查传递值是否符合协议,这种检查会在编译器的构建时间被优化掉,所以往往需要在运行时借助之前计算协议检查元数据来看对象是否真的符合了协议。一些元数据是在编译时建的,但还有很多元数据只能在启动时建立,特别是使用泛型时。协议多了,会增加耗时,差不多会多一半启动时间。

今年 Apple 推出新的 Swift 运行时,可以提前计算 Swift 协议元数据,作为 App 可执行文件和它在启动时使用的任何动态库的 dyld 闭包的一部分。这个是在系统上的,因此,只要是使用了今年最新系统的 App 都会享受这个优化,可以理解为,新系统上启动老 App 也会快些。

消息发送。

Xcode 14 中新的编译器和链接器已经将 ARM64 的消息发送调用从 12 字节减少到 8 字节。因此如果你的 App 都是 OC 代码的话,使用 Xcode 14 编出来的二进制文件可以少 2%。老系统也有效。

使用 objc_stubs_small 选项可以只优化大小,获得最大的大小优化。objc_msgSend 调动有 8 个字节指令,也就是2个指令是专门用来准备 selector 的,对于任何特定的 selector,总是相同的代码,由于始终是相同的代码,那么就可以对其共享,每个 selector 只 emit 一次,而不是每次发送消息时都 emit。共享这段代码地方是一个叫 selector stub 的函数。

ARC 会在编译器插入大量的 c 的 retain/release 函数调用。这些调用遵守平台应用二进制接口(ABI)所定义的 c 语言 call convention。也就意味着我们要更多代码来完成这些调用,用来传递正确寄存器的指针。Apple 今年推出了自定义的 call convention 根据指针位置,适时使用正确变量而不用移动它,从而摆脱了调用里的多余代码。Apple 果然是坚持用户体验优先,为了更好体验不惜修改 c 的 ABI。

autorelease elision 。

App 今年对 objc 运行时进行了修改,使 autorelease elision 更小更快。deployment target 为 iOS 16 今年新系统时才可享用哦。

Apple 怎么做的呢?

ARC 在调用方插入一个 retain,在被调用的函数中插入一个 release。当我们返回我们的临时对象时,我们需要在函数中先释放它,因为它要离开 scope。在它还没有任何其它引用时还不能这么做,不然返回前他就会被销毁。Apple 现在使用一个新的 convention ,让其可以返回临时对象。做法是当返回一个自动释放值,编译器会发出一个特殊标记,这个标记会告诉运行时这是符合自动释放条件的。它的后面是 retain,我们会在后面执行。获取返回地址,也就是一个指针,将它先保存起来,然后离开运行时的自动释放调用。在运行时,可以将保留时得到的指正和先前做自动释放时保存的指针进行比较,这样标记指令不再是数据之间的比较,比较指针内存访问少。比较成功就可以省去 autorelease/retain。

autorelease elision 的优化同样也可以减少 2% 大小。感谢 Apple 为了用户和开发者 OKR 的付出。

Combine

介绍

Combine 是什么?

WWDC 2019苹果推出Combine,Combine是一种响应式编程范式,采用声明式的Swift API。

Combine 写代码的思路是你写代码不同于以往命令式的描述如何处理数据,Combine 是要去描述好数据会经过哪些逻辑运算处理。这样代码更好维护,可以有效的减少嵌套闭包以及分散的回调等使得代码维护麻烦的苦恼。

声明式和过程时区别可见如下代码:

// 所有数相加// 命令式思维func sum1(arr: [Int]) -> Int {  var sum: Int = 0  for v in arr {    sum += v  }  return sum}// 声明式思维func sum2(arr: [Int]) -> Int {  return arr.reduce(0, +)}

Combine 主要用来处理异步的事件和值。苹果 UI 框架都是在主线程上进行 UI 更新,Combine 通过 Publisher 的 receive 设置回主线程更新UI会非常的简单。

已有的 RxSwift 和 ReactiveSwift 框架和 Combine 的思路和用法类似。

Combine 的三个核心概念

  • 发布者
  • 订阅者
  • 操作符

简单举个发布数据和类属性绑定的例子:

let pA = Just(0)let _ = pA.sink { v in    print("pA is: \(v)")}let pB = [7,90,16,11].publisherlet _ = pB    .sink { v in        print("pB: \(v)")    }class AClass {    var p: Int = 0 {        didSet {            print("property update to \(p)")        }    }}let o = AClass()let _ = pB.assign(to: \.p, on: o)

Combine 资料

官方文档链接 Combine | Apple Developer Documentation 。还有 Using Combine 这里有大量使用示例,内容较全。官方讨论Combine的论坛 Topics tagged combine 。StackOverflow上相关问题 Newest ‘combine’ Questions

WWDC上关于Combine的Session如下:

和Combine相关的Session:

使用说明

publisher

publisher 是发布者,sink 是订阅者

import Combinevar cc = Set<AnyCancellable>()struct S {    let p1: String    let p2: String}[S(p1: "1", p2: "one"), S(p1: "2", p2: "two")]    .publisher    .print("array")    .sink {        print($0)    }    .store(in: &cc)

输出

 array: receive subscription: ([戴铭的开发小册子.AppDelegate.(unknown context at $10ac82d20).(unknown context at $10ac82da4).S(p1: "1", p2: "one"), 戴铭的开发小册子.AppDelegate.(unknown context at $10ac82d20).(unknown context at $10ac82da4).S(p1: "2", p2: "two")])array: request unlimitedarray: receive value: (S(p1: "1", p2: "one"))S(p1: "1", p2: "one")array: receive value: (S(p1: "2", p2: "two"))S(p1: "2", p2: "two")array: receive finished

Just

Just 是发布者,发布的数据在初始化时完成

import Combinevar cc = Set<AnyCancellable>()struct S {    let p1: String    let p2: String}let pb = Just(S(p1: "1", p2: "one"))pb    .print("pb")    .sink {        print($0)    }    .store(in: &cc)

输出

pb: receive subscription: (Just)pb: request unlimitedpb: receive value: (S(p1: "1", p2: "one"))S(p1: "1", p2: "one")pb: receive finished

PassthroughSubject

PassthroughSubject 可以传递多值,订阅者可以是一个也可以是多个,send 指明 completion 后,订阅者就没法接收到新发送的值了。

import Combinevar cc = Set<AnyCancellable>()struct S {    let p1: String    let p2: String}enum CError: Error {    case aE, bE}let ps1 = PassthroughSubject<S, CError>()ps1    .print("ps1")    .sink { c in        print("completion:", c) // send 了 .finished 后会执行    } receiveValue: { s in        print("receive:", s)            }    .store(in: &cc)ps1.send(S(p1: "1", p2: "one"))ps1.send(completion: .failure(CError.aE)) // 和 .finished 一样后面就不会发送了ps1.send(S(p1: "2", p2: "two"))ps1.send(completion: .finished)ps1.send(S(p1: "3", p2: "three"))// 多个订阅者let ps2 = PassthroughSubject<String, Never>()ps2.send("one") // 订阅之前 send 的数据没有订阅者可以接收ps2.send("two")let sb1 = ps2    .print("ps2 sb1")    .sink { s in    print(s)    }ps2.send("three") // 这个 send 的值会被 sb1let sb2 = ps2    .print("ps2 sb2")    .sink { s in        print(s)    }ps2.send("four") // 这个 send 的值会被 sb1 和 sb2 接受sb1.store(in: &cc)sb2.store(in: &cc)ps2.send(completion: .finished)

输出

ps1: receive subscription: (PassthroughSubject)ps1: request unlimitedps1: receive value: (S(p1: "1", p2: "one"))receive: S(p1: "1", p2: "one")ps1: receive error: (aE)completion: failure(戴铭的开发小册子.AppDelegate.(unknown context at $10b15ce10).(unknown context at $10b15cf3c).CError.aE)ps2 sb1: receive subscription: (PassthroughSubject)ps2 sb1: request unlimitedps2 sb1: receive value: (three)threeps2 sb2: receive subscription: (PassthroughSubject)ps2 sb2: request unlimitedps2 sb1: receive value: (four)fourps2 sb2: receive value: (four)fourps2 sb1: receive finishedps2 sb2: receive finished

Empty

import Combinevar cc = Set<AnyCancellable>()struct S {    let p1: String    let p2: String}let ept = Empty<S, Never>() // 加上 completeImmediately: false 后面即使用 replaceEmpty 也不会接受值ept    .print("ept")    .sink { c in        print("completion:", c)    } receiveValue: { s in        print("receive:", s)    }    .store(in: &cc)ept.replaceEmpty(with: S(p1: "1", p2: "one"))    .sink { c in        print("completion:", c)    } receiveValue: { s in        print("receive:", s)    }    .store(in: &cc)

输出

ept: receive subscription: (Empty)ept: request unlimitedept: receive finishedcompletion: finishedreceive: S(p1: "1", p2: "one")completion: finished

CurrentValueSubject

CurrentValueSubject 的订阅者可以收到订阅时已发出的那条数据

import Combinevar cc = Set<AnyCancellable>()let cs = CurrentValueSubject<String, Never>("one")cs.send("two")cs.send("three")let sb1 = cs    .print("cs sb1")    .sink {        print($0)    }    cs.send("four")cs.send("five")let sb2 = cs    .print("cs sb2")    .sink {        print($0)    }cs.send("six")sb1.store(in: &cc)sb2.store(in: &cc)

输出

cs sb1: receive subscription: (CurrentValueSubject)cs sb1: request unlimitedcs sb1: receive value: (three)threecs sb1: receive value: (four)fourcs sb1: receive value: (five)fivecs sb2: receive subscription: (CurrentValueSubject)cs sb2: request unlimitedcs sb2: receive value: (five)fivecs sb1: receive value: (six)sixcs sb2: receive value: (six)sixcs sb1: receive cancelcs sb2: receive cancel

removeDuplicates

使用 removeDuplicates,重复的值就不会发送了。

import Combinevar cc = Set<AnyCancellable>()let pb = ["one","two","three","three","four"]    .publisherlet sb = pb    .print("sb")    .removeDuplicates()    .sink {        print($0)    }    sb.store(in: &cc)

输出

sb: receive subscription: (["one", "two", "three", "three", "four"])sb: request unlimitedsb: receive value: (one)onesb: receive value: (two)twosb: receive value: (three)threesb: receive value: (three)sb: request max: (1) (synchronous)sb: receive value: (four)foursb: receive finished

flatMap

flatMap 能将多个发布者的值打平发送给订阅者

import Combinevar cc = Set<AnyCancellable>()struct S {    let p: AnyPublisher<String, Never>}let s1 = S(p: Just("one").eraseToAnyPublisher())let s2 = S(p: Just("two").eraseToAnyPublisher())let s3 = S(p: Just("three").eraseToAnyPublisher())let pb = [s1, s2, s3].publisher    let sb = pb    .print("sb")    .flatMap {        $0.p    }    .sink {        print($0)    }sb.store(in: &cc)

输出

sb: receive subscription: ([戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher)])sb: request unlimitedsb: receive value: (S(p: AnyPublisher))onesb: receive value: (S(p: AnyPublisher))twosb: receive value: (S(p: AnyPublisher))threesb: receive finished

append

append 会在发布者发布结束后追加发送数据,发布者不结束,append 的数据不会发送。

import Combinevar cc = Set<AnyCancellable>()let pb = PassthroughSubject<String, Never>()let sb = pb    .print("sb")    .append("five", "six")    .sink {        print($0)    }sb.store(in: &cc)pb.send("one")pb.send("two")pb.send("three")pb.send(completion: .finished)

输出

sb: receive subscription: ([戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的开发小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher)])sb: request unlimitedsb: receive value: (S(p: AnyPublisher))onesb: receive value: (S(p: AnyPublisher))twosb: receive value: (S(p: AnyPublisher))threesb: receive finished

prepend

prepend 会在发布者发布前先发送数据,发布者不结束也不会受影响。发布者和集合也可以被打平发布。

import Combinevar cc = Set<AnyCancellable>()let pb1 = PassthroughSubject<String, Never>()let pb2 = ["nine", "ten"].publisherlet sb = pb1    .print("sb")    .prepend(pb2)    .prepend(["seven","eight"])    .prepend("five", "six")    .sink {        print($0)    }sb.store(in: &cc)pb1.send("one")pb1.send("two")pb1.send("three")

输出

fivesixseveneightninetensb: receive subscription: (PassthroughSubject)sb: request unlimitedsb: receive value: (one)onesb: receive value: (two)twosb: receive value: (three)threesb: receive cancel

merge

订阅者可以通过 merge 合并多个发布者发布的数据

import Combinevar cc = Set<AnyCancellable>()let ps1 = PassthroughSubject<String, Never>()let ps2 = PassthroughSubject<String, Never>()let sb1 = ps1.merge(with: ps2)    .sink {        print($0)    }    ps1.send("one")ps1.send("two")ps2.send("1")ps2.send("2")ps1.send("three")sb1.store(in: &cc)

输出

sb1: receive subscription: (Merge)sb1: request unlimitedsb1: receive value: (one)onesb1: receive value: (two)twosb1: receive value: (1)1sb1: receive value: (2)2sb1: receive value: (three)threesb1: receive cancel

zip

zip 会合并多个发布者发布的数据,只有当多个发布者都发布了数据后才会组合成一个数据给订阅者。

import Combinevar cc = Set<AnyCancellable>()let ps1 = PassthroughSubject<String, Never>()let ps2 = PassthroughSubject<String, Never>()let ps3 = PassthroughSubject<String, Never>()let sb1 = ps1.zip(ps2, ps3)    .print("sb1")    .sink {        print($0)    }    ps1.send("one")ps1.send("two")ps1.send("three")ps2.send("1")ps2.send("2")ps1.send("four")ps2.send("3")ps3.send("一")sb1.store(in: &cc)

输出

sb1: receive subscription: (Zip)sb1: request unlimitedsb1: receive value: (("one", "1", "一"))("one", "1", "一")sb1: receive cancel

combineLatest

combineLatest 会合并多个发布者发布的数据,只有当多个发布者都发布了数据后才会触发合并,合并每个发布者发布的最后一个数据。

import Combinevar cc = Set<AnyCancellable>()        let ps1 = PassthroughSubject<String, Never>()let ps2 = PassthroughSubject<String, Never>()let ps3 = PassthroughSubject<String, Never>()let sb1 = ps1.combineLatest(ps2, ps3)    .print("sb1")    .sink {        print($0)    }    ps1.send("one")ps1.send("two")ps1.send("three")ps2.send("1")ps2.send("2")ps1.send("four")ps2.send("3")ps3.send("一")ps3.send("二")sb1.store(in: &cc)

输出

sb1: receive subscription: (CombineLatest)sb1: request unlimitedsb1: receive value: (("four", "3", "一"))("four", "3", "一")sb1: receive value: (("four", "3", "二"))("four", "3", "二")sb1: receive cancel

Scheduler

Scheduler 处理队列。

import Combinevar cc = Set<AnyCancellable>()        let sb1 = ["one","two","three"].publisher    .print("sb1")    .subscribe(on: DispatchQueue.global())    .handleEvents(receiveOutput: {        print("receiveOutput",$0)    })    .receive(on: DispatchQueue.main)    .sink {        print($0)    }sb1.store(in: &cc)

输出

sb1: receive subscription: ([1, 2, 3])sb1: request unlimitedsb1: receive value: (1)receiveOutput 1sb1: receive value: (2)receiveOutput 2sb1: receive value: (3)receiveOutput 3sb1: receive finished123

使用场景

网络请求

网络URLSession.dataTaskPublisher使用例子如下:

let req = URLRequest(url: URL(string: "http://www.starming.com")!)let dpPublisher = URLSession.shared.dataTaskPublisher(for: req)

一个请求Github接口并展示结果的例子

//// CombineSearchAPI.swift// SwiftOnly (iOS)//// Created by Ming Dai on 2021/11/4.//import SwiftUIimport Combinestruct CombineSearchAPI: View {  var body: some View {    GithubSearchView()  }}// MARK: Github Viewstruct GithubSearchView: View {  @State var str: String = "Swift"  @StateObject var ss: SearchStore = SearchStore()  @State var repos: [GithubRepo] = []  var body: some View {    NavigationView {      List {        TextField("输入:", text: $str, onCommit: fetch)        ForEach(self.ss.repos) { repo -> GithubRepoCell in          GithubRepoCell(repo: repo)        }      }      .navigationTitle("搜索")    }    .onAppear(perform: fetch)  }     private func fetch() {    self.ss.search(str: self.str)  }}struct GithubRepoCell: View {  let repo: GithubRepo  var body: some View {    VStack(alignment: .leading, spacing: 20) {      Text(self.repo.name)      Text(self.repo.description)    }  }}// MARK: Github Servicestruct GithubRepo: Decodable, Identifiable {  let id: Int  let name: String  let description: String}struct GithubResp: Decodable {  let items: [GithubRepo]}final class GithubSearchManager {  func search(str: String) -> AnyPublisher<GithubResp, Never> {    guard var urlComponents = URLComponents(string: "https://api.github.com/search/repositories") else {      preconditionFailure("链接无效")    }    urlComponents.queryItems = [URLQueryItem(name: "q", value: str)]         guard let url = urlComponents.url else {      preconditionFailure("链接无效")    }    let sch = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)         return URLSession.shared      .dataTaskPublisher(for: url)      .receive(on: sch)      .tryMap({ element -> Data in        print(String(decoding: element.data, as: UTF8.self))        return element.data      })      .decode(type: GithubResp.self, decoder: JSONDecoder())      .catch { _ in        Empty().eraseToAnyPublisher()      }      .eraseToAnyPublisher()  }}final class SearchStore: ObservableObject {  @Published var query: String = ""  @Published var repos: [GithubRepo] = []  private let searchManager: GithubSearchManager  private var cancellable = Set<AnyCancellable>()     init(searchManager: GithubSearchManager = GithubSearchManager()) {    self.searchManager = searchManager    $query      .debounce(for: .milliseconds(500), scheduler: RunLoop.main)      .flatMap { query -> AnyPublisher<[GithubRepo], Never> in        return searchManager.search(str: query)          .map {            $0.items          }          .eraseToAnyPublisher()      }      .receive(on: DispatchQueue.main)      .assign(to: \.repos, on: self)      .store(in: &cancellable)  }  func search(str: String) {    self.query = str  }}

抽象基础网络能力,方便扩展,代码如下:

//// CombineAPI.swift// SwiftOnly (iOS)//// Created by Ming Dai on 2021/11/4.//import SwiftUIimport Combinestruct CombineAPI: View {  var body: some View {    RepListView(vm: .init())  }}struct RepListView: View {  @ObservedObject var vm: RepListVM     var body: some View {    NavigationView {      List(vm.repos) { rep in        RepListCell(rep: rep)      }      .alert(isPresented: $vm.isErrorShow) { () -> Alert in        Alert(title: Text("出错了"), message: Text(vm.errorMessage))      }      .navigationBarTitle(Text("仓库"))    }    .onAppear {      vm.apply(.onAppear)    }  }}struct RepListCell: View {  @State var rep: RepoModel  var body: some View {    HStack() {      VStack() {        AsyncImage(url: URL(string: rep.owner.avatarUrl ?? ""), content: { image in          image            .resizable()            .aspectRatio(contentMode: .fit)            .frame(width: 100, height: 100)        },        placeholder: {          ProgressView()            .frame(width: 100, height: 100)        })        Text("\(rep.owner.login)")          .font(.system(size: 10))      }      VStack(alignment: .leading, spacing: 10) {        Text("\(rep.name)")          .font(.title)        Text("\(rep.stargazersCount)")          .font(.title3)        Text("\(String(describing: rep.description ?? ""))")        Text("\(String(describing: rep.language ?? ""))")          .font(.title3)      }      .font(.system(size: 14))    }       }}// MARK: Repo View Modelfinal class RepListVM: ObservableObject, UnidirectionalDataFlowType {  typealias InputType = Input  private var cancellables: [AnyCancellable] = []     // Input  enum Input {    case onAppear  }  func apply(_ input: Input) {    switch input {    case .onAppear:      onAppearSubject.send(())    }  }  private let onAppearSubject = PassthroughSubject<Void, Never>()     // Output  @Published private(set) var repos: [RepoModel] = []  @Published var isErrorShow = false  @Published var errorMessage = ""  @Published private(set) var shouldShowIcon = false     private let resSubject = PassthroughSubject<SearchRepoModel, Never>()  private let errSubject = PassthroughSubject<APISevError, Never>()     private let apiSev: APISev     init(apiSev: APISev = APISev()) {    self.apiSev = apiSev    bindInputs()    bindOutputs()  }     private func bindInputs() {    let req = SearchRepoRequest()    let resPublisher = onAppearSubject      .flatMap { [apiSev] in        apiSev.response(from: req)          .catch { [weak self] error -> Empty<SearchRepoModel, Never> in            self?.errSubject.send(error)            return .init()          }      }    let resStream = resPublisher      .share()      .subscribe(resSubject)         // 其它异步事件,比如日志等操作都可以做成Stream加到下面数组内。    cancellables += [resStream]  }     private func bindOutputs() {    let repStream = resSubject      .map {        $0.items      }      .assign(to: \.repos, on: self)    let errMsgStream = errSubject      .map { error -> String in        switch error {        case .resError: return "network error"        case .parseError: return "parse error"        }      }      .assign(to: \.errorMessage, on: self)    let errStream = errSubject      .map { _ in        true      }      .assign(to: \.isErrorShow, on: self)    cancellables += [repStream,errStream,errMsgStream]  }   }protocol UnidirectionalDataFlowType {  associatedtype InputType  func apply(_ input: InputType)}// MARK: Repo Request and Modelsstruct SearchRepoRequest: APIReqType {  typealias Res = SearchRepoModel     var path: String {    return "/search/repositories"  }  var qItems: [URLQueryItem]? {    return [      .init(name: "q", value: "Combine"),      .init(name: "order", value: "desc")    ]  }}struct SearchRepoModel: Decodable {  var items: [RepoModel]}struct RepoModel: Decodable, Hashable, Identifiable {  var id: Int64  var name: String  var fullName: String  var description: String?  var stargazersCount: Int = 0  var language: String?  var owner: OwnerModel}struct OwnerModel: Decodable, Hashable, Identifiable {  var id: Int64  var login: String  var avatarUrl: String?}// MARK: API Request Fundationprotocol APIReqType {  associatedtype Res: Decodable  var path: String { get }  var qItems: [URLQueryItem]? { get }}protocol APISevType {  func response<Request>(from req: Request) -> AnyPublisher<Request.Res, APISevError> where Request: APIReqType}final class APISev: APISevType {  private let rootUrl: URL  init(rootUrl: URL = URL(string: "https://api.github.com")!) {    self.rootUrl = rootUrl  }     func response<Request>(from req: Request) -> AnyPublisher<Request.Res, APISevError> where Request : APIReqType {    let path = URL(string: req.path, relativeTo: rootUrl)!    var comp = URLComponents(url: path, resolvingAgainstBaseURL: true)!    comp.queryItems = req.qItems    print(comp.url?.description ?? "url wrong")    var req = URLRequest(url: comp.url!)    req.addValue("application/json", forHTTPHeaderField: "Content-Type")         let de = JSONDecoder()    de.keyDecodingStrategy = .convertFromSnakeCase    return URLSession.shared.dataTaskPublisher(for: req)      .map { data, res in        print(String(decoding: data, as: UTF8.self))        return data      }      .mapError { _ in        APISevError.resError      }      .decode(type: Request.Res.self, decoder: de)      .mapError(APISevError.parseError)      .receive(on: RunLoop.main)      .eraseToAnyPublisher()  }}enum APISevError: Error {  case resError  case parseError(Error)}

KVO

例子如下:

private final class KVOObject: NSObject {  @objc dynamic var intV: Int = 0  @objc dynamic var boolV: Bool = false}let o = KVOObject()let _ = o.publisher(for: \.intV)  .sink { v in    print("value : \(v)")  }

通知

使用例子如下:

extension Notification.Name {    static let noti = Notification.Name("nameofnoti")}let notiPb = NotificationCenter.default.publisher(for: .noti, object: nil)        .sink {            print($0)        }

退到后台接受通知的例子如下:

class A {  var storage = Set<AnyCancellable>()     init() {    NotificationCenter.default.publisher(for: UIWindowScene.didEnterBackgroundNotification)      .sink { _ in        print("enter background")      }      .store(in: &self.storage)  }}

Timer

使用方式如下:

let timePb = Timer.publish(every: 1.0, on: RunLoop.main, in: .default)let timeSk = timePb.sink { r in    print("r is \(r)")}let cPb = timePb.connect()

Concurrency

介绍

Swift Concurrency 是什么?

ABI 稳定后,Swift 的核心团队可以开始关注 Swift 语言一直缺失的原生并发能力了。最初是由 Chris Lattner 在17年发的 Swift并发宣言 ,从此开阔了大家的眼界。后来 Swift Evolution 社区讨论了十几个提案,几十个方案,以及几百页的设计文件,做了大量的改进,社区中用户积极的参与反馈,Chris 也一直在 Evolution 中积极的参与设计。

Swift Concurrency 的实现用了 LLVM的协程 把 async/await 函数转换为基于回调的代码,这个过程发生在编译后期,这个阶段你的代码都没法辨识了。异步的函数被实现为 coroutines,在每次异步调用时,函数被分割成可调用的函数部分和后面恢复的部分。coroutine 拆分的过程发生在生成LLVM IR阶段。Swift使用了哪些带有自定义调用约定的函数保证尾部调用,并专门为Swift进行了调整。

Swift Concurrency 不是建立在 GCD 上,而是使用的一个全新的线程池。GCD 中启动队列工作会很快在提起线程,一个队列阻塞了线程,就会生成一个新线程。基于这种机制 GCD 线程数很容易比 CPU 核心数量多,线程多了,线程就会有大量的调度开销,大量的上下文切换,会使 CPU 运行效率降低。而 Swift Concurrency 的线程数量不会超过 CPU 内核,将上下文切换放到同一个线程中去做。为了实现线程不被阻塞,需要通过语言特性来做。做法是,每个线程都有一个堆栈记录函数调用情况,一个函数占一个帧。函数返回后,这个函数所占的帧就会从堆栈弹出。await 的 async 函数被作为异步帧保存在堆上等待恢复,而不阻碍其它函数入栈执行。在 await 后运行的代码叫 continuation,continuation 会在要恢复时放回到线程的堆栈里。异步帧会根据需要放回栈上。在一个异步函数中调用同步代码将添加帧到线程的堆栈中。这样线程就能够一直向前跑,而不用创建更多线程减少调度。

Douglas 在 Swift 论坛里发的 Swift Concurrency 下个版本的规划贴 Concurrency in Swift 5 and 6 ,论坛里还有一个帖子是专门用来 征集Swift Concurrency意见 的,帖子本身列出了 Swift Concurrency 相关的所有提案,也提出欢迎有新提案发出来,除了这些提案可以看外,帖子回复目前已经过百,非常热闹,可以看出大家对 Swift Concurrency 的关注度相当的高。

非常多的人参与了 Swift Concurrency 才使其看起来和用起来那么简单。Doug Gregor 在参与 John Sundell 的播客后,发了很多条推聊 Swift Concurrency,可以看到参与的人非常多,可见背后付出的努力有多大。下面我汇总了 Doug Gregor 在推上发的一些信息,你通过这些信息也可以了解 Swift Concurrency 幕后信息,所做的事和负责的人。

@pathofshrines 是 Swift Concurrency 整体架构师,包括低级别运行时和编译器相关细节。 @illian 是 async sequences、stream 和 Fundation 的负责人。 @optshiftk 对 UI 和并发交互的极好的洞察力带来了很棒的 async 接口, @phausler 带来了 async sequences。Arnold Schwaighofer、 @neightchan@typesanitizer 还有 Tim Northover 实现了 async calling convention。

@ktosopl 有很深厚的 actor、分布式计算和 Swift-on-Server 经验,带来了 actor 系统。Erik Eckstein 为 async 函数和actors建立了关键的优化和功能。

SwiftUI是 @ricketson_@luka_bernardi 完成的async接口。async I/O的接口是 @Catfish_Man 完成的。 @slava_pestov 处理了 Swift 泛型问题,还指导其他人编译器实现的细节。async 重构工具是Ben Barham 做的。大量代码移植到 async 是由 @AirspeedSwift 领导,由 Angela Laar,Clack Cole,Nicole Jacques 和 @mishaldshah 共同完成的。

@lorentey 负责 Swift 接口的改进。 @jckarter 有着敏锐的语言设计洞察力,带来了语言设计经验和编译器及运行时实现技能。 @mikeash 也参与了运行时开发中。操作系统的集成是 @rokhinip 完成的, @chimz 提供了关于 Dispatch 和 OS 很好的建议,Pavel Yaskevich 和
@hollyborla 进行了并发所需要关键类型检查器的改进。 @kastiglione 、Adrian Prantl和 @fred_riss 实现了调试。 @etcwilde@call1cc 实现了语义模型中的重要部分。

@evonox 负责了服务器Linux 的支持。 @compnerd 将 Swift Concurrency 移植到了 Windows。

Swift Concurrency 模型简单,细节都被隐藏了,比 Kotlin 和 C++的 Coroutine 接口要简洁很多。比如 Task 接口形式就很简洁。Swift Concurrency 大体可分为 async/await、Async Sequences、结构化并发和 Actors。

async/await

通过类似 throws 语法的 async 来指定函数为异步函数,异步函数才能够使用 await,使用异步函数要用 await。await 修饰在 suspension point 时当前线程可以让给其它任务执行,而不用阻塞当前线程,等 await 后面的函数执行完成再回来继续执行,这里需要注意的是回来执行不一定是在离开时的线程上。async/await 提案是 SE-0296 。如果想把现有的异步开发带到 async/await 世界,请使用 withCheckedThrowingContinuation。

async/await 还有一个非常明显的好处,就是不会再有[weak self] dance 了。

Async Sequences

AsyncSequence 的使用方式是 for-await-in 和 for-try-await-in,系统提供了一些接口,如下:

  • FileHandle.standardInput.bytes.lines
  • URL.lines
  • URLSession.shared.data(from: URL)
  • let (localURL, _ ) = try await session.download(from: url) 下载和get请求数据区别是需要边请求边存储数据以减少内存占用
  • let (responseData, response) = try await session.upload(for: request, from: data)
  • URLSession.shared.bytes(from: URL)
  • NotificationCenter.default.notifications

结构化并发

使用这些接口可以一边接收数据一边进行显示,AsyncSequence 的提案是 SE-0298 (Swift 5.5可用)。AsyncStream 是创建自己异步序列的最简单的方法,处理迭代、取消和缓冲。AsyncStream 正在路上,提案是 SE-0314

Task 为一组并发任务创建一个运行环境,async let 可以让任务并发执行,结构化并发(Structured concurrency,提案在路上 SE-0304 )withTaskGroup 中 group.async 可以将并发任务进行分组。

Actors

我们写的程序会在进程中被拆成一个一个小指令,这些指令会在某刻会一个接一个同步的或者并发的执行。系统会用多个线程执行并行的任务,执行顺序是调度器来管理的,现代多核可以同时处理多个线程,当一个资源在多个线程上同时被更改时就会出问题。并发任务对数据资源操作容易造成数据竞争,以前需要手动放到串行队列、使用锁、调度屏障或 Atomics 的方式来避免。以前处理容易导致昂贵的上下文切换,过多线程容易导致线程爆炸,容易意外阻断线程导致后面代码没法执行,多任务相互的等待造成了死锁,block 和内存引用容易出错等等问题。

现在 Swift Concurrency 可以通过 actor 来创建一个区域,在这个区域会自动进行数据安全保护,保证一定时间只有一个线程访问里面数据,防止数据竞争。actor 内部对成员访问是同步的,成员默认是隔离的,actor 外部对 actor 内成员的访问只能是异步的,隐式同步以防止数据竞争。MainActor 继承自能确保全局唯一实例的 GlobalActor,保证任务在主线程执行,这样你就可以抛弃掉在你的 ViewModel 里写 DispatchQueue.main.async 了。

Actors 的概念通常被用于分布式计算,Actor 模型参看 Wikipedia 里的详细解释,Swift 中的实现效果也非常的理想。Actors 的提案 SE-0306 已在 Swift 5.5落实。

很多语言都支持 actors 还有 async/await,实现的方式也类似,actor 使用的不是锁,而是用的 async/await 这样能够在一个线程中切换上下文来避免线程空闲的线程模型。actor 还利用编译器,提前做会引起并发问题的检查。

actor 是遵循 Sendable 协议的,只有结构体和 final 类才能够遵循 Sendable,继承于 Sendable 协议的 Excutor 协议表示方法本身,SerialExecutor 表示以串行方式执行。actor 使用 C++写的,源码在 这里 ,可以看到 actor 主要是通过控制各个 job 执行的状态的管理器。job 执行优先级来自 Task 对象,排队时需要确保高优 job 先被执行。全局 Executor 用来为 job 排队,通知 actor 拥有或者放弃线程,实现在 这里 。由于等待而放弃当前线程让其他 actor 执行的 actor,在收到全局 Executor 创建一个新的 job 的通知,使其可以进入一个可能不同线程,这个过程就是并发模型中描述的 Actor Reentrancy。

Distributed Actors

actor 具有分布式形式工作能力,也就是可以 RPC 通过网络读取和写入属性或者调用方法。设计为保护在跨多个进程中的低级别数据竞争。Distributed actors 可以在两个进程间建立通道,隔离它们状态,并在它们之间异步通信。每个 distributed actors 在 actor 初始化时分配一个不可以手动创建的 id,在它所属整个 distributed actor 系统中唯一标识所指 actor,这样无论 distributed actors 在哪,都可以以相同的方式与之交互。

session Meet distributed actors in Swift 。这里有个 distributed actors 的代码示例 TicTacFish: Implementing a game using distributed actors

SE-0336 Distributed Actor IsolationSE-0344 Distributed Actor Runtime 是两个 Distributed Actors 的相关提案。

Apple 提供了一个参考的服务端 cluster actor 系统实现示例,cluster actor system implementation

相关提案

所有相关提案清单如下:

学习路径

如果打算尝试 Swift Concurrency 的话,按照先后顺序,可以先看官方手册介绍文章 Concurrency 。再看 Meet async/await in Swift 这个Session,了解背后原理看 Explore structured concurrency in Swift 。动手照着试示例代码,看Paul的 Swift Concurrency by Example 这个系列。接着看 Protect mutable state with Swift actors 来了解 actors 怎么防止数据竞争。通过 Discover concurrency in SwiftUI 看 concurrency 如何在 SwiftUI 中使用, Use async/await with URLSession 来看怎么在 URLSession 中使用 async/await。最后听听负责 Swift Concurrency 的 Doug Gregor 参加的一个 播客的访谈 ,了解下 Swift Concurrency 背后的故事。

Swift Concurrency 和 Combine

由于 Swift Concurrency 的推出和大量的 Session 发布,特别是 AsyncSequence 的出现,以及正在路上的 AsyncStream、AsyncThrowingStreamcontinuation 提案(在Xcode 13.0 beta 3 AsyncStream 正式 release ),这些越来越多和 Combine 功能重叠的特性出现在 Swift Concurrency 蓝图里时,大家开始猜测是否 Combine 会被 Swift Concurrency 替代。关于未来是 Swift Concurrency 还是 Combine,我的感觉是,Combine 更侧重在响应式编程上,而响应式编程并不是所有开发人员都会接受的,而 Swift Concurrency 是所有人都愿意接受的开发方式,从 Swift Concurrency 推出后开发者使用的数量和社区反应火热程度来看都比 Combine 要大。在苹果对 Combine 有下一步动作之前,我还是更偏向 Swift Concurrency。

Concurrency 5.7 版本更新

session Eliminate data races using Swift ConcurrencyVisualize and optimize Swift concurrencyMeet Swift Async Algorithms

表示持续时间有了新的放来来表达,对应提案是 SE-0329 Clock, Instant, and Duration ,continuous clock 是在系统睡眠状态还会增加时间,suspending clock 在系统睡眠状态不会增加时间。Instants 表示一个确定的时间。Duration 表示两个时间经历了多久。

新增 SE-0338 Clarify the Execution of Non-Actor-Isolated Async Functions 通过收紧可发送性检查的规则来避免潜在的数据竞争。

SE-0343 Concurrency in Top-level Code 这个提案主要是更好地支持命令行工具的开发,可以直接将 concurrency 代码写到 main.swift 文件里。

SE-0340 Unavailable From Async Attribute 提供 noasync 语法以允许我们将类型和函数标记为在异步上下文不可用。

Task 是按顺序执行的,是异步的,在 await 时可以暂停任意次数。task 是自包含的,有自己的资源,可以独立于任何其他 task 独立运行。task 通过在 body 末尾返回一个值来传递对象,值类型没问题,如果是引用类型有可能出现数据竞争。

通过 Sendable 协议 Swift 可以帮助告诉我们什么时候 task 之间共享数据是安全的。Sendable 描述的类型可以跨隔离 domain,不会有数据竞争,Swift 编译器会在构建时检查数据竞争。task 的返回类型要符合 Sendable。

引用类型只能在很少的情况下符合 Sendable。比如 final class 只有不可变的存储。对于自己内部同步的引用类型,比如锁,可以用 @unchecked Sendable

class ConcurrentCache<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {  var lock: NSLock  var storage: [Key: Value]  // ...}

Actor 提供了一种隔离状态的方法可以消除数据竞争。使用 task 来执行 actor 定义的代码。一次只能在一个 actor 上执行一个 task。actor 也是依赖 Sendable。actor 是引用类型,但隔离了他们所有属性和代码来防止并发访问。@MainActor 表示的是主线程,你要在应用中更新 UI 时来用它。

@MainActor func updateView() {}Task { @MainActor in  // update UI here}

@MainActor 也可以用于类,类的属性和方法只能在主 main actor 上访问,除非标记为 nonisolated

@MainActorclass ChickenValley: Sendable {  var flock: [Chicken]  var food: [Pineapple]  func advanceTime() {    for chicken in flock {      chicken.eat(from: &food)    }  }}

SwiftUI

介绍

SwiftUI 是什么?

对于一个基于UIKit的项目是没有必要全部用SwiftUI重写的,在UIKit里使用SwiftUI的视图非常容易,UIHostingController是UIViewController的子类,可以直接用在UIKit里,因此直接将SwiftUI视图加到UIHostingController中,就可以在UIKit里使用SwiftUI视图了。

SwiftUI的布局核心是 GeometryReader、View Preferences和Anchor Preferences。如下图所示:

SwiftUI的数据流更适合Redux结构,如下图所示:

如上图,Redux结构是真正的单向单数据源结构,易于分割,能充分利用SwiftUI内置的数据流Property Wrapper。UI组件干净、体量小、可复用并且无业务逻辑,因此开发时可以聚焦于UI代码。业务逻辑放在一起,所有业务逻辑和数据Model都在Reducer里。 ACHNBrowserUIMovieSwiftUI 开源项目都是使用的Redux架构。最近比较瞩目的TCA(The Composable Architecture)也是类Redux/Elm的架构的框架, 项目地址见

提到数据流就不得不说下苹果公司新出的Combine,对标的是RxSwift,由于是苹果公司官方的库,所以应该优先选择。不过和SwiftUI一样,这两个新库对APP支持最低的系统版本都要求是iOS13及以上。那么怎么能够提前用上SwiftUI和Combine呢?或者说现在使用什么库可以以相同接口方式暂时替换它们,又能在以后改为SwiftUI和Combine时成本最小化呢?

对于SwiftUI,AcFun自研了声明式UI Ysera,类似SwiftUI的接口,并且重构了AcFun里收藏模块列表视图和交互逻辑,如下图所示:

通过上图可以看到,swift代码量相比较OC减少了65%以上,原先使用Objective-C实现的相同功能代码超过了1000行,而Swift重写只需要350行,对于AcFun的业务研发工程师而言,同样的需求实现代码比之前少了至少30%,面对单周迭代这样的节奏,团队也变得更从容。代码可读性增加了,后期功能迭代和维护更容易了,Swift让AcFun驶入了iOS开发生态的“快车道”。

SwiftUI全部都是基于Swift的各大可提高开发效率特性完成的,比如前面提到的,能够访问只给语言特性级别行为的Property Wrapper,通过Property Wrapper包装代码逻辑,来降低代码复杂度,除了SwiftUI和Combine里@开头的Property Wrapper外,Swift还自带类似 @dynamicMemberLookup@dynamicCallable 这样重量级的Property Wrapper。还有 ResultBuilder 这种能够简化语法的特性,有些如GraphQL、REST和Networking实际使用ResultBuilder的 范例可以参考 。这些Swift的特性如果也能得到充分利用,即使不用SwiftUI也能使开发效率得到大幅提升。

网飞(Netflix)App已使用SwiftUI重构了登录界面,网飞增长团队移动负责人故胤道长记录了SwiftUI在网飞的落地过程,详细描述了 SwiftUI的收益 。网飞能够直接使用SwiftUI得益于他们最低支持iOS 13系统。

不过如最低支持系统低于iOS 13,还有开源项目 AltSwiftUI 也实现了SwiftUI的语法和特性,能够向前兼容到iOS 11。

Kuba Suder 做了一个 SwiftUI Index/Changelog ,从官方文档中提取版本信息,一目了然 SwiftUI 每个版本 view,modifier 还有属性做了哪些增加和改变。当然也包括这次 SwiftUI 4 的更新。还有份对今年更新整理的 cheat sheet What’s New In SwiftUI for iOS Cheat Sheet - WWDC22

SwiftUI 4 做了大量细节更新,比如添加了后台任务函数 backgroundTask(_:action:) 。List 改用 UICollectionView。AnyLayout 让 HStack 和 VStack 之间可以自由切换。scrollDismissesKeyboard() modifier 可以让键盘在滚动时自动 dismiss。scrollIndicators() modifier 可以隐藏 ScrollView 和 List 等视图的滚动指示。defersSystemGestures() modifier 允许我们的手势优先于系统的内置手势。颜色的 .gradient 可以获得很简单的渐变,Rectangle().fill(.red.gradient),还有 .shadow 用来创建投影 Rectangle().fill(.red.shadow(.drop(color: .black, radius: 10))),还有 .inner 内阴影。lineLimit() modifier 支持范围设置。还有一些 modifier 支持 toggle 参数,比如 .bold().italic() 等,这样利于运行时进行调整。

嵌入 UIKit
示例如下:

cell.contentConfiguration = UIHostingConfiguration {    VStack {        Image(systemName: "wand.and.stars")            .font(.title)        Text("Like magic!")            .font(.title2).bold()    }    .foregroundStyle(Color.purple)}

锁屏的 Widget 和 WatchOS 一样,可以瞟一眼就获取信息。

官方指南 Creating Lock Screen Widgets and Watch Complications

可以将 SwiftUI 的 View 生成图片。

官方参考文档 ImageRenderer

session Efficiency awaits: Background tasks in SwiftUI 了解如何使用 SwiftUI 后台任务 API 简洁地处理任务。展示如何使用 Swift Concurrency 来处理网络响应、后台刷新等——同时保持性能和功率。

SwiftUI 参考资料

session:

社区整理的和 SwiftUI 的 digital lounges 内容:

视图组件使用

SwiftUI 对标的 UIKit 视图

如下:

SwiftUI UIKit
Text 和 Label UILabel
TextField UITextField
TextEditor UITextView
Button 和 Link UIButton
Image UIImageView
NavigationView UINavigationController 和 UISplitViewController
ToolbarItem UINavigationItem
ScrollView UIScrollView
List UITableView
LazyVGrid 和 LazyHGrid UICollectionView
HStack 和 LazyHStack UIStack
VStack 和 LazyVStack UIStack
TabView UITabBarController 和 UIPageViewController
Toggle UISwitch
Slider UISlider
Stepper UIStepper
ProgressView UIProgressView 和 UIActivityIndicatorView
Picker UISegmentedControl
DatePicker UIDatePicker
Alert UIAlertController
ActionSheet UIAlertController
Map MapKit

Text

基本用法

// MARK: - Textstruct PlayTextView: View {    let manyString = "这是一段长文。总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么,总得说点什么吧。"    var body: some View {        ScrollView {            Group {                Text("大标题").font(.largeTitle)                Text("说点啥呢?")                    .tracking(30) // 字间距                    .kerning(30) // 尾部留白                Text("划重点")                    .underline()                    .foregroundColor(.yellow)                    .fontWeight(.heavy)                Text("可旋转的文字")                    .rotationEffect(.degrees(45))                    .fixedSize()                    .frame(width: 20, height: 80)                Text("自定义系统字体大小")                    .font(.system(size: 30))                Text("使用指定的字体")                    .font(.custom("Georgia", size: 24))            }            Group {                Text("有阴影")                    .font(.largeTitle)                    .foregroundColor(.orange)                    .bold()                    .italic()                    .shadow(color: .black, radius: 1, x: 0, y: 2)                Text("Gradient Background")                    .font(.largeTitle)                    .padding()                    .foregroundColor(.white)                    .background(LinearGradient(gradient: Gradient(colors: [.white, .black, .red]), startPoint: .top, endPoint: .bottom))                    .cornerRadius(10)                Text("Gradient Background")                    .padding(5)                    .foregroundColor(.white)                    .background(LinearGradient(gradient: Gradient(colors: [.white, .black, .purple]), startPoint: .leading, endPoint: .trailing))                    .cornerRadius(10)                ZStack {                    Text("渐变透明材质风格")                        .padding()                        .background(                            .regularMaterial,                            in: RoundedRectangle(cornerRadius: 10, style: .continuous)                        )                        .shadow(radius: 10)                        .padding()                        .font(.largeTitle.weight(.black))                }                .frame(width: 300, height: 200)                .background(                    LinearGradient(colors: [.yellow, .pink], startPoint: .topLeading, endPoint: .bottomTrailing)                )                Text("Angular Gradient Background")                    .padding()                    .background(AngularGradient(colors: [.red, .yellow, .green, .blue, .purple, .red], center: .center))                    .cornerRadius(20)                Text("带背景图片的")                    .padding()                    .font(.largeTitle)                    .foregroundColor(.white)                    .background {                        Rectangle()                            .fill(Color(.black))                            .cornerRadius(10)                        Image("logo")                            .resizable()                            .frame(width: 100, height: 100)                    }                    .frame(width: 200, height: 100)            }            Group {                // 设置 lineLimit 表示最多支持行数,依据情况依然有会被减少显示行数                Text(manyString)                    .lineLimit(3) // 对行的限制,如果多余设定行数,尾部会显示...                    .lineSpacing(10) // 行间距                    .multilineTextAlignment(.leading) // 对齐                                // 使用 fixedSize 就可以在任何时候完整显示                Text(manyString)                    .fixedSize(horizontal: false, vertical: true)                            }                        // 使用 AttributeString            PTextViewAttribute()                .padding()            // 使用 Markdown            PTextViewMarkdown()                .padding()                        // 时间            PTextViewDate()                        // 插值            PTextViewInterpolation()        }    }}

font 字体设置的样式对应 weight 和 size 可以在官方交互文档中查看 Typography

markdown 使用

// MARK: - Markdownstruct PTextViewMarkdown: View {    let mdaStr: AttributedString = {                var mda = AttributedString(localized: "这是一个 **Attribute** ~string~")                /// 自定义的属性语法是^[string](key:value)        mda = AttributedString(localized: "^[这是](p2:'one')^[一](p3:{k1:1,k2:2})个 **Attribute** ~string~", including: \.newScope)        print(mda)        /// 这是 {        ///     NSLanguage = en        ///     p2 = one        /// }        /// 一 {        ///     NSLanguage = en        ///     p3 = P3(k1: 1, k2: 2)        /// }        /// 个  {        ///     NSLanguage = en        /// }        /// Attribute {        ///     NSLanguage = en        ///     NSInlinePresentationIntent = NSInlinePresentationIntent(rawValue: 2)        /// }        ///   {        ///     NSLanguage = en        /// }        /// string {        ///     NSInlinePresentationIntent = NSInlinePresentationIntent(rawValue: 32)        ///     NSLanguage = en        /// }                // 从文件中读取 Markdown 内容        let mdUrl = Bundle.main.url(forResource: "1", withExtension: "md")!        mda = try! AttributedString(contentsOf: mdUrl,options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace), baseURL: nil) // .inlineOnlyPreservingWhitespace 支持 markdown 文件的换行                        // Markdown 已转换成 AtrributedString 结构。        for r in mda.runs {            if let ipi = r.inlinePresentationIntent {                switch ipi {                case .lineBreak:                    print("paragrahp")                case .code:                    print("this is code")                default:                    break                }            }            if let pi = r.presentationIntent {                for c in pi.components {                    switch c.kind {                    case .paragraph:                        print("this is paragraph")                    case .codeBlock(let lang):                        print("this is \(lang ?? "") code")                    case .header(let level):                        print("this is \(level) level")                    default:                        break                    }                }            }        }                return mda    }()    var body: some View {        Text(mdaStr)    }}

AttributedString 的使用

// MARK: - AttributedStringstruct PTextViewAttribute: View {    let aStr: AttributedString = {        var a1 = AttributedString("这是一个 ")        var c1 = AttributeContainer()        c1.font = .footnote        c1.foregroundColor = .secondary        a1.setAttributes(c1)                var a2 = AttributedString("Attribute ")        var c2 = AttributeContainer()        c2.font = .title        a2.setAttributes(c2)                var a3 = AttributedString("String ")        var c3 = AttributeContainer()        c3.baselineOffset = 10        c3.appKit.foregroundColor = .yellow // 仅在 macOS 里显示的颜色        c3.swiftUI.foregroundColor = .secondary        c3.font = .footnote        a3.setAttributes(c3)        // a3 使用自定义属性        a3.p1 = "This is a custom property."                // formatter 的支持        var a4 = Date.now.formatted(.dateTime                                        .hour()                                        .minute()                                        .weekday()                                        .attributed        )                let c4AMPM = AttributeContainer().dateField(.amPM)        let c4AMPMColor = AttributeContainer().foregroundColor(.green)                a4.replaceAttributes(c4AMPM, with: c4AMPMColor)        let c4Week = AttributeContainer().dateField(.weekday)        let c4WeekColor = AttributeContainer().foregroundColor(.purple)        a4.replaceAttributes(c4Week, with: c4WeekColor)                a1.append(a2)        a1.append(a3)        a1.append(a4)                                // Runs 视图        for r in a1.runs {            print(r)        }        /// 这是一个  {        ///     SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7ff91d4a5e90).FontBox<SwiftUI.Font.(unknown context at $7ff91d4ad5d8).TextStyleProvider>)        ///     SwiftUI.ForegroundColor = secondary        /// }        /// Attribute  {        ///     SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7ff91d4a5e90).FontBox<SwiftUI.Font.(unknown context at $7ff91d4ad5d8).TextStyleProvider>)        /// }        /// String  {        ///     SwiftUI.ForegroundColor = secondary        ///     SwiftUI.BaselineOffset = 10.0        ///     NSColor = sRGB IEC61966-2.1 colorspace 1 1 0 1        ///     SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7ff91d4a5e90).FontBox<SwiftUI.Font.(unknown context at $7ff91d4ad5d8).TextStyleProvider>)        ///     p1 = This is a custom property.        /// }        /// Tue {        ///     SwiftUI.ForegroundColor = purple        /// }        ///   {        /// }        /// 5 {        ///     Foundation.DateFormatField = hour        /// }        /// : {        /// }        /// 16 {        ///     Foundation.DateFormatField = minute        /// }        ///   {        /// }        /// PM {        ///     SwiftUI.ForegroundColor = green        /// }                return a1    }()    var body: some View {        Text(aStr)    }}// MARK: - 自定 AttributedString 属性struct PAKP1: AttributedStringKey {    typealias Value = String    static var name: String = "p1"        }struct PAKP2: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {    public enum P2: String, Codable {        case one, two, three    }    static var name: String = "p2"    typealias Value = P2}struct PAKP3: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {    public struct P3: Codable, Hashable {        let k1: Int        let k2: Int    }    typealias Value = P3    static var name: String = "p3"}extension AttributeScopes {    public struct NewScope: AttributeScope {        let p1: PAKP1        let p2: PAKP2        let p3: PAKP3    }    var newScope: NewScope.Type {        NewScope.self    }}extension AttributeDynamicLookup{    subscript<T>(dynamicMember keyPath:KeyPath<AttributeScopes.NewScope,T>) -> T where T:AttributedStringKey {        self[T.self]    }}

时间的显示

// MARK: - 时间struct PDateTextView: View {    let date: Date = Date()    let df: DateFormatter = {        let df = DateFormatter()        df.dateStyle = .long        df.timeStyle = .short        return df    }()    var dv: String {        return df.string(from: date)    }    var body: some View {        HStack {            Text(dv)        }        .environment(\.locale, Locale(identifier: "zh_cn"))    }}

插值使用

// MARK: - 插值struct PTextViewInterpolation: View {    let nf: NumberFormatter = {        let f = NumberFormatter()        f.numberStyle = .currencyPlural        return f    }()    var body: some View {        VStack {            Text("图文 \(Image(systemName: "sun.min"))")            Text("💰 \(999 as NSNumber, formatter: nf)")                .environment(\.locale, Locale(identifier: "zh_cn"))            Text("数组: \(["one", "two"])")            Text("红字:\(red: "变红了"),带图标的字:\(sun: "天晴")")        }    }}// 扩展 LocalizedStringKey.StringInterpolation 自定义插值extension LocalizedStringKey.StringInterpolation {    // 特定类型处理    mutating func appendInterpolation(_ value: [String]) {        for s in value {            appendLiteral(s + "")            appendInterpolation(Text(s + " ").bold().foregroundColor(.secondary))        }    }        // 实现不同情况处理,可以简化设置修改器设置    mutating func appendInterpolation(red value: LocalizedStringKey) {        appendInterpolation(Text(value).bold().foregroundColor(.red))    }    mutating func appendInterpolation(sun value: String) {        appendInterpolation(Image(systemName: "sun.max.fill"))        appendLiteral(value)    }}

Link

使用方法如下:

struct PlayLinkView: View {    @Environment(\.openURL) var openURL    var aStr: AttributedString {        var a = AttributedString("戴铭的博客")        a.link = URL(string: "https://ming1016.github.io/")        return a    }    var body: some View {        VStack {            // 普通            Link("前往 www.starming.com", destination: URL(string: "http://www.starming.com")!)                .buttonStyle(.borderedProminent)            Link(destination: URL(string: "https://twitter.com/daiming_cn")!) {                Label("My Twitter", systemImage: "message.circle.fill")            }                        // AttributedString 链接            Text(aStr)                        // markdown 链接            Text("[Go Ming's GitHub](https://github.com/ming1016)")                        // 控件使用 OpenURL            Link("小册子源码", destination: URL(string: "https://github.com/ming1016/SwiftPamphletApp")!)                .environment(\.openURL, OpenURLAction { url in                    return .systemAction                    /// return .handled 不会返回系统打开浏览器动作,只会处理 return 前的事件。                    /// .discard 和 .handled 类似。                    /// .systemAction(URL(string: "https://www.anotherurl.com")) 可以返回另外一个 url 来替代指定的url                })                        // 扩展 View 后更简洁的使用 OpenURL            Link("戴铭的微博", destination: URL(string: "https://weibo.com/allstarming")!)                .goOpenURL { url in                    print(url.absoluteString)                    return .systemAction                }                        // 根据内容返回不同链接            Text("戴铭博客有好几个,存在[GitHub Page](github)、[自建服务器](starming)和[知乎](zhihu)上")                .environment(\.openURL, OpenURLAction { url in                    switch url.absoluteString {                    case "github":                        return .systemAction(URL(string: "https://ming1016.github.io/")!)                    case "starming":                        return .systemAction(URL(string: "http://www.starming.com")!)                    case "zhihu":                        return .systemAction(URL(string: "https://www.zhihu.com/people/starming/posts")!)                    default:                        return .handled                    }                })        } // end VStack        .padding()            }        // View 支持 openURL 的能力    func goUrl(_ url: URL, done: @escaping (_ accepted: Bool) -> Void) {        openURL(url, completion: done)    }}// 为 View 扩展一个 OpenURL 方法extension View {    func goOpenURL(done: @escaping (URL) -> OpenURLAction.Result) -> some View {        environment(\.openURL, OpenURLAction(handler: done))    }}

View 的 onOpenURL 方法可以处理 Universal Links。

struct V: View {    var body: some View {        VStack {            Text("hi")        }        .onOpenURL { url in            print(url.absoluteString)        }    }}

Label

struct PlayLabelView: View {    var body: some View {        VStack(spacing: 10) {            Label("一个 Label", systemImage: "bolt.circle")                        Label("只显示 icon", systemImage: "heart.fill")                .labelStyle(.iconOnly)                .foregroundColor(.red)                        // 自建 Label            Label {                Text("自建 Label")                    .foregroundColor(.orange)                    .bold()                    .font(.largeTitle)                    .shadow(color: .black, radius: 1, x: 0, y: 2)            } icon: {                Image("p3")                    .resizable()                    .aspectRatio(contentMode: .fit)                    .frame(width: 30)                    .shadow(color: .black, radius: 1, x: 0, y: 2)            }                        // 自定义 LabelStyle            Label("有边框的 Label", systemImage: "b.square.fill")                .labelStyle(.border)                        Label("仅标题有边框", systemImage: "text.bubble")                .labelStyle(.borderOnlyTitle)                        // 扩展的 Label            Label("扩展的 Label", originalSystemImage: "cloud.sun.bolt.fill")                    } // end VStack    } // end body}// 对 Label 做扩展extension Label where Title == Text, Icon == Image {    init(_ title: LocalizedStringKey, originalSystemImage systemImageString: String) {        self.init {            Text(title)        } icon: {            Image(systemName: systemImageString)                .renderingMode(.original) // 让 SFSymbol 显示本身的颜色        }    }}// 添加自定义 LabelStyle,用来加上边框struct BorderLabelStyle: LabelStyle {    func makeBody(configuration: Configuration) -> some View {        Label(configuration)            .padding()            .overlay(RoundedRectangle(cornerRadius: 20)                        .stroke(.purple, lineWidth: 4))            .shadow(color: .black, radius: 4, x: 0, y: 5)            .labelStyle(.automatic) // 样式擦除器,防止样式被 .iconOnly、.titleOnly 这样的 LabelStyle 擦除了样式。                            }}extension LabelStyle where Self == BorderLabelStyle {    internal static var border: BorderLabelStyle {        BorderLabelStyle()    }}// 只给标题加边框struct BorderOnlyTitleLabelStyle: LabelStyle {    func makeBody(configuration: Configuration) -> some View {        HStack {            configuration.icon            configuration.title                .padding()                .overlay(RoundedRectangle(cornerRadius: 20)                            .stroke(.pink, lineWidth: 4))                .shadow(color: .black, radius: 1, x: 0, y: 1)                .labelStyle(.automatic)        }    }}extension LabelStyle where Self == BorderOnlyTitleLabelStyle {    internal static var borderOnlyTitle: BorderOnlyTitleLabelStyle {        BorderOnlyTitleLabelStyle()    }}

TextEditor

对应的代码如下:

import SwiftUIimport CodeEditorViewstruct PlayTextEditorView: View {    // for TextEditor    @State private var txt: String = "一段可编辑文字...\n"    @State private var count: Int = 0        // for CodeEditorView    @Environment(\.colorScheme) private var colorScheme: ColorScheme    @State private var codeMessages: Set<Located<Message>> = Set ()    @SceneStorage("editLocation") private var editLocation: CodeEditor.Location = CodeEditor.Location()    var body: some View {                // 使用 SwiftUI 自带 TextEditor        TextEditor(text: $txt)            .font(.title)            .lineSpacing(10)            .disableAutocorrection(true)            .padding()            .onChange(of: txt) { newValue in                count = txt.count            }        Text("字数:\(count)")            .foregroundColor(.secondary)            .font(.footnote)                // 使用的 CodeEditorView 显示和编辑代码高亮的代码,还有 minimap        CodeEditor(text: .constant("""static func number() {    // Int    let i1 = 100    let i2 = 22    print(i1 / i2) // 向下取整得 4    // Float    let f1: Float = 100.0    let f2: Float = 22.0    print(f1 / f2) // 4.5454545        let f4: Float32 = 5.0    let f5: Float64 = 5.0    print(f4, f5) // 5.0 5.0 5.0    // Double    let d1: Double = 100.0    let d2: Double = 22.0    print(d1 / d2) // 4.545454545454546    // 字面量    print(Int(0b10101)) // 0b 开头是二进制    print(Int(0x00afff)) // 0x 开头是十六进制    print(2.5e4) // 2.5x10^4 十进制用 e    print(0xAp2) // 10*2^2  十六进制用 p    print(2_000_000) // 2000000        // isMultiple(of:) 方法检查一个数字是否是另一个数字的倍数    let i3 = 36    print(i3.isMultiple(of: 9)) // true}"""),                   messages: $codeMessages,                   language: .swift,                   layout: CodeEditor.LayoutConfiguration(showMinimap: true)        )            .environment(\.codeEditorTheme, colorScheme == .dark ? Theme.defaultDark : Theme.defaultLight)                // 包装的 NSTextView        HSplitView {            PNSTextView(text: .constant("左边写...\n"), onDidChange: { (s, i) in                print("Typing \(i) times.")            })                .padding()            PNSTextView(text: .constant("右边写...\n"))                .padding()        } // end HSplitView    } // end body}// MARK: - 自己包装 NSTextViewstruct PNSTextView: NSViewRepresentable {    @Binding var text: String    var onBeginEditing: () -> Void = {}    var onCommit: () -> Void = {}    var onDidChange: (String, Int) -> Void = { _,_  in }        // 返回要包装的 NSView    func makeNSView(context: Context) -> PNSTextConfiguredView {        let t = PNSTextConfiguredView(text: text)        t.delegate = context.coordinator        return t    }        func updateNSView(_ view: PNSTextConfiguredView, context: Context) {        view.text = text        view.selectedRanges = context.coordinator.sRanges    }        // 回调    func makeCoordinator() -> TextViewDelegate {        TextViewDelegate(self)    }}// 处理 delegate 回调extension PNSTextView {    class TextViewDelegate: NSObject, NSTextViewDelegate {        var tView: PNSTextView        var sRanges: [NSValue] = []        var typeCount: Int = 0                init(_ v: PNSTextView) {            self.tView = v        }        // 开始编辑        func textDidBeginEditing(_ notification: Notification) {            guard let textView = notification.object as? NSTextView else {                return            }            self.tView.text = textView.string            self.tView.onBeginEditing()        }        // 每次敲字        func textDidChange(_ notification: Notification) {            guard let textView = notification.object as? NSTextView else {                return            }            typeCount += 1            self.tView.text = textView.string            self.sRanges = textView.selectedRanges            self.tView.onDidChange(textView.string, typeCount)        }        // 提交        func textDidEndEditing(_ notification: Notification) {            guard let textView = notification.object as? NSTextView else {                return            }            self.tView.text = textView.string            self.tView.onCommit()        }    }}// 配置 NSTextViewfinal class PNSTextConfiguredView: NSView {    weak var delegate: NSTextViewDelegate?        private lazy var tv: NSTextView = {        let contentSize = sv.contentSize        let textStorage = NSTextStorage()                let layoutManager = NSLayoutManager()        textStorage.addLayoutManager(layoutManager)                let textContainer = NSTextContainer(containerSize: sv.frame.size)        textContainer.widthTracksTextView = true        textContainer.containerSize = NSSize(            width: contentSize.width,            height: CGFloat.greatestFiniteMagnitude        )                layoutManager.addTextContainer(textContainer)                let t = NSTextView(frame: .zero, textContainer: textContainer)        t.delegate = self.delegate        t.isEditable = true        t.allowsUndo = true                t.font = .systemFont(ofSize: 24)        t.textColor = NSColor.labelColor        t.drawsBackground = true        t.backgroundColor = NSColor.textBackgroundColor                t.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)        t.minSize = NSSize(width: 0, height: contentSize.height)        t.autoresizingMask = .width        t.isHorizontallyResizable = false        t.isVerticallyResizable   = true                return t    }()        private lazy var sv: NSScrollView = {        let s = NSScrollView()        s.drawsBackground = true        s.borderType = .noBorder        s.hasVerticalScroller = true        s.hasHorizontalRuler = false        s.translatesAutoresizingMaskIntoConstraints = false        s.autoresizingMask = [.width, .height]        return s    }()        var text: String {        didSet {            tv.string = text        }    }        var selectedRanges: [NSValue] = [] {        didSet {            guard selectedRanges.count > 0 else {                return            }            tv.selectedRanges = selectedRanges        }    }    required init?(coder: NSCoder) {        fatalError("Error coder")    }        init(text: String) {        self.text = text        super.init(frame: .zero)    }        override func viewWillDraw() {        super.viewWillDraw()        sv.translatesAutoresizingMaskIntoConstraints = false        addSubview(sv)        NSLayoutConstraint.activate([            sv.topAnchor.constraint(equalTo: topAnchor),            sv.trailingAnchor.constraint(equalTo: trailingAnchor),            sv.bottomAnchor.constraint(equalTo: bottomAnchor),            sv.leadingAnchor.constraint(equalTo: leadingAnchor)        ])        sv.documentView = tv    } // end viewWillDraw}

SwiftUI 中用 NSView,可以通过 NSViewRepresentable 来包装视图,这个协议主要是实现 makeNSView、updateNSView 和 makeCoordinator 三个方法。makeNSView 要求返回需要包装的 NSView。每当 SwiftUI 的状态变化时触发 updateNSView 方法的调用。为了实现 NSView 里的 delegate 和 SwiftUI 通信,就要用 makeCoordinator 返回一个用于处理 delegate 的实例。

TextField

使用方法如下:

struct PlayTextFieldView: View {    @State private var t = "Starming"    @State private var showT = ""    @State private var isEditing = false    var placeholder = "输入些文字..."        @FocusState private var isFocus: Bool        var body: some View {        VStack {            TextField(placeholder, text: $t)                        // 样式设置            TextField(placeholder, text: $t)                .padding(10)                .textFieldStyle(.roundedBorder) // textFieldStyle 有三个预置值 automatic、plain 和 roundedBorder。                .multilineTextAlignment(.leading) // 对齐方式                .font(.system(size: 14, weight: .heavy, design: .rounded))                .border(.teal, width: 4)                .background(.white)                .foregroundColor(.brown)                .textCase(.uppercase)            // 多视图组合            HStack {                Image(systemName: "lock.circle")                    .foregroundColor(.gray).font(.headline)                TextField(placeholder, text: $t)                    .textFieldStyle(.plain)                    .submitLabel(.done)                    .onSubmit {                        showT = t                        isFocus = true                    }                    .onChange(of: t) { newValue in                        t = String(newValue.prefix(20)) // 限制字数                    }                Image(systemName: "eye.slash")                    .foregroundColor(.gray)                    .font(.headline)            }            .padding()            .overlay(                RoundedRectangle(cornerRadius: 8)                    .stroke(.gray, lineWidth: 1)            )            .padding(.horizontal)            Text(showT)            // 自定义 textFieldStyle 样式            TextField(placeholder, text: $t)                .textFieldStyle(PClearTextStyle())                .focused($isFocus)        }        .padding()    } // end body}struct PClearTextStyle: TextFieldStyle {    @ViewBuilder    func _body(configuration: TextField<_Label>) -> some View {        let mirror = Mirror(reflecting: configuration)        let bindingText: Binding<String> = mirror.descendant("_text") as! Binding<String>        configuration            .overlay(alignment: .trailing) {                Button(action: {                    bindingText.wrappedValue = ""                }, label: {                    Image(systemName: "clear")                })            }                let text: String = mirror.descendant("_text", "_value") as! String        configuration            .padding()            .background(                RoundedRectangle(cornerRadius: 16)                    .strokeBorder(text.count > 10 ? .pink : .gray, lineWidth: 4)            )    } // end func}

目前iOS 和 iPadOS上支持的键盘有:

  • asciiCapable:能显示标准 ASCII 字符的键盘
  • asciiCapableNumberPad:只输出 ASCII 数字的数字键盘
  • numberPad:用于输入 PIN 码的数字键盘
  • numbersAndPunctuation:数字和标点符号的键盘
  • decimalPad:带有数字和小数点的键盘
  • phonePad:电话中使用的键盘
  • namePhonePad:用于输入人名或电话号码的小键盘
  • URL:用于输入URL的键盘
  • emailAddress:用于输入电子邮件地址的键盘
  • twitter:用于Twitter文本输入的键盘,支持@和#字符简便输入
  • webSearch:用于网络搜索词和URL输入的键盘

可以通过 keyboardType 修改器来指定。

支持多行,使用 Axis.vertical 以允许多行。TextField 超过行限制可以变成滚动视图。

今年 TextField 可以嵌到 .alert 里了。

Button

struct PlayButtonView: View {    var asyncAction: () async -> Void = {        do {            try await Task.sleep(nanoseconds: 300_000_000)        } catch {}    }    @State private var isFollowed: Bool = false    var body: some View {        VStack {            // 常用方式            Button {                print("Clicked")            } label: {                Image(systemName: "ladybug.fill")                Text("Report Bug")            }            // 图标            Button(systemIconName: "ladybug.fill") {                print("bug")            }            .buttonStyle(.plain) // 无背景            .simultaneousGesture(LongPressGesture().onEnded({ _ in                print("长按") // macOS 暂不支持            }))            .simultaneousGesture(TapGesture().onEnded({ _ in                print("短按") // macOS 暂不支持            }))                                    // iOS 15 修改器的使用。role 在 macOS 上暂不支持            Button("要删除了", role: .destructive) {                print("删除")            }            .tint(.purple)            .controlSize(.large) // .regular 是默认大小            .buttonStyle(.borderedProminent) // borderedProminent 可显示 tint 的设置。还有 bordered、plain 和 borderless 可选。            .clipShape(RoundedRectangle(cornerRadius: 5))            .accentColor(.pink)            .buttonBorderShape(.automatic) // 会依据 controlSize 调整边框样式            .background(.ultraThinMaterial, in: Capsule()) // 添加材质就像在视图和背景间加了个透明层达到模糊的效果。效果由高到底分别是.ultraThinMaterial、.thinMaterial、.regularMaterial、.thickMaterial、.ultraThickMaterial。                        // 风格化            Button(action: {                //            }, label: {                Text("风格化").font(.largeTitle)            })            .buttonStyle(PStarmingButtonStyle())                                    // 自定义 Button            PCustomButton("点一下触发") {                print("Clicked!")            }                        // 自定义 ButtonStyle            Button {                print("Double Clicked!")            } label: {                Text("点两下触发")            }            .buttonStyle(PCustomPrimitiveButtonStyle())            // 将 Text 视图加上另一个 Text 视图中,类型仍还是 Text。            PCustomButton(Text("点我 ").underline() + Text("别犹豫").font(.title) + Text("🤫悄悄说声,有惊喜").font(.footnote).foregroundColor(.secondary)) {                print("多 Text 组合标题按钮点击!")            }                        // 异步按钮            ButtonAsync {                await asyncAction()                isFollowed = true            } label: {                if isFollowed == true {                    Text("已关注")                } else {                    Text("关注")                }            }            .font(.largeTitle)            .disabled(isFollowed)            .buttonStyle(PCustomButtonStyle(backgroundColor: isFollowed == true ? .gray : .pink))        }        .padding()        .background(Color.skeumorphismBG)            }}// MARK: - 异步操作的按钮struct ButtonAsync<Label: View>: View {    var doAsync: () async -> Void    @ViewBuilder var label: () -> Label    @State private var isRunning = false // 避免连续点击造成重复执行事件        var body: some View {        Button {            isRunning = true            Task {                await doAsync()                isRunning = false            }        } label: {            label().opacity(isRunning == true ? 0 : 1)            if isRunning == true {                ProgressView()            }        }        .disabled(isRunning)    }}// MARK: - 扩展 Button// 使用 SFSymbol 做图标extension Button where Label == Image {    init(systemIconName: String, done: @escaping () -> Void) {        self.init(action: done) {            Image(systemName: systemIconName)                .renderingMode(.original)        }    }}// MARK: - 自定义 Buttonstruct PCustomButton: View {    let desTextView: Text    let act: () -> Void        init(_ des: LocalizedStringKey, act: @escaping () -> Void) {        self.desTextView = Text(des)        self.act = act    }        var body: some View {        Button {            act()        } label: {            desTextView.bold()        }        .buttonStyle(.starming)    }}extension PCustomButton {    init(_ desTextView: Text, act: @escaping () -> Void) {        self.desTextView = desTextView        self.act = act    }}// 点语法使用自定义样式extension ButtonStyle where Self == PCustomButtonStyle {    static var starming: PCustomButtonStyle {        PCustomButtonStyle(cornerRadius: 15)    }}// MARK: - ButtonStylestruct PCustomButtonStyle: ButtonStyle {    var cornerRadius:Double = 10    var backgroundColor: Color = .pink    func makeBody(configuration: Configuration) -> some View {        HStack {            Spacer()            configuration.label            Spacer()        }        .padding()        .background(            RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)                .fill(backgroundColor)                .shadow(color: configuration.isPressed ? .white : .black, radius: 1, x: 0, y: 1)        )        .opacity(configuration.isPressed ? 0.5 : 1)        .scaleEffect(configuration.isPressed ? 0.99 : 1)            }}// MARK: - PrimitiveButtonStylestruct PCustomPrimitiveButtonStyle: PrimitiveButtonStyle {    func makeBody(configuration: Configuration) -> some View {        // 双击触发        configuration.label            .onTapGesture(count: 2) {                configuration.trigger()            }        // 手势识别        Button(configuration)            .gesture(                LongPressGesture()                    .onEnded({ _ in                        configuration.trigger()                    })            )    }}// MARK: - 风格化struct PStarmingButtonStyle: ButtonStyle {    var backgroundColor = Color.skeumorphismBG    func makeBody(configuration: Configuration) -> some View {        HStack {            Spacer()            configuration.label            Spacer()        }        .padding(20)        .background(            ZStack {                RoundedRectangle(cornerRadius: 10, style: .continuous)                    .shadow(color: .white, radius: configuration.isPressed ? 7 : 10, x: configuration.isPressed ? -5 : -10, y: configuration.isPressed ? -5 : -10)                    .shadow(color: .black, radius: configuration.isPressed ? 7 : 10, x: configuration.isPressed ? 5 : 10, y: configuration.isPressed ? 5 : 10)                    .blendMode(.overlay)                RoundedRectangle(cornerRadius: 10, style: .continuous)                    .fill(backgroundColor)            }        )        .scaleEffect(configuration.isPressed ? 0.98 : 1)    }}extension Color {    static let skeumorphismBG = Color(hex: "f0f0f3")}extension Color {    init(hex: String) {        var rgbValue: UInt64 = 0        Scanner(string: hex).scanHexInt64(&rgbValue)        let r = (rgbValue & 0xff0000) >> 16        let g = (rgbValue & 0xff00) >> 8        let b = rgbValue & 0xff        self.init(red: Double(r) / 0xff, green: Double(g) / 0xff, blue: Double(b) / 0xff)    }}

.buttonStyle 可组合,示例如下:

struct PButtonStyleComposition: View {    @State private var isT = false    var body: some View {        Section("标签") {            VStack(alignment: .leading) {                HStack {                    Toggle("Swift", isOn: $isT)                    Toggle("SwiftUI", isOn: $isT)                }                HStack {                    Toggle("Swift Chart", isOn: $isT)                    Toggle("Navigation API", isOn: $isT)                }            }            .toggleStyle(.button)            .buttonStyle(.bordered)        }    }}

Tap Location 可以获取点击的位置,示例代码如下:

Rectangle()    .fill(.green)    .frame(width: 50, height: 50)    .onTapGesture(coordinateSpace: .global) { location in        print("Tap in \(location)")    }

其中 coordinateSpace 指定为 .global 表示位置是相对屏幕左上角,默认是相对当前视图的左上角的位置。

进度

用 ProgressViewStyle 协议,可以创建自定义的进度条视图。在 WatchOS 上会多一个 Guage 视图。

struct PlayProgressView: View {    @State private var v: CGFloat = 0.0    var body: some View {        VStack {            // 默认旋转            ProgressView()                        // 有进度条            ProgressView(value: v / 100)                .tint(.yellow)                        ProgressView(value: v / 100) {                Image(systemName: "music.note.tv")            }            .progressViewStyle(CircularProgressViewStyle(tint: .pink))                        // 自定义样式            ProgressView(value: v / 100)                .padding(.vertical)                .progressViewStyle(PCProgressStyle1(borderWidth: 3))                        ProgressView(value: v / 100)                .progressViewStyle(PCProgressStyle2())                .frame(height:200)                        Slider(value: $v, in: 0...100, step: 1)        }        .padding(20)    }}// 自定义 Progress 样式struct PCProgressStyle1: ProgressViewStyle {    var lg = LinearGradient(colors: [.purple, .black, .blue], startPoint: .topLeading, endPoint: .bottomTrailing)    var borderWidth: Double = 2        func makeBody(configuration: Configuration) -> some View {        let fc = configuration.fractionCompleted ?? 0                return VStack {            ZStack(alignment: .topLeading) {                GeometryReader { g in                    Rectangle()                        .fill(lg)                        .frame(maxWidth: g.size.width * CGFloat(fc))                }            }            .frame(height: 20)            .cornerRadius(10)            .overlay(                RoundedRectangle(cornerRadius: 10)                    .stroke(lg, lineWidth: borderWidth)            )            // end ZStack        } // end VStack    }}struct PCProgressStyle2: ProgressViewStyle {    var lg = LinearGradient(colors: [.orange, .yellow, .green, .blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing)        var borderWidth: Double = 20        func makeBody(configuration: Configuration) -> some View {        let fc = configuration.fractionCompleted ?? 0                func strokeStyle(_ g: GeometryProxy) -> StrokeStyle {            StrokeStyle(lineWidth: 0.1 * min(g.size.width, g.size.height), lineCap: .round)        }                return VStack {            GeometryReader { g in                ZStack {                    Group {                        Circle()                            .trim(from: 0, to: 1)                            .stroke(lg, style: strokeStyle(g))                            .padding(borderWidth)                            .opacity(0.2)                        Circle()                            .trim(from: 0, to: fc)                            .stroke(lg, style: strokeStyle(g))                            .padding(borderWidth)                    }                    .rotationEffect(.degrees(90 + 360 * 0.5), anchor: .center)                    .offset(x: 0, y: 0.1 * min(g.size.width, g.size.height))                }                                Text("读取 \(Int(fc * 100)) %")                    .bold()                    .font(.headline)            }            // end ZStack        } // end VStack    }}

SwiftUI 引入一个新显示进度的视图 Gauge。

简单示例如下:

struct PGauge: View {    @State private var progress = 0.45    var body: some View {        Gauge(value: progress) {            Text("进度")        } currentValueLabel: {            Text(progress.formatted(.percent))        } minimumValueLabel: {            Text(0.formatted(.percent))        } maximumValueLabel: {            Text(100.formatted(.percent))        }                Gauge(value: progress) {                    } currentValueLabel: {            Text(progress.formatted(.percent))                .font(.footnote)        }        .gaugeStyle(.accessoryCircularCapacity)        .tint(.cyan)    }}

Image

struct PlayImageView: View {    var body: some View {        Image("logo")            .resizable()            .frame(width: 100, height: 100)                Image("logo")            .resizable()            .aspectRatio(contentMode: .fit)            .frame(width: 50, height: 50)            .clipShape(Circle())            .overlay(                Circle().stroke(.cyan, lineWidth: 4)            )            .shadow(radius: 10)                // SF Symbols        Image(systemName: "scissors")            .imageScale(.large)            .foregroundColor(.pink)            .frame(width: 40, height: 40)                // SF Symbols 多色时使用原色        Image(systemName: "thermometer.sun.fill")            .renderingMode(.original)            .imageScale(.large)    }}

ControlGroup

struct PlayControlGroupView: View {    var body: some View {        ControlGroup {            Button {                print("plus")            } label: {                Image(systemName: "plus")            }            Button {                print("minus")            } label: {                Image(systemName: "minus")            }        }        .padding()        .controlGroupStyle(.automatic) // .automatic 是默认样式,还有 .navigation    }}

GroupBox

struct PlayGroupBoxView: View {    var body: some View {        GroupBox {            Text("这是 GroupBox 的内容")        } label: {            Label("标题一", systemImage: "t.square.fill")        }        .padding()                GroupBox {            Text("还是 GroupBox 的内容")        } label: {            Label("标题二", systemImage: "t.square.fill")        }        .padding()        .groupBoxStyle(PCGroupBoxStyle())    }}struct PCGroupBoxStyle: GroupBoxStyle {    func makeBody(configuration: Configuration) -> some View {        VStack(alignment: .leading) {            configuration.label                .font(.title)            configuration.content        }        .padding()        .background(.pink)        .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))    }}

Stack

Stack View 有 VStack、HStack 和 ZStack

struct PlayStackView: View {    var body: some View {        // 默认是 VStack 竖排                // 横排        HStack {            Text("左")            Spacer()            Text("右")        }        .padding()                // Z 轴排        ZStack(alignment: .top) {            Image("logo")            Text("戴铭的开发小册子")                .font(.title)                .bold()                .foregroundColor(.white)                .shadow(color: .black, radius: 1, x: 0, y: 2)                .padding()        }                Color.cyan            .cornerRadius(10)            .frame(width: 100, height: 100)            .overlay(                Text("一段文字")            )    }}

Advanced layout control

session Compose custom layouts with SwiftUI

提供了新的 Grid 视图来同时满足 VStack 和 HStack。还有一个更低级别 Layout 接口,可以完全控制构建应用所需的布局。另外还有 ViewThatFits 可以自动选择填充可用空间的方式。

Grid 示例代码如下:

Grid {    GridRow {        Text("One")        Text("One")        Text("One")    }    GridRow {        Text("Two")        Text("Two")    }    Divider()    GridRow {        Text("Three")        Text("Three")            .gridCellColumns(2)    }}

gridCellColumns() modifier 可以让一个单元格跨多列。

ViewThatFits 的新视图,允许根据适合的大小放视图。ViewThatFits 会自动选择对于当前屏幕大小合适的子视图进行显示。Ryan Lintott 的示例效果 ,对应示例代码 LayoutThatFits.swift

新的 Layout 协议可以观看 Swift Talk 第 308 期 The Layout Protocol

通过符合 Layout 协议,我们可以自定义一个自定义的布局容器,直接参与 SwiftUI 的布局过程。新的 ProposedViewSize 结构,它是容器视图提供的大小。 Layout.Subviews 是布局视图的子视图代理集合,我们可以在其中为每个子视图请求各种布局属性。

public protocol Layout: Animatable {  static var layoutProperties: LayoutProperties { get }  associatedtype Cache = Void  typealias Subviews = LayoutSubviews  func updateCache(_ cache: inout Self.Cache, subviews: Self.Subviews)  func spacing(subviews: Self.Subviews, cache: inout Self.Cache) -> ViewSpacing  /// We return our view size here, use the passed parameters for computing the  /// layout.  func sizeThatFits(    proposal: ProposedViewSize,     subviews: Self.Subviews,     cache: inout Self.Cache // 👈🏻 use this for calculated data shared among Layout methods  ) -> CGSize    /// Use this to tell your subviews where to appear.  func placeSubviews(    in bounds: CGRect, // 👈🏻 region where we need to place our subviews into, origin might not be .zero    proposal: ProposedViewSize,     subviews: Self.Subviews,     cache: inout Self.Cache  )    // ... there are more a couple more optional methods}

下面例子是一个自定义的水平 stack 视图,为其所有子视图提供其最大子视图的宽度:

struct MyEqualWidthHStack: Layout {  /// Returns a size that the layout container needs to arrange its subviews.  /// - Tag: sizeThatFitsHorizontal  func sizeThatFits(    proposal: ProposedViewSize,    subviews: Subviews,    cache: inout Void  ) -> CGSize {    guard !subviews.isEmpty else { return .zero }    let maxSize = maxSize(subviews: subviews)    let spacing = spacing(subviews: subviews)    let totalSpacing = spacing.reduce(0) { $0 + $1 }    return CGSize(      width: maxSize.width * CGFloat(subviews.count) + totalSpacing,      height: maxSize.height)  }  /// Places the stack's subviews.  /// - Tag: placeSubviewsHorizontal  func placeSubviews(    in bounds: CGRect,    proposal: ProposedViewSize,    subviews: Subviews,    cache: inout Void  ) {    guard !subviews.isEmpty else { return }    let maxSize = maxSize(subviews: subviews)    let spacing = spacing(subviews: subviews)    let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height)    var nextX = bounds.minX + maxSize.width / 2    for index in subviews.indices {      subviews[index].place(        at: CGPoint(x: nextX, y: bounds.midY),        anchor: .center,        proposal: placementProposal)      nextX += maxSize.width + spacing[index]    }  }  /// Finds the largest ideal size of the subviews.  private func maxSize(subviews: Subviews) -> CGSize {    let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }    let maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize in      CGSize(        width: max(currentMax.width, subviewSize.width),        height: max(currentMax.height, subviewSize.height))    }    return maxSize  }  /// Gets an array of preferred spacing sizes between subviews in the  /// horizontal dimension.  private func spacing(subviews: Subviews) -> [CGFloat] {    subviews.indices.map { index in      guard index < subviews.count - 1 else { return 0 }      return subviews[index].spacing.distance(        to: subviews[index + 1].spacing,        along: .horizontal)    }  }}

自定义 layout 只能访问子视图代理 Layout.Subviews ,而不是视图或数据模型。我们可以通过 LayoutValueKey 在每个子视图上存储自定义值,通过 layoutValue(key:value:) modifier 设置。

private struct Rank: LayoutValueKey {  static let defaultValue: Int = 1}extension View {  func rank(_ value: Int) -> some View { // 👈🏻 convenience method    layoutValue(key: Rank.self, value: value) // 👈🏻 the new modifier  }}

然后,我们就可以通过 Layout 方法中的 Layout.Subviews 代理读取自定义 LayoutValueKey 值:

func placeSubviews(  in bounds: CGRect,  proposal: ProposedViewSize,  subviews: Subviews,  cache: inout Void) {  let ranks = subviews.map { subview in    subview[Rank.self] // 👈🏻  }  // ...}

要在布局之间变化使用动画,需要用 AnyLayout,代码示例如下:

struct PAnyLayout: View {    @State private var isVertical = false    var body: some View {        let layout = isVertical ? AnyLayout(VStack()) : AnyLayout(HStack())        layout {            Image(systemName: "star").foregroundColor(.yellow)            Text("Starming.com")            Text("戴铭")        }        Button("Click") {            withAnimation {                isVertical.toggle()            }        } // end button    } // end body}

同时 Text 和图片也支持了样式布局变化,代码示例如下:

struct PTextTransitionsView: View {    @State private var expandMessage = true    private let mintWithShadow: AnyShapeStyle = AnyShapeStyle(Color.mint.shadow(.drop(radius: 2)))    private let primaryWithoutShadow: AnyShapeStyle = AnyShapeStyle(Color.primary.shadow(.drop(radius: 0)))    var body: some View {        Text("Dai Ming Swift Pamphlet")            .font(expandMessage ? .largeTitle.weight(.heavy) : .body)            .foregroundStyle(expandMessage ? mintWithShadow : primaryWithoutShadow)            .onTapGesture { withAnimation { expandMessage.toggle() }}            .frame(maxWidth: expandMessage ? 150 : 250)            .drawingGroup()            .padding(20)            .background(.cyan.opacity(0.3), in: RoundedRectangle(cornerRadius: 6))    }}

Navigation

控制导航启动状态、管理 size class 之间的 transition 和响应 deep link。

Navigation bar 有新的默认行为,如果没有提供标题,导航栏默认为 inline title 显示模式。使用 navigationBarTitleDisplayMode(_:) 改变显示模式。如果 navigation bar 没有标题、工具栏项或搜索内容,它就会自动隐藏。使用 .toolbar(.visible) modifier 显示一个空 navigation bar。

参考:

NavigationStack 的示例:

struct PNavigationStack: View {    @State private var a = [1, 3, 9] // 深层链接    var body: some View {        NavigationStack(path: $a) {            List(1..<10) { i in                NavigationLink(value: i) {                    Label("第 \(i) 行", systemImage: "\(i).circle")                }            }            .navigationDestination(for: Int.self) { i in                Text("第 \(i) 行内容")            }            .navigationTitle("NavigationStack Demo")        }    }}

这里的 path 设置了 stack 的深度路径。

NavigationSplitView 两栏的例子:

struct PNavigationSplitViewTwoColumn: View {    @State private var a = ["one", "two", "three"]    @State private var choice: String?        var body: some View {        NavigationSplitView {            List(a, id: \.self, selection: $choice, rowContent: Text.init)        } detail: {            Text(choice ?? "选一个")        }    }}

NavigationSplitView 三栏的例子:

struct PNavigationSplitViewThreeColumn: View {    struct Group: Identifiable, Hashable {        let id = UUID()        var title: String        var subs: [String]    }        @State private var gps = [        Group(title: "One", subs: ["o1", "o2", "o3"]),        Group(title: "Two", subs: ["t1", "t2", "t3"])    ]        @State private var choiceGroup: Group?    @State private var choiceSub: String?        @State private var cv = NavigationSplitViewVisibility.automatic        var body: some View {        NavigationSplitView(columnVisibility: $cv) {            List(gps, selection: $choiceGroup) { g in                Text(g.title).tag(g)            }            .navigationSplitViewColumnWidth(250)        } content: {            List(choiceGroup?.subs ?? [], id: \.self, selection: $choiceSub) { s in                Text(s)            }        } detail: {            Text(choiceSub ?? "选一个")            Button("点击") {                cv = .all            }        }        .navigationSplitViewStyle(.prominentDetail)    }}

navigationSplitViewColumnWidth() 是用来自定义宽的,navigationSplitViewStyle 设置为 .prominentDetail 是让 detail 的视图尽量保持其大小。

SwiftUI 新加了个功能可以配置是否隐藏 Tabbar,这样在从主页进入下一级时就可以选择不显示底部标签栏了,示例代码如下:

ContentView().toolbar(.hidden, in: .tabBar)

相比较以前 NavigationView 增强的是 destination 可以根据值的不同类型展示不同的目的页面,示例代码如下:

struct PNavigationStackDestination: View {    var body: some View {        NavigationStack {            List {                NavigationLink(value: "字符串") {                    Text("字符串")                }                NavigationLink(value: Color.red) {                    Text("红色")                }            }            .navigationTitle("不同类型 Destination")            .navigationDestination(for: Color.self) { c in                c.clipShape(Circle())            }            .navigationDestination(for: String.self) { s in                Text("\(s) 的 detail")            }        }    }}

对 toolbar 的自定义,示例如下:

.toolbar(id: "toolbar") {    ToolbarItem(id: "new", placement: .secondaryAction) {        Button(action: {}) {            Label("New Invitation", systemImage: "envelope")        }    }}.toolbarRole(.editor)

以下是废弃的 NavigationView 的用法。

对应代码如下:

struct PlayNavigationView: View {    let lData = 1...10    var body: some View {        NavigationView {            ZStack {                LinearGradient(colors: [.pink, .orange], startPoint: .topLeading, endPoint: .bottomTrailing)                    .ignoresSafeArea()                                List(lData, id: \.self) { i in                    NavigationLink {                        PNavDetailView(contentStr: "\(i)")                    } label: {                        Text("\(i)")                    }                }            }                        ZStack {                LinearGradient(colors: [.mint, .yellow], startPoint: .topLeading, endPoint: .bottomTrailing)                    .ignoresSafeArea()                                VStack {                    Text("一个 NavigationView 的示例")                        .bold()                        .font(.largeTitle)                        .shadow(color: .white, radius: 9, x: 0, y: 0)                        .scaleEffect(2)                }            }            .safeAreaInset(edge: .bottom) {                HStack {                    Button("bottom1") {}                    .font(.headline)                    Button("bottom2") {}                    Button("bottom3") {}                    Spacer()                }                .padding(5)                .background(LinearGradient(colors: [.purple, .blue], startPoint: .topLeading, endPoint: .bottomTrailing))            }        }        .foregroundColor(.white)        .navigationTitle("数字列表")        .toolbar {            // placement 共有 keyboard、destructiveAction、cancellationAction、confirmationAction、status、primaryAction、navigation、principal、automatic 这些            ToolbarItem(placement: .primaryAction) {                Button("primaryAction") {}                .background(.ultraThinMaterial)                .font(.headline)            }            // 通过 ToolbarItemGroup 可以简化相同位置 ToolbarItem 的编写。            ToolbarItemGroup(placement: .navigation) {                Button("返回") {}                Button("前进") {}            }            PCToolbar(doDestruct: {                print("删除了")            }, doCancel: {                print("取消了")            }, doConfirm: {                print("确认了")            })            ToolbarItem(placement: .status) {                Button("status") {}            }            ToolbarItem(placement: .principal) {                Button("principal") {                                    }            }            ToolbarItem(placement: .keyboard) {                Button("Touch Bar Button") {}            }        } // end toolbar    }}// MARK: - NavigationView 的目的页面struct PNavDetailView: View {    @Environment(\.presentationMode) var pMode: Binding<PresentationMode>    var contentStr: String    var body: some View {        ZStack {            LinearGradient(colors: [.purple, .blue], startPoint: .topLeading, endPoint: .bottomTrailing)                .ignoresSafeArea()            VStack {                Text(contentStr)                Button("返回") {                    pMode.wrappedValue.dismiss()                }            }        } // end ZStack    } // end body}// MARK: - 自定义 toolbar// 通过 ToolbarContent 创建可重复使用的 toolbar 组struct PCToolbar: ToolbarContent {    let doDestruct: () -> Void    let doCancel: () -> Void    let doConfirm: () -> Void        var body: some ToolbarContent {        ToolbarItem(placement: .destructiveAction) {            Button("删除", action: doDestruct)        }        ToolbarItem(placement: .cancellationAction) {            Button("取消", action: doCancel)        }        ToolbarItem(placement: .confirmationAction) {            Button("确定", action: doConfirm)        }    }}

toolbar 的位置设置可选项如下:

  • primaryAction:放置到最主要位置,macOS 就是放在 toolbar 的最左边
  • automatic:根据平台不同放到默认位置
  • confirmationAction:一些确定的动作
  • cancellationAction:取消动作
  • destructiveAction:删除的动作
  • status:状态变化,比如检查更新等动作
  • navigation:导航动作,比如浏览器的前进后退
  • principal:突出的位置,iOS 和 macOS 会出现在中间的位置
  • keyboard:macOS 会出现在 Touch Bar 里。iOS 会出现在弹出的虚拟键盘上。

List

List 除了能够展示数据外,还有下拉刷新、过滤搜索和侧滑 Swipe 动作提供更多 Cell 操作的能力。

通过 List 的可选子项参数提供数据模型的关键路径来制定子项路劲,还可以实现大纲视图,使用 DisclosureGroup 和 OutlineGroup 可以进一步定制大纲视图。

下面是 List 使用,包括了 DisclosureGroup 和 OutlineGroup 的演示代码:

struct PlayListView: View {    @StateObject var l: PLVM = PLVM()    @State private var s: String = ""        var outlineModel = [        POutlineModel(title: "文件夹一", iconName: "folder.fill", children: [            POutlineModel(title: "个人", iconName: "person.crop.circle.fill"),            POutlineModel(title: "群组", iconName: "person.2.circle.fill"),            POutlineModel(title: "加好友", iconName: "person.badge.plus")        ]),        POutlineModel(title: "文件夹二", iconName: "folder.fill", children: [            POutlineModel(title: "晴天", iconName: "sun.max.fill"),            POutlineModel(title: "夜间", iconName: "moon.fill"),            POutlineModel(title: "雨天", iconName: "cloud.rain.fill", children: [                POutlineModel(title: "雷加雨", iconName: "cloud.bolt.rain.fill"),                POutlineModel(title: "太阳雨", iconName: "cloud.sun.rain.fill")            ])        ]),        POutlineModel(title: "文件夹三", iconName: "folder.fill", children: [            POutlineModel(title: "电话", iconName: "phone"),            POutlineModel(title: "拍照", iconName: "camera.circle.fill"),            POutlineModel(title: "提醒", iconName: "bell")        ])    ]        var body: some View {        HStack {            // List 通过$语法可以将集合的元素转换成可绑定的值            List {                ForEach($l.ls) { $d in                    PRowView(s: d.s, i: d.i)                        .listRowInsets(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15))                        .listRowBackground(Color.black.opacity(0.2))                }            }            .refreshable {                // 下拉刷新            }            .searchable(text: $s) // 搜索            .onChange(of: s) { newValue in                print("搜索关键字:\(s)")            }                        Divider()                        // 自定义 List            VStack {                PCustomListView($l.ls) { $d in                    PRowView(s: d.s, i: d.i)                }                // 添加数据                Button {                    l.ls.append(PLModel(s: "More", i: 0))                } label: {                    Text("添加")                }            }            .padding()                        Divider()                        // 使用大纲            List(outlineModel, children: \.children) { i in                Label(i.title, systemImage: i.iconName)            }                        Divider()                        // 自定义大纲视图            VStack {                Text("可点击标题展开")                    .font(.headline)                PCOutlineListView(d: outlineModel, c: \.children) { i in                    Label(i.title, systemImage: i.iconName)                }            }            .padding()                        Divider()                        // 使用 OutlineGroup 实现大纲视图            VStack {                Text("OutlineGroup 实现大纲")                                OutlineGroup(outlineModel, children: \.children) { i in                    Label(i.title, systemImage: i.iconName)                }                                // OutlineGroup 和 List 结合                Text("OutlineGroup 和 List 结合")                List {                    ForEach(outlineModel) { s in                        Section {                            OutlineGroup(s.children ?? [], children: \.children) { i in                                Label(i.title, systemImage: i.iconName)                            }                        } header: {                            Label(s.title, systemImage: s.iconName)                        }                    } // end ForEach                } // end List            } // end VStack        } // end HStack    } // end body}// MARK: - 自定义大纲视图struct PCOutlineListView<D, Content>: View where D: RandomAccessCollection, D.Element: Identifiable, Content: View {    private let v: PCOutlineView<D, Content>        init(d: D, c: KeyPath<D.Element, D?>, content: @escaping (D.Element) -> Content) {        self.v = PCOutlineView(d: d, c: c, content: content)    }        var body: some View {        List {            v        }    }}struct PCOutlineView<D, Content>: View where D: RandomAccessCollection, D.Element: Identifiable, Content: View {    let d: D    let c: KeyPath<D.Element, D?>    let content: (D.Element) -> Content    @State var isExpanded = true // 控制初始是否展开的状态        var body: some View {        ForEach(d) { i in            if let sub = i[keyPath: c] {                PCDisclosureGroup(content: PCOutlineView(d: sub, c: c, content: content), label: content(i))            } else {                content(i)            } // end if        } // end ForEach    } // end body}struct PCDisclosureGroup<C, L>: View where C: View, L: View {    @State var isExpanded = false    var content: C    var label: L    var body: some View {        DisclosureGroup(isExpanded: $isExpanded) {            content        } label: {            Button {                isExpanded.toggle()            } label: {                label            }            .buttonStyle(.plain)        }    }}// MARK: - 大纲模式数据模型struct POutlineModel: Hashable, Identifiable {    var id = UUID()    var title: String    var iconName: String    var children: [POutlineModel]?}// MARK: - List 的抽象,数据兼容任何集合类型struct PCustomListView<D: RandomAccessCollection & MutableCollection & RangeReplaceableCollection, Content: View>: View where D.Element: Identifiable {    @Binding var data: D    var content: (Binding<D.Element>) -> Content        init(_ data: Binding<D>, content: @escaping (Binding<D.Element>) -> Content) {        self._data = data        self.content = content    }        var body: some View {        List {            Section {                ForEach($data, content: content)                    .onMove { indexSet, offset in                        data.move(fromOffsets: indexSet, toOffset: offset)                    }                    .onDelete { indexSet in                        data.remove(atOffsets: indexSet) // macOS 暂不支持                    }            } header: {                Text("第一栏,共 \(data.count) 项")            } footer: {                Text("The End")            }        }        .listStyle(.plain) // 有.automatic、.inset、.plain、sidebar,macOS 暂不支持的有.grouped 和 .insetGrouped    }}// MARK: - Cell 视图struct PRowView: View {    var s: String    var i: Int    var body: some View {        HStack {            Text("\(i):")            Text(s)        }    }}// MARK: - 数据模型设计struct PLModel: Hashable, Identifiable {    let id = UUID()    var s: String    var i: Int}final class PLVM: ObservableObject {    @Published var ls: [PLModel]    init() {        ls = [PLModel]()        for i in 0...20 {            ls.append(PLModel(s: "\(i)", i: i))        }    }}

list 支持 Section footer。

list 分隔符可以自定义,使用 HorizontalEdge.leadingHorizontalEdge.trailing

list 不使用 UITableView 了。

今年 list 还新增了一个 EditOperation 可以自动生成移动和删除,新增了 edits 参数,传入 [.delete, .move] 数组即可。这也是一个演示如何更好扩展和配置功能的方式。

.searchable 支持 token 和 scope,示例如下:

struct PSearchTokensAndScopes: View {    enum AttendanceScope {        case inPerson, online    }    @State private var queryText: String    @State private var queryTokens: [InvitationToken]    @State private var scope: AttendanceScope        var body: some View {        invitationCountView()            .searchable(text: $queryText, tokens: $queryTokens, scope: $scope) { token in                Label(token.diplayName, systemImage: token.systemImage)            } scopes: {                Text("In Person").tag(AttendanceScope.inPerson)                Text("Online").tag(AttendanceScope.online)            }    }}

LazyVStack 和 LazyHStack

LazyVStack 和 LazyHStack 里的视图只有在滚到时才会被创建。

struct PlayLazyVStackAndLazyHStackView: View {    var body: some View {        ScrollView {            LazyVStack {                ForEach(1...300, id: \.self) { i in                    PLHSRowView(i: i)                }            }        }    }}struct PLHSRowView: View {    let i: Int    var body: some View {        Text("第 \(i) 个")    }    init(i: Int) {        print("第 \(i) 个初始化了") // 用来查看什么时候创建的。        self.i = i    }}

LazyVGrid 和 LazyHGrid

列的设置有三种,这三种也可以组合用。

  • GridItem(.fixed(10)) 会固定设置有多少列。
  • GridItem(.flexible()) 会充满没有使用的空间。
  • GridItem(.adaptive(minimum: 10)) 表示会根据设置大小自动设置有多少列展示。

示例:

struct PlayLazyVGridAndLazyHGridView: View {    @State private var colors: [String:Color] = [        "red" : .red,        "orange" : .orange,        "yellow" : .yellow,        "green" : .green,        "mint" : .mint,        "teal" : .teal,        "cyan" : .cyan,        "blue" : .blue,        "indigo" : .indigo,        "purple" : .purple,        "pink" : .pink,        "brown" : .brown,        "gray" : .gray,        "black" : .black    ]        var body: some View {        ScrollView {            LazyVGrid(columns: [                GridItem(.adaptive(minimum: 50), spacing: 10)            ], pinnedViews: [.sectionHeaders]) {                Section(header:                            Text("🎨调色板")                            .font(.title)                            .frame(maxWidth: .infinity, maxHeight: .infinity)                                .background(RoundedRectangle(cornerRadius: 0)                                                .fill(.black.opacity(0.1)))                ) {                    ForEach(Array(colors.keys), id: \.self) { k in                        colors[k].frame(height:Double(Int.random(in: 50...150)))                            .overlay(                                Text(k)                            )                            .shadow(color: .black, radius: 2, x: 0, y: 2)                    }                }            }            .padding()                        LazyVGrid(columns: [                GridItem(.adaptive(minimum: 20), spacing: 10)            ]) {                Section(header: Text("图标集").font(.title)) {                    ForEach(1...30, id: \.self) { i in                        Image("p\(i)")                            .resizable()                            .aspectRatio(contentMode: .fit)                            .shadow(color: .black, radius: 2, x: 0, y: 2)                    }                }            }            .padding()        }    }}

table

今年 iOS 和 iPadOS 也可以使用去年只能在 macOS 上使用的 Table了,据 digital lounges 里说,iOS table 的性能和 list 差不多,table 默认为 plian list。我想 iOS 上加上 table 只是为了兼容 macOS 代码吧。

table 使用示例如下:

Table(attendeeStore.attendees) {    TableColumn("Name") { attendee in        AttendeeRow(attendee)    }    TableColumn("City", value: \.city)    TableColumn("Status") { attendee in        StatusRow(attendee)    }}.contextMenu(forSelectionType: Attendee.ID.self) { selection in    if selection.isEmpty {        Button("New Invitation") { addInvitation() }    } else if selection.count == 1 {        Button("Mark as VIP") { markVIPs(selection) }    } else {        Button("Mark as VIPs") { markVIPs(selection) }    }}

ScrollView

ScrollView 使用 scrollTo 可以直接滚动到指定的位置。ScrollView 还可以透出偏移量,利用偏移量可以定义自己的动态视图,比如向下向上滚动视图时有不同效果,到顶部显示标题视图等。

示例代码如下:

struct PlayScrollView: View {    @State private var scrollOffset: CGFloat = .zero        var infoView: some View {        GeometryReader { g in            Text("移动了 \(Double(scrollOffset).formatted(.number.precision(.fractionLength(1)).rounded()))")                .padding()        }    }        var body: some View {        // 标准用法        ScrollViewReader { s in            ScrollView {                ForEach(0..<300) { i in                    Text("\(i)")                        .id(i)                }            }            Button("跳到150") {                withAnimation {                    s.scrollTo(150, anchor: .top)                }            } // end Button        } // end ScrollViewReader                // 自定义的 ScrollView 透出 offset 供使用        ZStack {            PCScrollView {                ForEach(0..<100) { i in                    Text("\(i)")                }            } whenMoved: { d in                scrollOffset = d            }            infoView                    } // end ZStack    } // end body}// MARK: - 自定义 ScrollViewstruct PCScrollView<C: View>: View {    let c: () -> C    let whenMoved: (CGFloat) -> Void        init(@ViewBuilder c: @escaping () -> C, whenMoved: @escaping (CGFloat) -> Void) {        self.c = c        self.whenMoved = whenMoved    }        var offsetReader: some View {        GeometryReader { g in            Color.clear                .preference(key: OffsetPreferenceKey.self, value: g.frame(in: .named("frameLayer")).minY)        }        .frame(height:0)    }        var body: some View {        ScrollView {            offsetReader            c()                .padding(.top, -8)        }        .coordinateSpace(name: "frameLayer")        .onPreferenceChange(OffsetPreferenceKey.self, perform: whenMoved)    } // end body}private struct OffsetPreferenceKey: PreferenceKey {  static var defaultValue: CGFloat = .zero  static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}}

新增 modifier

ScrollView {    ForEach(0..<300) { i in        Text("\(i)")            .id(i)    }}.scrollDisabled(false).scrollDismissesKeyboard(.interactively).scrollIndicators(.visible)

浮层

浮层有 HUD、ContextMenu、Sheet、Alert、ConfirmationDialog、Popover、ActionSheet 等几种方式。这些方式实现代码如下:

struct PlaySuperposedLayerView: View {    @StateObject var hudVM = PHUDVM()    @State private var isShow = false    @State private var isShowAlert = false    @State private var isShowConfirmationDialog = false    @State private var isShowPopover = false        var body: some View {        VStack {                                    List {                ForEach(0..<100) { i in                    Text("\(i)")                        .contextMenu {                            // 在 macOS 上右键会出现的菜单                            Button {                                print("\(i) is clicked")                            } label: {                                Text("Click \(i)")                            }                        }                }            }            .navigationTitle("列表")            .toolbar {                ToolbarItemGroup(placement: .automatic) {                    Button("查看 Sheet") {                        isShow = true                    }                                        Button("查看 Alert") {                        isShowAlert = true                    }                                        Button("查看 confirmationDialog", role: .destructive) {                        isShowConfirmationDialog = true                    }                                        // Popover 样式默认是弹出窗口置于按钮上方,指向底部。                    Button("查看 Popover") {                        isShowPopover = true                    }                    .popover(isPresented: $isShowPopover, attachmentAnchor: .point(.trailing), arrowEdge: .trailing) {                        Text("Popover 的内容")                            .padding()                    }                                    } // end ToolbarItemGroup            } // end toolbar            .alert(isPresented: $isShowAlert) {                Alert(title: Text("弹框标题"), message: Text("弹框内容"))            }            .sheet(isPresented: $isShow) {                print("dismiss")            } content: {                VStack {                    Label("Sheet", systemImage: "brain.head.profile")                    Button("关闭") {                        isShow = false                    }                }                .padding(20)            }            .confirmationDialog("确定删除?", isPresented: $isShowConfirmationDialog, titleVisibility: .hidden) {                Button("确定") {                    // do good thing                }                .keyboardShortcut(.defaultAction) // 使用 keyboardShortcut 可以设置成为默认选项样式                                Button("不不", role: .cancel) {                    // good choice                }                            } message: {                Text("这个东西还有点重要哦")            }                        Button {                hudVM.show(title: "您有一条新的短消息", systemImage: "ellipsis.bubble")            } label: {                Label("查看 HUD", systemImage: "switch.2")            }            .padding()        }        .environmentObject(hudVM)        .hud(isShow: $hudVM.isShow) {            Label(hudVM.title, systemImage: hudVM.systemImage)        }    }}// MARK: - 供全局使用的 HUDfinal class PHUDVM: ObservableObject {    @Published var isShow: Bool = false    var title: String = ""    var systemImage: String = ""        func show(title: String, systemImage: String) {        self.title = title        self.systemImage = systemImage        withAnimation {            isShow = true        }    }}// MARK: - 扩展 View 使其能够有 HUD 的能力extension View {    func hud<V: View>(        isShow: Binding<Bool>,        @ViewBuilder v: () -> V    ) -> some View {        ZStack(alignment: .top) {            self                        if isShow.wrappedValue == true {                PHUD(v: v)                    .transition(AnyTransition.move(edge: .top).combined(with: .opacity))                    .onAppear {                        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {                            withAnimation {                                isShow.wrappedValue = false                            }                        }                    }                    .zIndex(1)                    .padding()            }        }    }}// MARK: - 自定义 HUDstruct PHUD<V: View>: View {    @ViewBuilder let v: V        var body: some View {        v            .padding()            .foregroundColor(.black)            .background(                Capsule()                    .foregroundColor(.white)                    .shadow(color: .black.opacity(0.2), radius: 12, x: 0, y: 5)            )    }}

SwiftUI 新推出的 presentationDetents() modifier 可以创建一个可以定制的 bottom sheet。示例代码如下:

struct PSheet: View {    @State private var isShow = false    var body: some View {        Button("显示 Sheet") {            isShow.toggle()        }        .sheet(isPresented: $isShow) {            Text("这里是 Sheet 的内容")                .presentationDetents([.medium, .large])        }    }}

detent 默认值是 .large。也可以提供一个百分比,比如 .presentationDetents([.fraction(0.7)]),或者直接指定高度 .presentationDetents([.height(100)])

presentationDragIndicator modifier 可以用来显示隐藏拖动标识。

TabView

struct PlayTabView: View {    @State private var selection = 0        var body: some View {        ZStack(alignment: .bottom) {            TabView(selection: $selection) {                Text("one")                    .tabItem {                        Text("首页")                            .hidden()                    }                    .tag(0)                Text("two")                    .tabItem {                        Text("二栏")                    }                    .tag(1)                Text("three")                    .tabItem {                        Text("三栏")                    }                    .tag(2)                Text("four")                    .tag(3)                Text("five")                    .tag(4)                Text("six")                    .tag(5)                Text("seven")                    .tag(6)                Text("eight")                    .tag(7)                Text("nine")                    .tag(8)                Text("ten")                    .tag(9)            } // end TabView                                    HStack {                Button("上一页") {                    if selection > 0 {                        selection -= 1                    }                }                .keyboardShortcut(.cancelAction)                Button("下一页") {                    if selection < 9 {                        selection += 1                    }                }                .keyboardShortcut(.defaultAction)            } // end HStack            .padding()        }    }}

.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) 可以实现 UIPageViewController 的效果,如果要给小白点加上背景,可以多添加一个 .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) 修改器。

Swift Charts

可视化数据,使用 SwiftUI 语法来创建。还可以使用 ChartRenderer 接口将图标渲染成图。

官方文档 Swift Charts

入门参看 Hello Swift Charts

Apple 文章 Creating a chart using Swift Charts

高级定制和创建更精细图表,可以看这个 session Swift Charts: Raise the bar 这个 session 也会提到如何在图表中进行交互。这里是 session 对应的代码示例 Visualizing your app’s data

图表设计的 session,Design an effective chartDesign app experiences with charts

下面是一个简单的代码示例:

import Chartsstruct PChartModel: Hashable {    var day: String    var amount: Int = .random(in: 1..<100)}extension PChartModel {    static var data: [PChartModel] {        let calendar = Calendar(identifier: .gregorian)        let days = calendar.shortWeekdaySymbols        return days.map { day in            PChartModel(day: day)        }    }}struct PlayCharts: View {    var body: some View {        Chart(PChartModel.data, id: \.self) { v in            BarMark(x: .value("天", v.day), y: .value("数量", v.amount))                    }        .padding()    }}struct PSwiftCharts: View {    struct CData: Identifiable {        let id = UUID()        let i: Int        let v: Double    }        @State private var a: [CData] = [        .init(i: 0, v: 2),        .init(i: 1, v: 20),        .init(i: 2, v: 3),        .init(i: 3, v: 30),        .init(i: 4, v: 8),        .init(i: 5, v: 80)    ]        var body: some View {        Chart(a) { i in            LineMark(x: .value("Index", i.i), y: .value("Value", i.v))            BarMark(x: .value("Index", i.i), yStart: .value("开始", 0), yEnd: .value("结束", i.v))                .foregroundStyle(by: .value("Value", i.v))        } // end Chart    } // end body}

BarMark 用于创建条形图,LineMark 用于创建折线图。SwiftUI Charts 框架还提供 PointMark、AxisMarks、AreaMark、RectangularMark 和 RuleMark 用于创建不同类型的图表。注释使用 .annotation modifier,修改颜色可以使用 .foregroundStyle modifier。.lineStyle modifier 可以修改线宽。

AxisMarks 的示例如下:

struct MonthlySalesChart: View {    var body: some View {        Chart(data, id: \.month) {            BarMark(                x: .value("Month", $0.month, unit: .month),                y: .value("Sales", $0.sales)            )        }        .chartXAxis {            AxisMarks(values: .stride(by: .month)) { value in                if value.as(Date.self)!.isFirstMonthOfQuarter {                    AxisGridLine().foregroundStyle(.black)                    AxisTick().foregroundStyle(.black)                    AxisValueLabel(                        format: .dateTime.month(.narrow)                    )                } else {                    AxisGridLine()                }            }        }    }}

可交互图表示例如下:

struct InteractiveBrushingChart: View {    @State var range: (Date, Date)? = nil        var body: some View {        Chart {            ForEach(data, id: \.day) {                LineMark(                    x: .value("Month", $0.day, unit: .day),                    y: .value("Sales", $0.sales)                )                .interpolationMethod(.catmullRom)                .symbol(Circle().strokeBorder(lineWidth: 2))            }            if let (start, end) = range {                RectangleMark(                    xStart: .value("Selection Start", start),                    xEnd: .value("Selection End", end)                )                .foregroundStyle(.gray.opacity(0.2))            }        }        .chartOverlay { proxy in            GeometryReader { nthGeoItem in                Rectangle().fill(.clear).contentShape(Rectangle())                    .gesture(DragGesture()                        .onChanged { value in                            // Find the x-coordinates in the chart’s plot area.                            let xStart = value.startLocation.x - nthGeoItem[proxy.plotAreaFrame].origin.x                            let xCurrent = value.location.x - nthGeoItem[proxy.plotAreaFrame].origin.x                            // Find the date values at the x-coordinates.                            if let dateStart: Date = proxy.value(atX: xStart),                               let dateCurrent: Date = proxy.value(atX: xCurrent) {                                range = (dateStart, dateCurrent)                            }                        }                        .onEnded { _ in range = nil } // Clear the state on gesture end.                    )            }        }    }}

社区做的更多 Swift Charts 范例 Swift Charts Examples

Toggle

Toggle 可以设置 toggleStyle,可以自定义样式。使用示例如下

struct PlayToggleView: View {    @State private var isEnable = false    var body: some View {        // 普通样式        Toggle(isOn: $isEnable) {            Text("\(isEnable ? "开了" : "关了")")        }        .padding()                // 按钮样式        Toggle(isOn: $isEnable) {            Label("\(isEnable ? "打开了" : "关闭了")", systemImage: "cloud.moon")        }        .padding()        .tint(.pink)        .controlSize(.large)        .toggleStyle(.button)                // Switch 样式        Toggle(isOn: $isEnable) {            Text("\(isEnable ? "开了" : "关了")")        }        .toggleStyle(SwitchToggleStyle(tint: .orange))        .padding()                // 自定义样式        Toggle(isOn: $isEnable) {            Text(isEnable ? "录音中" : "已静音")        }        .toggleStyle(PCToggleStyle())            }}// MARK: - 自定义样式struct PCToggleStyle: ToggleStyle {    func makeBody(configuration: Configuration) -> some View {        return HStack {            configuration.label            Image(systemName: configuration.isOn ? "mic.square.fill" : "mic.slash.circle.fill")                .renderingMode(.original)                .resizable()                .frame(width: 30, height: 30)                .onTapGesture {                    configuration.isOn.toggle()                }        }    }}

Picker

有 Picker 视图,还有颜色和时间选择的 ColorPicker 和 DatePicker。

示例代码如下:

struct PlayPickerView: View {    @State private var select = 1    @State private var color = Color.red.opacity(0.3)        var dateFt: DateFormatter {        let ft = DateFormatter()        ft.dateStyle = .long        return ft    }    @State private var date = Date()        var body: some View {                // 默认是下拉的风格        Form {            Section("选区") {                Picker("选一个", selection: $select) {                    Text("1")                        .tag(1)                    Text("2")                        .tag(2)                }            }        }        .padding()                // Segment 风格,        Picker("选一个", selection: $select) {            Text("one")                .tag(1)            Text("two")                .tag(2)        }        .pickerStyle(SegmentedPickerStyle())        .padding()                // 颜色选择器        ColorPicker("选一个颜色", selection: $color, supportsOpacity: false)            .padding()                RoundedRectangle(cornerRadius: 8)            .fill(color)            .frame(width: 50, height: 50)                // 时间选择器        VStack {            DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {                Text("选时间")            }                        DatePicker("选时间", selection: $date)                .datePickerStyle(GraphicalDatePickerStyle())                .frame(maxHeight: 400)                        Text("时间:\(date, formatter: dateFt)")        }        .padding()    }}

选择多个日期

MultiDatePicker 视图会显示一个日历,用户可以选择多个日期,可以设置选择范围。示例如下:

struct PMultiDatePicker: View {    @Environment(\.calendar) var cal    @State var dates: Set<DateComponents> = []    var body: some View {        MultiDatePicker("选择个日子", selection: $dates, in: Date.now...)        Text(s)    }    var s: String {        dates.compactMap { c in            cal.date(from:c)?.formatted(date: .long, time: .omitted)        }        .formatted()    }}

PhotosPick

支持图片选择,示例代码如下:

import PhotosUIimport CoreTransferablestruct ContentView: View {    @ObservedObject var viewModel: FilterModel = .shared        var body: some View {        NavigationStack {            Gallery()                .navigationTitle("Birthday Filter")                .toolbar {                    PhotosPicker(                        selection: $viewModel.imageSelection,                        matching: .images                    ) {                        Label("Pick a photo", systemImage: "plus.app")                    }                    Button {                        viewModel.applyFilter()                    } label: {                        Label("Apply Filter", systemImage: "camera.filters")                    }                }        }    }}

Slider

struct PlaySliderView: View {    @State var count: Double = 0    var body: some View {        Slider(value: $count, in: 0...100)            .padding()        Text("\(Int(count))")    }}

Stepper

struct PlayStepperView: View {    @State private var count: Int = 0    var body: some View {        Stepper(value: $count, step: 2) {            Text("共\(count)")        } onEditingChanged: { b in            print(b)        } // end Stepper    }}

Form

Form 今年也得到了增强,示例如下:

Form {    Section {        LabeledContent("Location") {            AddressView(location)        }        DatePicker("Date", selection: $date)        TextField("Description", text: $eventDescription, axis: .vertical)            .lineLimit(3, reservesSpace: true)    }        Section("Vibe") {        Picker("Accent color", selection: $accent) {            ForEach(Theme.allCases) { accent in                Text(accent.rawValue.capitalized).tag(accent)            }        }        Picker("Color scheme", selection: $scheme) {            Text("Light").tag(ColorScheme.light)            Text("Dark").tag(ColorScheme.dark)        }#if os(macOS)        .pickerStyle(.inline)#endif        Toggle(isOn: $extraGuests) {            Text("Allow extra guests")            Text("The more the merrier!")        }        if extraGuests {            Stepper("Guests limit", value: $spacesCount, format: .number)        }    }        Section("Decorations") {        Section {            List(selection: $selectedDecorations) {                DisclosureGroup {                    HStack {                        Toggle("Balloons 🎈", isOn: $includeBalloons)                        Spacer()                        decorationThemes[.balloon].map { $0.swatch }                    }                    .tag(Decoration.balloon)                                        HStack {                        Toggle("Confetti 🎊", isOn: $includeConfetti)                        Spacer()                        decorationThemes[.confetti].map { $0.swatch }                    }                    .tag(Decoration.confetti)                                        HStack {                        Toggle("Inflatables 🪅", isOn: $includeInflatables)                        Spacer()                        decorationThemes[.inflatables].map { $0.swatch }                    }                    .tag(Decoration.inflatables)                                        HStack {                        Toggle("Party Horns 🥳", isOn: $includeBlowers)                        Spacer()                        decorationThemes[.noisemakers].map { $0.swatch }                    }                    .tag(Decoration.noisemakers)                } label: {                    Toggle("All Decorations", isOn: [                        $includeBalloons, $includeConfetti,                        $includeInflatables, $includeBlowers                    ])                    .tag(Decoration.all)                }#if os(macOS)                .toggleStyle(.checkbox)#endif            }                        Picker("Decoration theme", selection: themes) {                Text("Blue").tag(Theme.blue)                Text("Black").tag(Theme.black)                Text("Gold").tag(Theme.gold)                Text("White").tag(Theme.white)            }#if os(macOS)            .pickerStyle(.radioGroup)#endif        }    }    }.formStyle(.grouped)

Keyboard

键盘快捷键的使用方法如下:

struct PlayKeyboard: View {    var body: some View {        Button(systemIconName: "camera.shutter.button") {            print("按了回车键")        }        .keyboardShortcut(.defaultAction) // 回车                Button("ESC", action: {            print("按了 ESC")        })        .keyboardShortcut(.cancelAction) // ESC 键                Button("CMD + p") {            print("按了 CMD + p")        }        .keyboardShortcut("p")                Button("SHIFT + p") {            print("按了 SHIFT + p")        }        .keyboardShortcut("p", modifiers: [.shift])    }}

Transferable

Transferable 协议使数据可以用于剪切板、拖放和 Share Sheet。

可以在自己应用程序之间或你的应用和其他应用之间发送或接受可传输项目。

支持 SwiftUI 来使用。

官方文档 Core Transferable

session Meet Transferable

新增一个专门用来接受 Transferable 的按钮视图 PasteButton,使用示例如下:

struct PPasteButton: View {    @State private var s = "戴铭"    var body: some View {        TextField("输入", text: $s)            .textFieldStyle(.roundedBorder)        PasteButton(payloadType: String.self) { str in            guard let first = str.first else { return }            s = first        }    }}

ShareLink

ShareLink 视图可以让你轻松共享数据。示例代码如下:

struct PShareLink: View {    let url = URL(string: "https://ming1016.github.io/")!    var body: some View {        ShareLink(item: url, message: Text("戴铭的博客"))        ShareLink("戴铭的博客", item: url)        ShareLink(item: url) {            Label("戴铭的博客", systemImage: "swift")        }    }}

视觉

Color

struct PlayColor: View {    var body: some View {        ZStack {            Color.black.edgesIgnoringSafeArea(.all) // Color 也是一个 View                        VStack(spacing: 10) {                Text("这是一个适配了暗黑的文字颜色")                    .foregroundColor(light: .purple, dark: .pink)                    .background(Color(nsColor: .quaternaryLabelColor)) // 使用以前 NSColor                                Text("自定义颜色")                    .foregroundColor(Color(red: 0, green: 0, blue: 100))            }            .padding()                    }    }}// MARK: - 暗黑适配颜色struct PCColorModifier: ViewModifier {    @Environment(\.colorScheme) private var colorScheme    var light: Color    var dark: Color        private var adaptColor: Color {        switch colorScheme {        case .light:            return light        case .dark:            return dark        @unknown default:            return light        }    }        func body(content: Content) -> some View {        content.foregroundColor(adaptColor)    }}extension View {    func foregroundColor(light: Color, dark: Color) -> some View {        modifier(PCColorModifier(light: light, dark: dark))    }}

Effect

struct PlayEffect: View {    @State private var isHover = false        var body: some View {        ZStack {            LinearGradient(colors: [.purple, .black, .pink], startPoint: .top, endPoint: .bottom).ignoresSafeArea()                        VStack(spacing: 20) {                                // 材质                Text("材质效果")                    .font(.system(size:30))                    .padding(isHover ? 40 : 30)                    .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8, style: .continuous))                    .onHover { b in                        withAnimation {                            isHover = b                        }                    }                                // 模糊                Text("模糊效果")                    .font(.system(size: 30))                    .padding(30)                    .background {                        Color.black.blur(radius: 8, opaque: false)                    }                                // 选择                Text("3D 旋转")                    .font(.largeTitle)                    .rotation3DEffect(Angle(degrees: 45), axis: (x: 0, y: 20, z: 0))                    .scaleEffect(1.5)                    .blendMode(.hardLight)                    .blur(radius: 3)                            }                        }    }}

材质厚度从低到高有:

  • .regularMaterial
  • .thinMaterial
  • .ultraThinMaterial
  • .thickMaterial
  • .ultraThickMaterial

Gradient 和 Shadow 的 2022 的更新

下面是个简单示例:

struct PGradientAndShadow: View {    var body: some View {        Image(systemName: "bird")            .frame(width: 150, height: 150)            .background(in: Rectangle())            .backgroundStyle(.cyan.gradient)            .foregroundStyle(.white.shadow(.drop(radius: 1, y: 3.0)))            .font(.system(size: 60))    }}

Paul Hudson 使用 Core Motion 做了一个阴影随设备倾斜而变化的效果,非常棒,How to use inner shadows to simulate depth with SwiftUI and Core Motion

Animation

SwiftUI 里实现动画的方式包括有 .animation 隐式动画、withAnimation 和 withTransaction 显示动画、matchedGeometryEffect Hero 动画和 TimelineView 等。

示例代码如下:

struct PlayAnimation: View {    @State private var isChange = false    private var anis:[String: Animation] = [        "p1": .default,        "p2": .linear(duration: 1),        "p3": .interpolatingSpring(stiffness: 5, damping: 3),        "p4": .easeInOut(duration: 1),        "p5": .easeIn(duration: 1),        "p6": .easeOut(duration: 1),        "p7": .interactiveSpring(response: 3, dampingFraction: 2, blendDuration: 1),        "p8": .spring(),        "p9": .default.repeatCount(3)    ]    @State private var selection = 1        var body: some View {        // animation 隐式动画和 withAnimation 显示动画        Text(isChange ? "另一种状态" : "一种状态")            .font(.headline)            .padding()            .animation(.easeInOut, value: isChange) // 受限的隐式动画,只绑定某个值。            .onTapGesture {                // 使用 withAnimation 就是显式动画,效果等同 withTransaction(Transaction(animation: .default))                withAnimation {                    isChange.toggle()                }                // 设置 Transaction。和隐式动画共存时,优先执行 withAnimation 或 Transaction。                var t = Transaction(animation: .linear(duration: 2))                t.disablesAnimations = true // 用来禁用隐式动画                withTransaction(t) {                    isChange.toggle()                }            } // end onHover                LazyVGrid(columns: [GridItem(.adaptive(minimum: isChange ? 60 : 30), spacing: 60)]) {            ForEach(Array(anis.keys), id: \.self) { s in                Image(s)                    .resizable()                    .scaledToFit()                    .animation(anis[s], value: isChange)                    .scaleEffect()            }        }        .padding()        Button {            isChange.toggle()        } label: {            Image(systemName: isChange ? "pause.fill" : "play.fill")                .renderingMode(.original)        }                // matchedGeometryEffect 的使用        VStack {            Text("后台")                .font(.headline)            placeStayView            Text("前台")                .font(.headline)            placeShowView        }        .padding(50)                // 通过使用相同 matchedGeometryEffect 的 id,绑定两个元素变化。        HStack {            if isChange {                Rectangle()                    .fill(.pink)                    .matchedGeometryEffect(id: "g1", in: mgeStore)                    .frame(width: 100, height: 100)            }            Spacer()            Button("转换") {                withAnimation(.linear(duration: 2.0)) {                    isChange.toggle()                }            }            Spacer()            if !isChange {                Circle()                    .fill(.orange)                    .matchedGeometryEffect(id: "g1", in: mgeStore)                    .frame(width: 70, height: 70)            }            HStack {                Image("p1")                    .resizable()                    .scaledToFit()                    .frame(width: 50, height: 50)                if !isChange {                    Image("p19")                        .resizable()                        .scaledToFit()                        .frame(width: 50, height: 50)                        .matchedGeometryEffect(id: "g1", in: mgeStore)                }                Image("p1")                    .resizable()                    .scaledToFit()                    .frame(width: 50, height: 50)            }        }        .padding()                // 使用 isSource,作为移动到相同 matchedGeometryEffect id 的方法。        HStack {            Image("p19")                .resizable()                .scaledToFit()                .frame(width: isChange ? 100 : 50, height: isChange ? 100 : 50)                .matchedGeometryEffect(id: isChange ? "g2" : "", in: mgeStore, isSource: false)                        Image("p19")                .resizable()                .scaledToFit()                .frame(width: 100, height: 100)                .matchedGeometryEffect(id: "g2", in: mgeStore)                .opacity(0)        }                                // 点击跟随的效果        HStack {            ForEach(Array(1...4), id: \.self) { i in                Image("p\(i)")                    .resizable()                    .scaledToFit()                    .frame(width: i == selection ? 200 : 50)                    .matchedGeometryEffect(id: "h\(i)", in: mgeStore)                    .onTapGesture {                        withAnimation {                            selection = i                        }                    }                    .shadow(color: .black, radius: 3, x: 2, y: 3)            }        }        .background(            RoundedRectangle(cornerRadius: 8).fill(.pink)                .matchedGeometryEffect(id: "h\(selection)", in: mgeStore, isSource: false)        )                // matchedGeometryEffect 还可以应用到 List 中,通过 Array enumerated 获得 index 作为 matchedGeometryEffect 的 id。右侧固定按钮可以直接让对应 id 的视图滚动到固定按钮的位置                        // TimelineView        TimelineView(.periodic(from: .now, by: 1)) { t in            Text("\(t.date)")            HStack(spacing: 20) {                let e = "p\(Int.random(in: 1...30))"                Image(e)                    .resizable()                    .scaledToFit()                    .frame(height: 40)                    .animation(.default.repeatCount(3), value: e)                                TimelineSubView(date: t.date) // 需要传入 timeline 的时间给子视图才能够起作用。                                }            .padding()        }                // matchedGeometryEffect        /// TimelineScheduler 的使用,TimelineScheduler 有以下类型        /// .animation:制定更新的频率,可以控制暂停        /// .everyMinute:每分钟更新一次        /// .explicit:所有要更新的放到一个数组里        /// .periodic:设置开始时间和更新频率        /// 也可以自定义 TimelineScheduler        TimelineView(.everySecond) { t in            let e = "p\(Int.random(in: 1...30))"            Image(e)                .resizable()                .scaledToFit()                .frame(height: 40)        }                // 自定义的 TimelineScheduler        TimelineView(.everyLoop(timeOffsets: [0.2, 0.7, 1, 0.5, 2])) { t in            TimelineSubView(date: t.date)        }    }        // MARK: - TimelineSubView    struct TimelineSubView: View {        let date : Date        @State private var s = "let's go"        // 顺序从数组中取值,取完再重头开始        @State private var idx: Int = 1        func advanceIndex(count: Int) {            idx = (idx + 1) % count            if idx == 0 { idx = 1 }        }                var body: some View {            HStack(spacing: 20) {                Image("p\(idx)")                    .resizable()                    .scaledToFit()                    .frame(height: 40)                    .animation(.easeIn(duration: 1), value: date)                    .onChange(of: date) { newValue in                        advanceIndex(count: 30)                        s = "\(date.hour):\(date.minute):\(date.second)"                    }                    .onAppear {                        advanceIndex(count: 30)                    }                                    Text(s)            }        }    }        // MARK: - 用 matchedGeometryEffect 做动画    /// matchedGeometryEffect 可以无缝的将一个图像变成另外一个图像。    @State private var placeStayItems = ["p1", "p2", "p3", "p4"]    @State private var placeShowItems: [String] = []        @Namespace private var mgeStore        private var placeStayView: some View {        LazyVGrid(columns: [GridItem(.adaptive(minimum: 30), spacing: 10)]) {            ForEach(placeStayItems, id: \.self) { s in                Image(s)                    .resizable()                    .scaledToFit()                    .matchedGeometryEffect(id: s, in: mgeStore)                    .onTapGesture {                        withAnimation {                            placeStayItems.removeAll { $0 == s }                            placeShowItems.append(s)                        }                    }                    .shadow(color: .black, radius: 2, x: 2, y: 4)            } // end ForEach        } // end LazyVGrid    } // private var placeStayView        private var placeShowView: some View {        LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), spacing: 10)]) {            ForEach(placeShowItems, id: \.self) { s in                Image(s)                    .resizable()                    .scaledToFit()                    .matchedGeometryEffect(id: s, in: mgeStore)                    .onTapGesture {                        withAnimation {                            placeShowItems.removeAll { $0 == s }                            placeStayItems.append(s)                        }                    }                    .shadow(color: .black, radius: 2, x: 0, y: 2)                    .shadow(color: .white, radius: 5, x: 0, y: 2)            } // end ForEach        } // end LazyVGrid    } // end private var placeShowView    } // end struct PlayAnimation// MARK: - 扩展 TimelineScheduleextension TimelineSchedule where Self == PeriodicTimelineSchedule {    static var everySecond: PeriodicTimelineSchedule {        get {            .init(from: .now, by: 1)        }    }}// MARK: - 自定义一个 TimelineSchedule// timeOffsets 用完,就会再重头重新再来一遍struct PCLoopTimelineSchedule: TimelineSchedule {    let timeOffsets: [TimeInterval]        func entries(from startDate: Date, mode: TimelineScheduleMode) -> Entries {        Entries(last: startDate, offsets: timeOffsets)    }        struct Entries: Sequence, IteratorProtocol {        var last: Date        let offsets: [TimeInterval]        var idx: Int = -1        mutating func next() -> Date? {            idx = (idx + 1) % offsets.count            last = last.addingTimeInterval(offsets[idx])            return last        }    } // end Struct Entries}// 为自定义的 PCLoopTimelineSchedule 做一个 TimelineSchedule 的扩展函数,方便使用extension TimelineSchedule where Self == PCLoopTimelineSchedule {    static func everyLoop(timeOffsets: [TimeInterval]) -> PCLoopTimelineSchedule {        .init(timeOffsets: timeOffsets)    }}

Canvas

Canvas 可以画路径、图片和文字、Symbols、可变的图形上下文、使用 CoreGraphics 代码和做动画。

图形上下文可以被 addFilter、clip、clipToLayer、concatenate、rotate、scaleBy、translateBy 这些方法来进行改变。

示例代码如下:

struct PlayCanvas: View {    let colors: [Color] = [.purple, .blue, .yellow, .pink]        var body: some View {                // 画路径        PCCanvasPathView(t: .rounded)        PCCanvasPathView(t: .ellipse)        PCCanvasPathView(t: .circle)        // 图片和文字        PCCanvasImageAndText(text: "Starming", colors: [.purple, .pink])        // Symbol,在 Canvas 里引用 SwiftUI 视图        Canvas { c, s in            let c0 = c.resolveSymbol(id: 0)!            let c1 = c.resolveSymbol(id: 1)!            let c2 = c.resolveSymbol(id: 2)!            let c3 = c.resolveSymbol(id: 3)!            c.draw(c0, at: .init(x: 10, y: 10), anchor: .topLeading)            c.draw(c1, at: .init(x: 30, y: 20), anchor: .topLeading)            c.draw(c2, at: .init(x: 50, y: 30), anchor: .topLeading)            c.draw(c3, at: .init(x: 70, y: 40), anchor: .topLeading)        } symbols: {            ForEach(Array(colors.enumerated()), id: \.0) { i, c in                Circle()                    .fill(c)                    .frame(width: 100, height: 100)                    .tag(i)            }        }        // Symbol 动画和 SwiftUI 视图一样,不会受影响        Canvas { c, s in            let sb = c.resolveSymbol(id: 0)!            c.draw(sb, at: CGPoint(x: s.width / 2, y: s.height /  2), anchor: .center)        } symbols: {            PCForSymbolView()                .tag(0)        }    } // end var body}// MARK: - 给 Symbol 用的视图struct PCForSymbolView: View {    @State private var change = true    var body: some View {        Image(systemName: "star.fill")            .renderingMode(.original)            .font(.largeTitle)            .rotationEffect(.degrees(change ? 0 : 72))            .onAppear {                withAnimation(.linear(duration: 1.0).repeatForever(autoreverses: false)) {                    change.toggle()                }            }    }}// MARK: - 图片和文字struct PCCanvasImageAndText: View {    let text: String    let colors: [Color]    var fontSize: Double = 42        var body: some View {        Canvas { context, size in            let midPoint = CGPoint(x: size.width / 2, y: size.height / 2)            let font = Font.system(size: fontSize)            var resolved = context.resolve(Text(text).font(font))                        let start = CGPoint(x: (size.width - resolved.measure(in: size).width) / 2.0, y: 0)            let end = CGPoint(x: size.width - start.x, y: 0)                        resolved.shading = .linearGradient(Gradient(colors: colors), startPoint: start, endPoint: end)            context.draw(resolved, at: midPoint, anchor: .center)                    }    }}// MARK: - Pathstruct PCCanvasPathView: View {    enum PathType {        case rounded, ellipse, casual, circle    }    let t: PathType        var body: some View {        Canvas { context, size in                        conf(context: &context, size: size, type: t)        } // end Canvas    }        func conf( context: inout GraphicsContext, size: CGSize, type: PathType) {        let rect = CGRect(origin: .zero, size: size).insetBy(dx: 25, dy: 25)        var path = Path()        switch type {        case .rounded:            path = Path(roundedRect: rect, cornerRadius: 35.0)        case .ellipse:            let cgPath = CGPath(ellipseIn: rect, transform: nil)            path = Path(cgPath)        case .casual:            path = Path {                let points: [CGPoint] = [                    .init(x: 10, y: 10),                    .init(x: 0, y: 50),                    .init(x: 100, y: 100),                    .init(x: 100, y: 0),                ]                $0.move(to: .zero)                $0.addLines(points)            }        case .circle:            path = Circle().path(in: rect)        }                        let gradient = Gradient(colors: [.purple, .pink])        let from = rect.origin        let to = CGPoint(x: rect.width, y: rect.height + from.y)                // Stroke path        context.stroke(path, with: .color(.blue), lineWidth: 25)        context.fill(path, with: .linearGradient(gradient, startPoint: from, endPoint: to))    }}

SF Symbol

SF Symbol 支持变量值,可以通过设置 variableValue 来填充不同部分,比如 wifi 图标,不同值会亮不同部分,Image(systemName: "wifi", variableValue: 0.5)

开发者

Swift官方

社区

探索库

新鲜事

SwiftOldDriver/iOS-Weekly
老司机 iOS 周报
Star:4222 Issue:19 开发语言:
🇨🇳 老司机技术 iOS 周报

matteocrippa/awesome-swift
Star:22100 Issue:1 开发语言:Ruby
A collaborative list of awesome Swift libraries and resources. Feel free to contribute!

ruanyf/weekly
科技爱好者周刊
Star:24124 Issue:1902 开发语言:
科技爱好者周刊,每周五发布

ming1016/SwiftPamphletApp
戴铭的开发小册子
Star:0 Issue:0 开发语言:

封装易用功能

SwifterSwift/SwifterSwift
Handy Swift extensions
Star:11409 Issue:17 开发语言:Swift
A handy collection of more than 500 native Swift extensions to boost your productivity.

JoanKing/JKSwiftExtension
Swift常用扩展、组件、协议
Star:359 Issue:1 开发语言:Swift
Swift常用扩展、组件、协议,方便项目快速搭建,提供完整清晰的Demo示例,不断的完善中……

infinum/iOS-Nuts-And-Bolts
Star:178 Issue:0 开发语言:Swift
iOS bits and pieces that you can include in your project to make your life a bit easier.

gtokman/ExtensionKit
Star:101 Issue:0 开发语言:Swift
Helpful extensions for iOS app development 🚀

SwiftUI 扩展

SwiftUIX/SwiftUIX
扩展 SwiftUI
Star:4777 Issue:8 开发语言:Swift
Extensions and additions to the standard SwiftUI library.

SDWebImage/SDWebImageSwiftUI
Star:1403 Issue:45 开发语言:Swift
SwiftUI Image loading and Animation framework powered by SDWebImage

apptekstudios/ASCollectionView
SwiftUI collection
Star:1208 Issue:32 开发语言:Swift
A SwiftUI collection view with support for custom layouts, preloading, and more.

siteline/SwiftUI-Introspect
SwiftUI 引入 UIKit
Star:3262 Issue:53 开发语言:Swift
Introspect underlying UIKit components from SwiftUI

AvdLee/SwiftUIKitView
在 SwiftUI 中 使用 UIKit
Star:506 Issue:3 开发语言:Swift
Easily use UIKit views in your SwiftUI applications. Create Xcode Previews for UIView elements

danielsaidi/SwiftUIKit
给 SwiftUI 添加更多功能
Star:666 Issue:3 开发语言:Swift
SwiftUIKit contains additional functionality for SwiftUI.

Toni77777/awesome-swiftui-libraries
SwiftUI 可使用的库
Star:199 Issue:0 开发语言:Swift
:rocket: Awesome SwiftUI Libraries

rakutentech/AltSwiftUI
类 SwiftUI
Star:269 Issue:6 开发语言:Swift
Open Source UI framework based on SwiftUI syntax and features, adding backwards compatibility.

gymshark/ios-stack-kit
类 SwiftUI
Star:118 Issue:2 开发语言:Swift
The power of SwiftUI with UIKit

jordibruin/Swift-Charts-Examples
Swift Charts 制作的不同类型图表示例
Star:0 Issue:0 开发语言:

图片

onevcat/Kingfisher
Star:20267 Issue:76 开发语言:Swift
A lightweight, pure-Swift library for downloading and caching images from the web.

kean/Nuke
Star:6721 Issue:6 开发语言:Swift
Image loading system

suzuki-0000/SKPhotoBrowser
图片浏览
Star:2358 Issue:94 开发语言:Swift
Simple PhotoBrowser/Viewer inspired by facebook, twitter photo browsers written by swift

文字处理

gonzalezreal/MarkdownUI
Star:730 Issue:14 开发语言:Swift
Render Markdown text in SwiftUI

tophat/RichTextView
Star:1073 Issue:31 开发语言:Swift
iOS Text View (UIView) that Properly Displays LaTeX, HTML, Markdown, and YouTube/Vimeo Links

keitaoouchi/MarkdownView
Star:1778 Issue:32 开发语言:Swift
Markdown View for iOS.

johnxnguyen/Down
fast Markdown
Star:1963 Issue:25 开发语言:C
Blazing fast Markdown / CommonMark rendering in Swift, built upon cmark.

qeude/SwiftDown
Swift 写的可换主题的 Markdown 编辑器组件
Star:115 Issue:2 开发语言:Swift
📦 A themable markdown editor component for your SwiftUI apps.

JohnSundell/Ink
Markdown 解析器
Star:2117 Issue:22 开发语言:Swift
A fast and flexible Markdown parser written in Swift.

tnantoka/edhita
Star:1194 Issue:15 开发语言:Swift
Fully open source text editor for iOS written in Swift.

glushchenko/fsnotes
Star:5025 Issue:222 开发语言:Swift
Notes manager for macOS/iOS

coteditor/CotEditor
Star:4602 Issue:96 开发语言:Swift
Lightweight Plain-Text Editor for macOS

mchakravarty/CodeEditorView
SwiftUI 写的代码编辑器
Star:400 Issue:30 开发语言:Swift
SwiftUI code editor view for iOS and macOS

CodeEditApp/CodeEdit
原生,性能好的代码编辑器
Star:10816 Issue:93 开发语言:Swift
CodeEdit App for macOS – Elevate your code editing experience. Open source, free forever.

ZeeZide/CodeEditor
使用 Highlight.js 的来做语法高亮的 SwiftUI 编辑器
Star:202 Issue:3 开发语言:Swift
A SwiftUI TextEditor with syntax highlighting using Highlight.js

tw93/MiaoYan
轻灵的 Markdown 笔记本伴你写出妙言
Star:0 Issue:0 开发语言:

动画

recherst/kavsoft-swiftui-animations
Star:132 Issue:0 开发语言:Swift
SwiftUI animation tutorials, all of demos are consisted of youtube videos at website of kavsoft. 🔗 https://kavsoft.dev

timdonnelly/Advance
Physics-based animations
Star:4463 Issue:4 开发语言:Swift
Physics-based animations for iOS, tvOS, and macOS.

MengTo/Spring
动画
Star:13987 Issue:167 开发语言:Swift
A library to simplify iOS animations in Swift.

持久化存储

stephencelis/SQLite.swift
Star:8349 Issue:88 开发语言:Swift
A type-safe, Swift-language layer over SQLite3.

groue/GRDB.swift
Star:5185 Issue:2 开发语言:Swift
A toolkit for SQLite databases, with a focus on application development

caiyue1993/IceCream
CloudKit 同步 Realm 数据库
Star:1732 Issue:46 开发语言:Swift
Sync Realm Database with CloudKit

realm/realm-cocoa
Star:15375 Issue:367 开发语言:Objective-C
Realm is a mobile database: a replacement for Core Data & SQLite

PostgresApp/PostgresApp
PostgreSQL macOS 应用
Star:6332 Issue:125 开发语言:Makefile
The easiest way to get started with PostgreSQL on the Mac

编程范式

ReactiveX/RxSwift
函数响应式编程
Star:22278 Issue:10 开发语言:Swift
Reactive Programming in Swift

pointfreeco/swift-composable-architecture
Star:6377 Issue:20 开发语言:Swift
A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.

onmyway133/awesome-ios-architecture
Star:4628 Issue:0 开发语言:
:japanese_castle: Better ways to structure iOS apps

ReSwift/ReSwift
单页面状态和数据管理
Star:7206 Issue:39 开发语言:Swift
Unidirectional Data Flow in Swift - Inspired by Redux

gre4ixin/ReduxUI
SwiftUI Redux 架构
Star:29 Issue:0 开发语言:Swift
💎 Redux like architecture for SwiftUI

BohdanOrlov/iOS-Developer-Roadmap
Star:5834 Issue:7 开发语言:Swift
Roadmap to becoming an iOS developer in 2018.

ReactiveCocoa/ReactiveCocoa
Star:19983 Issue:4 开发语言:Swift
Cocoa framework and Obj-C dynamism bindings for ReactiveSwift.

mehdihadeli/awesome-software-architecture
软件架构
Star:291 Issue:2 开发语言:
A curated list of awesome articles, videos, and other resources to learn and practice software architecture, patterns, and principles.

nalexn/clean-architecture-swiftui
干净完整的SwiftUI+Combine例子,包含网络和单元测试等
Star:3517 Issue:17 开发语言:Swift
SwiftUI sample app using Clean Architecture. Examples of working with CoreData persistence, networking, dependency injection, unit testing, and more.

krzysztofzablocki/Sourcery
Swift 元编程
Star:6644 Issue:55 开发语言:Swift
Meta-programming for Swift, stop writing boilerplate code.

路由

pointfreeco/swiftui-navigation
Star:866 Issue:2 开发语言:Swift
Tools for making SwiftUI navigation simpler, more ergonomic and more precise.

静态检查

realm/SwiftLint
Star:16224 Issue:312 开发语言:Swift
A tool to enforce Swift style and conventions.

系统能力

devicekit/DeviceKit
UIDevice 易用封装
Star:3700 Issue:46 开发语言:Swift
DeviceKit is a value-type replacement of UIDevice.

kishikawakatsumi/KeychainAccess
Star:6935 Issue:39 开发语言:Swift
Simple Swift wrapper for Keychain that works on iOS, watchOS, tvOS and macOS.

nvzqz/FileKit
文件操作
Star:2217 Issue:11 开发语言:Swift
Simple and expressive file management in Swift

JohnSundell/Files
文件操作
Star:2298 Issue:23 开发语言:Swift
A nicer way to handle files & folders in Swift

kylef/PathKit
文件操作
Star:1348 Issue:12 开发语言:Swift
Effortless path operations in Swift

rushisangani/BiometricAuthentication
FaceID or TouchID authentication
Star:798 Issue:14 开发语言:Swift
Use Apple FaceID or TouchID authentication in your app using BiometricAuthentication.

sunshinejr/SwiftyUserDefaults
Star:4653 Issue:48 开发语言:Swift
Modern Swift API for NSUserDefaults

MonitorControl/MonitorControl
亮度和声音控制
Star:16608 Issue:65 开发语言:Swift
🖥 Control your display’s brightness & volume on your Mac as if it was a native Apple Display. Use Apple Keyboard keys or custom shortcuts. Shows the native macOS OSDs.

carekit-apple/CareKit
使用 SwiftUI 开发健康相关的库
Star:2224 Issue:71 开发语言:Swift
CareKit is an open source software framework for creating apps that help people better understand and manage their health.

Cay-Zhang/SwiftSpeech
苹果语言识别封装库,已适配 SwiftUI
Star:269 Issue:0 开发语言:Swift
A speech recognition framework designed for SwiftUI.

malcommac/SwiftDate
Swift编写的时间时区,时间比较等复杂处理的包装
Star:6943 Issue:68 开发语言:Swift
🐔 Toolkit to parse, validate, manipulate, compare and display dates, time & timezones in Swift.

接口

OAuthSwift/OAuthSwift
Star:3017 Issue:42 开发语言:Swift
Swift based OAuth library for iOS

p2/OAuth2
Star:1067 Issue:74 开发语言:Swift
OAuth2 framework for macOS and iOS, written in Swift.

public-apis/public-apis
Star:197421 Issue:19 开发语言:Python
A collective list of free APIs

接口应用

bpisano/Weather
天气应用
Star:282 Issue:2 开发语言:Swift
A Weather app in SwiftUI.

Dimillian/MovieSwiftUI
电影 MovieDB 应用
Star:6063 Issue:8 开发语言:Swift
SwiftUI & Combine app using MovieDB API. With a custom Flux (Redux) implementation.

chojnac/NotionSwift
Star:29 Issue:4 开发语言:Swift
Unofficial Notion API SDK for iOS & macOS

Dimillian/RedditOS
SwiftUI 写的 Reddit客户端
Star:3605 Issue:16 开发语言:Swift
The product name is Curiosity, a SwiftUI Reddit client for macOS Big Sur

carson-katri/reddit-swiftui
SwiftUI 写的 Reddit客户端
Star:1127 Issue:7 开发语言:Swift
A cross-platform Reddit client built in SwiftUI

Dimillian/SwiftHN
Hacker News 阅读
Star:1685 Issue:20 开发语言:Swift
A Hacker News reader in Swift

tatsuz0u/EhPanda
Star:1507 Issue:24 开发语言:Swift
An unofficial E-Hentai App for iOS built with SwiftUI & TCA.

Dimillian/MortyUI
GraphQL + SwiftUI 开发的瑞克和莫蒂应用
Star:431 Issue:4 开发语言:Swift
A very simple Rick & Morty app to demo GraphQL + SwiftUI

Finb/V2ex-Swift
V2EX 客户端
Star:1513 Issue:11 开发语言:Swift
An iOS client written in Swift for V2EX

v2er-app/iOS
V2EX 客户端
Star:194 Issue:4 开发语言:Swift
The source of V2er.iOS

sinaweibosdk/weibo_ios_sdk
Star:1423 Issue:66 开发语言:Objective-C
新浪微博 IOS SDK

miniLV/MNWeibo
Swift5 + MVVM 微博客户端
Star:251 Issue:4 开发语言:Swift
Swift5 + MVVM + 文艺复兴微博(纯代码 + 纯Swift),可作为第一个上手的Swift项目.

nerdishbynature/octokit.swift
Swift API Client for GitHub
Star:400 Issue:10 开发语言:Swift
A Swift API Client for GitHub and GitHub Enterprise

GitHawkApp/GitHawk
iOS app for GitHub
Star:2838 Issue:460 开发语言:Swift
The (second) best iOS app for GitHub.

fangzesheng/free-api
Star:12335 Issue:28 开发语言:
收集免费的接口服务,做一个api的搬运工

nerdsupremacist/Graphaello
SwiftUI 中使用 GraphQL 的工具
Star:454 Issue:22 开发语言:Swift
A Tool for Writing Declarative, Type-Safe and Data-Driven Applications in SwiftUI using GraphQL

nerdsupremacist/tmdb
GraphQL 包装电影数据接口
Star:16 Issue:1 开发语言:Swift
A GraphQL Wrapper for The Movie Database

macOS

serhii-londar/open-source-mac-os-apps
开源 macOS 程序合集
Star:30760 Issue:65 开发语言:Swift
🚀 Awesome list of open source applications for macOS. https://t.me/s/opensourcemacosapps

Ranchero-Software/NetNewsWire
Star:5773 Issue:551 开发语言:Swift
RSS reader for macOS and iOS.

overtake/TelegramSwift
Star:3876 Issue:485 开发语言:Swift
Source code of Telegram for macos on Swift 5.0

eonist/FileWatcher
macOS 上监听文件变化
Star:159 Issue:5 开发语言:Swift
Monitoring file system changes in macOS

waylybaye/XcodeCleaner-SwiftUI
清理 Xcode
Star:1204 Issue:3 开发语言:Swift
Make Xcode Clean Again

gao-sun/eul
SwiftUI 写的 macOS 状态监控工具
Star:7658 Issue:55 开发语言:Swift
🖥️ macOS status monitoring app written in SwiftUI.

Dimillian/ACHNBrowserUI
SwiftUI 写的动物之森小助手程序
Star:1528 Issue:32 开发语言:Swift
Animal Crossing New Horizon companion app in SwiftUI

lexrus/RegExPlus
正则表达式
Star:186 Issue:0 开发语言:Swift
A nifty RegEx test tool built with SwiftUI

v2ex/launcher
用来启动那些本地开发时需要的各种进程,及查看其输出
Star:212 Issue:5 开发语言:Swift

lukakerr/Pine
Markdown 编辑器
Star:2988 Issue:45 开发语言:Swift
A modern, native macOS markdown editor

root3nl/SupportApp
企业支持 macOS 软件
Star:283 Issue:20 开发语言:Swift
The Support App is developed by Root3, specialized in managing Apple devices. Root3 offers consultancy and support for organizations to get the most out of their Apple devices and is based in The Netherlands (Haarlem).

jaywcjlove/awesome-mac
macOS 软件大全
Star:49993 Issue:126 开发语言:JavaScript
 Now we have become very big, Different from the original idea. Collect premium software in various categories.

insidegui/WWDC
Star:8216 Issue:33 开发语言:Swift
The unofficial WWDC app for macOS

sindresorhus/Actions
Star:904 Issue:11 开发语言:Swift
⚙️ Supercharge your shortcuts

ObuchiYuki/DevToysMac
开发者工具合集
Star:5552 Issue:40 开发语言:Swift
DevToys For mac

jacklandrin/OnlySwitch
macOS 状态栏一键设置工具,隐藏桌面图标、清理 Xcode 缓存、一键隐藏刘海儿、进入夜览模式等数十种功能
Star:1389 Issue:10 开发语言:Swift
⚙️ All-in-One menu bar app, hide 💻MacBook Pro’s notch, dark mode, AirPods, Shortcuts

exelban/stats
macOS 系统资源监控
Star:11296 Issue:14 开发语言:Swift
macOS system monitor in your menu bar

brunophilipe/Cakebrew
可视化管理 Homebrew 软件包
Star:4234 Issue:57 开发语言:Objective-C
Manage your Homebrew formulas with style using Cakebrew.

应用

vinhnx/Clendar
SwiftUI 写的日历应用
Star:361 Issue:58 开发语言:Swift
Clendar - universal Apple-platform calendar app. Written in SwiftUI. Available on App Store. MIT License.

SvenTiigi/WhatsNewKit
欢迎屏
Star:2560 Issue:1 开发语言:Swift
Showcase your awesome new app features 📱

kickstarter/ios-oss
Kickstarter 的 iOS 版本
Star:7968 Issue:1 开发语言:Swift
Kickstarter for iOS. Bring new ideas to life, anywhere.

CoreOffice/CryptoOffice
Swift 解析 Office Open XML(OOXML)包括 xlsx, docx, pptx
Star:27 Issue:0 开发语言:Swift
Office Open XML (OOXML) formats (.xlsx, .docx, .pptx) decryption for Swift

CoreOffice/CoreXLSX
Swift编写的Excel电子表格(XLSX)格式解析器
Star:643 Issue:13 开发语言:Swift
Excel spreadsheet (XLSX) format parser written in pure Swift

analogcode/Swift-Radio-Pro
电台应用
Star:2679 Issue:13 开发语言:Swift
Professional Radio Station App for iOS!

bizz84/SwiftyStoreKit
应用内购框架
Star:6000 Issue:163 开发语言:Swift
Lightweight In App Purchases Swift framework for iOS 8.0+, tvOS 9.0+ and macOS 10.10+ ⛺

wikimedia/wikipedia-ios
Star:2462 Issue:6 开发语言:Swift
📱The official Wikipedia iOS app.

游戏

pointfreeco/isowords
单词搜索游戏
Star:1792 Issue:2 开发语言:Swift
Open source game built in SwiftUI and the Composable Architecture.

michelpereira/awesome-games-of-coding
教你学编程的游戏收集
Star:1459 Issue:1 开发语言:
A curated list of games that can teach you how to learn a programming language.

OpenEmu/OpenEmu
视频游戏模拟器
Star:13964 Issue:201 开发语言:Swift
🕹 Retro video game emulation for macOS

jVirus/swiftui-2048
Star:162 Issue:0 开发语言:Swift
🎲 100% SwiftUI 3.0, classic 2048 game [iOS 15.0+, iPadOS 15.0+, macOS 12.0+, Swift 5.5].

schellingb/dosbox-pure
DOS 游戏模拟器
Star:452 Issue:112 开发语言:C++
DOSBox Pure is a new fork of DOSBox built for RetroArch/Libretro aiming for simplicity and ease of use.

chrismaltby/gb-studio
拖放式复古游戏创建器
Star:6089 Issue:513 开发语言:C
A quick and easy to use drag and drop retro game creator for your favourite handheld video game system

darrellroot/Netrek-SwiftUI
SwiftUI 开发的1989年的 Netrek 游戏
Star:10 Issue:0 开发语言:Swift

freeCodeCamp/LearnToCodeRPG
学习编码的游戏
Star:828 Issue:10 开发语言:Ren’Py
A visual novel video game where you learn to code and get a dev job 🎯

pmgl/microstudio
游戏开发平台集搜索、开发、学习、体验、交流等功能于一身
Star:634 Issue:37 开发语言:JavaScript
Free, open source game engine online

InvadingOctopus/octopuskit
2D游戏引擎,用的 GameplayKit + SpriteKit + SwiftUI
Star:310 Issue:0 开发语言:Swift
2D ECS game engine in 100% Swift + SwiftUI for iOS, macOS, tvOS

a-little-org-called-mario/a-little-game-called-mario
用 Godot 引擎做的马里奥游戏
Star:1046 Issue:5 开发语言:GDScript
open source collective hell game

新技术展示

JakeLin/Moments-SwiftUI
SwiftUI、Async、Actor
Star:42 Issue:0 开发语言:Swift
WeChat-like Moments App implemented using Swift 5.5 and SwiftUI

twostraws/HackingWithSwift
示例代码
Star:4529 Issue:10 开发语言:Swift
The project source code for hackingwithswift.com

carson-katri/awesome-result-builders
Result Builders awesome
Star:798 Issue:2 开发语言:
A list of cool DSLs made with Swift 5.4’s @resultBuilder

pointfreeco/episode-code-samples
Star:684 Issue:3 开发语言:Swift
💾 Point-Free episode code.

SwiftGGTeam/the-swift-programming-language-in-chinese
中文版 Apple 官方 Swift 教程
Star:20535 Issue:5 开发语言:CSS
中文版 Apple 官方 Swift 教程《The Swift Programming Language》

jessesquires/TIL
学习笔记
Star:258 Issue:1 开发语言:
Things I’ve learned and/or things I want to remember. Notes, links, advice, example code, etc.

Combine 扩展

OpenCombine/OpenCombine
Combine 的开源实现
Star:2228 Issue:13 开发语言:Swift
Open source implementation of Apple’s Combine framework for processing values over time.

CombineCommunity/CombineExt
对 Combine 的补充
Star:1198 Issue:23 开发语言:Swift
CombineExt provides a collection of operators, publishers and utilities for Combine, that are not provided by Apple themselves, but are common in other Reactive Frameworks and standards.

聚合

dkhamsing/open-source-ios-apps
开源的完整 App 例子
Star:30612 Issue:0 开发语言:
:iphone: Collaborative List of Open-Source iOS Apps

vlondon/awesome-swiftui
Star:1246 Issue:5 开发语言:
A collaborative list of awesome articles, talks, books, videos and code examples about SwiftUI.

ivanvorobei/SwiftUI
Star:3896 Issue:3 开发语言:Swift
Examples projects using SwiftUI released by WWDC2019. Include Layout, UI, Animations, Gestures, Draw and Data.

kon9chunkit/GitHub-Chinese-Top-Charts
GitHub中文排行榜
Star:46675 Issue:88 开发语言:Java
:cn: GitHub中文排行榜,各语言分设「软件 | 资料」榜单,精准定位中文好项目。各取所需,高效学习。

onmyway133/awesome-swiftui
Star:402 Issue:4 开发语言:
🌮 Awesome resources, articles, libraries about SwiftUI

Juanpe/About-SwiftUI
汇总 SwiftUI 的资料
Star:6225 Issue:0 开发语言:Swift
Gathering all info published, both by Apple and by others, about new framework SwiftUI.

sindresorhus/awesome
内容广
Star:206308 Issue:37 开发语言:
😎 Awesome lists about all kinds of interesting topics

SwiftPackageIndex/PackageList
Swift 开源库索引
Star:627 Issue:0 开发语言:Swift
The master list of repositories for the Swift Package Index.

matteocrippa/awesome-swift
Star:22100 Issue:1 开发语言:Ruby
A collaborative list of awesome Swift libraries and resources. Feel free to contribute!

性能、工程构建及自动化

tuist/tuist
创建和维护 Xcode projects 文件
Star:2782 Issue:131 开发语言:Swift
🚀 Create, maintain, and interact with Xcode projects at scale

swift-server/vscode-swift
VSCode 的 Swift 扩展
Star:316 Issue:33 开发语言:TypeScript
Visual Studio Code Extension for Swift

peripheryapp/periphery
检测 Swift 无用代码
Star:3438 Issue:31 开发语言:Swift
A tool to identify unused code in Swift projects.

nalexn/ViewInspector
SwiftUI Runtime introspection 和 单元测试
Star:1251 Issue:24 开发语言:Swift
Runtime introspection and unit testing of SwiftUI views

shibapm/Komondor
Git Hooks for Swift projects
Star:513 Issue:20 开发语言:Swift
Git Hooks for Swift projects 🐩

SwiftGen/SwiftGen
代码生成
Star:7987 Issue:91 开发语言:Swift
The Swift code generator for your assets, storyboards, Localizable.strings, … — Get rid of all String-based APIs!

hyperoslo/Cache
Star:2602 Issue:23 开发语言:Swift
:package: Nothing but Cache.

kylef/Commander
命令行
Star:1492 Issue:3 开发语言:Swift
Compose beautiful command line interfaces in Swift

Carthage/Carthage
Star:14605 Issue:169 开发语言:Swift
A simple, decentralized dependency manager for Cocoa

NARKOZ/hacker-scripts
程序员的活都让机器干的脚本(真实故事)
Star:44591 Issue:67 开发语言:JavaScript
Based on a true story

RobotsAndPencils/XcodesApp
Xcode 多版本安装
Star:3276 Issue:51 开发语言:Swift
The easiest way to install and switch between multiple versions of Xcode - with a mouse click.

ZeeZide/5GUIs
可以分析程序用了哪些库,用了LLVM objdump
Star:189 Issue:11 开发语言:Swift
A tiny macOS app that can detect the GUI technologies used in other apps.

faisalmemon/ios-crash-dump-analysis-book
iOS Crash Dump Analysis Book
Star:474 Issue:1 开发语言:Objective-C
iOS Crash Dump Analysis Book

majd/ipatool
下载 ipa
Star:2303 Issue:12 开发语言:Swift
Command-line tool that allows searching and downloading app packages (known as ipa files) from the iOS App Store

测试

Quick/Quick
测试框架
Star:9456 Issue:31 开发语言:Swift
The Swift (and Objective-C) testing framework.

网络

Alamofire/Alamofire
Star:37835 Issue:29 开发语言:Swift
Elegant HTTP Networking in Swift

socketio/socket.io-client-swift
Star:4814 Issue:189 开发语言:Swift

Lojii/Knot
使用 SwiftNIO 实现 HTTPS 抓包
Star:1239 Issue:3 开发语言:C
一款iOS端基于MITM(中间人攻击技术)实现的HTTPS抓包工具,完整的App,核心代码使用SwiftNIO实现

swift-server/async-http-client
使用 SwiftNIO 开发的 HTTP 客户端
Star:609 Issue:83 开发语言:Swift
HTTP client library built on SwiftNIO

kean/Get
Star:406 Issue:0 开发语言:Swift
Web API client built using async/await

awesome-selfhosted/awesome-selfhosted
网络服务及上面的应用
Star:92737 Issue:151 开发语言:JavaScript
A list of Free Software network services and web applications which can be hosted on your own servers

daltoniam/Starscream
WebSocket
Star:7348 Issue:149 开发语言:Swift
Websockets in swift for iOS and OSX

shadowsocks/ShadowsocksX-NG
Star:30440 Issue:257 开发语言:Swift
Next Generation of ShadowsocksX

carson-katri/swift-request
声明式的网络请求
Star:648 Issue:7 开发语言:Swift
Declarative HTTP networking, designed for SwiftUI

alibaba/xquic
阿里巴巴发布的 XQUIC 库
Star:1132 Issue:21 开发语言:C
XQUIC Library released by Alibaba is a cross-platform implementation of QUIC and HTTP/3 protocol.

kasketis/netfox
获取所有网络请求
Star:3269 Issue:18 开发语言:Swift
A lightweight, one line setup, iOS / OSX network debugging library! 🦊

Moya/Moya
Swift 编写的网络抽象层
Star:14012 Issue:125 开发语言:Swift
Network abstraction layer written in Swift.

Kitura/BlueSocket
Star:1301 Issue:31 开发语言:Swift
Socket framework for Swift using the Swift Package Manager. Works on iOS, macOS, and Linux.

rhummelmose/BluetoothKit
蓝牙
Star:2086 Issue:35 开发语言:Swift
Easily communicate between iOS/OSX devices using BLE

WeTransfer/Mocker
Mock Alamofire and URLSession
Star:817 Issue:3 开发语言:Swift
Mock Alamofire and URLSession requests without touching your code implementation

bagder/everything-curl
记录了 curl 的一切
Star:1488 Issue:0 开发语言:Perl
The book documenting the curl project, the curl tool, libcurl and more. Simply put: everything curl.

LANDrop/LANDrop
全平台局域网文件传输
Star:2787 Issue:90 开发语言:C++
Drop any files to any devices on your LAN.

图形

willdale/SwiftUICharts
用于SwiftUI的图表绘图库
Star:560 Issue:30 开发语言:Swift
A charts / plotting library for SwiftUI. Works on macOS, iOS, watchOS, and tvOS and has accessibility features built in.

lludo/SwiftSunburstDiagram
SwiftUI 图表
Star:479 Issue:12 开发语言:Swift
SwiftUI library to easily render diagrams given a tree of objects. Similar to ring chart, sunburst chart, multilevel pie chart.

ivanschuetz/SwiftCharts
Star:2399 Issue:49 开发语言:Swift
Easy to use and highly customizable charts library for iOS

danielgindi/Charts
Star:25565 Issue:832 开发语言:Swift
Beautiful charts for iOS/tvOS/OSX! The Apple side of the crossplatform MPAndroidChart.

imxieyi/waifu2x-ios
waifu2x Core ML 动漫风格图片的高清渲染
Star:430 Issue:3 开发语言:Swift
iOS Core ML implementation of waifu2x

mecid/SwiftUICharts
支持 SwiftUI 的简单的线图和柱状图库
Star:1306 Issue:2 开发语言:Swift
A simple line and bar charting library that supports accessibility written using SwiftUI.

Tencent/libpag
PAG(Portable Animated Graphics)实时渲染库,多个平台渲染AE动画。
Star:1809 Issue:2 开发语言:C++
The official rendering library for PAG (Portable Animated Graphics) files that renders After Effects animations natively across multiple platforms.

jathu/UIImageColors
获取图片主次颜色
Star:3100 Issue:10 开发语言:Swift
Fetches the most dominant and prominent colors from an image.

BradLarson/GPUImage3
Metal 实现
Star:2362 Issue:74 开发语言:Swift
GPUImage 3 is a BSD-licensed Swift framework for GPU-accelerated video and image processing using Metal.

exyte/Macaw
SVG
Star:5845 Issue:129 开发语言:Swift
Powerful and easy-to-use vector graphics Swift library with SVG support

exyte/SVGView
支持 SwiftUI 的 SVG 解析渲染视图
Star:173 Issue:3 开发语言:Swift
SVG parser and renderer written in SwiftUI

efremidze/Magnetic
SpriteKit气泡支持SwiftUI
Star:1398 Issue:23 开发语言:Swift
SpriteKit Floating Bubble Picker (inspired by Apple Music) 🧲

NextLevel/NextLevel
相机
Star:1994 Issue:69 开发语言:Swift
⬆️ Rad Media Capture in Swift

Harley-xk/MaLiang
基于 Metal 的涂鸦绘图库
Star:1271 Issue:43 开发语言:Swift
iOS painting and drawing library based on Metal. 神笔马良有一支神笔(基于 Metal 的涂鸦绘图库)

frzi/Model3DView
毫不费力的使用 SwiftUI 渲染 3d models
Star:49 Issue:0 开发语言:Swift
Render 3d models with SwiftUI effortlessly

音视频

iina/iina
Star:30443 Issue:1348 开发语言:Swift
The modern video player for macOS.

shogo4405/HaishinKit.swift
RTMP, HLS
Star:2319 Issue:12 开发语言:Swift
Camera and Microphone streaming library via RTMP, HLS for iOS, macOS and tvOS.

AudioKit/AudioKit
Star:9194 Issue:2 开发语言:Swift
Swift audio synthesis, processing, & analysis platform for iOS, macOS and tvOS

josejuanqm/VersaPlayer
Star:698 Issue:3 开发语言:Swift
Versatile Video Player implementation for iOS, macOS, and tvOS

bilibili/ijkplayer
bilibili 播放器
Star:30489 Issue:2748 开发语言:C
Android/iOS video player based on FFmpeg n3.4, with MediaCodec, VideoToolbox support.

mpv-player/mpv
命令行视频播放器
Star:19139 Issue:867 开发语言:C
🎥 Command line video player

analogcode/Swift-Radio-Pro
广播电台
Star:2679 Issue:13 开发语言:Swift
Professional Radio Station App for iOS!

安全

krzyzanowskim/CryptoSwift
Star:9121 Issue:40 开发语言:Swift
CryptoSwift is a growing collection of standard and secure cryptographic algorithms implemented in Swift

rockbruno/SwiftInfo
提取和分析一个iOS应用
Star:1094 Issue:20 开发语言:Swift
📊 Extract and analyze the evolution of an iOS app’s code.

Web

Kitura/swift-html-entities
HTML5 规范字符编码/解码器
Star:145 Issue:5 开发语言:Swift
HTML5 spec-compliant character encoder/decoder for Swift

TokamakUI/Tokamak
SwiftUI 兼容,WebAssembly 构建 HTML
Star:1732 Issue:99 开发语言:Swift
SwiftUI-compatible framework for building browser apps with WebAssembly and native apps for other platforms

johnsundell/publish
用 swift 来写网站
Star:4050 Issue:29 开发语言:Swift
A static site generator for Swift developers

highlightjs/highlight.js
语法高亮
Star:20128 Issue:64 开发语言:JavaScript
JavaScript syntax highlighter with language auto-detection and zero dependencies.

sivan/heti
赫蹏(hètí)中文排版
Star:4864 Issue:17 开发语言:SCSS
赫蹏(hètí)是专为中文内容展示设计的排版样式增强。它基于通行的中文排版规范而来,可以为网站的读者带来更好的文章阅读体验。

kevquirk/simple.css
简单大方基础 CSS 样式
Star:2619 Issue:5 开发语言:CSS
Simple.css is a classless CSS template that allows you to make a good looking website really quickly.

mozilla-mobile/firefox-ios
Star:10907 Issue:1062 开发语言:Swift
Firefox for iOS

liviuschera/noctis
好看的代码编辑器配色主题
Star:388 Issue:19 开发语言:JavaScript
Noctis is a collection of light & dark themes with a well balanced blend of warm and cold colors

服务器

vapor/vapor
Star:21853 Issue:86 开发语言:Swift
💧 A server-side Swift HTTP web framework.

Lakr233/Rayon
SSH 机器管理,Swift 编写
Star:2190 Issue:21 开发语言:Swift
yet another SSH machine manager

系统

spevans/swift-project1
Swift编写内核,可在 Mac 和 PC 启动
Star:243 Issue:1 开发语言:Swift
A minimal bare metal kernel in Swift

Web 3.0

chaozh/awesome-blockchain-cn
区块链 awesome
Star:16629 Issue:15 开发语言:JavaScript
收集所有区块链(BlockChain)技术开发相关资料,包括Fabric和Ethereum开发资料

argentlabs/web3.swift
以太坊 Swift API,支持智能合约、ENS 和 ERC20
Star:439 Issue:14 开发语言:Swift
Ethereum Swift API with support for smart contracts, ENS & ERC20

chainfeeds/RSSAggregatorforWeb3
web3 的 rss feed 订阅源
Star:1502 Issue:4 开发语言:Python
Bootstrapping your personal Web3 info hub from more than 500 RSS Feeds.

Planetable/Planet
由 IPFS 和以太坊名称系统提供支持的去中心化博客和网站
Star:531 Issue:13 开发语言:Swift
Decentralized blogs and websites powered by IPFS and Ethereum Name System

Apple

apple/swift
Star:59937 Issue:6020 开发语言:C++
The Swift Programming Language

apple/swift-evolution
提案
Star:13440 Issue:42 开发语言:Markdown
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.

apple/swift-corelibs-foundation
Star:4624 Issue:607 开发语言:Swift
The Foundation Project, providing core utilities, internationalization, and OS independence

apple/swift-package-manager
Star:8819 Issue:513 开发语言:Swift
The Package Manager for the Swift Programming Language

apple/swift-markdown
Star:1840 Issue:17 开发语言:Swift
A Swift package for parsing, building, editing, and analyzing Markdown documents.

apple/sourcekit-lsp
Star:2599 Issue:32 开发语言:Swift
Language Server Protocol implementation for Swift and C-based languages

apple/swift-nio
Star:7023 Issue:170 开发语言:Swift
Event-driven network application framework for high performance protocol servers & clients, non-blocking.

apple/swift-syntax
解析、生成、转换 Swift 代码
Star:1842 Issue:24 开发语言:Swift
SwiftPM package for SwiftSyntax library.

apple/swift-crypto
CryptoKit 的开源实现
Star:1208 Issue:10 开发语言:C
Open-source implementation of a substantial portion of the API of Apple CryptoKit suitable for use on Linux platforms.

apple/swift-driver
用 Swift 语言重新实现的编译器的驱动程序库
Star:629 Issue:25 开发语言:Swift
Swift compiler driver reimplementation in Swift

apple/swift-numerics
用简单的方式用浮点型进行数值计算
Star:1405 Issue:45 开发语言:Swift
Advanced mathematical types and functions for Swift

apple/swift-atomics
Swift 的低级原子操作
Star:763 Issue:11 开发语言:Swift
Low-level atomic operations for Swift

apple/swift-async-algorithms
Combine 的官方开源替代
Star:1546 Issue:21 开发语言:Swift
Async Algorithms for Swift

计算机科学

raywenderlich/swift-algorithm-club
Star:26593 Issue:55 开发语言:Swift
Algorithms and data structures in Swift, with explanations!

扩展知识

trimstray/the-book-of-secret-knowledge
Star:71271 Issue:19 开发语言:
A collection of inspiring lists, manuals, cheatsheets, blogs, hacks, one-liners, cli/web tools and more.

rossant/awesome-math
Star:6046 Issue:7 开发语言:Python
A curated list of awesome mathematics resources

待分类

krzysztofzablocki/KZFileWatchers
Swift编写的观察本地或者网络上,比如网盘和FTP的文件变化
Star:1029 Issue:2 开发语言:Swift
A micro-framework for observing file changes, both local and remote. Helpful in building developer tools.

博客和资讯

  • Swift.org:Swift 官方博客
  • Release notes from iOS-Weekly:老司机 iOS 周报
  • iOS摸鱼周报:iOS 摸鱼周报
  • Michael Tsai:一名 macOS 开发者的博客
  • 少数派:少数派致力于更好地运用数字产品或科学方法,帮助用户提升工作效率和生活品质
  • OneV’s Den:上善若水,人淡如菊。这里是王巍 (onevcat) 的博客,用来记录一些技术和想法,主要专注于 Swift 和 iOS 开发。
  • SwiftLee:A weekly blog about Swift, iOS and Xcode Tips and Tricks
  • Swift with Majid:Majid’s blog about Swift development
  • 肘子的Swift记事本
  • 戴铭的博客 - 星光社:一个人走得快,一群人走的远
  • Swift by Sundell:Weekly Swift articles, podcasts and tips by John Sundell
  • FIVE STARS:Exploring iOS, SwiftUI & much more.
  • SwiftUI Weekly:The curated collection of links about SwiftUI. Delivered every Monday.
  • Not Only Swift Weekly:Xcode tips & tricks, Swift, SwiftUI, Combine, Firebase, computing and internet history, and - of course - some fun stuff.
  • SwiftlyRush Weekly:SwiftlyRush Weekly is a weekly curated publication full of interesting, relevant links, alongside industry news and updates. Subscribe now and never miss an issue.
  • iOS Dev Weekly:Subscribe to a hand-picked round-up of the best iOS development links every week. Curated by Dave Verwer and published every Friday. Free.
  • 阮一峰的网络日志:Ruan YiFeng’s Blog 科技爱好者周刊
  • The.Swift.Dev.:Weekly Swift articles
  • 爱范儿:让未来触手可及
  • 机核:不止是游戏

我写技术文章的一点心得

作者 戴铭
2021年7月24日 05:52

前言

非常感谢大家抽出生命中宝贵的一段时间来听我接下来的一大段关于写文章那些事的唠叨。写文章的好处看看《觉醒年代》就知道了。

这篇文章我不会写一些常说的技巧,比如文章的内容前后要有逻辑关系,内容之间有关联。所讲知识前后的层次要平,不要在某个部分挖掘过深。写作过程中牢牢抓住要表达的内容,不要过于偏离主题。类似这样的技巧不会说。都说一流的人讨论思想、普通人讨论事情、三流的人讨论人,那么为了提高文章高度,除了说些事情,我还打算加些思想的内容。

大家都习惯去阅读他人人生体验来体验不一样的人生,这样的方式和评头论足一样简单舒服,轻松爽快。而主动去对自己思想进行研究和开发,通过写作输出自己独特的经历和思考却是困难且难受的,但这样从0到1和从0到10的创造过程获得的乐趣却是前者的百倍甚至更多,这背后所遵循的原则是怎样的呢?

每个心得都会基于某些原则,以至于思路不会散架,而所有的原则都无法违背物热力学第二定律,也就是熵增定律(强烈建议先看知乎这篇介绍)。就连进化论都是遵循熵增定律。

对于写作的心得我提炼出独特性、真实感、故事性和新意四个点,其中的独特性和新意都是逆着熵增的过程,其过程是非常难受煎熬的,可能做了大量付出也没结果,因为逆熵增是非线性的,无法预测的,只有在偶然的机会才会有开挂的感觉。对于真实感和故事性属于线性积累,和阅读别人的文章一样,是很容易做到的事情,都是熵增过程,有必要,容易看到结果,但会有内耗,如果没有更多独特性的经历来逆熵,可输出的内容会越来越混乱,落后,渐渐无用。因此独特性、真实感、故事性和新意这四个点之间需要平衡与演进,才能够保持进化的活力。

接下来我就详细展开来跟你说说独特性、真实感、故事性和新意四个点,通过我以前写的一些文章来详细说明。如果你还不知道如何下笔,我还会介绍一个容易着手去做记录和分享输出的方法步骤,最后会从头到尾举个例子按照前面介绍的步骤演示如何写完一篇技术文章。特别是那些逆熵的过程,让你能够多些体感。

四个点

先分别介绍下这四个点。

独特性

独特性也就是自己的经历和体验,这个是独一无二的,文章的内容如果有更多的个人经历,作为读者也就能够体验到更多的生活。

行万里路,多去做不同事情,多尝试不同方法,也就能够获取到更多的经历。独特性是内容中最重要的部分,如果这四点重要性共分十层的话,我认为独特性就可以占到六层。

真实感

记得一个美剧编剧分享过他写编剧的经验,其中提到要写的题材,他至少会花上一年以上的时间去收集和整理相关资料。他认为只有把题材相关细节都吃透了,编排到剧里,观众才会感觉更真实,代入感才会强,身同感受才会有共鸣。如果观众感到假,感觉不到用心,那他怎么会去了解你想表达的内容呢。

因此真实感是表达内容的基础,而且是最费时的。相较于独特的个人经历,真实感是需要花费大量时间去调研作者以前不了解的东西。而这个过程也是了解别人经历的过程,可以学习到很多以前不知道的事情。

真实感是对独特性的扩充,是丰富和挖掘沉淀独特性的,可以占到两层,后面的故事性和新意各占一层。

故事性

我很喜欢金庸的小说,特别是射雕英雄传、神雕侠侣和倚天屠龙记这三部,起初对金庸其它小说兴趣不大,也可能拍的电视剧不是我的菜。后来抱着爱屋及乌的态度,我尝试着打开笑傲江湖小说的开头开始读,一下子就被吸引进去了。不得不佩服金庸写故事的能力,太强大了。故事一开始制造了一个令人无法解释的案件,你会非常好奇的一直看下去,很想知道到底发生了什么。小说都进行了很长的部分令狐冲才出现,主角出现前还能吸引你看下去,可见金庸讲故事能力有多厉害。

自从迷上金庸的小说后,我也会试着写些小故事,同样我会注重把一些自身独特的经历穿插到写的小故事里,这些故事我发到了我的博客上,有白龙班十中白芈花野

故事性是一种技巧,是线性的,很容易通过大量积累掌握好,最终是好是差还是强依赖于独特性和真实感。

新意

新意这个点非常关键和重要,也是演进的重要因素,你仔细想想看,很多深度高的文章其实底层知识都是差不多的,能够真正有翻天覆地突进的技术演进不会很频繁,而且这些技术往往都在硬件厂商和实验室中产生出来。对于已有底层知识的输出区别只是应用场景和组合运用技巧上有区别,精彩的发掘和效果奇佳的收益也能够获得掌声。因此技术知识和经验输出的形式也非常重要和关键,如果没有新意,大家势必会对那些知识感觉到疲倦,没人看,写作也就没有了动力。

关于新意可以看到淘系公众号最近使用了视频的方式来讲他们的技术,看起来就很有趣。这方面只有你想不到,没有你做不到,打开脑壳,充分发挥想象吧。

新意之所以只占一层,因为新意获得成功的概率较低,是非线性的,因此需要不断去尝试不同的方式。需要依赖天时地利人和以及前三个点都做的足够好了,新意才会取得非常好的效果。

我以前的文章

前面讲了四个我觉得写技术文章最重要的点,只是说了下理论上的逻辑,体感还不够强,下面我结合我以前写的文章我们一起来看看这些文章背后那些独特的经历吧。

A站 的 Swift 实践

《A站 的 Swift 实践》,当时发这篇文章时,关于 Swift 实践的文章也有很多,都是各厂自身实践经验,对于独特性这个点,开始想着把 A 站做过的事情说清楚就可以了,但是很多的经验和做的事情和其他厂做的差不多,这样写出来会没有什么特别的,所以需要着重说下做的和别人不一样的事情。A站比较有特色的是文章里提到的A站自研的声明式 UI Ysera 框架,这个是别人没有的,并且由于 Ysera 框架带来了和 SwiftUI 类似的优雅简洁,提升了整体开发的效率和体验。由于 A 站很早就进入了 Swift 开发模式,并且已有将近一半业务使用了 Swift 开发,所以 A 站相较其它厂走得更快些,对于 Swift 新特性运用的也更广,比如对于 Property Wrapper 的广泛应用,使得代码复杂度骤然降低。走得更远还表现在 Module 化上,A 站大半 Pod 都完成了 Module 化,这方面的经验也很多。

有了独特性,为了能够让阅读的人更有体感,需要对一些技术点进行进一步的描述,使得文章一方面能够让自己得到知识的总结沉淀,还能够对他人有用。这篇文章主要是在混编的内在原理上进行了剖析,这比只描述解决混编问题过程要更加通用些,同时也能起到授人以渔的目的。但掌握原理就需要去学习和提炼相关知识,所下的功夫也更大些。另外采用 Swift 的话,还有个绕不过去的担忧点需要面对,这就是 Swift 的动态性,Swift 这方面由于在 Swift 核心团队工作优先级中较低,相较于 OC 要弱和不成熟很多。所以关于动态化就要说清楚,说的全面点,最好是能够自己进行实验去验证,这个过程会往往枯燥漫长,需要较大的热情才能够完成。

关于故事性,故事性往往是用来引入读进去的一种办法,A 站的 Swift 实践这篇文章的开头通过讲述使用 Swift 的必要性、A 站为之付出的努力和收获、Swift 语言的演进的过程的方式尽量避开具体技术描述,而是使用通俗易懂的描述让读的人可以被轻松带入到文章中来。

深入剖析Auto Layout,分析iOS各版本新增特性

《深入剖析Auto Layout,分析iOS各版本新增特性》。写这个文章也是有着一段不同寻常的经历。那时刚到公司,所有布局都还是使用的 frame 方式,而 Auto Layout 苹果公司才推出不久,在另一位跟我一样新进公司熟悉 Auto Layout 同事的怂恿下,我打算在改版需求中使用 Auto Layout 来替换原有布局方式。但在需求开发刚开始时,那位熟悉 Auto Layout 的新同事突然离职了,我感觉失去了援手,但是我认可了这个技术,还是坚持使用它。期间碰到的苦难无数,布局思路带来了很多开发方式的改变,还有动画的结合会出现的各种效果不一致,其间公司老员工还不断劝我还是走老路比较稳妥。改版完后大部分主流程,包括首页发单、等待页、接单进行页都被改造成 Auto Layout。

更困难的事情还在后面呢,测试期间发现在 iOS6 上会出现各种崩溃、页面布局混乱、动画效果不一致等问题,我的 Bug 始终保持在 Bug 列表前十页。改 Bug 那些天,晚上调的眼发疼,深夜想的难入眠。线下 Bug 改完,上线后才是噩梦的开始,当时我们 App 的 iOS6 用户依旧很多,于是很多偶现崩溃被放大了,我的崩溃问题一直排在 Top1,虽然我很快找到了改好的办法,但是对于这几个偶现的问题还需要一个可靠可信服的解释,这样后面才能够让大家放心使用 Auto Layout。还记得当时周末坐在得实大厦窗户边的工位上,在查完和试完所有资料后依然无果时的无力感。本想着改回以前的 frame 布局算了,后又觉不甘。下几个周末跑到各大图书馆查看所有涉有 Auto Layout 的书,也是那个时候了解到了 VFL 语言。皇天不负有心人,WWDC 开始了,其中有个 Session 叫 Mysteries of Auto Layout,分为两个部分,把 Auto Layout 的原理讲得非常透彻了,至此,透过原理我也找到了问题的根因,并把他们记录在了文章中。这部分内容我还在一个沙龙做了分享,下面是当时分享的 Auto Layout 的原理部分的内容:

完整幻灯片参看这里

这些经验的总结在当时是非常新的,因为官方也是刚公布出其内部的原理,没有人能够更早的知道这些信息,估计也很少有人会考究这么多。有了这些由于一直坚持下来去找根因的经历才使得文章有了独特性。

当然,深入剖析 Auto Layout 这篇文章也加了 Auto Layout 的历史、生命周期、VFL 语言的介绍用来丰富内容的广度,以提升真实感,但你会发现独特性在这里显得尤为重要。

另外,在查找崩溃问题根因时,没有放弃,一直坚持的去找答案的过程也让我难忘。经常会听说到要去找自己热爱的事情,遵循自己所想。而实际上是那件热爱的事情是你愿意花很久甚至很多年需要克服痛苦,还能够继续忍耐,能忍他人所不能忍,赢过他人不是靠的热爱和能力,而是在万般艰难,别人都放弃而你坚持下来才赢的。巴菲特21年资产5000亿美元,其中4997亿美元是50岁之后赚到的,如果49岁那年他就不继续做了,那么他就不会有今年这样巨大的财富,就不会显示巨大的复利效应。

后来我还发现,不断坚持的一个窍门就是去庆祝大目标方向上的每个小小的成功,把这个小小的成功当成最后的成就那样去庆祝。

制作一个类似苹果VFL(Visual Format Language)的格式化语言来描述类似UIStackView那种布局思路,并解析生成页面

这篇文章 诞生的原因是我写了一个视图布局的库 AssembleView,通过这篇文章做了一个记录。这篇的独特性在于文章背后我特殊的经历。首先写 AssembleView 的起因在于之前大半年我使用自动布局写了大量的页面和一些动画,虽然有比系统更加简化的 Masonry 库可以使用,但是对于很早以前写过 H5 页面的我来说无论是从布局思路还有编写体验上,Masonry 依旧差的很远。苹果为自动布局发明的简洁 VFL 语言却没能用在更加先进的 UIStackView 布局思路上,于是在一次中午吃饭散步的过程中,我突然有了把 VFL 语言和 UIStackView 布局结合起来的想法,同时还想好了名字,叫做 AssembleView,也就是组装的视图的意思,心动不如行动,在接下来的一个需求周期中,我就着手一边开发 AssembleView 一边开发需求。每个需求只有一周的开发时间,当时需求只是更新评价的几个小页面部件,但为了将 AssembleView 运用进来,我把整个评价页面和功能进行了重写,包括标签云等复杂布局采用新库的重写。而这样的工作量仅在一周内完成了。

短时间完成 AssembleView 并应用到产品中,得益于 Deadline 的限制,设置时间节点,没有时间节点的目标那就是梦想,有了时间节点会让你保持一段时间专注,在限制的时间里,你没法去把事情做到方方面面都好,因此才会激发你,让你发挥自身的独特性,和别的不同,其实这种独特会让这件事情完成的更有价值。不要试着做最好的,而是力求做与众不同的。与众不同意味着创新,画草图和下笔写稿子都是创造的方式,这些过程不要去做雕琢、检查、取舍、反思这样的事情,而是释放自己的本能,去自由的发挥自己的积累和沉淀。艺术总是来自不完美,始于杂乱。

有了这样非同寻常的经历,使得这篇文章本身独特性的意义更大了。记录并分享,能够获得做着同样事情人的共鸣。

AssembleView 本身就是全新,因此从头到尾都是新意。

当时写这个库也是为了能够提高完成需求和维护需求的时间,有了精力才能够做更有趣有意义的事情嘛。五年后,苹果终于将 VFL 这种 DSL 语言运用 Swift 强大的 ResultBuilder 和不透明类型等特性进行了更好地完善,配合 Property Wrapper 和 Combine 还无缝衔接了先进的数据流架构,推出了 SwiftUI。

深入剖析 JavaScriptCore

《深入剖析 JavaScriptCore》 这篇文章要说独特性,那就是对 JavaScript 语言的好奇心。我很早就开始使用 JavaScript 来开发网站,工作和个人网站的前端都是依赖于这门语言,其实知情人都知道,选择 JavaScript 也是没有选择的选择。年轻时只顾着使用技术去做东西,也做了自己觉得非常有趣的程序,满足感十足,现在转向对其背后的机制技术好奇和感兴趣了。还有一个迫使自己去了解 JavaScript 引擎的原因是工作中做动态页面时需要用到对业务逻辑的解释执行处理。为了避免使用中出了问题会一脸懵,深入了解它显得很有必要。

光有想法是没有一点用的,JavaScriptCore 其实非常的庞大且复杂,当时能找到的大部分资料都是 Bridge 和 RN 的运用,好在开源了,了解内部的话还可以拉代码来看。但是直接埋进去看代码,代码量比较大,很容易 miss 掉其精妙之处。好在发现了 JavaScriptCore 项目核心开发者 Filip Pizlo,通过他的个人网站找到了大量 JavaScriptCore 的一手资料,没日没夜的啃内容,同时还试着动手去实现一些技术细节,最终了解和学习了很多解释器、虚机相关知识。获取一样东西带来的满足感是没有获取经验带来的满足感更深刻。我把学到的这些经验都记录在了这篇文章中,这使得文章的独特性更加深刻,真实感达到了满棚。

深入剖析 JavaScript 编译器/解释器引擎 QuickJS - 多了解些 JavaScript 语言

对于 JavaScript 引擎,我先前就看了 JavaScriptCore,为啥还要再去看 QuickJS 这个轻量的 JavaScript 引擎呢。写这篇文章动力主要还是对QuickJS如何使用精简高效的代码实现了那么复杂功能,还有极高的性能。QuickJS 基本是从头看到尾,一点一点的分析,整个过程也都记录了下来。但是我觉得记录源码的分析还不够,虽然这些分析使得文章的真实感很高,前提是读的人也会埋进代码里。为了提高文章的独特性和故事性,我在文章开头加入了一些 JavaScript 的一些背景内容,还有些当年使用前端技术的体会和经验。

只看代码不去修改和调试,往往会很枯燥,我在分析代码前,也写了些和 QuickJS 工程配置 makefile 相关的内容,并以 QuickJS 本身的 makefile 的用法进行举例说明。另外还手把手说明了怎么用 Xcode 来编译安装调试 QuickJS 代码,这些都是比较独特的内容。QuickJS 的核心代码基本都写在一个文件里,阅读分析时需要非常的专注,如果不专注,可能这篇文章也就没法写完。如果你花大量时间在家玩游戏、看电视剧和刷短视频,而不工作,那么就会有危机感和负罪感。但是如果会去工作,但是期间总会找着间隙去做其他事情,刷刷微博,看看朋友圈,瞅瞅新闻什么的,那就不会有负罪感,因为你会觉得你还是在工作着呢。没全力去工作,而在假装工作着,可比完全不工作的危害更大。一心一意的长时间去做工作外的事情,反而能够开眼界扩视野,从而反哺工作,工作的更好更开心。新时代就会有新机会,同时也会有新的要求,比如知识的获取从单一感官方式变成了动态的,多感官的方式。以前只是文字和图片的书和博客,新时代就是视频、直播和播客,新时代你有更多方式出现在大家面前,出现的更多就代表了成功。未来会有更多感官方式,而且更加的智能。获取知识的门槛低了,人群也就更广了,也可以理解为新的机会更大了,保持专注不设限去感受新时代,对自己不断做出新的要求,就会有新的机会。

深入剖析 iOS 编译 Clang / LLVM

这篇文章的原因主要来自在公司做的 App 安装包体积瘦身的事情,经过各种工具使用和分析后,总是找不到突破口。需求还在不断叠加,也没有好的思路。当你遇到困难时,做不成的人才会告诉你你也做不成,如果你真的想做成,有这个理想就要自己去守护他。遇到困难离成功才会更接近。那些困难来自于有限的资源,比如没人、没钱等,但是正是由于这些资源的限制,才会迫使你去创新,你会通过自己的热情还有毅力来寻找独特更有效的方法,所以由于有限资源带来的困难才会让你去突破、思变和进取。

就在百般无奈,各种资源条件受限的情况下。我想着看能不能把需要繁琐手动检查的动作试着写成程序自动完成。于是我用一个周末开发了查找无用方法工具,能够自动查找出工程中没有用到的方法,也兼顾了我们工程的一些运行时调用的方法检查。这样重复繁重的检查工作就变得轻松了很多。

工具开发完后,我发现这工具的实现并无相关成熟理论来进行支持,以后怎么完善和优化这个工具也没有一点思路。为此我还苦恼了蛮久的。

经过一位同事提醒,说大学有门编译原理的课里面就有讲怎么分析代码的,于是我就开始针对性的翻阅相关资料。于是乎,我发现了一片蓝海,这里面涉及到的技术不光是分析代码,还有很多以前不了解的程序怎么跑起来的细节,这里的知识就像可以无限递归的树,能够将你所有时间都吞没。这篇文章我更新添加内容的次数不下十次,每当get到了新的东西都忍不住记录下来。这期间动手去实践一些知识点,也遇到很多问题,解决这些问题的过程,对相关知识理解就更深入了。后来在17年的@Swift 大会上还做了 LLVM 相关内容的分享,下图是其中一张 Slide。

完整幻灯片参看这里

深入剖析 WebKit

为了完成网页到原生代码的转换,我开始学习 Web 的标准,而 WebKit 是苹果公司对 Web 标准实现,V8和 Flutter 渲染技术的源头,WebKit 的学习能够让我更完整的了解网页从请求到布局再到渲染的流程和使用的相关技术。WebKit的这篇文章我罗列了大量的 Web 规范资料,由于 WebKit 非常的庞大,架构也很复杂,文章里对架构也进行了详细的说明,对源码的结构做了详细的说明。全文按照一个页面从请求到最终渲染的流程顺序,依次对其关键环节里对应源代码和原理进行了详细的说明。完成这篇花费时间巨大,代码基本读了个遍。之后我对于前端技术有了更深的理解,特别是页面异步加载的流程和布局原理。

感觉这篇完全靠的是对前端技术的热情完成的。手冢治虫说过,那些投稿的人,都是热爱着漫画,把画出一部作品作为自己生命意义的人。所以他们才能获得成功,成为马拉松里跑到最后的人。热情可以增加25个 IQ 值。如果一个人仿佛开悟了的高僧,失去任何欲求、愿望、不甘、烦恨与伤痛,那么即使他去画漫画,即使基因再好,天赋再高,也只会画成佛教的禅画罢了。在比尔.布莱森在《人体简史》这本书中提到一个镜子相关的实验,实验来自一名防碎眼镜商人,在1980年创办了胚种精选择库(Repository for Germinal Choice),这个精子库只有诺贝尔获奖者和其他杰出知识权威的镜子。他想的是能够提供最好的精子生出天才婴儿,结果是在出生的200名儿童里,没有一个杰出天才,甚至连一个眼镜工程师都没能造出来,可见对做的事情有热情更加重要,而不仅仅只要基因好就行。

深入剖析 iOS 性能优化

性能这篇最重要的是独特性,开始只是针对日常开发性能需要注意的一些点进行了归纳总结,后来需要对启动项进行分析,于是做了分析的工具,其间我无意多查看了下 thread_basic_info_t 这个结构体里的字段,发现了 cpu_usage,觉得日后必有用,于是留了个心眼。后来负责性能的同学看了我的这篇文章,跑来找我,跟我说 App 连续几个版本都有线上反馈耗电太大,他们自己也很容易复现出来。这几个版本调整了定位频率,排查了各种怀疑的点,电量消耗依然很大。起初我也没有思路,instruments 也看不出问题来,于是我使用分析启动项的方法,查看运行中方法调用次数,排序来看谁调用的频繁,后来发现调用频繁的方法数量太多很难排查定位。

这时先前留意的 cpu_usage 字段起来关键的作用,通过定时刷新获取线程中 CPU 使用情况,连续高使用就揪出详细线程堆栈,后来小范围灰度上线检测,直接定位到了问题的堆栈,很快的解决了这个大难题。而且有了这个手段,后面也有了底气,在遇到问题也不会慌了,而且线下也可以使用这个方法进行压力测试,以免把问题带到了线上。这个方案也更新记录到了文章中,有了这个不寻常的经历,文章也就有了很强的独特性。

启动

关于启动我写了两篇文章。第一篇是《如何对 iOS 启动阶段耗时进行分析》,另一篇是《App 启动提速实践和一些想法》。说起这两篇的独特性,那绝对是独一无二。我负责的这场血淋淋的战役真可以说是毕生难忘。项目起因不用猜也可以想到,启动速度持续劣化,导致用户体验变差,落后对手一倍,提速困难重重。临危受命,当时想到的只有一个字,那就是干。

开始最难的还是定方向和定策略以及决策。明确了整体的思路,所有任务就开始并行跑起来了。由于项目的重要性不言而喻,因此投入资源巨大,不光是我的人都参与了进来,还有很多其他团队也一起加入。停下所有手上低优事情,握紧拳头全力打赢关键战役。要的就是能够速战速决,一旦拖延,不光是士气没了,结果没达成,还会留下一堆烂摊子难有资源去清理。

由于初期谋划的方案全面、稠密以及有效,多个团队通力合作配合奇佳,使得在三周内超预期达成了目标,不光是领先了对手一倍,还比大部分头部 App 都要快。这三周说长不长,说短也不短,大量的开发、调试、工具设计开发、数据分析、检测和验证工作集中式的进行,对体力和脑力都是极大的挑战,且压力巨大。

第一篇记录了前期的策划内容以及一些提效工具的开发过程。对这三周干的事情进行了沉淀,沉淀的是一次独特的成功经验。第二篇是在一年后写的,更多的是记录了这一年我对启动这件事情的思考,一年时间的经历也很多,还主负责过包体积的项目,所以内容就显得更加丰富了些,有记录些对性能和调试工具的研究。

第二篇文章里我提到我发现了一个宝藏男孩Michael Eisel,发现了很多二手资料都是源自他的博客。另外由于这一年也发现了性能防劣化中,自动化分析工具和能力相关技术了解的不够深入。于是专门去探索了下这方面的情况。对于目前为了保持双端一直 libimobiledevice,我发现了 Facebook 专门针对苹果系统开发的 idb,idb 做法明显更聪明些。

这些探究的过程至少是独特的。更独特的地方是文中写的那个A库多线程问题的排查经历。痛苦的经历我已在文中清晰详细的记录了,历时三天三夜,当大家试完所有情况,士气全无时,才柳暗花明又一村。全因苹果的一个 bug。经历这么一遭,对于 GCD 的队列排查定位问题难这点,我看国外对 iOS 并发开发方式吐槽的声音也很大,于是我很想了解多线程问题苹果未来会怎么处理。这就有了文中 Swift 并发提案部分的分析。当时这份提案还未进入正式流程(现在已经在 Swift 5.5正式发布了),未来并不明朗,我也担心会遗漏关键信息,于是对涉及相关的提案都进行了阅读,包括那些提案下所有的评论也都看了。

这两篇文章跨越了整整一年时间,这一年期间我基本没有写其他的文章,但是却沉淀了很多,所以第二篇实际上可写的内容非常多,一口气挑着重点的说了一大篇后,还删减了大量内容。写完第二篇我感觉到化繁为简的巨大好处。自己做的记录、素材和资料往往都是大量的,深究下去都是无穷无尽的感觉。因此需要从中提炼出自己的观点。从那么多内容中提炼出观点是需要足够的休息和放松,让你的潜意识主动来帮助你。这些休息和放松也可以是在日常的行为中,比如洗澡、去超市买东西、骑自行车、走路、锻炼、吃饭和睡觉等,特别是走路和睡觉持续时间长,最容易进入深度思考。不断给自己提问题进而更大量的阅读找答案,思考内在逻辑和联系。发散的找,专注的收敛提取观点,这样的观点是用钱买不到的。

通过大白话讲清楚,分享出去,这样的观点在他人接收时是自然地,意识不到其背后所花的时间和功夫,这就跟优秀的 App 一样,用起来是那么简单有效,丝毫不拖泥带水,用户也意识不到开发 App 所付出的脑动。这种化繁为简的过程也是将无序杂乱的东西清理掉,让你宝贵精炼的思想能够有地方存放。

灌篮高手中流川枫打篮球行云流水,天赋异禀。背后的努力谁又能知晓。我印象最深的一段是樱木花道为了取得晴子芳心,但始终技不如流川枫,总以为是天赋不够。一天晚上樱木花道很晚来到篮球馆,发现流川枫还在苦练,才发现原来白天看起来懒散傲慢的流川枫原来比谁都要刻苦,简单轻松从来都不是廉价的。

对于分享,有智慧的人都懂得给予越多收获越大。友情比金钱价值更高,就好像有一个开电影公司的朋友比拥有一家电影公司要好。分享不是要得到他人的认可,如果你知道这点,你拥有的能量就是无穷的,力量也是无敌的。

没怎么写过,那下一步怎么行动

看到这里,你一定会想“看你说了那么多,但我双手放在键盘前,脑袋还是一片空白,无从下手”。

如果想帮其他人,让他真的动手去做些什么事情,其实更应该是要让做这件事情变得容易很多倍,但方向是一样的,这样下次他就更好接受些。互联网开始发布内容门槛高,后来有了微博和朋友圈这种能够一句话就快速发布出去的产品后,大家发内容就比以前更多了。去读资料和文章,可以懂更多的知识,自身能力还是需要通过练习才能够有提升。想把事情做好,还是需要去做。

因此你应该更重视动手写,如果你不知道如何写,可能就不知道如何思考。有叛逆和逆向思维的人常常是爱问问题的人,爱自问爱思考,对那些已经共识正在运作的事物提出疑问,寻找和关注答案,这样才会有打破现状的意识。一些人小时候就能看到有这样的特点,因此在别人教你怎样怎样做时,不要太当回事,相信自己实践出来的答案。多听你喜欢人说话,多倾听,不断问还有没想说的。

还要从各种类型人那学习,甚至是和你观点不同的人。因为在每个人坚持的思想里,都会有他自己独特的经历和实践总结来的结论。通过他们的结论,你也可以自己去实践和验证形成自己的观点,这样就会有复利效应。做的结果其实并不重要,重要的是在做的过程中,你自己有没有变得更好。

你说的话,你的观点,你的评论都不能代表你,而是你所做的事情,花了很多时间做的事情那才是你。改变一个人的行为来改变思维,比改变一个人的思维来改变行为要容易的多很多。

因此,光看光听不动手写是没用的。那行动起来的话,怎么做更好些呢?

四个步骤

第一步,零散的想法、工作内容和看到的好的技术资料及时记录,先按照时间轴的方式记录。这一步是很容易操作的,几乎不用费脑,只需要机械的做记录就行,也不用考虑先前提到四个点里任何一个。

第二步,对于记录的内容进行分类,开始粒度可以粗一点,比如性能、架构、构建、编程语言、管理、成长、旅行和科技等,根据自身兴趣点和期望发展方向来就好。

第三步,做完一个项目,或者想对先前做的事情进行总结时,先一口气快速写出想表达的内容出来,这时写的内容体现出独特性,搭好骨架。然后针对写的内容中的一些技术点,进行真实感的完善。真实感的完善是需要很多素材和资料的,这时在第一步和第二步做的工作和积累就能够派上用场了。找到相关大分类进行细分来补充文章的血肉。

第四步,也是最后一步,可以充分发挥自己软实力和创造力,通过故事性和新意来披上文章的皮肤,让文章能够看起来更加完整和吸引人,提高阅读的体验。

完整完成这四个步骤并不容易,经常就会因为惰性半途而废。这时就需要 push 自己一把,方法的话,我这边的经验就是定目标,定时间节点。比如定好一个对外分享的时间,这样目标性更强,同时也有了约束和责任,自己的惰性在这一段时间内就能够得到很好的消减。

为了达成目的,彻底理清你想要啥,还需要清空干扰,方法很简单,除了当前最重要的事情,其它所有待做事情都记在备忘录里以便追踪防止遗漏。完成当前事情后,再去查看备忘录,然后定新目标新计划。

完成文章后可以通过下面八个问题来检查下文章的完成度。

  1. 我为什么做这件事?
  2. 谁已经做了?他们都是怎么做的?效果怎样?
  3. 我和他们做的不一样在哪?怎么想到的?能详细具体说出涉及相关知识点吗?(⭐️重点,写好了的话,其他问题可有可无)
  4. 我碰到了什么困难?
  5. 我怎么解决的?
  6. 做的有亮点吗?为什么是亮点?
  7. 做完后效果是怎样的?超预期地方在哪?
  8. 以后还有计划打算吗?为什么?

所用软件

下面是我写文章会用到的一些软件,以及我关注和用到的一些特性:

系统自带备忘录

  • 零散想法和灵感记录
  • 待做事项记录(一个一个直接删掉的感觉不错)
  • 聚焦想法思路,不用去考虑分类整理等

熊掌记

  • 本地文档管理(多设备同步收费)
  • 标签系统简化分类

Notion

  • 在线文档管理
  • 数据库方式管理,分类、检索和排序
  • 字段自定义添加,比如标签、类别、链接、标题等等都可以自定义
  • 基于数据库和自定义字段可生成看板、时间轴、日历、列表、表格、网格等不同视图样式查看。
  • 有chrome插件

VS Code

  • 本地文档管理(文件夹,Git支持可多端同步)
  • Markdown 插件支持(Markdown All in One、Pangu-Markdown、Markdown Preview Enhanced、Word Count CJK)

Obsidian

  • 本地文档管理(文件夹)
  • Markdown原生支持
  • 插件系统,比如有大纲和看板等插件可用
  • 双向链接与关系图谱

Procreate

  • 可以把在纸上的草图配上颜色

软件使用上,我会通过备忘录或熊掌记快速记录一些素材和想法,定期挪到 Notion 里,我是把 Notion 当做一个大仓库,写作的第二阶段整理分类我就是在 Notion 中完成的,充分利用 Notion 的自定义字段能力,对所有资料进行各种维度划分和归档。开始写文章时,初期会用 VS Code 来写,如果文章写长了就会打开 Obsidian 来继续写,主要是 Obsidian 的大纲效果比较好些。最后文章的配图我会使用 Procreate 来画,里面有辅助线,打开后可以很方便做参照,写图中文字就不容易偏了。

工具只是工具,记录的内容和自己的思想才是核心。我现在读书还是喜欢在纸上写笔记,特别有感触的才会提炼一些观点敲到备忘录中,比如我看了网飞(Nexflix)的《不拘一格》后提炼了一些观点做了记录,笔记如下:

制度都是围绕着怎么不阻碍所要的人发挥。比如假期自由安排、无审批、决策权非自上而下,而是在认识一致情况下松散耦合。要和不要什么样的人呢?不要的人:与人相处好,但能力平平工作狂,缺少判断力天资好,行动力强,但悲观、牢骚有才华的混蛋:特征听到赞美就自觉优秀对想法不明智的人,会进行嘲笑会侮辱天赋不如自己的人表现喜欢会上慷慨陈词,重复表达自己观点如没抓住他的要点,会打断别人的话别人发言,不赞同时会不听,做自己的事情别人啰嗦,没抓住要点,立刻打断总想着怎么做才能表现好,得奖金,缺少开放的认知空间为什么:管理花费精力多,讨论质量低,会排挤卓越员工。要的人:非凡创造力、工作出色(完成繁重任务)、合作好在放松状态下,会灵光乍现公司利益至上自觉追求成功,无论是否有奖金(已给予能力匹配市场最高价)当某一固定思维遇到瓶颈时,他总有办法摆脱瓶颈,或尝试不同角度看待问题在有才能,受爱戴的前提下,自己犯错大声说,成功小声说,让人感觉亲近、真诚和体贴。有良好的判断力为什么:优秀的人激励其他优秀的人,出色成果感染更多人才。只有公司里的员工都是上面提到的要的人时,公司的密度才高。这样的公司不是家庭而是专业运动队,运动队追求卓越,每个位置都是最佳人选;训练就是为了胜利,大家都能给予和接受反馈;成绩要好,不能只用努力就够了。书中详细介绍了网飞的制度由来,大量员工的实际案例,碰到了问题如何完善了制度。非常全面进行了制度介绍,甚至包含了进行创新的几个步骤的详细说明,还有网飞创始人里德是如何做到让大家认识一致的。最后是书中引用的小王子那段:如果你想造艘船,不要老催人去采木,忙着分配工作和发号施令。而是要激起他们对浩瀚无垠的大海的向往。

举个例子,怎么写这次WWDC21的见闻文章

光说不练,这样不好吧,那就现举个例子,看看怎么按照上面的四个步骤一步一步写一篇技术文章。那就以现在刚开完的 WWDC21 为主题,写个《WWDC21我的见闻》吧。

首先我们先做第一步,从 WWDC21 开始,我就将我看到的信息、还有看感兴趣 Session 中有用的点都记录了下来,只考虑是否要记,二不考虑其它任何事情。你可以看我WWDC21第一天的记录,我将其发到了我的博客和公众号上。后面几天我也不断的收集记录着零碎的信息。然后对这些记录进行分类。接下来再开始内容的撰写。

写 WWDC21 见闻录,你可以先想想着你想要什么内容,有没人提供,有的话可以直接链过来,没有的话可以自己去体会,去想,去经历,然后分享出来。

我会先写个总览,内容如下。

总览

WWDC21 官方通过一个页面汇总了发布的新技术,详见这里。WWDC21 里的代码范例官方都有提供和汇总,详见这里。WWDC21 期间苹果也列出了苹果公司之外围绕 WWDC 其它组织的学习、交流和娱乐的活动。

如果没有太多时间看 Session 视频,也可以直接看其他人的笔记,国外有WWDC NOTES,国内有老司机技术周刊的WWDC21 内参。往届内容也有人做了汇总

简单笔记可以查缺补漏,Alejandro Martinez 在这篇文章WWDC21 notes中对各种主题做了简单的记录,列出了关键字方便检索。

Session推荐

全部 Session,在这里查看。这里有份推荐清单。我也列了下我关注的 Session。如下:

SwiftUI 相关 Session:

Swift Concurrency 相关 Session:

DocC:

其它感兴趣的 Session:

Swift 的一些更新

Paul Hudson 的这篇What’s new in Swift 5.5? 已经把这些更新说的非常详细了,每个更新点都有对应的例子可以试。今年苹果公司推出 AttributedString 用来替代 OC 时代的 NSAttributedString。AttributedString 是值类型,可以直接在 SwiftUI 的 Text 里使用。AttributedString 还支持简单的 Markdown 语法,Markdown 单行没问题,多行功能受限。

DocC 是通过 Xcode 编译后生成的文档,使用 Product -> Build Documentation 就会生成DocC。在函数接口代码上使用 Shift+Cmd+A 快捷键就会创建文档模板,有参数和返回值的话也会将其提取出来,包括参数类型等,并生成标准文档格式,方便你进行内容编写。基本 Markdown 语法是支持的。详细的介绍可以看前面列出的官方 Session,或者看这篇文章How to document your project with DocC

今年重头戏 Swift Concurrency

ABI 稳定后,Swift 的核心团队可以开始关注 Swift 语言一直缺失的原生并发能力了。最初是由Chris Lattner在17年发的Swift并发宣言,从此开阔了大家的眼界。后来 Swift Evolution 社区讨论了十几个提案,几十个方案,以及几百页的设计文件,做了大量的改进,社区中用户积极的参与反馈,Chris 也一直在 Evolution 中积极的参与设计。

Swift Concurrency 的实现用了LLVM的协程把 async/await 函数转换为基于回调的代码,这个过程发生在编译后期,这个阶段你的代码都没法辨识了。异步的函数被实现为 coroutines,在每次异步调用时,函数被分割成可调用的函数部分和后面恢复的部分。coroutine 拆分的过程发生在生成LLVM IR阶段。Swift使用了哪些带有自定义调用约定的函数保证尾部调用,并专门为Swift进行了调整。

Swift Concurrency 不是建立在 GCD 上,而是使用的一个全新的线程池。GCD 中启动队列工作会很快在提起线程,一个队列阻塞了线程,就会生成一个新线程。基于这种机制 GCD 线程数很容易比 CPU 核心数量多,线程多了,线程就会有大量的调度开销,大量的上下文切换,会使 CPU 运行效率降低。而 Swift Concurrency 的线程数量不会超过 CPU 内核,将上下文切换放到同一个线程中去做。为了实现线程不被阻塞,需要通过语言特性来做。做法是,每个线程都有一个堆栈记录函数调用情况,一个函数占一个帧。函数返回后,这个函数所占的帧就会从堆栈弹出。await 的 async 函数被作为异步帧保存在堆上等待恢复,而不阻碍其它函数入栈执行。在 await 后运行的代码叫 continuation,continuation 会在要恢复时放回到线程的堆栈里。异步帧会根据需要放回栈上。在一个异步函数中调用同步代码将添加帧到线程的堆栈中。这样线程就能够一直向前跑,而不用创建更多线程减少调度。

Douglas 在 Swift 论坛里发的 Swift Concurrency 下个版本的规划贴 Concurrency in Swift 5 and 6,论坛里还有一个帖子是专门用来征集Swift Concurrency意见的,帖子本身列出了 Swift Concurrency 相关的所有提案,也提出欢迎有新提案发出来,除了这些提案可以看外,帖子回复目前已经过百,非常热闹,可以看出大家对 Swift Concurrency 的关注度相当的高。

非常多的人参与了 Swift Concurrency 才使其看起来和用起来那么简单。Doug Gregor 在参与 John Sundell 的播客后,发了很多条推聊 Swift Concurrency,可以看到参与的人非常多,可见背后付出的努力有多大。下面我汇总了 Doug Gregor 在推上发的一些信息,你通过这些信息也可以了解 Swift Concurrency 幕后信息,所做的事和负责的人。

@pathofshrines是 Swift Concurrency 整体架构师,包括低级别运行时和编译器相关细节。@illian是 async sequences、stream 和 Fundation 的负责人。@optshiftk对 UI 和并发交互的极好的洞察力带来了很棒的 async 接口,@phausler带来了 async sequences。Arnold Schwaighofer、@neightchan@typesanitizer还有 Tim Northover 实现了 async calling convention。

@ktosopl有很深厚的 actor、分布式计算和 Swift-on-Server 经验,带来了 actor 系统。Erik Eckstein 为 async 函数和actors建立了关键的优化和功能。

SwiftUI是@ricketson_@luka_bernardi完成的async接口。async I/O的接口是@Catfish_Man完成的。@slava_pestov处理了 Swift 泛型问题,还指导其他人编译器实现的细节。async 重构工具是Ben Barham 做的。大量代码移植到 async 是由@AirspeedSwift领导,由 Angela Laar,Clack Cole,Nicole Jacques 和@mishaldshah共同完成的。

@lorentey负责 Swift 接口的改进。@jckarter有着敏锐的语言设计洞察力,带来了语言设计经验和编译器及运行时实现技能。@mikeash 也参与了运行时开发中。操作系统的集成是@rokhinip完成的,@chimz提供了关于 Dispatch 和 OS 很好的建议,Pavel Yaskevich 和
@hollyborla进行了并发所需要关键类型检查器的改进。@kastiglione、Adrian Prantl和@fred_riss实现了调试。@etcwilde@call1cc实现了语义模型中的重要部分。

@evonox负责了服务器Linux 的支持。@compnerd将 Swift Concurrency 移植到了 Windows。

Swift Concurrency 模型简单,细节都被隐藏了,比 Kotlin 和 C++的 Coroutine 接口要简洁很多。比如 Task 接口形式就很简洁。Swift Concurrency 大体可分为 async/await、Async Sequences、结构化并发和 Actors。下面展开说下。

async/await

通过类似 throws 语法的 async 来指定函数为异步函数,异步函数才能够使用 await,使用异步函数要用 await。await 修饰在 suspension point 时当前线程可以让给其它任务执行,而不用阻塞当前线程,等 await 后面的函数执行完成再回来继续执行,这里需要注意的是回来执行不一定是在离开时的线程上。async/await 提案是SE-0296。如果想把现有的异步开发带到 async/await 世界,请使用 withCheckedThrowingContinuation。

async/await 还有一个非常明显的好处,就是不会再有[weak self] dance 了。

Async Sequences

AsyncSequence 的使用方式是 for-await-in 和 for-try-await-in,系统提供了一些接口,如下:

  • FileHandle.standardInput.bytes.lines
  • URL.lines
  • URLSession.shared.data(from: URL)
  • let (localURL, _ ) = try await session.download(from: url) 下载和get请求数据区别是需要边请求边存储数据以减少内存占用
  • let (responseData, response) = try await session.upload(for: request, from: data)
  • URLSession.shared.bytes(from: URL)
  • NotificationCenter.default.notifications

结构化并发

使用这些接口可以一边接收数据一边进行显示,AsyncSequence 的提案是SE-0298(Swift 5.5可用)。AsyncStream 是创建自己异步序列的最简单的方法,处理迭代、取消和缓冲。AsyncStream 正在路上,提案是SE-0314

Task 为一组并发任务创建一个运行环境,async let 可以让任务并发执行,结构化并发(Structured concurrency,提案在路上SE-0304)withTaskGroup 中 group.async 可以将并发任务进行分组。

Actors

我们写的程序会在进程中被拆成一个一个小指令,这些指令会在某刻会一个接一个同步的或者并发的执行。系统会用多个线程执行并行的任务,执行顺序是调度器来管理的,现代多核可以同时处理多个线程,当一个资源在多个线程上同时被更改时就会出问题。并发任务对数据资源操作容易造成数据竞争,以前需要手动放到串行队列、使用锁、调度屏障或 Atomics 的方式来避免。以前处理容易导致昂贵的上下文切换,过多线程容易导致线程爆炸,容易意外阻断线程导致后面代码没法执行,多任务相互的等待造成了死锁,block 和内存引用容易出错等等问题。

现在 Swift Concurrency 可以通过 actor 来创建一个区域,在这个区域会自动进行数据安全保护,保证一定时间只有一个线程访问里面数据,防止数据竞争。actor 内部对成员访问是同步的,成员默认是隔离的,actor 外部对 actor 内成员的访问只能是异步的,隐式同步以防止数据竞争。MainActor 继承自能确保全局唯一实例的 GlobalActor,保证任务在主线程执行,这样你就可以抛弃掉在你的 ViewModel 里写 DispatchQueue.main.async 了。

Actors 的概念通常被用于分布式计算,Actor 模型参看Wikipedia里的详细解释,Swift 中的实现效果也非常的理想。Actors 的提案SE-0306已在 Swift 5.5落实。

很多语言都支持 actors 还有 async/await,实现的方式也类似,actor 使用的不是锁,而是用的 async/await 这样能够在一个线程中切换上下文来避免线程空闲的线程模型。actor 还利用编译器,提前做会引起并发问题的检查。

actor 是遵循 Sendable 协议的,只有结构体和 final 类才能够遵循 Sendable,继承于 Sendable 协议的 Excutor 协议表示方法本身,SerialExecutor 表示以串行方式执行。actor 使用 C++写的,源码在这里,可以看到 actor 主要是通过控制各个 job 执行的状态的管理器。job 执行优先级来自 Task 对象,排队时需要确保高优 job 先被执行。全局 Executor 用来为 job 排队,通知 actor 拥有或者放弃线程,实现在这里。由于等待而放弃当前线程让其他 actor 执行的 actor,在收到全局 Executor 创建一个新的 job 的通知,使其可以进入一个可能不同线程,这个过程就是并发模型中描述的 Actor Reentrancy。

Swift Concurrency相关提案集合

所有相关提案清单如下:

学习路径

如果打算尝试 Swift Concurrency 的话,按照先后顺序,可以先看官方手册介绍文章Concurrency。再看Meet async/await in Swift这个Session,了解背后原理看Explore structured concurrency in Swift。动手照着试示例代码,看Paul的Swift Concurrency by Example这个系列。接着看Protect mutable state with Swift actors来了解 actors 怎么防止数据竞争。通过Discover concurrency in SwiftUI看 concurrency 如何在 SwiftUI 中使用,Use async/await with URLSession来看怎么在 URLSession 中使用 async/await。最后听听负责 Swift Concurrency 的 Doug Gregor 参加的一个播客的访谈,了解下 Swift Concurrency 背后的故事。

Swift Concurrency 和 Combine

由于 Swift Concurrency 的推出和大量的 Session 发布,特别是AsyncSequence的出现,以及正在路上的AsyncStream、AsyncThrowingStreamcontinuation提案(在Xcode 13.0 beta 3 AsyncStream 正式release),这些越来越多和 Combine 功能重叠的特性出现在 Swift Concurrency 蓝图里时,大家开始猜测是否 Combine 会被 Swift Concurrency 替代。关于未来是 Swift Concurrency 还是 Combine,我的感觉是,Combine 更侧重在响应式编程上,而响应式编程并不是所有开发人员都会接受的,而 Swift Concurrency 是所有人都愿意接受的开发方式,从 Swift Concurrency 推出后开发者使用的数量和社区反应火热程度来看都比 Combine 要大。在苹果对 Combine 有下一步动作之前,我还是更偏向 Swift Concurrency。

见闻写到这里,把独特性比作骨架,真实感比作血肉,故事性和新意比作皮肤,你会发现没有写出自己的经历的话,就像进击巨人里的那些小巨人,即使有了完整的皮肤,但骨头架子不大是不会有开头踢破大门的只有骨架和血肉的巨型大巨人那么强大且震撼有力。

那么接下来我就描写一些我在 WWDC21 期间独特的一些经历。

WWDC.playground直播活动

想想 WWDC21 过程中我还是有些经历,比如参加了苹果官方推荐的外围活动WWDC.playgournd by SwiftGG

连续看了5天活动直播,还参加了一天的 Live Coding 介绍 SwiftUI 的新特性。直播 Live Coding 准备的时间很少,而且以前我还没有现场当着几千人面写代码的经历,直播前一天晚上赶着通宵达旦看完了相关 Session,写了些代码样例测试,当天白天还开了一个很长的会,回家前和同事讨论一个技术问题时,我发现我嗓子还哑了。到家坐在桌前脚还抽筋了,你可想象到我当时内心有多崩溃。

在直播前,我还专门的给思琦先演练了一遍,其中在介绍 AsyncImage 处理失败、空白、成功还有默认情况时,编译器报错提示无法找到原因,还提示让我提交 bug 的错误信息。直播开始前一直没有找到原因,重新敲了一遍才解决,所以心里没底,直播开始时还一直担心这个问题会重现。直播时在写到这段时果然编译器错误又出现了,当时我脑袋一片空白,心中大呼救我。好在没多一会我突然发现先前一段演示的 placeholder 接口没有删掉,原因真的就是这个,删掉后就正常了,别提有多开心了。后面就轻松了很多。由于只有一天时间准备,很多内容准备了,当时一边敲代码一边说也漏说了很多,比如 AsyncImage 使用的是 URLSession,用的是 URLCache,还不能自定义缓存。Refreshable 只能用在 List 里。SwiftUI 和数组绑定的代码是可以兼容前一个版本的。

另外还有个 WWDC 期间很火的老系统UI挑战赛让我印象深刻,其中有个18岁小伙用 SwiftUI 开发了经典 iPhone4可用版本最火爆,Github 地址在这里

SwitUI 新特性太多了,直播没提到的还有 task modifier、separator、macOS 上的 table、Canvas、preview in landscape、@FocusState、more button 等等。当时直播有回放,可以在这里看。更完整详细介绍建议看前面提到 SwiftUI 相关 Session。

WWDC.playgournd 最后一天直播有场 WWDC21 学生挑战赛获奖者张紫怡的分享,她分享了怎样准备挑战赛的过程,通过详细的过程介绍,心得体会,还有思考,让大家了解到了她的热情和才华,而且分享的形式和效果非常有新意。最后一场的回放看这里。看完这场后,我打算在19号 SwiftGG 和快手中学合办的 WWDC 沙龙活动中使用一种不同的方式进行分享。原先打算的是使用先前写好的一个示例展示使用 SwiftUI 开发复杂应用如何快捷,同时介绍背后的技术。几天想来想去,反复推敲推翻,一直没有新思路。最后到了前一天,我有了个主意,可以使用 SwiftUI 来编写一个幻灯片程序来分享 SwiftUI 的内容啊,同时还能够分享这个幻灯片开发过程心得,这样才有独特性和真实感嘛。于是把准备了一年的内容都删了,就像当时启动那篇删得只剩一万字的文章一样,那篇文章发布前共删掉了四万个字。

WWDC沙龙活动

可想法总是很容易,实践起来却又是另一种情况。我对自制幻灯片的初步设想是第一能够前后翻页展示内容,第二能够支持和 Keynote 不一样的动画效果和页面美化,第三能够直接在幻灯片上进行一些 SwiftUI 功能的交互演示。

接下来就要开始实际去做了,我先拿出上周用铅笔在 A4 纸画的人草图加工来丰富展示,发现加工的时间来不及了,虽然现在加工速度比以前快了,但是时间太紧,还要写幻灯片程序呢。SwiftUI 开发确实快,每个页面我都写成一个 View,标题、大纲和示意图的组合我做成了通用 View,通过传入不同标题、大纲数组和图片数组来展示不同页面的内容,定义一个 ObservableObject 的类 GlobalStateInfo 作为 View Model 来存储需要的状态数据,比如当前在哪页,当前文字颜色,当前页背景颜色等,每个 View 里使用 @EnvironmentObject 就可以去获取和设置 GlobalStateInfo 了。

关于为了传递数据,是直接调用 EnvironmentObject,还是通过子视图传递 ObservedObject,两种方式哪个更好,在 WWDC21 的 Digital Lounges 里,苹果工程师的回答是两者用途不同。当大部分 View 都需要用到一些通用数据时,推荐使用 EnvironmentObject,因为没有实际使用 ObservableObject 的 View 不会被与之相关的代码搞乱。如果模型不是基于 View 层次结构的对象图,使用 ObservedObject。另外还有个 Digital Lounges 的问题,是问怎么从旧的 AppDelegate/SceneDelegate 生命周期转换到新的 SwiftUI 2 生命周期。苹果工程师说可以使用 UIApplicationDelegateAdaptor 属性包装器,SwiftUI 将实例化你的 UIApplicationDelegate 的一个实例,并以正常方式调用它。更详细的解答和其他的话题可以参看这篇SwiftUI Lounge QAs,内容都是 roblack 从 Digital Lounges 里摘出来的,WWDC21 那几天我也在 Digital Lounges(报名早)看大家和苹果工程师的互动,后来看别人说 Digital Lounges 的 SwiftUI 那场爆满,已经超负荷运转了,感觉苹果最近变得更开放了,很多苹果工程师都开通了 Twitter 账号在 WWDC 期间积极和大家互动。

为了使页面不单调,我打算每页大纲的颜色做些区分,发现11页每个都配一遍看效果时间太紧,于是我选择了一些背景色通过随机读取,每次看到的颜色都是不同的,由于都是一个一个手动选出来的,所以不同组合效果也不会太差。

现在前后翻页展示内容这个想法是完成了,这也是 SwiftUI 开发的优势,能够快速构建页面架子和简单的数据页面同步设置。但是第二个想法,完成起来就非常费时费力且不那么顺利了。

首先说下字体,系统默认字体很正式,以往我都是直接用 iPad 手写,但是这次时间紧没法一个字一个字的写了,所以我打算选择其它字体,View 的 .font 修改器可以选择其它字体,方法是 .font(Font.custom(“font-name”, size: 110))。如果直接在Finder里查看字体没法得到可用的字体名,需要使用 NSFontManager.shared.availableMembers 来获取可用字体名。

接下来是标题,以往做幻灯,经常讲到具体内容时,特别是细节时,容易让看的人忘记当前页主题是啥。如果标题太大,可展示内容就少了,及时这样,观看的人也容易忽视主题。因此,我打算把标题做成一个循环的动画,这样就可以在我展开说内容的时候,看的人即使走神了还能够注意到当前页主题。标题的动画主要是控制好动画的时间,不能太快,不然会过于吸引注意。

以前 keynote 的转场动画我基本都试过,每次来回都是那些,很难和其他人做出差异来。只能靠图和配色作区分。这次我利用每页的内容大纲进入效果来作为转场动画。我先将大纲列表放到 VStack 里,ForEach 里获取到下标,通过下标获取列表数组里的 Text View。之所有要得到下标而不是直接获取列表数组里的 Text View,其原因是还会将这个下标用在转场动画效果上,我希望大纲列表的内容是一个接一个进来的,需要这个下标值来做时间间隔。Animation 的效果使用的是 interpolatingSpring,我将 damping 参数设置为0.3,这样弹性效果更佳。列表内容进入的是 GeometryEffect 协议,用来替代 AnimatableModifier,通过 AnimatablePair 来设置移动位置新旧值。直接一个方块滑入略显单调,使用 CGAffineTransform 里的 c 参数可以设置将矩形进行变形,会有一种被拉进来的感觉。变形过程配合滑入动画再加上 interpolatingSpring 设置的弹性效果,会让转场更有动感。

并行执行的动画越丰富,转场感觉就会更好,我想着每页都做个不同的效果,使用 Shape 绘制一些图形做背景动画,这样会有新鲜感。当第一页和第二页弄完后已经天亮了,经过一个上午,下午就要分享了。我还没有困意,因为后面还有那么多页面没有做完区分转场的动画和配色,更别说 SwiftUI 功能的交互演示了。而且具体分享的内容我还没有整体串一遍逻辑。一天一夜完成这个项目时间还是太紧,当时想着要再能多一天时间就好了。2点开始分享,1点我在旁边一个小会议室把整个内容自己在心里试着说了一遍。分享内容包括了自制低版本兼容 AsyncImage 演示、SwiftUI 那些版本兼容问题、SwiftUI 背后关键技术简介、SwiftUI 生命周期、布局、Modifier、不透明返回类型、属性包装、Result Builder、Geometry、Preview用的技术。

其中 SwiftUI 内部运作的机制是每个 View 都有自己的 Identity,SwiftUI 会将给 State 和 StateObject 分配内存空间的 Storage 和 View 的 Identity 绑定起来,共存亡。当相同 Identity 的状态数据发生变化了或者和 View 依赖关系改变了,就会重新建立 View 和 RenderNode 的依赖关系,他们之间的关系是图结构,图结构可以降低依赖关系检查复杂度。最后渲染出来。总的来说 SwiftUI 运行原理有三个点最重要,Identifier、生命周期和依赖。视图的生命周期是 Identifier 来决定的,state 生命周期和视图的生命周期是相同的。在生命周期中,state 有变化的时候会做diff,diff和渲染效率提升是使用图型依赖结构,只渲染状态依赖的视图,如果按照 UIKit 那样的树形结构做diff,效率会特别差。

现在很多常用开源库都已经对SwiftUI做了适配,苹果公司自己的App,比如天气、相册、快捷指令、地图和相册都有用到SwiftUI。以下是SwiftUI用到的语法特性:

  • ResultBuilder
  • ViewBuilder
  • Trailing Closure
  • Opaque Type
  • Inline
  • PropertyWrapper
  • KeyPath
  • DynamicMemberLookup

如果你使用这些特性也能够再造一个兼容低版本的类似 SwiftUI 的框架。SwiftUI 最显现的 DSL 技术使用的就是 ResultBuilder 语法特性,Result Builder的提案SE-0289 里有详细的描述,通过 Result Builder 下面的方法可以自定义出一个简洁的 DSL 出来,提高特定业务开发效率。

  • buildBlock:构建基本语句的block组合结果。
  • buildExpression:可选,给表达式提供上下文类型信息。
  • buildOptional:对没有else的if语句支持。
  • buildEither:构建选择语句不同结果。通过条件结果折叠成一个结果,实现对if-else和switch语句的支持。
  • buildArray:将所有迭代结果合并成一个结果的方式实现对for…in语句的支持。
  • buildFinalResult:可选,可以调用顶层函数体的结果进行处理,产生最终的返回结果。
  • buildLimitedAvailability:会在if #available的block部分结果上调用,使result builder可以擦除类型信息。

这次的 WWDC 还专门有个 Session 讲解了怎么用 Result Builder 来做 DSL,这个 Session 是 Write a DSL in Swift using result builders

Swift 视图返回的类型是不固定的,因此使用了 Swift 的不透明类型语法特性来进行支持,支持其返回带有大量泛型参数的庞大类型,这个类型中还包括了 Result Builder 中的 if 条件类型值,支持多分支类型。Opaque Types的提案在这里SE-0244

跟着视图后面的点语法是 modifier,每个 modifier 都会在视图树中新建一个层,因此 modifier 的写的先后顺序不同,效果是不一样的。

对于数据的监听和响应使用的是 swift 里的属性包装语法特性,属性包装的提案是SE-0258。包装后数据的使用就方便了很多,对于不同属性包装类别的选择可以按照数据类型和应用场景来,对于值类型,如果是只读的数据可以什么都不加,如果数据是可读写的,使用@State,如果数据是需要在其他视图进行读写并自己也同步响应的,使用@Binding进行声明。对于对象类型的数据,指向对象的引用能发生变化要用@ObservedObject来声明,引用不可改变,那么就用@StateObject,使用环境传递对象用@EnvironmentObject。

完整的 WWDC沙龙活动回放可以扫下图中的二维码:


下面是当时现场演示的部分幻灯片,动画的效果可以看上面的视频回放:











至此,这篇WWDC21见闻就写完了,详细描写自己WWDC21期间的一些独特经历和其中涉及相关技术,这样会让文章的独特性和真实感有很大的提升。

对独特性和新意的思考

通篇看下来,你是不是感觉到故事性和真实性其实是非常容易做到的。可以理解为只要努力些,时间再长些,这两点就能够完成,且能线性得到提高。只埋头做事情比较容易和舒适,但一直这么干,熵就会越来越多,不可逆的无用能量无法排除。而独特性意味着你会去体验适应新的环境,去获取实践新的认知,去结识新的朋友碰撞新的思路,使得自己体验到不同以往的经验。新意成功几率很低,非线性的,类似于基因突变产生的进化,这和努力无关。新意和独特性一样属于逆熵过程,不能忽视,大跨步的进步需要对传统的颠覆。新意会带来新的独特经历形成一个新的循环,不去尝试就不会有新的机会。

如果把本文当成一篇笔记,其间又融入了写作心得;如果把本文当做一篇写作心得,其间又穿插了大量笔记内容。你说这是不是也是一种新意呢。

对于新意,我印象最深的还是权力的游戏的血色婚礼,神来之笔,当Joffrey正最可气,少狼主正得势时,剧情完全打破传统,效果非常震撼。凡人皆有一死,凡事皆有可能,于是乎对后面剧情的推进更加期待了。而这个新意是建立在整个剧对真实感上的毫不含糊,包括了扎实的世界观构建,服饰道具高度的还原,完全把观众带入了故事中。另外作者对古历史的专研和记者经历的结合产生出的鲜明的人物刻画和独特的剧情设计也是本剧的骨架支撑,独特性的体现。

Finally

今天我说的这些心得可以作为下笔“记录和分享”技术的一个契机,但是对于自己技术的成长,写文章并不是最终的目的,写作是你对自己思想的研究和开发。文章的上限是你的技术能力,文章只是让人了解你技术一种手段。因此更重要的是你做的技术是否有突破有演进,获得应用,并在产品中取得了好的效果。还有那些孤独着研究技术的时光,经历着一直努力着奋斗着却一直不被看见,得不到认同,也没有结果的岁月,还能够一直被自己的热情感动而不放弃去取得一点点进步带来的满足感。

❌
❌