普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月1日iOS

Harbeth:高性能Metal图像处理库,让你的图片处理速度飞起来!

2026年4月1日 10:40

🎯 项目简介

Harbeth是一个基于Metal的高性能图像处理库,为iOS和macOS开发者提供了一套简洁、高效的图像滤镜解决方案。它不仅支持传统的图像滤镜效果,还能处理HDR图像,让你的应用在图像处理方面如虎添翼。

用故障艺术美学建立动态RGB通道分离 实时检测边缘并添加霓虹灯发光效果
ShiftGlitch.gif EdgeGlow.gif

✨ 核心功能与优势

1. 高性能Metal渲染

  • 利用Metal GPU加速,处理速度比CPU实现快10-50倍
  • 支持命令缓冲区池管理,优化GPU资源使用
  • 双缓冲技术,进一步提升处理性能

2. 丰富的滤镜效果

  • 基础滤镜:亮度、对比度、饱和度、色相调整
  • 高级效果:高斯模糊、锐化、边缘检测、色调映射
  • 创意滤镜:复古、赛博朋克、电影效果、HDR增强
  • 自定义滤镜:支持自定义Metal着色器

超过 200+ 内置滤镜,组织成直观的类别,涵盖从基本颜色调整到高级艺术效果的各种功能

3. HDR图像处理

  • 支持rgba16Float和rgba32Float格式的HDR纹理
  • 内置HDR到SDR的色调映射算法
  • 保留HDR图像的细节和动态范围

4. 易用的API设计

  • 链式调用风格,代码简洁易读
  • 统一的输入输出接口,支持多种图像类型
  • 异步处理支持,避免主线程阻塞

5. 跨平台支持

  • 同时支持iOS、macOS和tvOS
  • 适配不同设备的Metal性能特性
  • 自动处理设备内存限制

🚀 快速开始

安装方式

// Swift Package Manager
.package(url: "https://github.com/yangKJ/Harbeth.git", from: "0.0.1")

// CocoaPods
pod 'Harbeth'

基础使用示例

import Harbeth

// 加载图像
let image = UIImage(named: "example")!

// 创建滤镜
let filter = C7Brightness(brightness: 0.2)

// 应用滤镜
let result = try? HarbethIO(element: image, filter: filter).output() as? UIImage

// 显示结果
imageView.image = result

链式滤镜示例

// 组合多个滤镜
let filters: [C7FilterProtocol] = [
    C7Brightness(brightness: 0.1),
    C7Contrast(contrast: 1.2),
    C7Saturation(saturation: 1.3),
    C7GaussianBlur(radius: 2.0)
]

// 应用滤镜链
let result = try? HarbethIO(element: image, filters: filters).output() as? UIImage

🎨 高级特性

1. 自定义Metal着色器

// 创建自定义滤镜
struct CustomFilter: C7FilterProtocol {
    var modifier: ModifierEnum {
        return .compute(kernel: "customKernel")
    }
    
    var factors: [Float] = [0.5, 0.5, 0.5]
}

// 应用自定义滤镜
let customFilter = CustomFilter()
let result = try? HarbethIO(element: image, filter: customFilter).output() as? UIImage

2. HDR图像处理

// 加载HDR图像
let hdrImage = UIImage(named: "hdr_example")!

// 应用HDR到SDR转换
let hdrFilter = HDRToSDR()
let result = try? HarbethIO(element: hdrImage, filter: hdrFilter).output() as? UIImage

3. 实时处理

// 实时处理相机捕获的图像
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    // 异步处理
    HarbethIO(element: sampleBuffer, filter: C7Vibrance(vibrance: 0.5)).transmitOutput { result in
        switch result {
        case .success(let processedBuffer):
            // 处理成功,显示结果
            DispatchQueue.main.async {
                self.previewLayer.enqueue(processedBuffer)
            }
        case .failure(let error):
            print("处理失败: \(error)")
        }
    }
}

⚡ 性能优势

Harbeth在性能方面的表现令人印象深刻:

  • 处理速度:比Core Image快3-5倍,比CPU处理快10-50倍
  • 内存使用:智能纹理池管理,减少内存分配
  • 电池消耗:优化的GPU使用,降低能耗
  • 大图像处理:支持处理高分辨率图像和视频帧

📱 适用场景

Harbeth适用于各种需要图像处理的场景:

  1. 照片编辑应用:快速应用滤镜效果
  2. 相机应用:实时预览和处理
  3. 视频编辑:逐帧处理视频
  4. AR/VR应用:实时图像处理
  5. 社交媒体:快速滤镜效果
  6. HDR图像处理:专业图像处理

🖥️ macOS 支持

Harbeth 完全支持 macOS 平台,为桌面应用提供强大的图像处理能力,打造原生、优化的用户体验:

🎨 macOS 展示

探索 Harbeth 在 macOS 上的强大功能:

🌟 总结

Harbeth是一个功能强大、性能优异的Metal图像处理库,它不仅提供了丰富的滤镜效果,还支持HDR图像处理,为iOS和macOS开发者提供了一套完整的图像处理解决方案。

无论是快速原型开发还是生产环境应用,Harbeth都能满足你的需求。它的高性能特性让图像处理不再成为应用的性能瓶颈,而简洁的API设计则让开发过程更加愉快。

如果你正在寻找一个强大而灵活的图像处理库,Harbeth绝对值得尝试!

📁 项目链接

  • GitHub: github.com/yangKJ/Harb…
  • 文档: 详细的API文档和使用示例
  • 示例应用: 包含多种使用场景的示例代码

让Harbeth为你的应用添加绚丽的图像处理能力,让每一张图片都成为艺术品! 🎨✨

聊聊我最近都干了些什么,AI 时代的手动撸码人

作者 season_zhu
2026年4月1日 09:47

前言

许久未更新内容了,除了被公司的项目倒腾、拉扯之外,其实最近几个月还是干了许多事情的。我就随便聊聊吧。


一、RxStudy 项目尝试同时集成Flutter模块与UniApp模块

其实这个尝试,没有使用AI的功能,完全就是我自己无聊做的一点尝试,我将自己的UniAppPlayAndroid打包成为wgt,然后把GetXStudy项目的Flutter模块全部都集成到RxStudy项目,做了一个超级大杂烩,并且尝试几个端的通信,大家看看效果。

项目截图

玩安卓 原生 Flutter UniApp.gif

二、RxStudy 项目从 CocoaPods 向 Tuist 迁移

CocoaPods 停止维护的消息,iOS 开发者应该都有所耳闻。趁着这个机会,我拿自己 2019 年就开始维护的 RxStudy 练手项目做了一次大迁移。

迁移方案:CocoaPods → Tuist + Swift Package Manager (SPM)

迁移耗时:前前后后大概 3天(从开始到项目能跑起来)。说实话,本来以为会花费更多时间,没想到有了 AI 的帮助,大概就花了这么点时间就搞定了,大大出乎我的意料。

迁移内容

  • Project.swift 定义项目结构、Targets、SPM 依赖引用
  • Tuist/Package.swift 管理 20+ 第三方库的 SPM 版本
  • 本地 Package 封装(HUD、网络请求封装、工具类、路由框架)
  • 双 Target 架构:RxStudy(UIKit + RxSwift) 和 SwiftUIApp(SwiftUI)

![Tuist + SPM 架构图]

AI 表现:大部分时间花在 Tuist 配置文件的编写上,AI 生成的代码基本可以直接使用,复盘时发现主要还是项目结构本身比较规范。


三、RxStudy 项目从 UIKit 向 SwiftUI 迁移

在完成 CocoaPods 向 Tuist 迁移后,我又给 AI 安排了一个新任务:把 RxSwift 里的 UIKit 代码向 SwiftUI 进行迁移。

迁移策略:采用双 Target 并行架构,而非一次性替换

Target 技术栈 说明
RxStudy UIKit + RxSwift 原有代码
SwiftUIApp SwiftUI + @Observable + async/await 新迁移代码

迁移结果:SwiftUIApp 这个 Target 里的代码,95% 都是 AI 写的,我只是给出了部分建议,以及尝试在两个 Target 中复用网络请求层代码。

迁移模块(共10个):

模块 功能
Home Banner + 文章列表
Project 项目分类
PublicNumber 公众号
Tree 体系结构(二级树形)
Mine 用户中心
Login 登录
Collect 收藏列表
Coin 积分明细
CoinRankList 积分排行榜
Search 热搜 + 搜索结果

技术栈变化

类型 迁移前 迁移后
状态管理 RxSwift (RxSwift 6.9.0) @Observable + async/await
状态绑定 RxCocoa SwiftUI 原生
网络层 Moya + RxSwift Moya + async/await

说明:SwiftUI 迁移没有使用 Combine,而是使用了 iOS 17+ 的 @Observable 宏和 Swift 的 async/await,代码更简洁。

项目截图

ScreenRecording_04-01-2026 09-38-01_1.gif

AI 表现:对迁移的功能表示满意,尤其是网络请求层的复用处理得不错。不过 SwiftUI 部分复杂的交互动画(比如下拉刷新 + 列表滚动 + 头部视差效果),还是需要自己动手调整。


四、UniAppPlayAndroid 小程序 Vue2 向 Vue3 升级

实际上我很久之前写过一个 UniApp 版本的玩安卓,只是很久没有维护了。由于我想把这个 UniApp 打包的 wgt 文件在 HarmonyOS Next 里通过小程序运行,但 uniCloud 环境仅支持 Vue3 版本的小程序。

想着 AI 不用白不用,于是让它帮我进行迁移。

迁移耗时:大约 2小时 完成全部迁移。

技术栈变化

类型 Vue2 Vue3
Vue 2.x 3.4.21
状态管理 Vuex Pinia 2.1.7
构建工具 webpack Vite 5.2.8
uni-app 旧版本 3.0.0-alpha
页面写法 Options API Composition API

支持平台

平台 状态 说明
H5 使用 Vite 代理解决跨域
微信小程序 完全支持
Android App 可编译 wgt 热更新包
iOS App 可编译 wgt 热更新包
HarmonyOS Next 存在 WebView bug,使用条件编译规避

典型问题与解决方案

问题 解决方案
根目录缺少 index.html 创建 Vue 3 入口 HTML
uview-plus 样式找不到 改用原生组件
可选链 ?. 不支持 替换为 && 短路求值
CORS 跨域 Vite devServer 代理
HarmonyOS WebView 崩溃 使用条件编译显示占位页

AI 表现:18个页面全部迁移完成,有完整的迁移文档和迁移指南。迁移过程中遇到的一些边界问题,AI 给出的解决方案都比较合理。


五、HarmonyStudy 项目 HarmonyOS Next 代码 5.0 向 6.0 迁移

让 AI 将项目从 5.0 向 6.0 迁移,它顺便把一些第三方库也帮我进行了迁移和升级。

路由系统 API 重大变更

// 5.0 (已废弃)
router.pushNamedRoute({ name: 'pageName', params: {} })
router.getParams()

// 6.0
router.push({ uri: 'pages/pageName', params: {} })

LoadingDialog 兼容性问题

  • 5.0:CustomDialogController 必须在正确的 UI 上下文中创建
  • 6.0 解决方案:引入 @jxt/xt_hud 库,通过全局 UIContext 初始化

第三方库依赖

版本 说明
@ohos/axios 2.2.7 HTTP 网络请求
@pura/harmony-utils 1.4.0 工具库
@jxt/xt_hud 3.4.0 Loading/Toast(6.0 新增)
@ohos/imageknife 3.2.8 图片加载缓存

项目截图: 配合上面UniAppPlayAndroid的Vue2到Vue3的升级,我终于可以在打包好的wgt文件在HarmonyOS Next正常运行起来了。

录屏2026-04-01 09.17.53.gif

AI 表现:路由迁移采用了最小改动方案,保留兼容性。AI 还顺便优化了 Router 类的实现,并完成了 Network HAR 模块的封装。


六、GetXStudy 项目优化代码

我个人觉得这个 Flutter 项目可以优化的地方有限,但 AI 还是给了一些不少的中肯意见,没事就让它跑跑,还是做了不少提交。

优化内容

优化项 详情 状态
修复废弃 API MaterialStateProperty → WidgetStateProperty ✅ 已完成
替换 print 8处 print → logger.d() ✅ 已完成
图片压缩 launchImage.png 4.9MB → 可压缩 70-90% ✅ 已完成
Git Hooks 添加 pre-commit 自动化检查脚本 ✅ 已完成
清理导入 5处未使用的 import 移除 ✅ 已完成
密码安全 明文存储 → flutter_secure_storage 加密 ✅ 已完成
网络缓存 减少约 60% 重复请求 ✅ 已完成
异常处理 统一 ErrorHandler 工具类 ✅ 已完成

AI 表现:提供了详细的优化报告(OPTIMIZATION_REPORT.md、ADDITIONAL_OPTIMIZATION.md),优化效果量化可查。


结论

AI 使用组合:Claude + MLG 4.7 和 Claude + MiniMax 2.5

实话实说

  • 对于 AI 的使用我并不算特别多,MLG 是试用了同事的,后来 MiniMax 因为个人原因买了 490 元的套餐
  • AI 确实解放了不少生产力,比如自己有的时候不太想写的代码,或者需要阅读理解的旧代码
  • 对于迁移、分析这种事情 AI 表现不错
  • 对于移动端开发,如果你写好一个模板,让它按照模板写一些功能与业务它也接得住
  • 不要期望它写过于复杂的交互就可以

几个项目的共同特点

  • 都是基于 WanAndroid 开放 API 的客户端
  • 都是一个人维护的个人项目
  • 都经历了较大的技术架构升级

个人感悟:时常在想,就这么付费上班,是不是也挺肉疼。后来想想,上班没那么累,下班可以正常走,也算行吧。


附录:项目地址

项目 GitHub 地址 相关分支
RxStudy (iOS) seasonZhu/RxStudy refactor/tuist-migration (CocoaPods→Tuist)
refactor/swiftui-migration (UIKit→SwiftUI)
develop_flutter (集成Flutter、UniApp模块)
UniAppPlayAndroid (跨平台) seasonZhu/UniAppPlayAndroid develop_vue3 (Vue2→Vue3)
HarmonyStudy (HarmonyOS) seasonZhu/HarmonyStudy develop_os6 (5.0→6.0)
GetXStudy (Flutter) seasonZhu/GetXStudy optimize-project (代码优化)

作者 GitHub@seasonZhu

昨天 — 2026年3月31日iOS

LottieConverter:一键生成 .lottie 文件

2026年3月31日 20:48

现在的lottie有了自己的文件格式:.lottie,实际上就是json文件和多张图片的压缩包(可以执行unzip命令查看)。

由于现在项目的lottie资源基本都是「json文件+多张图片」的文件夹组合,存储和远程加载方面确实不太方便,既然新版本支持单文件形式,那肯定跟进啦。

只不过项目的lottie资源实在太多了,让设计重新出资源有点费劲,于是乎弄了个转换脚本,但我还是觉得不太方便,最后让GPT帮忙参考脚本的实现写了个Finder扩展:LottieConverter

example.gif

只需要对lottie文件夹点一下右键,选择"Convert to .lottie"就好了。

下载地址:LottieConverter

  • 支持.lottie和lottie文件夹的切换
  • 支持批量处理

Win11 抓包工具怎么选?网页请求与设备流量抓取

2026年3月31日 17:23

在 Windows 11 上抓包时,问题一般是当前任务应该用哪一类工具

同样是抓包,不同目标差异很大:

  • 想看浏览器接口
  • 想调试 App 请求
  • 想抓手机流量
  • 想分析 TCP 连接

如果一开始选错工具,就会在配置上面搞半天


一、只想看网页接口就先不用装工具

如果目标是查看网页请求和分析接口返回,直接用浏览器即可。


操作步骤(Chrome / Edge)

  1. 打开网页
  2. 按 F12
  3. 切换到 Network 面板
  4. 刷新页面

可以看到什么

  • 请求 URL
  • 请求方法
  • Header
  • Response

验证

点击某个按钮(例如登录),Network 面板会新增请求。

可以直接点进去查看参数。


二、需要修改请求或重放接口

浏览器工具只能查看,不能灵活修改。

这时需要代理抓包工具,例如:

  • Fiddler
  • Charles(Windows 版)
  • Sniffmaster

在 Win11 上配置 Fiddler

操作步骤:

  1. 启动 Fiddler
  2. 打开 Tools → Options
  3. 开启 HTTPS 解密
  4. 安装证书

让浏览器走代理

Fiddler 会自动设置系统代理。

打开网页后,可以在 Fiddler 中看到请求。


修改请求

  1. 开启断点(Breakpoints)
  2. 发送请求
  3. 修改参数
  4. 再发送

观察变化

修改参数后,服务器返回的数据会发生变化。


三、抓 Windows 本机程序流量

如果目标不是浏览器,而是:

  • 桌面软件
  • 本地客户端

仍然可以用 Fiddler、Charles 或 Sniffmaster。


验证方法

  1. 启动抓包工具
  2. 打开目标程序
  3. 触发网络请求

判断结果

  • 如果出现请求 → 程序走系统代理
  • 如果没有 → 程序未使用代理

四、抓 iPhone 流量(Win11 场景)

如果需要在 Windows 上抓 iPhone 的请求,可以先尝试代理方式。


配置步骤

  1. Win11 上启动 工具
  2. 查看端口(例如 8888)
  3. iPhone 连接同一 Wi-Fi
  4. 在 iPhone 设置代理
  5. 安装证书

端口


测试

用 Safari 打开网页:

  • 如果 Fiddler 有请求 → 配置成功

问题分支

如果 Safari 有请求,但 App 没有,说明 App 没走代理


五、使用数据线直接对设备进行抓包

在 Win11 上,如果代理抓不到移动端流量,可以使用 SniffMaster(抓包大师)


操作步骤

  1. 用 USB 连接 iPhone
  2. 解锁设备
  3. 点击“信任此电脑”
  4. 启动 SniffMaster
  5. 选择设备
  6. 安装驱动(Win11 会提示)
  7. 安装描述文件
  8. 进入 HTTPS 暴力抓包 / 数据流抓包模式
  9. 点击开始

------暴力

观察结果

在界面中可以看到:

  • iPhone 发起的请求
  • 包括未经过代理的流量

https


六、在 Win11 上减少抓包噪音

对设备抓包数据会很多,可以通过筛选降低复杂度。


按 App 筛选

在 SniffMaster 中:

  1. 点击 选择 App
  2. 勾选目标应用
  3. 再触发请求

app选择


再做一次控制变量

  1. 清空记录
  2. 点击开始
  3. 只触发一次操作

这样请求数量会明显减少。


七、分析 TCP / 网络问题

如果问题不是接口,而是:

  • 请求超时
  • 网络波动

可以结合 Wireshark。


操作方式

  1. 在 Win11 上启动 Wireshark
  2. 选择网卡
  3. 开始抓包
  4. 触发请求

可以看到

  • TCP 三次握手
  • 重传
  • 连接断开

在 Win11 上抓包,可以按任务选择工具:

  • 看网页 → 浏览器
  • 改接口 → Fiddler/Sniffmaster
  • 抓 App → 代理工具
  • 抓不到 → SniffMaster
  • 查连接 → Wireshark

《SWIFTER -Swift开发者必备Tips》学习笔记

作者 逍遥归来
2026年3月31日 16:19

一、函数与闭包核心特性

1. 柯里化 (Currying)

重点说明

  • 核心定义:将接收多个参数的函数,拆解为一系列单参数函数,每个函数返回下一个函数,实现分步传参、参数固化与函数复用,是Swift函数式编程的核心特性之一。
  • 注意:Swift 3+ 移除了原生柯里化语法糖,需通过函数嵌套手动实现。

代码示例

// 传统多参数函数
func addTwoNum(_ a: Int, _ b: Int) -> Int {
    a + b
}

// 柯里化实现:分步接收参数
func curryingAdd(_ a: Int) -> (Int) -> Int {
    return { b in a + b }
}

// 使用:固化参数生成新函数
let add10 = curryingAdd(10)
let result1 = add10(5)  // 15
let result2 = add10(20) // 30

// 多参数柯里化扩展
func curryingThreeParams(_ a: Int) -> (Int) -> (Int) -> Int {
    return { b in
        return { c in a + b + c }
    }
}
let finalResult = curryingThreeParams(5)(3)(2) // 10

2. @autoclosure 自动闭包

重点说明

  • 核心定义:将传入的表达式自动封装为无参闭包,实现延迟执行,仅当闭包被调用时才执行表达式,避免不必要的性能开销。
  • 核心场景:空合运算符??底层基于@autoclosure实现,适用于条件判断中的默认值处理。
  • 注意:仅支持无参闭包,禁止滥用导致可读性下降。

代码示例

// 传统闭包:需手动写闭包语法
func logIfFalse(_ condition: Bool, errorMsg: () -> String) {
    if !condition { print(errorMsg()) }
}
logIfFalse(1 > 2, errorMsg: { "条件错误" })

// @autoclosure 自动封装表达式
func autoLogIfFalse(_ condition: Bool, errorMsg: @autoclosure () -> String) {
    if !condition { print(errorMsg()) }
}
// 调用时直接写表达式,自动封装为闭包
autoLogIfFalse(1 > 2, errorMsg: "条件错误")

// 自定义??运算符(底层基于@autoclosure)
func customNilCoalescing<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
    switch optional {
    case .some(let value): return value
    case .none: return defaultValue()
    }
}
let optionalStr: String? = nil
let finalStr = customNilCoalescing(optional: optionalStr, defaultValue: "默认值")

3. @escaping 逃逸闭包

重点说明

  • 核心定义:闭包作为函数参数,在函数返回后才会被执行,称为逃逸闭包,必须用@escaping标记。
  • 核心场景:异步网络请求、GCD延时操作、函数内属性存储闭包。
  • 注意:逃逸闭包内必须显式引用self,需警惕循环引用风险;非逃逸闭包默认无循环引用风险。

代码示例

import Foundation

// 非逃逸闭包:函数执行结束前完成执行
func syncTask(closure: () -> Void) {
    print("任务开始")
    closure()
    print("任务结束")
}

// 逃逸闭包:函数返回后才执行,必须标记@escaping
var globalClosure: (() -> Void)?
func asyncTask(closure: @escaping () -> Void) {
    print("异步任务开始")
    // 1. 存储到全局变量,函数返回后执行
    globalClosure = closure
    // 2. 异步延时执行
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        closure()
    }
    print("异步函数执行结束")
}

// 调用
asyncTask {
    print("逃逸闭包执行")
}

4. 函数基础增强特性

重点说明

  • 方法嵌套:函数内部可定义子函数,封装内部逻辑,实现作用域隔离。
  • 可变参数:用...标记,接收0个或多个同类型参数,函数内自动转为数组使用。
  • 默认参数:给参数设置默认值,调用时可省略该参数,提升灵活性。
  • inout参数:输入输出参数,可在函数内修改外部变量的值。

代码示例

// 1. 方法嵌套
func calculateTotalPrice(price: Double, count: Int) -> Double {
    // 嵌套函数:仅外层函数可访问
    func calculateDiscount(_ total: Double) -> Double {
        if total > 1000 { return total * 0.8 }
        else if total > 500 { return total * 0.9 }
        return total
    }
    let total = price * Double(count)
    return calculateDiscount(total)
}
let finalPrice = calculateTotalPrice(price: 200, count: 6) // 960

// 2. 可变参数
func sumNumbers(_ numbers: Int...) -> Int {
    numbers.reduce(0, +)
}
let sum = sumNumbers(1,2,3,4,5) // 15

// 3. 默认参数
func userInfo(name: String, age: Int = 18, city: String = "未知") {
    print("姓名:\(name),年龄:\(age),城市:\(city)")
}
userInfo(name: "张三") // 自动使用默认值

// 4. inout参数
func swapValue<T>(_ a: inout T, _ b: inout T) {
    (a, b) = (b, a)
}
var num1 = 10, num2 = 20
swapValue(&num1, &num2) // num1=20, num2=10

二、协议与面向协议编程

1. protocol 方法的 mutating 声明

重点说明

  • 核心定义:Swift的protocol可被class、struct、enum实现,struct和enum是值类型,默认无法在实例方法中修改自身属性,必须用mutating修饰方法。
  • 最佳实践:协议方法若会修改实例自身,必须声明mutating,保证struct/enum的实现兼容性;class实现时可忽略该关键字。

代码示例

// 正确协议定义:声明mutating
protocol Countable {
    mutating func addCount()
    mutating func resetCount()
}

// struct实现:必须保留mutating
struct Counter: Countable {
    private(set) var count: Int = 0
    mutating func addCount() { count += 1 }
    mutating func resetCount() { count = 0 }
}

// class实现:可忽略mutating
class ClassCounter: Countable {
    private(set) var count: Int = 0
    func addCount() { count += 1 }
    func resetCount() { count = 0 }
}

// 使用
var structCounter = Counter()
structCounter.addCount()
print(structCounter.count) // 1

2. Sequence 与 IteratorProtocol

重点说明

  • 核心定义:Swift中for...in循环的底层基于Sequence协议,实现Sequence必须先实现IteratorProtocol协议,定义元素的遍历规则。
  • 核心关系:IteratorProtocol负责生成元素,通过next()方法返回下一个元素,nil表示遍历结束;Sequence负责创建迭代器,提供遍历能力。

代码示例

// 1. 实现迭代器协议:反向遍历迭代器
struct ReverseIterator<T>: IteratorProtocol {
    typealias Element = T
    private var elements: [T]
    private var currentIndex: Int
    
    init(elements: [T]) {
        self.elements = elements
        self.currentIndex = elements.count - 1
    }
    
    mutating func next() -> T? {
        guard currentIndex >= 0 else { return nil }
        let element = elements[currentIndex]
        currentIndex -= 1
        return element
    }
}

// 2. 实现Sequence协议:提供遍历能力
struct ReverseSequence<T>: Sequence {
    private var elements: [T]
    
    init(elements: [T]) {
        self.elements = elements
    }
    
    func makeIterator() -> ReverseIterator<T> {
        ReverseIterator(elements: elements)
    }
}

// 使用:支持for...in循环
let numbers = [1,2,3,4,5]
let reverseSequence = ReverseSequence(elements: numbers)
for num in reverseSequence {
    print(num) // 输出:5 4 3 2 1
}

3. associatedtype 关联类型

重点说明

  • 核心定义:协议中的占位类型,协议实现时才指定具体类型,实现协议的泛型能力,是面向协议编程的核心特性。
  • 核心场景:协议中需要使用多个关联的泛型类型,保证类型安全,避免Any类型滥用。

代码示例

// 带关联类型的协议
protocol Stackable {
    // 定义关联类型,实现时指定具体类型
    associatedtype Element
    mutating func push(_ element: Element)
    mutating func pop() -> Element?
    var topElement: Element? { get }
}

// 泛型实现:自动推断关联类型
struct GenericStack<T>: Stackable {
    private var elements: [T] = []
    
    mutating func push(_ element: T) {
        elements.append(element)
    }
    
    mutating func pop() -> T? {
        elements.popLast()
    }
    
    var topElement: T? {
        elements.last
    }
}

// 使用
var stringStack = GenericStack<String>()
stringStack.push("Swift")
stringStack.push("iOS")
print(stringStack.topElement ?? "空") // iOS

4. Protocol Extension 协议扩展

重点说明

  • 核心定义:给协议添加扩展,提供方法、计算属性的默认实现,是Swift实现可选协议、功能横向复用、面向协议编程的核心。
  • 核心价值:无需继承即可给多个类型添加统一功能;实现协议可选方法,无需@objc限制,支持struct/enum。

代码示例

// 协议定义
protocol Runnable {
    func run()
    func stop()
}

// 协议扩展:提供默认实现
extension Runnable {
    func run() { print("默认的跑步行为") }
    func stop() { print("默认的停止行为") }
    // 扩展新增方法,所有遵循者自动获得
    func warmUp() { print("热身准备") }
}

// 遵循协议,无需实现任何方法,自动获得默认实现
struct Person: Runnable {}

// 重写默认实现
class Car: Runnable {
    func run() { print("汽车启动行驶") }
    func stop() { print("汽车刹车停止") }
}

// 使用
let person = Person()
person.warmUp() // 热身准备
person.run() // 默认的跑步行为

let car = Car()
car.run() // 汽车启动行驶
car.warmUp() // 热身准备

// 实现可选协议:替代@objc可选方案
protocol OptionalProtocol {
    func necessaryMethod() // 必须实现
    func optionalMethod()  // 可选,扩展提供默认实现
}

extension OptionalProtocol {
    func optionalMethod() {}
}

// 只需实现必须方法,可选方法自动获得默认实现
struct ProtocolImplementer: OptionalProtocol {
    func necessaryMethod() {
        print("必须实现的方法")
    }
}

三、可选类型与安全编程

1. 可选链 Optional Chaining

重点说明

  • 核心定义:通过?在可选值上链式调用属性、方法、下标,可选值为nil时,整个链式调用直接返回nil,不会崩溃,替代多层强制解包。
  • 注意:可选链的返回值始终是可选类型,即使调用的属性/方法返回非可选类型。

代码示例

// 定义嵌套模型
class Address {
    var city: String?
    var detail: String?
}

class User {
    var name: String
    var address: Address?
    
    init(name: String) {
        self.name = name
    }
}

// 可选链使用
var user: User? = User(name: "张三")
// 1. 链式访问属性
let city = user?.address?.city
// 类型:String?,任意一层为nil直接返回nil
print(city ?? "地址为空") // 地址为空

// 2. 安全赋值:可选值不为nil时才执行
user?.address = Address()
user?.address?.city = "成都"
let newCity = user?.address?.city
print(newCity ?? "地址为空") // 成都

// 3. 多层解包
if let detail = user?.address?.detail {
    print("详细地址:\(detail)")
}

2. 多重 Optional

重点说明

  • 核心定义:Optional类型本身可作为另一个Optional的包装值,形成嵌套可选类型(如String??),本质是Optional枚举的嵌套。
  • 核心坑点:直接判空!= nil只能判断外层是否为nil;字面量nil赋值给多重可选是外层为nil,把nil的可选值赋值给多重可选是外层.some、内层.none。

代码示例

// 多重可选的核心区别
var aNil: String? = nil // 单层nil
var anotherNil: String?? = aNil // 外层.some,内层.none
var literalNil: String?? = nil // 外层直接.none

// 判空区别
if anotherNil != nil {
    print("anotherNil 外层不为nil") // 会执行
}
if literalNil != nil {
    print("literalNil 外层不为nil") // 不会执行
}

// 正确解包方式
// 1. 多层if let
if let outer = anotherNil, let inner = outer {
    print("解包成功:\(inner)")
} else {
    print("内层为nil")
}

// 2. 空合运算符直接解包
let finalValue = anotherNil ?? "默认值"
print(finalValue) // 默认值

// 3. switch模式匹配
switch anotherNil {
case .some(.some(let value)):
    print("有值:\(value)")
case .some(.none):
    print("外层有值,内层nil")
case .none:
    print("外层直接nil")
}

3. Optional Map 与 flatMap

重点说明

  • map:对可选值的非nil值进行转换,nil则直接返回nil,闭包返回非可选类型。
  • flatMap:用于转换后仍是可选值的场景,自动解包一层,避免生成多重可选。

代码示例

// Optional Map
let optionalNum: Int? = 5
let mapResult = optionalNum.map { num in
    "数字:\(num)"
}
// 类型:String?,optionalNum为nil时直接返回nil
print(mapResult ?? "空") // 数字:5

// flatMap:避免多重可选
let strNum: String? = "123"
// map转换后生成Int??双重可选
let mapDoubleOptional = strNum.map { Int($0) }
// flatMap自动解包一层,返回Int?
let flatMapResult = strNum.flatMap { Int($0) }
print(flatMapResult ?? "转换失败") // 123

4. ?? 空合运算符

重点说明

  • 核心定义:可选值非nil则解包返回,nil则返回后面的默认值,底层基于@autoclosure实现,默认值延迟执行。
  • 核心价值:替代三目运算符,简化可选值的默认值处理,支持链式使用。

代码示例

// 基础使用
let optionalStr: String? = "Swift"
let result = optionalStr ?? "默认值"
// 类型:String,非可选
print(result) // Swift

// 链式使用
let level1: String?? = nil
let level2: String?? = "第二层值"
let finalResult = level1 ?? level2 ?? "最终默认值"
print(finalResult) // 第二层值

// 延迟执行特性:默认值仅在nil时执行
func getDefaultValue() -> String {
    print("执行了默认值方法")
    return "默认值"
}

let hasValue: String? = "有值"
let test1 = hasValue ?? getDefaultValue()
// 不会执行getDefaultValue,无打印

四、语法糖与基础类型增强

1. Tuple 多元组

重点说明

  • 核心定义:将多个不同类型的值组合成一个复合值,无需提前定义结构体/类,是轻量级数据组合方案。
  • 核心场景:多返回值、变量交换、多值绑定、函数参数简化。

代码示例

// 1. 基础定义
// 命名元素多元组
let userInfo = (name: "张三", age: 20, city: "成都")
print(userInfo.name) // 张三
print(userInfo.age) // 20

// 2. 变量交换:无需临时变量
var a = 10, b = 20
(a, b) = (b, a)
print(a, b) // 20 10

// 3. 函数多返回值
func calculateStats(_ numbers: [Int]) -> (sum: Int, average: Double, max: Int)? {
    guard !numbers.isEmpty else { return nil }
    let sum = numbers.reduce(0, +)
    let average = Double(sum) / Double(numbers.count)
    return (sum, average, numbers.max()!)
}

if let stats = calculateStats([1,2,3,4,5]) {
    print("总和:\(stats.sum),平均值:\(stats.average)")
}

2. 自定义操作符

重点说明

  • 核心定义:Swift支持自定义中缀、前缀、后缀操作符,扩展运算符功能,需先声明操作符,再实现对应的函数。
  • 注意:避免滥用导致代码可读性下降,自定义操作符需符合语义。

代码示例

import Foundation

// 1. 自定义正则匹配中缀操作符 =~
infix operator =~: ComparisonPrecedence
func =~ (input: String, pattern: String) -> Bool {
    guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else {
        return false
    }
    let range = NSRange(input.startIndex..., in: input)
    return regex.firstMatch(in: input, range: range) != nil
}

// 使用:邮箱校验
let mailPattern = "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
let isMail = "test@example.com" =~ mailPattern
print(isMail) // true

// 2. 自定义前缀平方操作符 **
prefix operator **
prefix func ** (num: Int) -> Int {
    num * num
}
let square = **5 // 25

3. 下标 subscript

重点说明

  • 核心定义:给类、结构体、枚举定义下标,通过下标语法快速访问集合元素,支持多参数、重载、只读/读写。
  • 核心场景:自定义集合类型、安全数组访问、快捷模型属性访问。

代码示例

// 安全数组:避免下标越界崩溃
struct SafeArray<T> {
    private var elements: [T]
    
    init(elements: [T]) {
        self.elements = elements
    }
    
    // 基础下标:越界返回nil,不崩溃
    subscript(index: Int) -> T? {
        get {
            guard index >= 0, index < elements.count else { return nil }
            return elements[index]
        }
        set {
            guard let newValue = newValue, index >= 0, index < elements.count else { return }
            elements[index] = newValue
        }
    }
    
    // 重载下标:范围访问
    subscript(range: ClosedRange<Int>) -> [T] {
        let start = max(range.lowerBound, 0)
        let end = min(range.upperBound, elements.count - 1)
        guard start <= end else { return [] }
        return Array(elements[start...end])
    }
}

// 使用
let array = SafeArray(elements: [1,2,3,4,5])
print(array[2] ?? "越界") // 3
print(array[10] ?? "越界") // 越界,无崩溃
print(array[1...3]) // [2,3,4]

4. 区间运算符与模式匹配

重点说明

  • 区间运算符:...闭区间(包含首尾)、..<半开区间(包含首不包含尾),支持单侧区间,用于遍历、切片、范围判断。
  • 模式匹配:Swift强大的语法特性,支持元组、可选值、枚举、范围、类型匹配,核心基于switch,也可用于if case/for case

代码示例

// 区间运算符
let numbers = [1,2,3,4,5,6,7,8,9,10]
let slice1 = numbers[2...5] // [3,4,5,6]
let slice2 = numbers[2..<5] // [3,4,5]
let slice3 = numbers[5...] // [6,7,8,9,10]

// 模式匹配:switch范围判断
let score = 85
switch score {
case 0..<60: print("不及格")
case 60..<80: print("及格")
case 80...100: print("优秀")
default: print("无效分数")
}

// 可选值模式匹配
let optionalNum: Int? = 5
if case let num? = optionalNum, num > 0 {
    print("正数:\(num)")
}

// for case 遍历匹配
let numArray: [Int?] = [1, nil, 3, nil, 5]
for case let num? in numArray {
    print(num) // 输出:1 3 5
}

五、类与结构体初始化规则

1. Designated、Convenience、Required 初始化

重点说明

  • 指定初始化(Designated):类的主初始化方法,必须初始化所有未赋值的存储属性,调用父类的指定初始化方法。
  • 便利初始化(Convenience):辅助初始化方法,必须调用同类的指定初始化方法,提供便捷的初始化入口。
  • 必须初始化(Required):强制子类必须重写的初始化方法,子类重写时必须加required,无需override
  • 核心规则:便利初始化必须横向调用,指定初始化必须纵向调用父类方法。

代码示例

// 父类
class Person {
    var name: String
    var age: Int
    
    // 指定初始化
    init(name: String, age: Int) {
        self.name = name
        self.age = age
        super.init()
    }
    
    // 便利初始化
    convenience init(name: String) {
        self.init(name: name, age: 18)
    }
    
    // 必须初始化
    required init() {
        self.name = "未知"
        self.age = 0
        super.init()
    }
}

// 子类
class Student: Person {
    var studentID: String
    
    // 子类指定初始化
    init(name: String, age: Int, studentID: String) {
        // 先初始化子类属性,再调用父类初始化
        self.studentID = studentID
        super.init(name: name, age: age)
    }
    
    // 重写必须初始化
    required init() {
        self.studentID = "000000"
        super.init()
    }
}

2. 可失败初始化(init?)

重点说明

  • 核心定义:初始化方法可返回nil,用init?()声明,在初始化过程中进行参数校验,校验失败返回nil,中断初始化。
  • 注意:Swift 5+ 支持在属性初始化完成前提前返回nil。

代码示例

class User {
    let name: String
    let age: Int
    
    init?(name: String, age: Int) {
        // 参数校验,失败返回nil
        guard !name.isEmpty else { return nil }
        guard age >= 0, age <= 150 else { return nil }
        // 校验通过,初始化属性
        self.name = name
        self.age = age
    }
}

// 使用
let validUser = User(name: "张三", age: 20) // 非nil
let invalidUser = User(name: "", age: 20) // nil

六、类型系统与核心关键字

1. static 与 class

重点说明

  • static:可用于class、struct、enum、protocol,定义的方法/属性不能被子类重写,静态绑定。
  • class:仅用于class类型,定义的方法/计算属性可被子类重写,动态绑定;不能用于存储属性。

代码示例

class Animal {
    // static存储属性:不可重写
    static let species = "动物"
    // class计算属性:可重写
    class var typeName: String { "Animal" }
    
    // static方法:不可重写
    static func breathe() { print("动物需要呼吸") }
    // class方法:可重写
    class func makeSound() { print("动物发出声音") }
}

class Cat: Animal {
    // 重写class计算属性
    override class var typeName: String { "Cat" }
    // 重写class方法
    override class func makeSound() { print("猫发出喵喵声") }
}

// 调用
print(Animal.typeName) // Animal
print(Cat.typeName) // Cat
Cat.makeSound() // 猫发出喵喵声

2. 属性观察 willSet / didSet

重点说明

  • 核心定义:监听存储属性的值变化,willSet在值变化前触发,didSet在值变化后触发,初始化时不会触发,仅赋值时触发。
  • 核心场景:数据绑定、值校验、UI更新、状态同步。

代码示例

class User {
    var name: String {
        willSet(newName) {
            print("即将把名字从\(name)改为\(newName)")
        }
        didSet(oldName) {
            print("已经把名字从\(oldName)改为\(name)")
            if name.isEmpty { name = oldName } // 空值恢复
        }
    }
    
    var age: Int {
        didSet {
            // 年龄自动修正
            if age < 0 { age = 0 }
            else if age > 150 { age = 150 }
        }
    }
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// 使用
let user = User(name: "张三", age: 20)
user.name = "李四" // 触发属性观察
user.age = 200 // 自动修正为150

3. lazy 懒加载修饰符

重点说明

  • 核心定义:延迟存储属性的初始化,只有在属性第一次被访问时,才会执行初始化代码,仅执行一次。
  • 核心特性:只能用于var变量,初始化时可访问self,线程不安全。
  • 核心场景:耗时初始化、依赖其他属性的初始化、不常用的属性,提升初始化性能。

代码示例

class DataManager {
    // 普通属性:初始化时立即创建
    let createTime = Date()
    
    // lazy属性:第一次访问时才初始化
    lazy var fileManager: FileManager = {
        print("执行fileManager初始化")
        return FileManager.default
    }()
    
    // lazy属性:依赖其他属性
    lazy var documentPath: String = {
        print("执行documentPath初始化")
        return fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!.path
    }()
    
    init() {
        print("DataManager初始化完成")
    }
}

// 使用
let manager = DataManager()
// 输出:DataManager初始化完成,lazy属性未初始化
_ = manager.fileManager
// 输出:执行fileManager初始化

4. 元类型、.self 与 AnyClass

重点说明

  • 元类型:类型的类型,用Type表示,如String.Type,代表类型本身。
  • AnyClassAnyObject.Type的别名,代表任意类的元类型。
  • .self:类型.self获取元类型,实例.self返回实例本身。
  • 核心场景:动态创建实例、工厂模式、runtime动态操作。

代码示例

// 获取元类型
let stringType: String.Type = String.self
print(stringType) // String

// AnyClass与动态创建实例
class Person {
    required init() {}
    func sayHello() { print("Hello") }
}

class Student: Person {
    override func sayHello() { print("Student Hello") }
}

let personClass: AnyClass = Person.self
let studentClass: AnyClass = Student.self

func createInstance(_ classType: AnyClass) -> Person? {
    guard let type = classType as? Person.Type else { return nil }
    return type.init()
}

let student = createInstance(studentClass)
student?.sayHello() // Student Hello

七、进阶特性

1. Reflection 与 Mirror

重点说明

  • 核心定义:Swift的反射机制,通过Mirror在运行时获取、遍历实例的所有属性信息,是Swift少有的运行时自省能力。
  • 核心特性:只读反射,支持struct、class、enum、tuple,无法修改属性值。
  • 核心场景:模型转字典、JSON序列化、对象属性打印。

代码示例

// 模型定义
struct User {
    let name: String
    let age: Int
    let isVip: Bool
}

// 模型转字典通用方法
func modelToDictionary(_ model: Any) -> [String: Any] {
    let mirror = Mirror(reflecting: model)
    var dict: [String: Any] = [:]
    for (propertyName, propertyValue) in mirror.children {
        guard let propertyName = propertyName else { continue }
        dict[propertyName] = propertyValue
    }
    return dict
}

// 使用
let user = User(name: "张三", age: 20, isVip: true)
let userDict = modelToDictionary(user)
print(userDict) // ["name": "张三", "age": 20, "isVip": true]

2. 正则表达式

重点说明

  • Swift语言层面未内置正则表达式,基于FoundationNSRegularExpression实现,可通过封装简化调用。
  • 注意:Swift字符串的range需基于utf16转换,避免中文/表情符号的range错误。

代码示例

import Foundation

// 正则工具类
struct RegexHelper {
    private let regex: NSRegularExpression
    
    init(pattern: String, options: NSRegularExpression.Options = .caseInsensitive) throws {
        self.regex = try NSRegularExpression(pattern: pattern, options: options)
    }
    
    // 匹配是否成功
    func isMatch(_ input: String) -> Bool {
        let range = NSRange(input.startIndex..., in: input)
        return regex.firstMatch(in: input, range: range) != nil
    }
    
    // 提取匹配结果
    func matches(_ input: String) -> [String] {
        let range = NSRange(input.startIndex..., in: input)
        let matches = regex.matches(in: input, range: range)
        return matches.compactMap {
            guard let range = Range($0.range, in: input) else { return nil }
            return String(input[range])
        }
    }
}

// 常用正则枚举
enum RegexPattern: String {
    case email = "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
    case phone = "^1[3-9]\\d{9}$"
}

// 便捷扩展
extension String {
    func isMatch(pattern: RegexPattern) -> Bool {
        guard let helper = try? RegexHelper(pattern: pattern.rawValue) else {
            return false
        }
        return helper.isMatch(self)
    }
}

// 使用
let isEmail = "test@example.com".isMatch(pattern: .email) // true
let isPhone = "13800138000".isMatch(pattern: .phone) // true

3. 命名空间

重点说明

  • Swift无原生命名空间关键字,通过无case枚举+静态成员模拟命名空间,解决类名冲突,实现代码模块化,替代OC的前缀方案。

代码示例

// 模拟命名空间
enum MyApp {
    // 网络模块
    enum Network {
        static let baseURL = "https://api.myapp.com"
        static func request(url: String) {
            print("发起请求:\(url)")
        }
    }
    
    // UI模块
    enum UI {
        static let mainColor = "#FF0000"
        static func showToast(message: String) {
            print("显示Toast:\(message)")
        }
    }
}

// 使用
print(MyApp.Network.baseURL)
MyApp.UI.showToast(message: "操作成功")

// 解决类名冲突
enum ModuleA {
    class User { var name = "ModuleA User" }
}
enum ModuleB {
    class User { var name = "ModuleB User" }
}

let userA = ModuleA.User()
let userB = ModuleB.User()

八、OC混编与工程化特性

1. 条件编译

重点说明

  • 根据编译环境、平台、配置等条件选择性编译代码,是编译时执行,不符合条件的代码不会被编译到二进制中。
  • 常用条件:平台(os(iOS)/os(macOS))、模式(DEBUG/RELEASE)、Swift版本、自定义标记。

代码示例

// 调试/发布模式区分
#if DEBUG
let serverURL = "https://test-api.example.com"
#else
let serverURL = "https://api.example.com"
#endif

// 平台适配
#if os(iOS)
import UIKit
let screenWidth = UIScreen.main.bounds.width
#elseif os(macOS)
import AppKit
let screenWidth = NSScreen.main?.frame.width ?? 0
#endif

// Swift版本适配
#if swift(>=5.5)
print("支持async/await新特性")
#else
print("使用低版本兼容方案")
#endif

2. 编译标记 MARK / TODO / FIXME

重点说明

  • 代码标记注释,Xcode会在导航栏中显示,用于代码分块、标记待办事项、待修复问题,提升代码可维护性。
  • 核心类型:
    • // MARK::代码分块,加-生成分隔线
    • // TODO::待办事项
    • // FIXME::待修复的bug

代码示例

import UIKit

class ViewController: UIViewController {
    
    // MARK: - 生命周期
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    // MARK: - UI设置
    private func setupUI() {
        view.backgroundColor = .white
        // TODO: 添加导航栏按钮
        // FIXME: 适配iPhone SE布局
    }
    
    // MARK: - 事件处理
    @objc private func buttonClick() {
        // TODO: 实现按钮点击逻辑
    }
}

3. @objc、dynamic 与 Selector

重点说明

  • @objc:将Swift的类、方法、属性暴露给OC runtime,支持OC代码调用、runtime特性(KVO、target-action)。
  • dynamic:强制方法/属性使用OC runtime动态派发,支持方法交换、动态修改。
  • Selector:OC的方法选择器,Swift中用#selector()创建,用于动态方法调用、target-action。

代码示例

import UIKit

@objc class SwiftPerson: NSObject {
    @objc var name: String
    @objc var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
        super.init()
    }
    
    @objc func sayHello() {
        print("Hello,我是\(name)")
    }
    
    // dynamic标记:动态派发
    @objc dynamic func dynamicMethod() {
        print("原始动态方法")
    }
}

// Selector动态调用
let person = SwiftPerson(name: "张三", age: 20)
let selector = #selector(SwiftPerson.sayHello)
person.perform(selector) // 输出:Hello,我是张三

// target-action使用
let button = UIButton()
button.addTarget(person, action: #selector(SwiftPerson.sayHello), for: .touchUpInside)

SwiftUI 如何使用 UIKit 组件

2026年3月31日 10:06

先理解问题是什么

现实情况是:SwiftUI 原生组件不够用。很多组件SwiftUI 自己没有直接提供,但 UIKit 里有。

那怎么办?苹果提供了一个"桥接协议":UIViewRepresentable


UIViewRepresentable 是什么

它是一个协议(Protocol) ,作用是:

把一个 UIKit 的 UIView包装成 SwiftUI 能认识的 View

你可以把它理解成一个翻译官,SwiftUI 和 UIKit 说的不是同一种语言,UIViewRepresentable 负责在中间翻译。

SwiftUI 世界          翻译官                    UIKit 世界
─────────────    ──────────────────────    ──────────────────
  some View  ←→  UIViewRepresentable  ←→   UIView(任意)

它要求你实现两个方法

protocol UIViewRepresentable {
    // 方法一:创建 UIKit 视图(只调用一次)
    func makeUIView(context: Context) -> 某种UIView
    
    // 方法二:更新 UIKit 视图(状态变化时调用)
    func updateUIView(_ uiView: 某种UIView, context: Context)
}

就这两个,不多(有没有想到什么,OC的NSProxy 是不是也是实现两个方法,虽然八杆子打不着,但是突然想到了)。

  • makeUIView → 负责初始化,相当于 viewDidLoad,只跑一次
  • updateUIView → 负责同步状态,SwiftUI 的数据变了,你要在这里手动更新 UIKit 视图

我写了一个BlurView,早期SwiftUI background不支持毛玻璃效果

struct BlurView: UIViewRepresentable {
    let style: UIBlurEffect.Style   // ← 从 SwiftUI 传进来的参数
    
    // 第一步:创建真实的 UIKit 视图
    func makeUIView(context: Context) -> some UIView {
        let view = UIView(frame: .zero)
        view.backgroundColor = .clear
        
        // 这才是核心:UIKit 的毛玻璃视图
        let blurEffect = UIBlurEffect(style: style)
        let blurView = UIVisualEffectView(effect: blurEffect)
        
        // 用 AutoLayout 让它撑满父视图
        blurView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(blurView)
        NSLayoutConstraint.activate([
            blurView.heightAnchor.constraint(equalTo: view.heightAnchor),
            blurView.widthAnchor.constraint(equalTo: view.widthAnchor)
        ])
        
        return view  // ← 把这个 UIKit 视图交给 SwiftUI 管理
    }
    
    // 第二步:状态更新时同步(这里暂时不需要做任何事)
    func updateUIView(_ uiView: UIViewType, context: Context) {
        // 如果 style 会动态变化,就在这里更新
    }
}

重点理解makeUIView 返回的那个 UIView,之后就由 SwiftUI 的布局系统接管了。你不需要手动设置 frame,SwiftUI 会帮你处理尺寸。


然后 View Extension 做了什么

extension View {
    func blurBackground(style: UIBlurEffect.Style) -> some View {
        ZStack {
            BlurView(style: style)  // ← UIKit 毛玻璃,铺在底层
            self                    // ← 原来的 SwiftUI 视图,叠在上层
        }
        //两个方法都行
        //self.background(BlurView(style: style))
    }
}

BlurView 在这里和任何 SwiftUI 原生 View 完全没有区别,可以直接放进 ZStack。这就是 UIViewRepresentable 的意义:让 UIKit 视图假装自己是 SwiftUI 视图


整体调用链是这样的

.blurBackground(style: .systemMaterial)
        ↓
    ZStack 叠加
   ┌────────────┐
   │  BlurView  │ ← UIViewRepresentable 在这里翻译
   │   (UIKit)  │   makeUIView() 被 SwiftUI 自动调用
   └────────────┘
        ↑
   self(原 SwiftUI 视图)叠在上面

什么时候用 UIViewRepresentable(有些SwfitUI 现在自己已经有了)

场景 推荐方案
毛玻璃、特效 UIVisualEffectViewUIViewRepresentable
地图 MKMapView → 或直接用 SwiftUI 的 Map
网页 WKWebViewUIViewRepresentable
富文本编辑 UITextViewUIViewRepresentable
相机预览 AVCaptureVideoPreviewLayerUIViewRepresentable
SwiftUI 能搞定的 直接用 SwiftUI,别绕弯子

还有一个兄弟协议:UIViewControllerRepresentable

如果你要包装的不是 UIView,而是整个 UIViewController(比如系统的图片选择器、分享弹窗),用这个:

struct ImagePickerView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIImagePickerController {
        return UIImagePickerController()
    }
    
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
        // 同上,同步状态用
    }
}

逻辑完全一样,只是把 UIView 换成了 UIViewController


总结

UIViewRepresentable 本质上就是:

实现两个方法(创建更新),让 SwiftUI 知道怎么驾驭一个 UIKit 视图

它解决的核心问题是:SwiftUI 和 UIKit 生命周期不同,这个协议负责在两套系统之间搭桥

BlurView 是一个非常标准的使用案例——SwiftUI 没有,UIKit 有,包一下,用上。

Swift 杀进 Android,Google 和 Apple 都要失眠了?

作者 黄林晴
2026年3月31日 08:49

本文同步自微信公众号 “Android技术圈”

Swift 终于正式杀进 Android 了。 不是社区 Demo,不是民间魔改,而是 Swift 官方第一次亲自发布 Android SDK。 这件事现在看起来像一条技术新闻,再往后看,很可能会变成移动开发格局变化的起点。

先别急着喊 “Kotlin 要完了”。 但也别把它轻飘飘理解成 “Swift 又做了个跨平台实验”。 因为这次最狠的地方在于:Swift 官方第一次给出了进入 Android 工程体系的真实路径。

cover.png

官方到底宣布了什么

比“Apple 要不要抢 Android 地盘”更值得看的,其实是官方这次到底把支持范围推进到了哪一步。

Swift 官方博客对这件事的表述,其实非常克制,但也非常明确。核心就三点:

  • Swift 6.3 包含首个官方 Android Swift SDK
  • 可以开始用 Swift 开发原生 Android 程序
  • 可以通过 Swift JavaSwift Java JNI Core,把 Swift 代码集成进现有 Kotlin / Java Android 应用

这三句话加起来,真正说明的是:

Swift on Android,已经从社区探索,进入官方支持阶段。

这一步为什么重要?

因为过去你看到的很多 “Swift 跑 Android”,本质上都还是社区项目、实验性方案,或者少数团队自己打补丁维护的链路。

而这次不一样。 这次是 Swift 官方把 SDK、文档、安装方式、交叉编译路径和互操作方案一起摆上台面。

对开发者来说,差别非常大。

社区方案是“能折腾出来”。 官方方案才是“值得认真观察是否能进生产”。

02_ecosystem_bridge.png

别高估,也别低估

这类新闻最容易出现两个极端判断。

第一种是:
“Kotlin 要凉了,Android 以后可以全用 Swift 写。”

第二种是:
“没意义,不就是把命令行程序编到 Android 上吗?”

这两种都不准确。

从 Swift 官方文档来看,今天已经明确成立的是:

  • 你可以把 Swift 代码交叉编译到 Android
  • 你可以在 Android 设备或模拟器上运行 Swift 程序
  • 你可以把 Swift 模块构建成共享库
  • 你可以让现有 Kotlin / Java Android 应用去调用这些 Swift 代码

这意味着什么?

意味着 Swift 现在已经不是“只能在 Apple 生态里玩”的语言了。 但它也还远远没到“Android 主流团队明天集体切语言”的程度。

它真正有价值的地方,不是在口号层。 而是在工程层。

它第一次让 Swift 有机会进入 Android 的真实项目结构里。

真正值得关注的,是这条工程路径

Swift 官方的 Android 入门文档其实很务实,没有画大饼。

要跑起来,你得准备 3 样东西:

  • Swift 6.3 toolchain
  • Swift SDK for Android
  • Android NDK 27d 或更高版本

然后通过 swift build --swift-sdk ... 做 Android 目标的交叉编译。

官方演示的第一步,也不是完整 App,而是先把一个 Swift 可执行程序编译到 Android 上运行。

很多人看到这里会下意识地说:

“那不还是离真正 Android App 很远?”

但官方文档后面紧接着补了一句特别关键的话:

Swift 模块可以构建为共享库,并被打进 Android 应用,再由 Java / Kotlin 代码调用。

这句话,才是整件事的核心。

因为这说明 Swift on Android 的第一落点,并不是 UI 层,也不是整项目重写。 而是更现实、更符合团队采纳路径的地方:

  • 共享业务逻辑
  • 数据模型
  • 网络层
  • 算法模块
  • 某些性能敏感代码

说白了,Swift 现在不是先来抢 Compose 的饭碗。 它更像是先从“共享模块语言”这个位置切进去。

03_shared_logic_path.png

这件事为什么会让很多 iOS 团队心动

对纯 Android 团队来说,这条新闻现在更像观察项。 但对已经重度使用 Swift 的团队,这个信号就完全不一样了。

过去很多团队都会面临一个老问题:

“iOS 侧已经有一套成熟的 Swift 代码资产了,Android 还要不要重写一遍?”

如果这些资产只是 UI,那没办法,平台差异太大。 但如果是业务规则、网络协议、加解密逻辑、通用数据处理,重写本身就是重复劳动。

Swift 官方 Android SDK 的出现,至少让这类团队开始有了一个新的选项:

把 Swift 资产往 Android 端延伸,而不是永远困在 Apple 生态内部。

这点其实很关键。

因为它改变的不是某个 API,而是开发者对 Swift 的心理预期。

过去大家默认 Swift 是: “写 iPhone、写 Mac、写 Apple 设备。”

现在这个认知边界开始松了。

Swift 正在从 Apple 平台语言,继续往真正的跨平台语言走。

Kotlin 会不会被威胁

短期答案很直接:不会。

Kotlin 在 Android 的位置,不是一两条技术新闻能撼动的。 它背后是 Android 官方支持、Jetpack 生态、Android Studio、社区实践、招聘市场和海量线上项目。

Swift 6.3 现在拿出来的是“首个官方 Android SDK”。 这离大规模生产落地,中间还隔着很多现实问题:

  • 工具链稳定性怎么样
  • 桥接 Kotlin / Java 的成本高不高
  • 调试链路顺不顺
  • CI 能不能稳定接入
  • 团队愿不愿意为它付出学习和维护成本

所以更合理的判断不是“Swift 替代 Kotlin”。

而是:

  • Kotlin 仍然会是 Android 主语言
  • Swift 可能成为共享逻辑和多端模块的新候选
  • 真正决定它能不能站稳的,是工程成熟度,不是新闻热度

04_adoption_stages.png

这一步对 Swift 自己更重要

从更大的角度看,这次最受益的,也许不是 Android,而是 Swift 本身。

一门语言如果长期绑定单平台,它再强,外界对它的认知上限也很明确。

只有当它开始稳定支持更多平台,开发者才会真正把它当成一门独立的软件工程语言,而不是某家平台公司的附属工具。

Swift 6.3 这次最值得玩味的地方就在这里。

它不是一句空泛的“我们支持跨平台”。 而是给出了 SDK、文档、安装方式、构建路径、互操作方案。

这说明 Swift 官方现在回答的问题,已经不再只是:

“Swift 能不能把 Apple 生态服务好?”

而是:

Swift 能不能成为一门覆盖更多平台的软件工程语言?

Android,就是这个问题里最关键的一块拼图。

开发者现在该怎么看

如果你是 Android 开发者,不需要焦虑,也没必要跟风重写任何项目。

你更应该做的是持续观察这几个点:

  • Swift Android SDK 后续更新速度快不快
  • Swift Java 的接入体验到底顺不顺
  • 有没有团队先把 Swift 用到 Android 共享模块里
  • 工具链能不能稳定进入 CI 和生产流程

如果你是 iOS / Swift 开发者,这件事反而更值得持续盯住。

因为这可能是 Swift 在未来两三年里,最有战略意义的一次边界扩张。

它未必马上改变你的项目。 但它很可能会慢慢改变未来的技术选型。

写在最后

Swift 6.3 的重点,不是简单一句“Swift 能写安卓了”。

更准确的说法应该是:

Swift 官方第一次正式发布 Android SDK,并给出了进入现有 Android 工程的可执行路径。

这一步还远远谈不上改写 Android 生态。 但它已经足够说明一件事:

平台边界,还在继续变薄。

你看好 Swift 在 Android 上的发展吗? 你觉得它会先成为共享逻辑工具,还是最终走到更完整的 Android 开发场景?

欢迎评论区聊聊。 如果你身边正好有做 iOS、Android、跨平台架构的朋友,也欢迎把这篇文章转给他们,一起讨论。

参考资料:

  • Swift 官方博客:Swift 6.3 Released
  • Swift 官方文档:Getting Started with the Swift SDK for Android

一墙之隔,不同的时空 -- 肘子的 Swift 周报 #129

作者 东坡肘子
2026年3月31日 07:50

issue129.webp

一墙之隔,不同的时空

一年一度的 Let's Vision 大会在上海如期举行,今年的主题是:“Born to Create, Powered by AI”。除了与 Swift、空间计算相关的常规 Session,大会还邀请了许多开发者分享他们在工作中对 AI 的应用与理解。通过这些讲师对 AI 工作流的介绍,我也受益匪浅。原本只能容纳 300 人的 AI 主题会场,里三层外三层站满了热情高涨的观众。

然而,在众多优秀的 Session 中,一场由 YuChe Cheng 准备的、名为《Let's Create 1-liner Code in Swift》的演讲却将我的注意力引向了另一个会场。这究竟是一个怎样的话题?带着疑问我走了进去。作为一个 LeetCode 积分 2200+ 的开发者,YuChe Cheng 在演讲中展示了如何通过 Foundation 以及 Swift Algorithms 提供的大量高阶函数,将原本平淡无奇的 For-loop 代码,转换成更加优雅、美观、极具 Swift 风格的 Function Chaining(1-liner code),并在易读性与性能之间取得了很好的平衡。

看着幻灯片上的 Function Chaining 被一次又一次地优雅迭代,我有种茅塞顿开的畅快。整整 30 分钟的演讲,让我始终处于一种纯粹的兴奋之中——这种感觉,通常只在我绞尽脑汁最终攻克了一个难题,或是深刻理解了一个新概念后才会涌现。

尽管与主会场只有一墙之隔,但由于 AI 话题的绝对热度,本场演讲的听众明显偏少。与其说我为许多人错失了一场精彩演讲而感到遗憾,我真正担心的其实是:随着 AI 的进一步渗透,许多开发者原本在追求功能之外所赋予代码的那份“气质”,会不会就此消亡?

开发者不应该只关心编译后冷冰冰的二进制功能,代码本身也是个人风格的载体。它就像文章一样,在输出逻辑与结果之外,还承载着美学表达,体现着编写者的个人品味与巧思。

在今年的 Let's Vision 上,我感觉我们正站在一个时间的十字路口:我们是该一味追求 AI 带来的极致高效,还是在拥抱变化的同时,依然让属于开发者的那份骄傲与手艺,在 AI 时代得以保留?

本期内容 | 前一期内容 | 全部周报列表

本期推荐

Swift 6.3 Released

从 Swift 6 开始,语言演进已经稳定在半年一个 minor 版本的节奏,上周 Swift 6.3 如期发布。与前几个版本相比,这一版本并未引入明显的重磅特性,更多是对既有体系的打磨:并发模型在诊断准确性方面有所改进,新增的 @c 特性(attribute)进一步强化了 C/C++ 互操作能力,同时编译优化的控制粒度也变得更加细致。

尽管如此,这一版本也释放出一个清晰的信号:Swift 正在从“以 Apple 平台为中心的应用开发语言”,逐步向“具备跨平台与系统级能力的通用语言”演进。Embedded Swift、Android 支持的持续推进,以及 SwiftPM 构建体系的统一,都在指向这一方向。对多数 iOS 开发者而言,短期体感或许有限,但从更长的时间维度来看,这更像是一次为未来铺路的基础性更新。


如何在 Swift 中承接尚未稳定的 JSON (Designing a type-driven JSON in Swift)

当 API 契约尚未稳定、前后端对字段的理解又经常漂移时,Swift 的强类型系统反而会放大数据与 JSON 之间转换时的边界问题。Roman Niekipielov 在本文中介绍了一个刻意做小的 JSONValue 类型,用来承接这类过渡阶段的 JSON 数据。相比 [String: Any],它保留了更明确的类型结构;相比直接编写 Codable 模型,又更适合应对频繁变化的契约。这个实现并不试图替代正式模型,而是将不确定性暂时限制在边界层。


Swift 原生 AI Agent 开发实践系列

市面上有大量开发者使用 Python、TypeScript 开发 AI Agent,但 Chris Karani 认为,Swift 的并发模型天然更适合 Agent 的隔离与调度,强类型系统和宏功能也带来了额外的安全保证。他用 6 篇文章、从多个角度实践了这一观点——从统一多个 LLM Provider 的 SDK Conduit,到基于 Apple Foundation Models 的 Agent 运行时 Colony,再到用 Metal 加速的上下文记忆管理。如果你正在考虑在 Apple 平台上构建 AI 功能,这个系列是目前少见的完整原生方案。


Liquid Glass 设计工作坊 (Talking Liquid Glass with Apple)

Danny Bolella 在纽约参加了苹果举办的 Liquid Glass 设计工作坊,与设计团队和 SwiftUI 工程师进行了为期三天的深入交流。本次活动传递出非常明确的信号:Liquid Glass 并非过渡性尝试,而是苹果未来数年的设计方向,且将在后续工具链中成为默认前提。与此同时,苹果反复强调“层级(Hierarchy)”的重要性——界面应围绕内容构建,控件只是服务于内容的辅助元素,应尽量退居边缘,让信息本身成为视觉与交互的中心。除此之外,Danny 还在本文中记录了其他一些 SwiftUI 工程师给出的建议和技巧。本文记录的内容可以帮助你更早理解这场设计演进的节奏与方向。


App Store Connect 大更新 (Apple Dropped 100+ New Metrics. Your Competitors Are Already Using Them)

苹果对 App Store Connect 进行了近年来最大的一次更新,一口气引入了 100+ 官方指标、按来源划分的 cohort 分析、同行基准对比(转化率与单下载收益)以及可通过 API 导出的订阅数据。Jessica Chung 在本文中对这些关键变化进行了系统梳理。由于所有数据均来自苹果一手统计,这意味着开发者在 ASO 和增长决策中,将不再依赖第三方估算,而可以直接基于真实用户行为进行分析与优化。更重要的是,这次更新补齐了长期缺失的关键能力:你可以追踪不同关键词与渠道带来的用户质量,建立从曝光、下载到订阅与续费的完整转化链路,并通过同行基准明确自身所处位置。

本次更新对于开发者而言无疑是利好,但对于部分第三方 App Store 分析服务来说,也在一定程度上提高了竞争门槛,促使其提供更具附加值的能力。


Package Traits in Xcode

在创建 SPM 时,某些依赖可能只被特定 API 使用,但一旦用户引入该包,即便不使用这些 API,也需要一并引入相关依赖。Package Traits 正是为了解决这一问题而引入的,它为 SPM 提供了一种声明可选特性的方式,使使用者能够按需启用功能,从而避免引入不必要的依赖。遗憾的是,在该功能推出后,一直只能在社区版本的 Swift 工具链中使用。随着 Xcode 26.4 的发布,Package Traits 终于获得了苹果官方支持,有望迎来更广泛的应用。Matt Massicotte 在本文中对该特性进行了介绍,并展示了其基本用法。


优化感官性能,让用户感觉更快 (Why your SwiftUI app feels slow even though Instruments says it’s fine?)

用户投诉响应慢,一定是应用性能问题吗?Rafał Dubiel 将关注点从“实际性能”转向“感知性能(Perceived Performance)”,讨论如何通过界面反馈与交互节奏,让用户感觉应用“更快”。例如通过 skeleton view、延迟加载,以及合理的动画与状态过渡来掩盖等待时间。作者指出,在许多场景下,用户体验的关键并不在于减少毫秒级的计算时间,而在于是否及时提供反馈。相比单纯优化性能指标,这种从用户感知出发的思路,往往更直接地影响用户对应用流畅度的判断。


在 SwiftUI 中控制行高 (Adjusting line height in SwiftUI on iOS 26)

iOS 26 为 SwiftUI 新增了 lineHeight(_:) modifier,用于控制文本相邻两行基线之间的距离。Natalia Panferova 在本文中对各种配置方式进行了详细对比:内置预设(.loose.tight)、基于字号倍数的 .multiple(factor:)、固定增量的 .leading(increase:),以及绝对值控制的 .exact(points:)。此外,lineHeight(_:) 与已有的 lineSpacing(_:) 并不相同:前者控制基线间距,后者控制行底到下一行行顶的距离。

Natalia Panferova 曾是 Apple SwiftUI 核心团队成员,参与过多个关键 API 的设计与开发。本月她刚刚出版了新书 The SwiftUI Way,面向有一定 SwiftUI 经验的开发者,聚焦于生产环境中的模式选择、常见反模式识别,以及如何与框架“顺势而为”而非对抗。

工具

Cove:Swift 6 编写的 macOS 开源数据库客户端

Cove 是由 Emanuele Micheletti 开发的一款原生 macOS 数据库客户端,整个项目完全使用 Swift 6 构建,目前已经支持 PostgreSQL、MySQL、MariaDB、SQLite、MongoDB、Redis、ScyllaDB、Cassandra 和 Elasticsearch 等多种后端。它采用 SwiftUI 搭配 AppKit 原生控件实现,没有走 Electron 或 Web 技术栈,因此整体更轻量,也更符合 macOS 用户熟悉的交互体验。

相比“又一个数据库 GUI”,Cove 更值得关注的是它的实现思路。作者将所有数据库能力统一抽象为 DatabaseBackend 协议,UI 层不包含任何针对特定后端的分支逻辑。无论是 SQL 数据库、Redis 这类键值数据库,还是 MongoDB、Elasticsearch 这类非关系型后端,最终都会被整理为统一的表格模型交由界面渲染。项目目前仍处于 v0.1.0 的早期阶段,但已经具备查询、结构浏览、编辑、SSH 隧道和多标签等基础能力。即便你并不打算把它作为日常数据库工具,Cove 依然是一个很值得 Swift 开发者研究的桌面应用架构样本。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

昨天以前iOS

Tauri 2 iOS 开发避坑指南:文件保存、Dialog 和 Documents 目录的那些坑

作者 ssshooter
2026年3月30日 23:06

很多开发者在把 Tauri 2 应用上架到 iOS(真机或模拟器)时,都会在文件保存这一步踩坑:明明代码代码在其他平台没问题,在 iOS 路径就返回 null,或者在「文件」App 里根本看不到自己的 App 文件夹。

下面我把最常见的几个坑总结成一份避坑科普文,帮你一次性避开这些“iOS 特色”问题。

坑 1:@tauri-apps/plugin-dialogsave() 在 iOS 上经常返回 null 或路径不可用

现象
调用 const path = await save({...}) 后,一个 0KB 的文件写入成功,但是 path 返回是 null

避坑方法

  • 不要过度依赖 dialog.save() 来实现“用户任意选择保存位置”。
  • 优先使用 直接写入 App 的 Documents 目录(见坑 3)。
  • capabilities 中确保开启 dialog:save 权限。

坑 2:文件明明写入了,但「文件」App 里完全看不到 “Mind Elixir” 文件夹

现象: 用了 BaseDirectory.Document 保存文件后,在「文件」App → 浏览 → On My iPhone 里找不到你的 App 文件夹。

原因: iOS 沙盒机制严格控制 App 的 Documents 目录是否对「文件」App 可见。Tauri 默认生成的 iOS 项目不会自动添加暴露文件夹的配置,就算你写再多文件,文件夹也不会出现。

避坑方法(最关键的一步): 在 Info.plist 中添加以下两个 key(必须同时添加):

<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>

位置:通常在 src-tauri/gen/apple/ios/App/App/Info.plist(或你的项目对应路径),加在 <dict> 标签内,</dict> 之前。

添加后必须重新构建并安装 Appcargo tauri ios build 或用 Xcode 编译),然后:

  • 先执行一次写入操作(创建文件)。
  • 完全退出「文件」App(上滑关闭),重新打开并下拉刷新「On My iPhone」。

此时你应该能看到和 App 同名的文件夹(显示名称来自 productName 或 Xcode Display Name)。

注意:这两个 key 只控制可见性,不影响代码读写。

坑 3:iOS 上最好的保存方式其实不是 dialog,而是直接用 Documents 目录

推荐做法

import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'

await writeTextFile('my-note.md', '你的内容...', {
  dir: BaseDirectory.Document
})

优点:

  • 最稳定,几乎不会出现 0 字节文件的问题。
  • 用户可以在「文件」App 里直接看到和管理文件(添加上面两个 plist key 后)。
  • 无需处理复杂的 URI 和潜在的 fs bug。

如果你想让用户输入文件名,可以结合 prompt 或自定义输入框实现。

总结建议

在 Tauri 2 + iOS 开发中:

  1. 优先使用 BaseDirectory.Document 直接保存(最稳)。
  2. 必须在 Info.plist 添加 UIFileSharingEnabledLSSupportsOpeningDocumentsInPlace
  3. 谨慎使用 dialog.save() + writeFile,因为移动端兼容性还有待完善(官方 issue 仍在跟进)。
  4. 开发时多用控制台日志 + Safari/XCode 调试,遇到路径问题先检查 plist 和权限。

避开这几个坑后,你的 Mind Elixir(或其他 App)在 iOS 上的文件保存功能就会顺畅很多。iOS 的沙盒和文件系统规则和桌面差异很大,提前了解这些“Apple 特色”能省下大量调试时间。

(本文基于 Tauri 2 常见 issue 和实际开发经验总结,iOS 规则可能随系统版本微调,建议以 Apple 官方文档为准。)

ViewModifier 和 圆角以及渐变色

2026年3月30日 11:31

ViewModifier

是什么

把一组样式或 UI 结构打包成可复用的东西,用 .modifier() 链式调用贴到任意 View 上。

类比 UIKit

UIKit 里你会封装一个函数来复用样式:

func styleToolButton(_ button: UIButton) {
    button.titleLabel?.font = .systemFont(ofSize: 25)
    button.setTitleColor(.white, for: .normal)
    button.frame.size = CGSize(width: 30, height: 30)
}

ViewModifier 干的是同一件事,但它不只能改属性,还能在原有 View 外面包一层新的 View 结构,这是普通函数做不到的:

struct BadgeModifier: ViewModifier {
    func body(content: Content) -> some View {
        ZStack(alignment: .topTrailing) {
            content  // 原来的 View 原封不动
            Text("99")
                .background(Color.red)
                .clipShape(Circle())
                .offset(x: 10, y: -10)
        }
    }
}

Image(systemName: "bell").modifier(BadgeModifier())
Image(systemName: "message").modifier(BadgeModifier())

本质

本质就是一个语法糖,功能上等价于自定义一个 View 然后把其他 View 塞进去,但它能融入 SwiftUI 的链式调用语法,用起来跟 .font() .foregroundColor() 一模一样。


圆角 + 渐变色 + 描边

是什么

SwiftUI 没有 UIKit 那样直接设置 layer.borderWidth 的属性,填充和描边需要两个图层叠加来实现。

类比 UIKit

UIKit 两行搞定:

view.layer.borderWidth = 4
view.layer.borderColor = UIColor.green.cgColor

SwiftUI 必须用 ZStack 叠两个 RoundedRectangle:

.background(
    ZStack {
        RoundedRectangle(cornerRadius: 20)
            .stroke(model.color, style: StrokeStyle(lineWidth: 4))
        RoundedRectangle(cornerRadius: 20)
            .fill(gradientStyle)
    }
)

为什么先 stroke 再 fill

stroke(描边)默认居中描边,线宽一半在内一半在外。fill 只填充内部区域,所以 fill 会覆盖 stroke 内侧的那一半。先画 stroke 再盖 fill,能让 stroke 外侧的一半露出来,边框视觉上更完整。反过来的话内侧边框线被盖住,边框显得细一半。

本质

这块 UIKit 确实更直观,SwiftUI 的声明式思路在这个场景下反而绕了一圈


本质

SwiftUI 没有 layer,只有 Shape + 绘制规则


fill 和 stroke 的区别

操作 本质
fill 填充 Shape 内部
stroke 沿路径画边

stroke 的问题

.stroke(lineWidth: 4)

👉 描边在路径两侧(内 + 外)


推荐方案(更精准)

.strokeBorder(lineWidth: 4)

👉 描边完全在内部


推荐结构

.fill(...)
.overlay(stroke)

👉 语义清晰:先填充,再叠加边框

ViewModifier

本质是 (View) -> View,不是修改 View,而是生成新 View


Modifier 顺序

顺序不是语法问题,而是 View 树结构

描边本质

边框不是属性,而是绘制结果(Shape + stroke)


Swift 6.3 正式发布支持 Android ,它能在跨平台发挥什么优势?

2026年3月30日 11:23

最近 Swift 发布了 6.3 版本,而这个版本最特殊的地方在于:把 Android SDK 作为首个官方发布版本给加了进来,其实这个话题在去年的 《Swift 官方正式支持 Android》我们就已经聊过,而这两天正式版的发布,也是广大 Swift 开发者最高涨的时刻,iOSer 终于也有了自己的原生跨平台基础了:

那 Swfit for Android 到底是什么?其实和之前我们聊的一样,目前并不是在 Android 上原生跑 SwiftUI ,这个能力目前是一个 SKIP 的第三方项目在做, Swfit for Android 主要完成了「Swift 官方支持把 Swift 代码交叉编译到 Android」。

所以 Swfit for Android 目前主要由三个部分组成:

  • Swift Toolchain:目标平台上的 Swift 编译器、标准库、LLVM 后端

  • Swift SDK for Android:给 Android 目标平台准备的 Swift 库、头文件、配置

  • Android NDK:提供 Android 的系统头文件、链接器、目标架构工具链等

也就是说,它的核心原理是「交叉编译」:

  • 需要 macOS / Linux 上装 Swift 工具链
  • 需要 Android target 的 Swift SDK artifact bundle
  • 需要 Android NDK 的 sysroot、linker、headers
  • 由 Swift 编译器把代码交叉编译成 Android 可执行文件或本地库(.so
swift build --swift-sdk x86_64-unknown-linux-android28 --static-swift-stdlib

最终结果来看,就是把代码的构建产物变成 Android 上可运行的 ELF 二进制。

其实这也是在 iOS 的 LLVM 的技术领域,比如 KMP 目前大多也是利用 iOS 分支的 LLVM 交叉编译还到鸿蒙 ,所以 Swift 编译器本身还是走在自己的前端和 LLVM 后端,只是 target 换成 Android,例如:

  • x86_64-unknown-linux-android28
  • aarch64-unknown-linux-android28

这里的 android28 可以看出来,是明确绑定到特定 Android API Level ,而 Swift runtime / Foundation 依赖的一些能力也需要较新的 Android API,所以会用 API 28 为基础。

当然,Swift 与 Android 的 Java/Kotlin 的协调桥梁不出意外是 JNI,当然这里不会让你手写 JNI,而是用自动桥接工具来完成。

而在实际应用层面,Swift 官方现在推荐的不是 “整 App 全 Swift”,而是“Swift 库 + Kotlin/Java 壳” ,比如官方 examples 仓库里推荐方案 `hello-swift-java,它的结构是:

  • 一个 Swift package / Swift library
  • 一个 Kotlin Android app(Jetpack Compose UI)
  • Kotlin 调 Swift,不需要你手写 JNI,交给 swift-java 自动生成 Java wrapper 和 JNI bindings

也就目前而言,推荐的是 business logic / algorithms / libraries 写成 Swift ,而前端仍然保持标准 Kotlin/Java Android app 形态 ,简单来说就是:

  • UI:Kotlin / Jetpack Compose
  • 共享逻辑:Swift
  • 桥接层:swift-java / JNI

这强烈的即视感,不就是最初的 KMP 那会么,CMP 还没支持 UI 的时候,KMP 也是这样的路线

比如 Swift 官方提供的例子:一个 Android ( weather-app )和 Swift 库( weather-lib ),用于获取当前位置的天气信息,weather-lib 内部使用 swift-openapi-generator 调用 OpenMeteo 天气 API ,并公开LocationFetcher protocol , 然后 swift-java 和 JNI 自动生成 Java Wrapper ,从而让 Kotlin 可以直接调用 Swift Library。

而在这次 Swift 官方开源的项目里,核心的项目就是:swift-javaswift-java-jni-core ,他们分别作为“上层桥接工具”与“底层 JNI 基建”支撑起整个 Swfit for Android 的生态。

其实也很有趣,Kotlin 在 Android 跑 JVM ,在 iOS 跑 KN 二进制;而现在反过来 Swift 跑 Android ,也是跑二进制。

swift-java-jni-core

swift-java-jni-core 是一个 Swift-friendly 的 JNI 低层封装,本质上是 jni.h 上的一层薄封装,加上一些预打包的类型转换能力,用来和 JVM / Android Runtime(ART)交互,也就是:

  • 负责 JVM/ART 句柄
  • 找类、找方法
  • 处理线程、锁、引用
  • 做 Java/Swift 类型桥接

它的整个结构大概为:

Sources/  
├── CSwiftJavaJNI/          // C 模块:纯 JNI 头文件 ABI  
└── SwiftJavaJNICore/       // Swift 模块:所有上层封装  
    ├── VirtualMachine/     // JVM 句柄、线程、锁  
    ├── BridgedValues/      // 类型桥接  
    └── *.swift             // 类型系统、签名、Mangling  

整个链路从 Swift 应用代码开始,通过 JavaValue 协议进行类型转换,然后通过 JavaVirtualMachine 管理 JVM 交互,最终利用 CSwiftJavaJNI 的 C 接口调用实际的 JVM 实现:

swift-java

swift-java 是更高一层的互操作工具平台,大致可以分为:

  • Swift 调 Java
  • Java 调 Swift
  • 自动生成绑定代码
  • Android 上支持 jextract --mode=jni,因为 Android/ART 没有服务端 Java 那套 FFM 路线的前提条件

在实现上主要有两个代码生成管道:

1、Swift 调用 Java

  • 通过反射分析 Java 类文件,生成 Swift Wrapper
  • 使用 Swift 宏(@JavaClass@JavaMethod)在编译时展开为 JNI 调用

更具体的就是通过 swift-java wrap-java 命令,工具在运行时利用 Java 反射读取 Java 类(包括 .jar 文件),给每个 Java 类生成对应的 Swift 类型,生成的 Swift 类型使用 @JavaClass@JavaMethod@JavaField 等宏进行标注

用途
@JavaClass 声明一个 Swift 类型是 Java 类的包装
@JavaInterface 声明一个 Swift 类型是 Java 接口的包装
@JavaMethod 将 Swift 方法变为调用 Java 方法的桥接
@JavaField 访问 Java 实例字段
@JavaStaticField 访问 Java 静态字段
@JavaImplementation 在 Swift 中实现 Java native 方法

例如,可以将HelloSwiftMain类型扩展为符合ParsableCommand接口,并使用 Swift 参数解析器来处理 Java 提供的参数:

import ArgumentParser
import SwiftJNI

@JavaClass("org.swift.jni.HelloSwiftMain")
struct HelloSwiftMain: ParsableCommand {
  @Option(name: .shortAndLong, help: "Enable verbose output")
  var verbose: Bool = false

  @JavaImplementation
  static func main(arguments: [String], environment: JNIEnvironment? = nil) {
    let command = Self.parseOrExit(arguments)
    command.run(environment: environment)
  }
  
  func run(environment: JNIEnvironment? = nil) {
    print("Verbose = \(verbose)")
  }
}

所以,对应也存在类型映射的需求:

Java type Swift type
boolean Bool
byte Int8
char UInt16
short Int16
int Int32
long Int64
float Float
double Double
void Void (rare)
T[] [T]
String String
Java class Swift class Swift module
java.lang.Object JavaObject SwiftJava
java.lang.Class<T> JavaClass<T> SwiftJava
java.lang.Throwable Throwable SwiftJava
java.net.URL URL JavaNet

2、Java 调用 Swift(jextract)

主要是让 Java 程序调用 Swift 库 ,生成 Java 绑定和 Swift thunk 文件,支持 FFM 和 JNI 两种生成模式。

比如 jextract 这个流程会分两步:

  • Swift Thunk:用 @_cdecl 暴露 C 符号入口
  • Java 绑定:生成 Java 代码,通过 JNI 或 FFM 调用这些 C 入口

所以,整个 swift-java 主要就是提供自动化方式生成 Swift/Java 绑定,其中:

  • jni 模式兼容性最广,主要支持 Android
  • FFM 模式更偏 Java 22+/25+ 服务端场景,不是 Android 主战场

也就是,swift-java 不只是支持 Android ,它还可以支持 Java Web 场景,野心还是有的。

而整个链路上其实是「Swift - 编译成本地库 - 通过生成的 JNI/Java wrapper 暴露给 Kotlin/Java 调用」 这样一个实现:

image-20260330104416812

那聊到这里,目前局限性也很明显了,它没有一个「 Swift UI for Android 」的支持,它能够让 iOS 的同学把自己的业务逻辑或者纯 Swift 代码共享给 Android ,但是 UI 还是得 Android 自己写。

另外,Swift 社区官方论坛里也有人提到:目前的 Swift Android SDK 下,二进制体积过大,其中一个原因是 Foundation 依赖的 ICU 很重,因为你需要把 Swift runtime、Foundation、ICU 这些东西都带到 Android app 。

同时,在系统 API 上还是差了点意思,比如你需要调用 Andorid 的系统 API 时,大概需要:

Swift ↔ JNI ↔ Java/Kotlin ↔ Android 系统 API

目前这个链路还没有全套完整的官方实现,大概需要后续社区和官方继续补齐,所以当前更多是逻辑和算法等场景的复用,直接调用系统 API 还是会麻烦一些

当然,最终使用过程里,也可以把 Swfit 打包成独立的 Lib,比如在官方例子里的 hello-swift-raw-jni-library ,通过就可以构建出 hello-swift-raw-jni-library-release.aar

./gradlew :hello-swift-raw-jni-library:bundleReleaseAar

所以对于 Swift for Android 来说,目前还是处于起步阶段,作为第一个正式版的起步。

最后,不得不说,语言的最终归宿就是跨平台,UI 的最终归宿也是,现在的 Swift for Android 就像是当年的 KMP ,从语言的跨平台开始切入,未来要发展, Swift UI for Android 的路径看起来也不是不可能,至于最终能否发展起来,这就考验 Swift 社区的运营水平了。

image.png

链接

github.com/swiftlang/s…

github.com/swiftlang/s…

www.swift.org/documentati…

一墙之隔,不同的时空 - 肘子的 Swift 周报 #129

作者 Fatbobman
2026年3月31日 22:00

一年一度的 Let's Vision 大会在上海如期举行,今年的主题是:“Born to Create, Powered by AI”。除了与 Swift、空间计算相关的常规 Session,大会还邀请了许多开发者分享他们在工作中对 AI 的应用与理解。通过这些讲师对 AI 工作流的介绍,我也受益匪浅。原本只能容纳 300 人的 AI 主题会场,里三层外三层站满了热情高涨的观众。

老司机 iOS 周报 #367 | 2026-03-30

作者 ChengzhiHuang
2026年3月29日 22:21

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

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

文章

🐕 用好你的 jj - 重新思考 Agent 时代的版本控制

@阿权:文章是 onevcat(喵神)安利 jj(Jujutsu) 在 AI Agent 时代替代 Git 进行本地版本控制。jj 是可与 Git 无缝兼容的本地版本控制工具(兼容方式为远端仍是 Git 提交),核心优势在于以 change 为核心,无 staging area 等中间态,操作直观,完美适配 AI Agent “先生成后整理”的工作模式,无需开发者打断业务思考指挥版本操作,比 Git 更适合 Agent 参与的本地开发。

🐎 Copy-On-Write in Swift: Semantics, Misconceptions, and a Custom Implementation

@Barney:这篇文章把 Swift 中的 Copy-on-Write 讲得很清楚,重点不是重复 “值类型修改时会复制” 这类结论,而是澄清 COW 只是某些类型选择采用的实现策略,并不是所有 struct 天生自带的机制。作者先从“值类型外壳 + 引用类型存储”的经典结构切入,说明标准库集合为什么能同时兼顾值语义和复制成本;再结合 isKnownUniquelyReferenced(_:) 展示写入前如何判断底层存储是否需要分离,并用一个自定义 SharedBuffer 例子串起完整实现。后半部分还补充了 _read / _modify accessor 在减少额外复制上的作用,以及自定义 COW 真正值得引入的场景:数据量大、复制频繁、读多写少且又希望保留值语义。对需要设计高性能数据结构的同学,这是一篇兼顾原理和落地实现的好文章。

🐎 OpenMAIC

@JonyFang:OpenMAIC(Open Multi-Agent Interactive Classroom)是清华开源的 AI 互动课堂平台,能够将任意主题或文档一键转化为沉浸式学习体验。核心亮点包括:多智能体协作(AI 老师 + AI 同学实时授课讨论)、丰富场景类型(幻灯片、测验、HTML 交互模拟、项目制学习)、白板语音实时讲解,以及 OpenClaw 集成支持在飞书、Slack、Telegram 等 20+ 聊天应用中直接生成课堂。项目支持 Vercel 一键部署和 Docker 本地运行,兼容主流 LLM 服务商,开箱即用。

🐕 Array expression trailing closures in Swift

@Smallfly:这篇文章深入解析了 SE-0508 提案带来的 Swift 语法改进,解决了数组与字典类型长期存在的尾随闭包使用限制,让语言一致性与 API 设计灵活性得到显著提升。核心亮点包括:

历史痛点解决:此前 Swift 解析器因 [T][K:V] 的语法歧义,禁止在数组 / 字典类型表达式后使用尾随闭包,导致自定义初始化器(如 builder 风格、@resultBuilder API)必须使用 .initArray<T> 形式,破坏代码简洁性。SE-0508 移除该限制,允许 [String] { ... } 这类符合直觉的语法。

API 设计赋能:库作者现在可以为数组 / 字典设计更自然的 DSL 风格 API,比如基于 @resultBuilder 的集合初始化器、流式生成数组的构造函数,语法与自定义类型保持统一,降低开发者学习成本。

扩展交互能力:支持与 callAsFunction 特性结合,实现数组字面量后直接接闭包的转换操作(如 ["a","b"] { $0.uppercased() }),进一步提升代码表达力。

语言一致性提升:消除了集合类型与自定义类型在尾随闭包语法上的差异,让 Swift 语言的语法规则更统一,同时仅存在极窄的兼容性影响,整体是小而美的语法优化。

这个提案虽然没有引入新的 runtime 特性,但通过平滑语法边缘,为开发者带来更符合直觉的编码体验,尤其对依赖闭包初始化的集合 API 场景帮助显著。

🐎 Xcode 26 Compilation Cache

@david-clang:Xcode 26 Compilation Cache 的根本目标不仅是让编译器提速 5%,而是彻底停止重复已完成的工作。相比缺乏复用能力的 DerivedData,新机制在输入源未变时会直接提取缓存。这在切换分支、清理重建及高频 CI 场景下,能免去大量无谓的编译损耗。当然,若项目真正的瓶颈在于资源处理或繁杂的脚本,它也并非一劳永逸的银弹。

🐎 Testing with Event Streams

@AidenRao:这篇文章讲的是把一批“靠回调驱动的异步测试”从 XCTest 迁移到 Swift Testing 时,如何既验证回调是否发生,又验证发生顺序。作者对比了 XCTestExpectation、Swift Testing 的 confirmation(容易写成层层嵌套且难区分顺序),最终给出一个很实用的解法:用 AsyncStream 把回调事件“汇总成事件流”,再在测试里收集并断言事件序列,顺手还封装了一个小型 EventStream wrapper 来减少样板代码。

工具

App-Store-Connect-CLI

asc-cli 是一款强大的 App Store Connect 命令行工具。相比于 Fastlane 庞大的体系,它更加聚焦且现代。直接调用 Apple 官方的 App Store Connect API,提供简洁的命令来处理从 Beta 邀请到内购项创建的所有杂活。

最重要的是,它不需要你懂 Ruby,没有复杂的环境配置。 对于追求极致简洁、想在终端或 CI 环境中快速调动 App Store 能力的开发者来说,这是一款足以取代 Fastlane 大部分功能的利器。

代码

🐕 MotionEyes

@Cooper Chen:MotionEyes 是一个面向 AI Agent 的 SwiftUI 动画可观测性工具,它将原本“只能靠肉眼判断”的 UI 动画行为,转化为可量化、可分析的结构化数据。通过在应用中插入轻量级 tracing(如位置、几何、滚动等),开发者可以实时记录动画过程,并以时间序列日志形式还原真实运动轨迹。

项目的亮点在于其“ agent-first ”设计:不仅提供底层埋点能力,还配套自动化调试 workflow 和视觉分析工具,能够生成关键帧、网格标注、像素差异等结果,帮助精确定位动画异常。

相比传统调试方式,MotionEyes 更像一个“动画黑盒分析仪”,适用于排查错位、卡顿、时序错误等复杂 UI 问题。对于构建高质量交互动效或探索 AI 辅助开发流程的团队来说,这是一个非常有前瞻性的基础设施工具。

内推

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

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

关注我们

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

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

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

说明

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

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

Swift多线程方案-Concurrency

作者 Muen
2026年3月28日 11:28

简介

Swift Concurrency(async/await)是从 Swift 5.5 开始引入的一套并发编程模型,用来替代传统的回调(callback)、闭包嵌套(callback hell)、以及部分 GCD 使用场景,让异步代码写起来像同步代码一样清晰。

async / await

  • async:标记函数是 异步函数
  • await:表示 等待异步结果

本质:await 会“挂起当前任务”,但不会阻塞线程

示例

func fetchUser() async -> String {
    return "Tom"
}

func loadData() async {
    let user = await fetchUser()
    print(user)
}

async throws

支持错误处理(替代 callback 的 error)

enum NetworkError: Error {
    case failed
}

func fetchData() async throws -> String {
    throw NetworkError.failed
}

func load() async {
    do {
        let result = try await fetchData()
        print(result)
    } catch {
        print("error: \(error)")
    }
}

Task

Task 是并发执行的基本单位,类似 GCD 的 block,也是一个对象。

  • 普通 Task
Task {
    let data = await fetchUser()
    print(data)
}
let task = Task {
    ...
}
  • Detached Task(独立线程)
Task.detached {
    await doSomething()
}

区别:

Task:继承当前 Actor / 优先级 / 上下文

detached:完全独立(慎用)

async let

并发执行

场景:多个接口同时请求

func loadData() async {
    async let user = fetchUser()
    async let posts = fetchPosts()
    
    let result = await (user, posts)
    print(result)
}

TaskGroup

任务组

场景:批量请求 / 并发处理列表

func fetchAll() async {
    // 每个子任务返回值类型是 String
    await withTaskGroup(of: String.self) { group in
        // 动态创建多个异步任务 并发执行,逐个获取结果
        for i in 1...3 {
            group.addTask {
                return "Task \(i)"
            }
        }
        
        for await result in group {
            print(result)  // 打印每个任务的结果;顺序不确定-先完成先打印
        }
    }
}

生命周期:当代码执行完withTaskGroup { ... },会自动等待所有子任务完成,然后自动释放资源。另外,也能自动取消未完成任务(如果提前退出)。任一任务抛错 就会全部取消。

手动取消 group.cancelAll() ,如 退出页面

使用场景:

  • 动态任务数量 (对比 async let - 任务数固定)
  • 适合列表/批量处理

1、批量接口请求

func fetchUsers(ids: [Int]) async -> [User] {
    var result: [User] = []
    
    await withTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask {
                return await fetchUser(id: id)
            }
        }
        
        for await user in group {
            result.append(user)
        }
    }
    
    return result
}

2、批量下载

func downloadImages(urls: [URL]) async -> [UIImage] {
    var images: [UIImage] = []
    
    await withTaskGroup(of: UIImage?.self) { group in
        for url in urls {
            group.addTask {
                return try? await downloadImage(url: url)
            }
        }
        
        for await img in group {
            if let img = img {
                images.append(img)
            }
        }
    }
    
    return images
}

3、并发计算任务(CPU)

func processData(items: [Int]) async {
    await withTaskGroup(of: Void.self) { group in
        for item in items {
            group.addTask {
                heavyWork(item)
            }
        }
    }
}

4、限制最大并发数(手动处理)

func fetchWithLimit(ids: [Int]) async {
    let maxConcurrent = 2
    await withTaskGroup(of: String.self) { group in
        var iterator = ids.makeIterator()
        // 先启动前 N 个任务
        for _ in 0..<maxConcurrent {
            if let id = iterator.next() {
                group.addTask {
                    return await fetchUser(id: id)
                }
            }
        }
        // 每完成一个,就补一个
        for await result in group {
            print(result)
            if let nextId = iterator.next() {
                group.addTask {
                    return await fetchUser(id: nextId)
                }
            }
        }
    }
}

Actor

线程安全方案

用于解决数据竞争问题(替代锁)

actor Counter {
    private var value = 0
    
    func increment() {
        value += 1
    }
    
    func getValue() -> Int {
        return value
    }
}
let counter = Counter()

Task {
    await counter.increment()
    let v = await counter.getValue()
    print(v)
}

Actor = 自动串行队列 + 数据隔离

对比

方案 问题
NSLock 易死锁
DispatchQueue 需要手动管理
Actor 天然安全

实战应用场景

  • 网络请求
func fetchUser() async -> User { ... }
func fetchPosts() async -> [Post] { ... }

func load() async {
    let user = await fetchUser()
    let posts = await fetchPosts()
}

——对比旧方案

fetchUser { result in
    fetchPosts { posts in
        // 嵌套地狱
    }
}
  • 多个接口并发请求

多个请求任务并行执行,等待异步结果

func loadPage() async {
    async let banner = fetchBanner()
    async let list = fetchList()
    async let profile = fetchProfile()
    
    let (b, l, p) = await (banner, list, profile)
}
  • 主线程更新UI
func loadData() {
    Task {
        let data = await fetchData()
        
        await MainActor.run {
            self.label.text = data
        }
    }
}

或 使用@MainActor

@MainActor
func updateUI() {
    label.text = "Hello"
}
  • 取消任务
let task = Task {
    let data = await fetchData()
}

task.cancel()
  • 图片加载
func downloadImage(url: URL) async throws -> UIImage {
    let (data, _) = try await URLSession.shared.data(from: url)
    return UIImage(data: data)!
}
let task = Task {
    let image = try await downloadImage(url: url)
    cell.imageView.image = image
}

注意:需要处理 cell 复用问题(取消任务)-》 避免图片错位

override func prepareForReuse() {
    task?.cancel()
}
  • 顺序依赖
func process() async {
    let token = await login()
    let data = await fetchData(token: token)
    let result = await parse(data)
}

对比

方案 特点
GCD 底层强,但难维护
Operation 可控但复杂
async/await 简洁 + 可读性强

本质: •GCD:你管理线程 •async/await:系统帮你调度

总结

Swift Concurrency 本质就是:

用“同步写法”写“异步代码”,并且保证线程安全

SwiftUI 如何实现 Infinite Scroll?

作者 RickeyBoy
2026年3月28日 00:56

欢迎点个 star:github.com/RickeyBoy/R…

面试题:用 SwiftUI 实现一个无限滚动列表,支持分页加载。

这道题我在面试中遇到过好几次,说实话第一次答的时候以为随便写个 LazyVStack + onAppear 就完事了。后来才发现,面试官真正想考的不是你会不会用 API,而是你对状态管理、性能优化、Task 生命周期这些东西到底理解多深。

我的思路是从最简方案出发,一步步暴露问题、一步步优化。在开始写代码之前,先聊一下架构选型。

为什么选 MVVM?

先说一下 SwiftUI 里常见的架构选择。MVC 就不聊了,那是 UIKit 时代的标配,Controller 跟 UIKit 强耦合,到了 SwiftUI 里根本没有 UIViewController 这个角色,MVC 自然也就退出舞台了。

SwiftUI 里最常见的架构,从简单到复杂大概是这么几个:

架构 特点 适合场景
MV(Model-View) 没有 ViewModel,状态直接放 View 里,Apple 官方示例的典型写法 逻辑简单的页面
MVVM 抽出 ViewModel 管理状态和逻辑,SwiftUI 里最主流的选择 中等复杂度,需要可测试性
TCA 单向数据流,State + Action + Reducer + Effect,强约束 大型项目,需要严格的状态管理

其中 MV 是最基础的,逻辑简单的页面,@State 往 View 里一放就完事了,Apple 自己的 WWDC 示例大量都是这么写的。但 infinite scroll 涉及分页状态、加载状态、错误处理、Task 生命周期管理这些东西,全塞 View 里会很乱。抽一个 ViewModel 出来专门管理这些状态,View 只负责渲染和转发用户操作,职责就清晰多了。

所以这道题用 MVVM 是最合适的,不是因为 MVVM 最好,而是这个场景的复杂度刚好适合。并且采用 MVVM 结构规整,可拓展性也强,从面试回答的角度来讲也是正好的。

而 SwiftUI 天然就鼓励这种模式,@Observable 本身就是 binding 机制,ViewModel 状态一变,View 自动更新,不需要手动同步。我们后面的代码就是按这个思路来的。

一、最小可用版本

先写一个能跑的最简版本。

核心思路很简单:LazyVStack 只在 item 即将可见时才实例化 View,我们利用 onAppear 检测"最后一个 item 出现了",然后触发下一页请求。

Model

struct Item: Identifiable, Equatable {
    let id: String
    let title: String
}

struct PageInfo {
    let endCursor: String?
    let hasNextPage: Bool
}

ViewModel

@MainActor @Observable
final class ItemListViewModel {
    private(set) var items: [Item] = []
    private var pageInfo: PageInfo?

    func loadNextPage() async {
        let response = try? await APIService.fetchItems(after: pageInfo?.endCursor)
        guard let response else { return }
        items.append(contentsOf: response.items)
        pageInfo = response.pageInfo
    }
}

View

struct ItemListView: View {
    @State private var viewModel = ItemListViewModel()

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(viewModel.items) { item in
                    ItemRow(item: item)
                        .onAppear {
                            if item == viewModel.items.last {
                                Task { await viewModel.loadNextPage() }
                            }
                        }
                }
            }
        }
        .task { await viewModel.loadNextPage() }
    }
}

代码很短,逻辑也直白:

  1. 每当最后一个 item 出现在屏幕上,就触发 loadNextPage()
  2. loadNextPage() 请求后台去 fetch,拿到数据然后塞进 items 中
  3. View 检测到有更新,自动刷新页面

一句话总结:最后一个 item onAppear 的时候,就进行请求。

1.1 分页方式:cursor vs offset

可能有同学会问:为什么 fetchItems(after: cursor) 用的是 cursor,而不是传统的 pageoffset

分页一般有两种方式:

  • Offset-basedfetchItems(page: 3, size: 20),按页码或偏移量取数据
  • Cursor-basedfetchItems(after: "abc123"),传上一页最后一条的标识,从那里往后取

对于 infinite scroll 这种场景,cursor-based 更合适。详细对比一下:

Cursor-based Offset-based
数据一致性 不受中间插入/删除影响 插入新数据会导致重复或遗漏
性能 数据库只需定位到 cursor 后续 大 offset 需要 skip N 行
适用场景 实时 feed、社交流 固定数据集、后台管理列表

简单来说,cursor-based 更适合"数据随时在变"的场景(比如社交 feed),offset-based 更适合"数据基本不变"的场景(比如后台管理列表)。infinite scroll 的数据通常是动态的,所以用 cursor-based。

1.2 LazyVStack vs List

可能有同学会问:为什么用 LazyVStack 而不是 List

先说浅显的回答:LazyVStack 布局更自由,没有 List 自带的分割线、背景色、cell 样式这些限制,适合高度自定义的 UI。而 List 开箱即用,自带滑动删除、拖拽排序这些交互,适合标准列表场景。

当然,如果想要深入回答,还有可以继续。二者还有一个关键区别其实是内存模型

LazyVStack List
View 回收 ❌ 不回收,创建后常驻内存 ✅ 内部回收机制
内存增长 随滚动距离线性增长 基本恒定
自定义布局 完全自由 受限于 List 样式
万级数据 可能有内存压力 表现更好

为什么会有这个区别?因为它们底层的实现不一样。List 底层是基于 UICollectionView(iOS 16 之前是 UITableView),天然有 cell 回收复用机制,滚出屏幕的 cell 会被回收,滚入时再复用,所以内存占用基本恒定。而 LazyVStack 底层只是一个普通的布局容器,"Lazy" 的意思是延迟创建,item 滚入可见区域时才创建 View,但创建之后就一直留在内存里,不会回收。

所以如果列表数据量很大(比如社交 feed 那种上万条的),List 在内存上更有优势。如果需要高度自定义的 UI,那就用 LazyVStack,但要心里有数:用户滚得越远,内存占用越大。

1.3 为什么加 @MainActor

上面的 ViewModel 代码加了 @MainActor,这个很容易被忽略但其实很关键。

@Observable 本身不会自动保证在主线程更新状态。而我们的 loadNextPage() 是在 Task 里通过 await 拿数据,await 之后的代码在哪个线程执行是不确定的。如果恰好在后台线程执行了 items.append(...),SwiftUI 收到状态变更通知后会在后台线程刷新 UI,这就会导致紫色警告("Publishing changes from background threads is not allowed")甚至崩溃。

加上 @MainActor 之后,这个类的所有属性访问和方法调用都会被隔离到主线程,从根源上避免线程安全问题。

另外补充一下:Swift 6.2(Xcode 26)引入了模块级别的 Default Actor Isolation 设置,可以把整个模块的默认隔离改为 MainActor,开启之后所有类型都默认跑在主线程,不用再手动加 @MainActor。但这是一个 opt-in 的设置,默认值还是 nonisolated,而且不是所有项目都会立刻升级。所以目前来说,显式写 @MainActor 仍然是更稳妥的做法。

1.4 几个小细节

有几个代码细节,不影响功能,但代码质量会好不少,属于面试加分项。

private(set) 控制可见性

itemsprivate(set) 修饰,外部只能读不能写。这样 View 就没法直接改 items,所有数据变更都必须经过 ViewModel 的方法,数据流向是单向的。这个习惯在 MVVM 里很重要,不然 View 和 ViewModel 的职责边界很容易模糊。

让 Item 遵循 Equatable

上面的代码里 Item 已经加了 Equatable,所以判断"是不是最后一个"可以直接写 item == viewModel.items.last,不用绕一圈去比 id。后面加叠加更多功能的时候也可以用,代码更简洁。

.task = .onAppear + Task

View 里首次加载用的是 .task { await viewModel.loadNextPage() },这其实等价于在 .onAppear 里手动创建一个 Task。但 .task 有个好处:当 View 消失时会自动 cancel 这个 Task。手动写 Task {} 的话你得自己管 cancel,容易漏掉,所以首次加载优先用 .task

二、防重复请求

上一个最基础的版本,有个明显的问题:快速滚动时 onAppear 有可能会被多次触发,导致同一页被重复请求了。

怎么解决?思路也很直接:加一个 isLoading 标记 + hasNextPage 判断,双重 guard,然后通过这些属性来判断是否需要发送请求。

@MainActor @Observable
final class ItemListViewModel {
    private(set) var items: [Item] = []
    private(set) var isLoading = false
    private var pageInfo: PageInfo?

    var canLoadMore: Bool {
        guard let pageInfo else { return items.isEmpty } // 首次加载
        return pageInfo.hasNextPage && !isLoading
    }

    func loadNextPage() async {
        guard canLoadMore else { return }
        isLoading = true
        defer { isLoading = false }

        let response = try? await APIService.fetchItems(after: pageInfo?.endCursor)
        guard let response else { return }
        items.append(contentsOf: response.items)
        pageInfo = response.pageInfo
    }
}

canLoadMore 这个 computed property 干了两件事:

  • 没有下一页时不请求(通过后端返回的 pageInfo.hasNextPage 来判断)
  • 正在加载时不重复请求(通过 isLoading 来判断)

2.1 小细节

defer 管理状态翻转

注意 isLoading 的写法:开头设为 true,然后紧接着 defer { isLoading = false }。这样不管后面是正常返回还是提前 returnisLoading 都会被重置回 false

如果不用 defer,你就得在每个 return 之前手动加一句 isLoading = false,路径一多很容易漏掉,漏掉的后果就是列表永远卡在 loading 状态,再也加载不了下一页。

canLoadMore 作为 computed property

把"能不能加载"的判断收到一个 computed property 里,而不是在 loadNextPage() 里写一堆 if。好处是逻辑集中,后面要加新条件(比如错误状态下不加载)直接改这一个地方就行,调用方不用动。

三、提前预加载:Threshold Prefetch

目前的逻辑是"最后一个 item 出现了才开始加载",那么用户的感受就是:滚到底 → 停顿 → 等数据 → 新数据出现。那个停顿虽然可能只有几百毫秒,但体感上还是挺明显的。

怎么办?提前触发。 不等最后一个 item,而是在还剩 N 个 item 时就开始加载下一页。

// View
ForEach(viewModel.items) { item in
    ItemRow(item: item)
        .onAppear { viewModel.onItemAppear(item) } // View 层仅透传,将逻辑交给 ViewModel
}

// ViewModel,新增 prefetch threshold
private let prefetchThreshold = 5

func onItemAppear(_ item: Item) {
    guard let index = items.firstIndex(of: item),
          index >= items.count - prefetchThreshold else { return } // 判断是否该加载下一页了
    Task { await loadNextPage() }
}

这样一来,用户还剩 5 个 item 可以滚的时候,网络请求就已经在跑了,等滚到底部时,数据大概率已经回来了,体验上就是"无缝衔接"。

那 threshold 到底设多少合适?这个纯属经验值,根据具体的数据量、UI 复杂度都相关,5 只是一个经验值。总的来讲就是一个 trade-off:

  • threshold 太小:快速滚动还是会看到停顿
  • threshold 太大:用户可能只看前几条就走了,白白浪费请求

四、Task 取消 + 错误处理

到这里基本功能已经没问题了。接下来聊聊 Task 生命周期管理和错误恢复,这部分在面试里属于加分项。

4.1 Task 取消

为什么需要管理 Task 取消?我们目前的例子中,单一列表的情况可能不需要考虑。但是如果是搜索页面的列表,或者叠加筛选功能,问题就复杂了。

举个具体的例子:

  1. 用户在搜索页搜"咖啡",然后在列表页向下滑动,触发了一个 loadNextPage 的请求 A
  2. 还没等数据回来,用户改成搜"奶茶",请求 B 又发出去了
  3. 这时候网络上同时有两个请求在跑。如果请求 B 先于 A 回来,那么等请求 A 回来的时候,用户就会发现明明搜索的是“奶茶”,但是却又展示了不少“咖啡”内容。

这种 bug 不是每次都能复现(取决于网络时序),但一旦出现用户会很困惑,所以解决方式就是:发新请求前先 cancel 旧的,被 cancel 的任务即便返回了 response 也不处理

@MainActor @Observable
final class ItemListViewModel {
    private(set) var items: [Item] = []
    private(set) var isLoading = false
    private(set) var error: Error?
    private var pageInfo: PageInfo?
    private var loadTask: Task<Void, Never>? // 💾 持有当前请求的引用

    func loadNextPage() {
        guard canLoadMore else { return }
        loadTask?.cancel() // ❌ 发新请求前,先 cancel 旧的
        isLoading = true

        loadTask = Task { [weak self] in // 🔒 weak self 防止循环引用
            guard let self else { return }
            defer { self.isLoading = false }

            do {
                let response = try await APIService.fetchItems(after: pageInfo?.endCursor)
                guard !Task.isCancelled else { return } // 🛡️ 被 cancel 了就不写入
                self.items.append(contentsOf: response.items)
                self.pageInfo = response.pageInfo
                self.error = nil
            } catch {
                guard !Task.isCancelled else { return } // 🛡️ 同上
                self.error = error
            }
        }
    }

    func reset() {
        loadTask?.cancel() // ❌ 先 cancel,再清空
        items = []
        pageInfo = nil
        isLoading = false
        error = nil
    }

    // ...
}

4.2 错误重试

错误处理其实是一个很容易被忽略,同时也非常复杂的事情。这里我们的方案是当出现错误的时候,展现一个重试按钮。从 UI 的角度来讲不好看,但实际上面试阶段时间有限,能够展示出有错误处理的思维就可以了。

@MainActor @Observable
final class ItemListViewModel {
    // ...
    private(set) var error: Error?

    func retry() {
        error = nil
        loadNextPage()
    }
}
// View — 列表底部
if viewModel.error != nil {
    RetryButton { viewModel.retry() }
} else if viewModel.isLoading {
    ProgressView()
}

4.3 空状态处理

还有一个容易忽略的边界情况:首次加载完成后,后端返回了 0 条数据。

当前的代码里,items 为空有两种可能:一种是"还在加载第一页",另一种是"加载完了但确实没数据"。如果不区分这两种状态,用户看到的就是一片空白,不知道是在等数据还是真的没有内容。

处理方式也很简单,加一个 computed property 判断一下:

var isEmpty: Bool {
    !isLoading && items.isEmpty && error == nil && pageInfo != nil
}

这里的关键是 pageInfo != nil,说明至少请求过一次了(首次加载前 pageInfonil)。四个条件同时满足,才说明"确实没数据"。

View 里对应的处理:

if viewModel.isEmpty {
    ContentUnavailableView("暂无数据", systemImage: "tray")
} else if viewModel.isLoading && viewModel.items.isEmpty {
    ProgressView() // 首次加载中
} else {
    // 正常的列表内容
}

这样用户就能清楚地区分"加载中"和"没有数据"这两种状态了。

4.4 用 enum 收敛 View 状态

到这里你会发现,View 层需要处理的状态越来越多:首次加载中、有数据、空数据、出错。如果全用 if/else if 判断,条件一多很容易写乱,漏掉某个分支也不会有编译器提醒。

可以定义一个 enum 来收敛这些状态:

enum ViewState {
    case initialLoading    // 首次加载中
    case loaded            // 有数据,正常展示列表
    case empty             // 加载完了但没数据
    case error(String)     // 出错了
}

然后在 ViewModel 里加一个 computed property,从现有属性推导出当前的 View 状态:

var viewState: ViewState {
    if let error, items.isEmpty {
        return .error(error.localizedDescription)
    }
    if isLoading && items.isEmpty {
        return .initialLoading
    }
    if isEmpty {
        return .empty
    }
    return .loaded
}

注意这里的关键:ViewState 是 computed property,不是存储属性。底层的数据源还是 isLoadingitemserrorpageInfo 这些独立属性,viewState 只是把它们组合成 View 更容易消费的形式。这样既不会出现之前 LoadingState enum 耦合状态的问题,又让 View 的代码变得很干净:

var body: some View {
    Group {
        switch viewModel.viewState {
        case .initialLoading:
            ProgressView()
        case .empty:
            ContentUnavailableView("暂无数据", systemImage: "tray")
        case .error(let message):
            ErrorView(message: message) { viewModel.retry() }
        case .loaded:
            ScrollView {
                LazyVStack(spacing: 0) {
                    ForEach(viewModel.items) { item in
                        ItemRow(item: item)
                            .onAppear { viewModel.onItemAppear(item) }
                    }
                    loadingFooter
                }
            }
        }
    }
    .task { viewModel.loadNextPage() }
}

switch 替代 if/else if,每个分支对应一种状态,漏掉任何一个编译器都会报错。用 Group 包裹 switch 是为了能在外层挂 .task 触发首次加载。

五、完整代码

前面一步步拆解完了,最后把所有东西整合到一起。先看一下整体架构:

graph LR
    View -->|用户操作| ViewModel
    ViewModel -->|状态更新| View
    ViewModel -->|网络请求| APIService
    APIService -->|响应数据| ViewModel

    style View fill:#E8F5E9,stroke:#4CAF50
    style ViewModel fill:#E3F2FD,stroke:#2196F3
    style APIService fill:#FFF3E0,stroke:#FF9800

View 只管渲染和转发用户操作,ViewModel 管状态和请求编排,APIService 做实际的网络调用。数据流向是单向的:用户操作 → ViewModel 处理 → APIService 请求 → 数据回来更新状态 → View 自动刷新。

Model

struct Item: Identifiable, Equatable {
    let id: String
    let title: String
}

struct PageInfo: Equatable {
    let endCursor: String?
    let hasNextPage: Bool
}

struct PagedResponse {
    let items: [Item]
    let pageInfo: PageInfo
}

ViewState

enum ViewState {
    case initialLoading
    case loaded
    case empty
    case error(String)
}

ViewModel

@MainActor @Observable
final class ItemListViewModel {
    // MARK: - State

    private(set) var items: [Item] = []
    private(set) var isLoading = false
    private(set) var error: Error?

    // MARK: - Private

    private let prefetchThreshold = 5
    private var pageInfo: PageInfo?
    private var loadTask: Task<Void, Never>?

    // MARK: - Computed

    var canLoadMore: Bool {
        guard !isLoading else { return false }
        guard let pageInfo else { return items.isEmpty }
        return pageInfo.hasNextPage
    }

    var isEmpty: Bool {
        !isLoading && items.isEmpty && error == nil && pageInfo != nil
    }

    var viewState: ViewState {
        if let error, items.isEmpty {
            return .error(error.localizedDescription)
        }
        if isLoading && items.isEmpty {
            return .initialLoading
        }
        if isEmpty {
            return .empty
        }
        return .loaded
    }

    // MARK: - Trigger

    func onItemAppear(_ item: Item) {
        guard let index = items.firstIndex(of: item),
              index >= items.count - prefetchThreshold else { return }
        loadNextPage()
    }

    // MARK: - Actions

    func loadNextPage() {
        guard canLoadMore else { return }
        loadTask?.cancel()
        isLoading = true

        loadTask = Task { [weak self] in
            guard let self else { return }
            defer { self.isLoading = false }

            do {
                let response = try await APIService.fetchItems(after: pageInfo?.endCursor)
                guard !Task.isCancelled else { return }
                self.items.append(contentsOf: response.items)
                self.pageInfo = response.pageInfo
                self.error = nil
            } catch is CancellationError {
                // Task was cancelled, do nothing
            } catch {
                guard !Task.isCancelled else { return }
                self.error = error
            }
        }
    }

    func retry() {
        error = nil
        loadNextPage()
    }

    func reset() {
        loadTask?.cancel()
        items = []
        pageInfo = nil
        isLoading = false
        error = nil
    }
}

View

struct ItemListView: View {
    @State private var viewModel = ItemListViewModel()

    var body: some View {
        Group {
            switch viewModel.viewState {
            case .initialLoading:
                ProgressView()
            case .empty:
                ContentUnavailableView("暂无数据", systemImage: "tray")
            case .error(let message):
                ErrorView(message: message) { viewModel.retry() }
            case .loaded:
                ScrollView {
                    LazyVStack(spacing: 0) {
                        ForEach(viewModel.items) { item in
                            ItemRow(item: item)
                                .onAppear { viewModel.onItemAppear(item) }
                        }
                        loadingFooter
                    }
                }
            }
        }
        .task { viewModel.loadNextPage() }
    }

    @ViewBuilder
    private var loadingFooter: some View {
        if viewModel.error != nil {
            VStack(spacing: 8) {
                Text("加载失败")
                    .font(.caption)
                    .foregroundStyle(.secondary)
                Button("Retry") { viewModel.retry() }
                    .buttonStyle(.bordered)
            }
            .frame(maxWidth: .infinity)
            .padding()
        } else if viewModel.isLoading {
            ProgressView()
                .frame(maxWidth: .infinity)
                .padding()
        }
    }
}

总结

回顾一下整个思路:

  1. 从简单方案说起LazyVStack + onAppear last item,先把原理讲清楚
  2. 暴露问题并优化 — 重复请求 → guard;体验停顿 → threshold prefetch
  3. 展示工程素养 — Task 取消、error handling、retry
  4. 完整架构 — View 只渲染 + 转发,ViewModel 管状态 + 编排

厌倦了那些看着像一个模版复刻出来的抓包工具,我开发了一款iOS端HTTPS抓包调试工具

作者 吴就业
2026年3月27日 22:29

最近的一份工作,因为对业务不熟悉,产品经理出的需求又不考虑历史兼容性,问同事同事也不清楚,作为一个后端开发,我也拿不到客户端的代码,于是我就想到了抓包,通过安装app,抓取某块功能使用了哪些接口。

因为我手机是iPhone, 我因此试用了很多款在app store下载的HTTPS抓包工具,包括免费的Stream、ProxyPin、付费了一款螃蟹抓包。但这些工具感觉都是出自于同一个模版,体验雷同,因为没有别得选择,当时只好忍受。

当时没被满足的一些需求:

1、发现一些图片无法抓取到(我想知道图片用的域名和路径,知道是直接访问云存储,还是用的哪个文件系统服务,这在后端项目中看不出来,因为这个项目的后端也没提供文件上传功能)。

2、JSON无高亮、无搜索功能,也无法对比某个业务参数(比如当商品类型是电子钥匙时、以及商品类型是摄像头时,实际传的参数以及响应的Body有哪些不同的字段)。

3、除体验外,我当时还希望能满足我这个需求:我想把这些接口导入到Apifox,并且基于当前接口和新的迭代需求在此基础上去修改接口,并在团队中共享这份接口。 而当时我只能基于抓取的响应结构,自己在Apifox里面写接口,这耗费了我整整一天时间。

经过那次之后,我决定自己研究写一个,这个HTTPS抓包工具一定把用户体验做好,一定支持抓图片、支持JSON高亮和搜索(甚至是JSON Diff),以及支持自动生成API文档,可以一键导出到Apifox。

2026年1月我开发出来了,这款APP就叫ApiCatcher(因为一开始的目的就是抓API的,所以取名ApiCatcher),所有产品功能皆为原创设计。

能做出来要感谢那些开源项目的,比如ProxyPin,或许是因为开源项目没有盈利,所以体验没做好吧。我似乎也能理解为什么大多数抓包工具长得那么相似了。

我研究了他们的核心抓包功能是如何实现,用了哪些技术,然后自己花两周时间在Claude辅助下用Swift造了一份轮子(就是核心的NIO代理服务器以及SSL握手),在此基础又花两周时间做了优化性能,降低CPU和内存的占用,同时支持抓取大文件请求,避免进程被系统kill掉。我使用SwiftData和文件来存储抓包数据,将请求和响应Body存文件,其它字符串存SwiftData,然后通过边读边写文件来降低对内存的占用,而SwitData则提供更强大的搜索能力,这为产品做查询过滤功能提供了支持,所以ApiCatcher支持非常多的过滤条件。

以下是产品最初几个核心功能的产品设计:

1、极简风格的抓包页面。(我还加了个小创意:正在抓包中的背景是一张蜘蛛网,有一只蜘蛛在上面爬) ApiCatcher | HTTPS抓包工具

2、请求详情内容聚合,便于在手机这种小设备上更好的查看数据,同时减少操作步骤。请求响应的每个部分都是一个卡片,卡片可展开收起。Body可导出和一键复制。Body可展开全屏预览。Body目前支持渲染图片、svg、html、xml和json。 ApiCatcher |请求详情页

3、JSON格式化、高亮、搜索、Diff支持: ApiCatcher | JSON格式化、高亮、搜索、Diff支持

4、接口文档自动生成,以及导出接口文档到Apifox等API调试工具,因为海外用户不用Apifox,所以也支持了Postman和Bruno: ApiCatcher | api导出到Apifox、Postman、Bruno

5、可以抓文件,其实任何HTTP请求都支持,不仅仅是图片,而且没有限制图片大小,多大都能抓,这些图片还可以导出来拿来测试用(一些需要上传特定图片测试的接口):

在这里插入图片描述

经过两个月时间,加上有不少用户给我提需求,于是慢慢功能都完善了。基本app store上的https抓包工具有的功能ApiCatcher都支持了,并且体验更好,像一些正则表达式、脚本都集成AI生成功能提升效率,让用户自己填API Key 。

工具本就是为开发者提升工作效率而开发,所以我们做了支持导入企业内部使用的受信的自签私钥和证书,也可以自己开发一个接收器实时接收抓包流量,实现API扫描分析需求。

这款工具不支持iOS17以下系统,因为用了SwiftData,SwiftData需要17.0以上才支持。整个项目纯SwiftUI开发,核心功能代码用swift-nio等apple官网库。代码高亮则用了WebView+CodeMirror+Highlight.js以及一些插件。这些在app关于我们->开源组件许可都有声明。

ApiCatcherChatTCP这两款网络数据包抓包分析工具都是我自己原创设计、开发的作品,目前两款产品在海外还是不少用户喜欢的,我知道国内大家都喜欢用免费的,比如Stream、ProxyPin、Reqable,但我还是要在各个平台上分享一下的,避免后面被人借鉴反被别人说是我们抄袭,赚不赚钱是次要的,得先证明自己是原创的。

不同阶段的 iOS 应用混淆工具怎么组合使用,源码混淆、IPA混淆

2026年3月27日 18:00

如果把 iOS 应用的混淆只理解成改类名,就会低估这个问题。实际项目里,信息暴露点分散在多个阶段,源码命名、编译产物、资源目录、甚至签名后的 IPA 结构。只用一个工具,很难覆盖完整路径。

这篇文章沿着构建流程往下走,看看每个阶段可以做什么处理,以及不同工具如何拼在一起使用。

在源码阶段先做可控改名

项目还在开发阶段时,可以先处理一部分明显暴露语义的命名,例如:

class VipSubscriptionManager
class PaymentOrderController

如果直接进入编译阶段,这些名称会被带入二进制。

可以通过脚本做一轮批量替换,例如:

  • 使用 Python 脚本扫描类名
  • 生成映射表
  • 替换为无语义名称

这一步的特点是:

  • 控制粒度高
  • 需要改动工程
  • 对团队规范有要求

如果项目已经稳定,这一步不一定适合继续做。

利用 Xcode 构建参数裁剪符号

进入构建阶段,可以先减少一部分信息暴露。

在 Release 配置中:

Strip Debug Symbols = YES
Dead Code Stripping = YES

构建后检查:

strings AppBinary | head

输出会比 Debug 包干净,但核心类名仍然存在。

这一阶段主要是“减少冗余”,不是混淆。

用命令行工具检查当前暴露程度

在进入下一步之前,可以用工具做一次快速判断:

strings AppBinary | grep ViewController

如果输出类似:

LoginViewController
ProfileViewController

说明结构仍然清晰,也可以用:

  • class-dump 查看接口
  • Hopper 查看符号表

这一步的目的是明确需要处理的范围。


在 IPA 层做统一混淆

当项目已经打包成 IPA 后,可以用专门的 iOS 应用混淆工具进行处理。

这里引入 Ipa Guard,它的处理方式不是修改源码,而是直接解析 Mach-O 文件并替换符号。

操作流程:

  1. 打开工具,加载 IPA
  2. 进入代码模块
  3. 选择需要处理的内容

可以看到:

OC 类
Swift 类
OC 方法
Swift 方法

代码混淆

在实际项目中,我们会筛选:

UserManager
PaymentService
VipController

执行混淆后:

UserManager → a82k3

再次用 strings 查看,原名称不会再出现。


资源文件处理不要忽略

很多人只处理代码,但资源同样是入口。

例如:

config/payment.json
assets/vip_banner.png

这些文件名称直接说明业务。

Ipa Guard 的资源模块可以:

  • 批量改名
  • 更新引用路径

处理后:

payment.json → x92ks.json
vip_banner.png → a8d3k.png

重命名


引入前端工具处理 JS / H5

如果项目中有 WebView 或 H5 页面,仅改名不够。

可以在构建阶段执行:

terser main.js -o main.min.js

或:

uglifyjs page.js -o page.min.js

压缩后再交给 IPA 混淆工具处理文件名。

这样组合后:

  • 内容不可读
  • 文件名无语义

修改资源指纹用于打散特征

当多个应用使用相同资源时,文件内容会成为识别依据。

Ipa Guard 支持修改资源 MD5:

md5 banner.png

处理前后结果不同。

这一层不影响功能,但会改变资源特征。 md5


清理调试信息

很多项目在 Release 包中仍然保留日志。

可以检查:

strings AppBinary | grep NSLog

如果输出较多,可以在 IPA 处理阶段删除。

Ipa Guard 支持清理调试信息,使二进制更简洁。


签名工具补上最后一步

所有修改完成后,必须重新签名。

可以使用:

kxsign sign app.ipa \
-c cert.p12 \
-p password \
-m dev.mobileprovision \
-z test.ipa \
-i

或者直接在 Ipa Guard 中配置签名参数。

安装到设备后,验证:

  • 页面是否正常
  • 动态调用是否有效
  • 资源是否加载

重签名


iOS 应用混淆不是某个工具的功能,而是一整条流程。源码阶段、构建阶段、IPA 阶段,各自能做的事情不同。把这些步骤串起来,比单独使用某一个工具更有效。

参考链接:ipaguard.com/blog/161

关于Xcode26.4 踩坑适配

2026年3月27日 17:36

Xcode26.4 踩坑适配

不建议升级Xcode 26.4,Xcode底部控制台无法使用po命令;

iOS 26.4模拟器启动加载巨缓慢,建议保持26.3.1。

随着 Xcode 26.4 正式版发布,编译器对私有头文件访问链式比较语法C++标准库特化的校验规则进一步收紧,导致 iOS 开发中常用的 AFNetworking、YYText、WCDB 三个主流第三方库出现编译报错/警告。本文针对这三类问题提供修复方案,帮助开发者快速完成 Xcode 26.4 适配。

一、AFNetworking:私有头文件访问报错

报错信息

Use of private header from outside its module: 'netinet6/in6.h'

问题原因

Xcode 26.4 强化了模块私有头文件的访问权限校验,AFNetworking 源码中直接引入了系统私有头文件 <netinet6/in6.h>,违反了 Xcode 的模块访问规则,触发编译报错。

解决方案

直接注释掉AFNetworking 中引入该私有头的代码行,无需其他修改即可解决。

  1. 找到 AFNetworking 中包含 #import <netinet6/in6.h> 的文件(通常为AFURLSessionManager.m或核心头文件);
  2. 注释该行代码:
// #import <netinet6/in6.h>
  1. Clean 项目缓存,重新编译即可。

二、YYText:链式比较语法错误

报错信息

Chained comparison 'X < Y < Z' does not behave the same as a mathematical expression

问题原因

Xcode 26.4 编译器对链式比较语法做了严格校验:X < Y < Z 在 OC/C 语言中并非数学意义的连续比较,而是先计算X<Y得到布尔值(0/1),再用该值与 Z 比较,逻辑完全错误。编译器会强制抛出警告,影响编译流程。

解决方案

前半段比较逻辑添加括号,明确运算优先级,修复语法歧义。

代码修改
[self _insideComposedCharacterSequences:line position:position block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) {
    if (isVertical) {
-        position = fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next);
+        position = (fabs(left - point.y) < fabs(right - point.y)) < (right ? prev : next);
    } else {
-        position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next);
+        position = (fabs(left - point.x) < fabs(right - point.x)) < (right ? prev : next);
    }
}];
  1. 按上述代码添加括号;
  2. 重新编译,错误自动消失。

三、WCDB:C++标准库特化报错

报错信息

'is_integral' cannot be specialized: Users are not allowed to specialize this standard library entity

报错文件:Tag.hpp

报错截图

问题原因

Xcode 26.4 升级了底层 Clang/LLVM 编译器,严格遵循 C++标准规范:禁止开发者手动特化std::is_integral等标准库实体,WCDB 旧版源码的 Tag.hpp 文件触发了该规则限制。

解决方案

官方暂未提供修复方案,推荐两种任选其一

方案 1:其他开发者提交的修复 PR(源码修改)

直接应用 WCDB 其他开发者针对该问题的修复 PR,一键修复源码:

  • PR 地址:#1540
  • 操作:拉取 PR 代码替换本地 Tag.hpp 文件,重新编译即可。
方案 2:脚本打包 XCFramework(推荐)

使用 WCDB 官方脚本打包为xcframework,绕过源码编译的规则限制:

  1. 进入 WCDB 源码根目录;
  2. 执行官方打包脚本:
# 路径:/tools/version/build_xcframework.sh

./build_xcframework.sh \
  --scheme WCDBObjc \
  --configuration Release \
  --platforms ios ios-simulator \
  --output ./wcdb_xcframework
🧩 Creating XCFramework for WCDBObjc ...

[cmd] xcodebuild -create-xcframework -archive /Users/fjl/GitHub/wcdb/./wcdb_xcframework/archives/WCDBObjc-ios.xcarchive -framework WCDBObjc.framework -archive /Users/fjl/GitHub/wcdb/./wcdb_xcframework/archives/WCDBObjc-ios-simulator.xcarchive -framework WCDBObjc.framework -output /Users/fjl/GitHub/wcdb/./wcdb_xcframework/xcframeworks/WCDBObjc.xcframework

xcframework successfully written out to: /Users/fjl/GitHub/wcdb/wcdb_xcframework/xcframeworks/WCDBObjc.xcframework

✅ Created XCFramework: /Users/fjl/GitHub/wcdb/./wcdb_xcframework/xcframeworks/WCDBObjc.xcframework

⏱ Total elapsed time: 1 min 7 sec (67 s)
  1. 用生成的 xcframework 替换项目中原有 WCDB 的集成方式;
  2. Clean 项目后编译,问题彻底解决。

参考链接:WCDB Tag.hpp 报错官方 issue


适配总结

  1. AFNetworking:注释私有头引入行,解决模块访问权限问题;
  2. YYText:链式比较加括号,修复编译器语法校验;
  3. WCDB:合并官方 PR 或脚本打包 xcframework,解决 C++标准库特化限制。

完成以上修改后,清理 Xcode 缓存(Cmd+Shift+K),即可适配 Xcode 26.4,正常编译运行。

总结

  1. 三个第三方库的报错均由 Xcode 26.4编译器规则升级导致,修复无需改动业务代码;
  2. AFNetworking、YYText 为轻量代码修改,WCDB 推荐用官方脚本打包方案,稳定性更高;
  3. 适配后务必清理项目缓存,避免编译缓存残留问题。

iOS 26 适配 | 使用 `hidesSharedBackground` 保持导航栏按钮原有样式

作者 iceiceiceice
2026年3月27日 15:19

iOS 26 适配 | 使用 hidesSharedBackground 保持导航栏按钮原有样式

背景

iOS 26 引入了全新的液态玻璃(Liquid Glass)设计语言,导航栏按钮的默认视觉风格发生了较大变化——多个按钮会被合并在一个统一的玻璃背景块中展示。对于希望在 iOS 26 下保持 iOS 26 之前导航栏按钮样式的开发者来说,苹果提供了 hidesSharedBackground API,用于将共享背景拆分,让每个 item 拥有独立的 Liquid Glass 背景:

if (@available(iOS 26.0, *)) {
    item.hidesSharedBackground = YES;
}

启用后,每个 item 的玻璃背景块会被单独渲染,视觉上更接近旧版导航栏中按钮各自独立的呈现方式。但问题随之而来:系统会在每个玻璃背景块之间插入默认间距,开发者无法通过常规 API 将这个间距收紧为 0,导致多个按钮之间出现明显的视觉割裂感,与 iOS 26 之前的紧凑排列效果存在差异。

因此,仅设置 hidesSharedBackground = YES 还不够,还需要额外处理 PlatterView 的间距问题,才能真正还原旧版导航栏的按钮布局样式。


问题根因分析

在 iOS 26 中,每个 UIBarButtonItem 的 Liquid Glass 背景块由私有容器 _UINavigationBarPlatterView 承载。

UINavigationBar
  └── _UINavigationBarContentView
        ├── _UINavigationBarPlatterView   ← 左侧按钮容器(含独立玻璃背景)
        │     └── _UIButtonBarButton
        └── _UINavigationBarPlatterView   ← 右侧按钮容器(含独立玻璃背景)
              └── _UIButtonBarButton

每个 PlatterView 负责绘制该按钮的 Liquid Glass 背景块,同时也决定了按钮在导航栏中的排列位置。系统在计算这些容器的布局时,会在相邻 PlatterView 之间注入固定的默认间距,且这个间距:

  • 无法通过 UIBarButtonSystemItemFixedSpace 负间距消除(iOS 26 已失效)
  • 无法通过修改 customView 的约束影响
  • 无法通过 UINavigationBar 的公开布局 API 干预

解决方案

核心思路:在布局完成后,运行时递归查找所有 PlatterView 容器,强制重置其 x 坐标与 Leading 约束,将相邻玻璃背景块之间的间距收紧为 0,从而还原 iOS 26 之前导航栏按钮的紧凑排列效果。

完整代码

#pragma mark - iOS 26 PlatterView 间距修复

- (void)fixPlatterViewSpace {
    // 收集所有 PlatterView
    NSMutableArray<UIView *> *platterViews = [NSMutableArray array];
    [self collectPlatterViews:self result:platterViews];
    
    if (platterViews.count == 0) return;
    
    CGFloat navBarWidth = self.frame.size.width;
    CGFloat midX = navBarWidth / 2.0;
    
    // 按中心点分左右
    NSMutableArray *leftViews  = [NSMutableArray array];
    NSMutableArray *rightViews = [NSMutableArray array];
    
    for (UIView *v in platterViews) {
        CGFloat centerX = v.frame.origin.x + v.frame.size.width / 2.0;
        if (centerX < midX) {
            [leftViews addObject:v];
        } else {
            [rightViews addObject:v];
        }
    }
    
    // 左侧:按 x 升序,从 0 开始依次排列
    [leftViews sortUsingComparator:^NSComparisonResult(UIView *a, UIView *b) {
        return a.frame.origin.x > b.frame.origin.x
            ? NSOrderedDescending : NSOrderedAscending;
    }];
    CGFloat leftX = 0;
    for (UIView *v in leftViews) {
        [self fixPlatterView:v toX:leftX];
        leftX += v.frame.size.width;
    }
    
    // 右侧:按 x 降序,从右边缘 -5 开始向左排列
    [rightViews sortUsingComparator:^NSComparisonResult(UIView *a, UIView *b) {
        return a.frame.origin.x < b.frame.origin.x
            ? NSOrderedDescending : NSOrderedAscending;
    }];
    CGFloat rightX = navBarWidth - 5;
    for (UIView *v in rightViews) {
        rightX -= v.frame.size.width;
        [self fixPlatterView:v toX:rightX];
    }
}

- (void)collectPlatterViews:(UIView *)view result:(NSMutableArray *)result {
    for (UIView *subview in view.subviews) {
        if ([NSStringFromClass(subview.class) containsString:@"PlatterView"]) {
            [result addObject:subview];
        } else {
            [self collectPlatterViews:subview result:result];
        }
    }
}

- (void)fixPlatterView:(UIView *)platterView toX:(CGFloat)x {
    // 优先修改约束
    for (NSLayoutConstraint *constraint in platterView.superview.constraints) {
        if (constraint.firstItem == platterView &&
            constraint.firstAttribute == NSLayoutAttributeLeading) {
            constraint.constant = x;
        }
    }
    // frame 兜底
    CGRect frame = platterView.frame;
    frame.origin.x = x;
    platterView.frame = frame;
}

调用时机

该方法需要在UINavigationBar布局完成后调用,推荐在 layoutSubviews 末尾触发:

- (void)layoutSubviews {
    [super layoutSubviews];
    
    if (@available(iOS 26.0, *)) {
        [self fixPlatterViewSpace];
    }
}

逻辑拆解

1. 递归收集 PlatterView

[self collectPlatterViews:self result:platterViews];

使用类名字符串匹配 PlatterView,而非直接引用私有类,规避了编译报错。找到 PlatterView 后立即收集,不再递归其子视图,防止嵌套层级的重复收集。

2. 以中线划分左右语义区

CGFloat midX = navBarWidth / 2.0;

导航栏天然地以中线分隔 leftBarButtonItemsrightBarButtonItems 的语义区域,以此作为分组依据,保证左右按钮的 PlatterView 不会被错误归类。

3. 左侧从 x=0 紧密排列

leftX = 0
[BackButton]x = 0
[OtherButton]x = BackButton.width

从导航栏左侧起点开始,将各 PlatterView 依次紧贴排列,彻底消除相邻玻璃背景块之间的系统默认间距,还原旧版左侧按钮的紧凑布局。

4. 右侧从右边缘留 5pt 向左排列

rightX = navBarWidth - 5
[Button2] → rightX -= Button2.width
[Button1] → rightX -= Button1.width

保留 5pt 右侧安全边距,确保最右侧玻璃背景块不会贴边,同时各 PlatterView 之间零间距紧密排布,与旧版右侧按钮排列保持一致。

5. 约束修改 + frame 双保险

// 先改约束(正确路径)
constraint.constant = x;
// 再改 frame(兜底)
platterView.frame = frame;

优先走 Auto Layout 路径修改 Leading 约束保证一致性,frame 赋值作为兜底,确保在纯 frame 布局场景下同样生效。


注意事项

事项 说明
仅限 iOS 26+ @available(iOS 26.0, *) 包裹调用,避免影响低版本行为
调用时机 必须在 layoutSubviews 之后,frame 确定后才能正确分组
Safe Area 左侧从 x=0 起排,刘海屏 / Dynamic Island 下需结合 safeAreaInsets.left 调整起始偏移
私有类名风险 依赖类名包含 PlatterView 的字符串匹配,若苹果后续改名则需同步更新
约束冲突 当前仅修改 Leading 约束;若 PlatterView 同时存在 Trailing / Center 约束,可能引发冲突,需一并处理

小结

iOS 26 的 Liquid Glass 设计语言改变了导航栏按钮的默认视觉风格。对于需要在 iOS 26 下维持旧版导航栏样式的项目,完整的适配路径分为两步:第一步通过 hidesSharedBackground = YES 拆分共享玻璃背景,让每个 item 独立渲染;第二步通过运行时遍历 PlatterView 并强制重置间距,将按钮排列收紧为旧版的紧凑样式。两步缺一不可。

❌
❌