普通视图

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

Swift 基础语法全景(三):元组、错误处理与断言

作者 unravel2025
2025年10月14日 08:02

元组:轻量级“匿名结构体”

  1. 快速组装
// 1. 不命名元素
let http404 = (404, "Not Found")
print(http404.0)   // 404

// 2. 命名元素(推荐,可读性≈struct)
let http200 = (code: 200, description: "OK")
print(http200.code)
  1. 解构赋值
let (statusCode, statusText) = http404
// 只想要其中一个
let (code, _) = http404
  1. 函数多返回值——官方最推崇场景
/// 返回“商”和“余”
func divide(_ a: Int, by b: Int) -> (quotient: Int, remainder: Int)? {
    guard b != 0 else { return nil }   // 除 0 返回 nil
    return (a / b, a % b)
}

if let result = divide(10, by: 3) {
    print("商:\(result.quotient),余:\(result.remainder)")
}

对比单独建 struct:

  • 一次性接口,无需额外类型污染命名空间;
  • 超过 3 个字段或需要方法 → 果断建 struct/class。
  1. 与 switch 搭配
let point = (0, 0)
switch point {
case (0, 0):
    print("原点")
case (_, 0):
    print("在 x 轴")
case (0, _):
    print("在 y 轴")
default:
    print("象限内")
}

错误处理:比 Optional 更丰富的失败信息

  1. 错误类型必须遵 Error 协议
enum SandwichError: Error, LocalizedError {
    case outOfCleanDishes
    case missingIngredients([String])
    
    var errorDescription: String? {
        switch self {
        case .outOfCleanDishes:
            return "没有干净的盘子"
        case .missingIngredients(let list):
            return "缺少食材:\(list.joined(separator: ", "))"
        }
    }
}
  1. 函数标记 throws
class Sandwich {}

func makeSandwich() throws -> Sandwich {
    let dishes = arc4random()
    let pantry = Set<String>(["Bread", "Any", "has"])
    guard dishes > 0 else { throw SandwichError.outOfCleanDishes }
    guard pantry.contains("Bread") && pantry.contains("Ham") else {
        throw SandwichError.missingIngredients(["Bread", "Ham"])
    }
    return Sandwich()
}
  1. 调用方三种写法
写法 场景 备注
try? 失败就变 nil 丢弃错误细节
try! 100% 不会错
do-catch 要细分错误 最常用
// 1) try? —— 快速转 Optional
let sandwich = try? makeSandwich()   // Sandwich?

// 2) do-catch —— 细粒度处理
func eat(_ wich: Sandwich) {
    
}
func washDishes() {
    
}
func buyGroceries(_ list: [String]) {
    
}

do {
    let sandwich = try makeSandwich()
    eat(sandwich)
} catch SandwichError.outOfCleanDishes {
    washDishes()
} catch SandwichError.missingIngredients(let list) {
    buyGroceries(list)
} catch {
    // 兜底,必须写,否则新增错误 case 会漏
    print("未知错误:\(error)")
}
  1. rethrows——高阶函数也能“转发”错误
func retry(_ times: Int, _ body: () throws -> Void) rethrows {
    for _ in 0..<times {
        do {
            try body()
        } catch {
            throw error
        }
    }
}
  1. 错误传递链——跨层穿透
// DAO → Service → UI
func loadUser() throws -> User   // DAO
func presentProfile() throws     // UI
// 任意一层不 catch,编译器自动加 throw,无需手工 return

Assertions vs Preconditions——何时让程序“死”

方法 生效构建 失败行为 用途 性能影响
assert Debug 终止 + 日志 开发期抓 Bug Release 构建中被移除
precondition Debug + Release 终止 + 日志 生产期防脏数据 Release 构建中仍会检查

示例:

let depth = arc4random() % 10
// 开发期:算法入参必须 ≥ 0
assert(depth >= 0, "递归深度不能为负")

let number = "1252412342390847"
// 生产期:银行卡号长度必须 16-19
precondition(number.count >= 16 && number.count <= 19,
             "卡号非法,立即终止流程")

进阶:

  • assertionFailure / preconditionFailure —— 条件已隐含,直接报失败
  • 单元测试可用 XCTAssert 家族,与 assert 不冲突

综合实战

目标:写个 CLI 小工具,扫描指定目录下的 .swift 文件,统计

① 代码行数 ② 空行数 ③ 注释行数,结果通过元组返回;

若目录不存在则抛错;用断言保证“行数 ≥ 0”这一不变式。

#!/usr/bin/env swift

import Foundation

enum CLIError: Error {
    case directoryNotFound
}

typealias Stat = (code: Int, blank: Int, comment: Int)   // 类型别名

/// 统计单个文件
func analyze(_ path: String) throws -> Stat {
    let content = try String(contentsOfFile: path)
    var code = 0, blank = 0, comment = 0
    for line in content.split(separator: "\n", omittingEmptySubsequences: false) {
        let trim = line.trimmingCharacters(in: .whitespaces)
        if trim.isEmpty {
            blank += 1
        } else if trim.hasPrefix("//") {
            comment += 1
        } else {
            code += 1
        }
    }
    assert(code >= 0 && blank >= 0 && comment >= 0, "行数不能为负")
    return (code, blank, comment)
}

/// 递归目录
func analyzeDirectory(_ path: String) throws -> Stat {
    guard FileManager.default.fileExists(atPath: path) else {
        throw CLIError.directoryNotFound
    }
    var total: Stat = (0, 0, 0)
    let files = FileManager.default.enumerator(atPath: path)!
    for case let file as String in files {
        guard file.hasSuffix(".swift") else { continue }
        let sub = try analyze("\(path)/\(file)")
        total = (total.code + sub.code,
                 total.blank + sub.blank,
                 total.comment + sub.comment)
    }
    return total
}

// CLI 入口
do {
    let result = try analyzeDirectory(FileManager.default.currentDirectoryPath)
    print("Swift 文件统计")
    print("代码行:\(result.code)")
    print("空行:\(result.blank)")
    print("注释行:\(result.comment)")
} catch CLIError.directoryNotFound {
    print("错误:目录不存在")
} catch {
    print("未知错误:\(error)")
}

运行:

❯ swift stat.swift
Swift 文件统计
代码行:544157
空行:105382
注释行:101237

再谈“什么时候用哪个特性”——一张思维导图

需要表达“没有值”
├─ 仅关心“有没有” → Optional
├─ 关心“为什么失败” → Error
├─ 开发期断言 → assert
└─ 生产期哨兵 → precondition

需要多值返回
├─ 临时一次性 → Tuple
├─ 需要方法/协议 → Struct/Class
└─ 需要引用语义 → Class

需要类型别名
├─ 底层数据不变,语义更清晰 → typealias
└─ 跨平台兼容 → typealias + 条件编译 #if arch()

Swift 基础语法全景(二):可选型、解包与内存安全

作者 unravel2025
2025年10月14日 08:00

为什么需要 Optional?—— 把“没有值”做成类型

Objective-C 用 nil 指针表示“无”,但运行时才发现野指针;

Swift 把“可能有值 / 可能没有”编译期就写进类型系统,消灭空指针异常。

// 错误:普通 Int 永远不能为 nil
var age: Int = nil      // ❌ Compile-time error

// 正确:可选型才能表达“缺值”
var age: Int? = nil     // ✅

Optional 的本质——语法糖背后的 enum

// 伪代码,真实定义在标准库
enum Optional<Wrapped> {
    case none          // 无值
    case some(Wrapped) // 有值
}

因此 Int? 只是 Optional<Int> 的简写。

你可以手动拼出 Optional:

let x: Optional<Int> = .some(5)
let y: Int? = .none

产生 Optional 的 6 大场景

  1. 可失败构造器
let str = "123"
let num = Int(str)   // 返回 Int?,因为 "abc" 无法转数字
  1. 字典下标
let dict = ["a": 1]
let value = dict["b"] // Int?,键缺失时为 nil
  1. 反射 & KVO
  2. 异步回调“结果/错误”
  3. 链式访问可能中断
  4. 服务端 JSON 解析字段缺失

解包 4 件套

方式 语法 适用场景 风险
强制解包 ! optional! 100% 确定有值 运行时崩溃
可选绑定 if let if let x = optional { } 临时只读常量
守护式 guard let guard let x = optional else { return } 提前退出(如函数/作用域中)
nil-coalescing ?? optional ?? default 提供兜底值

代码对比:

// 1. 强制解包—— Demo 可用,生产禁止
let serverPort = Int("8080")!   // 若配置写错直接崩溃

// 2. if let—— 最常用
if let port = Int("8080") {
    print("绑定端口:\(port)")
}

// 3. guard let—— 函数早期退出,减少嵌套
func connect(host: String, portText: String) -> Bool {
    guard let port = Int(portText) else { return false }
    // port 从这一行开始是非可选
    return true
}

// 4. ?? —— 给默认值,代码最短
let port = Int("8080") ?? 80

可选链 Optional Chaining—— 一句话 安全穿透

class Address {
    var street: String?
}
class Person {
    var address: Address?
}

let bob: Person? = Person()
let streetLength = bob?.address?.street?.count ?? 0
// 任意环节 nil 立即返回 nil,不崩

隐式解包 Optional(IUO===implicit unwrap optional)—— 99% 的场景你不需要

语法:类型后加 ! 而非 ?

var name: String!       // 隐式解包
print(name.count)       // 编译器帮你偷偷加 `!`

看似方便,实际埋雷:

  • 值后来变成 nil → 运行时崩
  • 与 Objective-C 接口对接时,系统 API 可能标记为 IUO,仍需手动判空

官方建议:只在“初始化后立刻有值,且之后不会 nil” 使用,例如 Storyboard IBOutlet。

其他场景用普通 Optional + guard let 最稳。

内存安全四件套——编译期即消灭悬垂指针

Swift 在编译期强制以下规则:

  1. 变量使用前必须初始化(Definite Initialization)
let x: Int
print(x)   // ❌ 报错:使用前未初始化
  1. 数组越界立即崩溃
let arr = [1, 2, 3]
let v = arr[5]   // 运行期崩溃,而非缓冲区溢出
  1. 对象释放后无法访问(ARC + 强引用)
  2. 并发访问冲突检测(Swift 5.5+ Actor & Sendable)

Optional 实战:解析 JSON 字段

struct User: Decodable {
    let id: Int
    let name: String
    let avatar: URL?   // 用户可能没上传头像
}

let json = """
{"id": 1, "name": "Alice"}
""".data(using: .utf8)!

do {
    let user = try JSONDecoder().decode(User.self, from: json)
    let url = user.avatar?.absoluteString ?? "default.png"
    print(url)
} catch {
    print(error)
}

利用 Optional 天然表达“字段缺失”,无需写大量 if xxx != NSNull 判断。

性能Tips:Optional 会多占内存吗?

  • Optional 底层会多 1 个 byte 存放“是否有值”标记,对齐后几乎无感知。
  • 在值类型栈空间,编译器会做内联优化,不用担心“装箱”开销。
  • 高频率调用处(如 3D 顶点数据)可用 UnsafeBufferPointer 避开 Optional。

小结 & checklist

  1. 永远优先用 if let / guard let 而非 !
  2. 对外暴露的 API 返回 Optional,表明“可能失败”
  3. 隐式解包 只留给 @IBOutlet 或立即初始化的常量
  4. 可选链 + nil-coalescing 可让代码保持“一行表达”
  5. 编译期内存安全四件套让 C 式野指针错误几乎绝迹

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

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

常量与变量:let vs var

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

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

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

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

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

✅ 合法

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

❌ 非法

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

基本数据类型一览表

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

整数字面量“花式写法”

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

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

类型推断与类型注解

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

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

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

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

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

浮点与整数互转:

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

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

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

typealias Byte = UInt8
typealias AudioSample = UInt16

let maxAmplitude = AudioSample.min   // 0

工程场景:

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

Print & 字符串插值

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

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

注释:可嵌套的多行注释

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

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

分号:可加可不加

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

小结 & 工程化思考

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

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

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

issue106.webp

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

高通收购 Arduino:历史的轮回

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

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

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

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

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

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

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

前一期内容全部周报列表

近期推荐

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

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


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

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

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


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

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

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


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

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


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

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


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

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

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

工具

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

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

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


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

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

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

Foundation Models Playgrounds

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

image-20251011081702134

往期内容

THANK YOU

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

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

Magpie 和 「AI 贼船」- 再谈 vibe coding,当代码变得廉价时...

2025年10月13日 23:10
最近科技界一扫前些年死气沉沉的阴霾,各种新东西乘着 AI 的东风纷至沓来。我自己分享欲也有点爆表,所以闲暇时 vibe coding 了两个小项目,想要尝试拓展一下自己表达的边界和形式。这篇文章先简单介绍一下两个项目,然后谈谈(作为一个“资深”程序员)在开发过程中的一些体会和感受。 项目简介 Magpie 首先是驱动我个人链接收藏页面的 Magpie (喜鹊,没错我就是很喜欢用鸟来给项目命名的人),它是一个轻量级的链接收藏,后端接入 AI 模型,可以从链接 URL 中获取内容,自动提取标签以及合适的分类,甚至按需求写一些短评。你可以把它想成上个世代的各种 Read it later 服务的 AI 加强版,并且配套的管理后台、快捷指令和 Chrome 插件也都齐备了。 AI 贼船 其次是一个更简单一些的静态页面「上了AI的贼船」。我会不定期在这个页面分享我在使用 AI 工具进行日常开发,生活和娱乐等方面的心得以及实际的使用案例。在 build 时,我使用中文书写的内容会通过 LLM 自动翻译成其他支持的语言;当然,深浅颜色切换,feed 订阅这些基本要素也都完备。 ...

Swift-SOLID编程原则的实践

作者 Muen
2025年10月13日 11:55

SOLID 设计原则概述

SOLID 是 面向对象程序设计中,非常重要的原则,遵循此原则进行编程,意在提高代码的可维护性、可扩展性、可测试性,并减少耦合。

缩写 名称 含义
S Single Responsibility Principle 单一职责
O Open/Closed Principle 开闭原则
L Liskov Substitution Principle 里氏替换
I Interface Segregation Principle 接口隔离
D Dependency Inversion Principle 依赖倒置

单一职责

一个类只负责一项职责。也就是说,一个类应该只有一个引起它变化的原因。

  • 反面教材🚫
class UserManager {

    func registerUser(username: String, password: String) {
        // 注册逻辑
        print("注册一个用户: \(username)")
        
        // 发送邮件(这属于另一种职责)
        sendWelcomeEmail(to: username)
    }

    private func sendWelcomeEmail(to username: String) {
        print("发邮件给: \(username)")
    }
}

这里 ,UserManager 同时负责“注册用户”和“发送邮件”,职责不单一。另外,发送邮件的业务 原则上只能通过调用sendWelcomeEmail来实现。而这里,调用registerUser也能触发 邮件的发送。不合理。

  • 正确示例✅
class UserRegistrationService {
    func register(username: String, password: String) {
        print("User registered: \(username)")
    }
}

class EmailService {
    func sendWelcomeEmail(to username: String) {
        print("Welcome email sent to \(username)")
    }
}

// 使用时
let registrationService = UserRegistrationService()
let emailService = EmailService()
registrationService.register(username: "Tom", password: "123")
emailService.sendWelcomeEmail(to: "Tom")

开闭原则

类、模块、函数应该对扩展开放,对修改关闭。即:不要修改已有代码来适配新需求,而是通过扩展实现。

  • 反面教材🚫
class PaymentProcessor {
    func pay(amount: Double, method: String) {
        if method == "wechat" {
            print("微信支付: \(amount)")
        } else if method == "alipay" {
            print("支付宝支付: \(amount)")
        } else {
            print("其他支付")
        }
    }
}

这样写,每新增一种支付方式,都要修改 pay方法。

  • 正确示例✅
protocol PaymentMethod {
    func pay(amount: Double)
}

class WeChatPay: PaymentMethod {
    func pay(amount: Double) {
        print("微信支付: \(amount)")
    }
}

class Alipay: PaymentMethod {
    func pay(amount: Double) {
        print("支付宝支付: \(amount)")
    }
}

class PaymentProcessor {
    func process(amount: Double, using method: PaymentMethod) {
        method.pay(amount: amount)
    }
}

// 使用
let processor = PaymentProcessor()
processor.process(amount: 100, using: WeChatPay())
processor.process(amount: 100, using: Alipay())

这样,扩展支付方式时,只需新增类,不改旧代码。

在面向对象编程中,一个类要做什么,考虑扩展需要,最好设计为接口(protocol),而不是直接设计为函数及实现。这也是面向协议编程的思想。

里氏替换

子类必须能够替代父类使用,且不影响程序的正确性。

  • 反面教材🚫
class Bird {
    func fly() {
        print("鸟 在飞")
    }
}

class Penguin: Bird {
    override func fly() {
        fatalError("企鹅不能飞")
    }
}

这里, 子类通过重载写自己的业务,但是修改了父类方法实现。这样写代码不好。

  • 正确示例✅
protocol Bird {
    func eat()
}

protocol FlyingBird: Bird {
    func fly()
}

class Sparrow: FlyingBird {
    func eat() { print("麻雀 吃") }

    func fly() { print("麻雀 飞") }
}

class Penguin: Bird {
    func eat() { print("企鹅 吃") }
}

通过继承接口,按需得到相应的能力/做需要的事情

协议的继承

接口隔离

不应强迫客户端依赖它不使用的方法。也就是:设计接口 应精简、专一。外界按需继承。

  • 反面教材🚫
protocol Worker {
    func work()
    func eat()
}

class Robot: Worker {
    func work() { print("机器人工作") }
    
    func eat() { }   // 不需要
}
  • 正确示例✅
protocol Workable {
    func work()
}

protocol Eatable {
    func eat()
}

class Human: Workable, Eatable {
    func work() { print("人 工作") }
    func eat() { print("人 吃饭") }
}

class Robot: Workable {
    func work() { print("机器人工作") }
}

依赖倒置

高层模块不应依赖低层模块,两者都应依赖抽象。换句话说:依赖接口,不依赖实现。(解耦)

  • 反面教材🚫
class MySQLDatabase {
    func save(data: String) {
        print("保存到 MySQL: \(data)")
    }
}

class UserManager {
    let db = MySQLDatabase()
    
    func saveUser(name: String) {
        db.save(data: name)
    }
}

这样写,类 UserManager 直接依赖具体数据库实现,耦合严重。

  • 正确示例✅
protocol Database {
    func save(data: String)
}

class MySQLDatabase: Database {
    func save(data: String) {
        print("保存到 MySQL: \(data)")
    }
}

class SQLiteDatabase: Database {
    func save(data: String) {
        print("保存到 SQLite: \(data)")
    }
}

class UserRepository {
    private let database: Database
    
    init(database: Database) {
        self.database = database
    }
    
    func saveUser(name: String) {
        database.save(data: name)
    }
}

// 使用时 可自由替换数据库
let repo = UserRepository(database: SQLiteDatabase())
repo.saveUser(name: "Tom")

依赖倒置让代码更易于扩展、测试与切换实现。

通过公共协议 来实现类与类的交互,实现解耦

Swift 6.2 类型安全 NotificationCenter:告别字符串撞车

作者 unravel2025
2025年10月13日 11:26

传统通知的痛点

老式 NotificationCenter 三板斧:

import Foundation
extension Notification.Name {
    // 这里的字符创容易拼接错误
    static let didUpdate = Notification.Name("DocumentDidUpdate")
}

class Doc {}
let doc = Doc()
let note = Notification(name: .didUpdate,
                        object: doc,
                        userInfo: ["title": "Hi", "content": "Text"])
NotificationCenter.default.addObserver(forName: .didUpdate, object: nil, queue: nil) { notity in
    // 监听消息 这里需要类型转换
    let userInfo = notity.userInfo as? [String: Any] ?? [:]
    // 这里只是一层title,如果还有第二层的属性,还需要as?转换
    let name = (userInfo["title"] as? [String: String])?["name"]
    print(name ?? "no name")
}

问题清单:

  • 字符串 key 易拼错 → 运行时 nil
  • 手动 as? 强转 → 类型错也 nil
  • userInfo 可选链地狱 → 代码臃肿
  • 无并发安全 → Sendable 检查红成海

Swift 6.2 给出官方解:NotificationCenter.Message 协议族 —— 把通知变成强类型结构体。

Swift 6.2 新武器:Message 协议

@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *)
extension NotificationCenter {
    ///
    /// For example, if there exists a ``Notification`` posted on `MainActor` identified by the ``Notification/Name`` `"eventDidFinish"` with a ``Notification/userInfo``
    /// dictionary containing the key `"duration"` as an ``NSNumber``, an app could post and observe the notification with the following ``MainActorMessage``:
    ///
    /// ```swift
    /// struct EventDidFinish: NotificationCenter.MainActorMessage {
    ///     typealias Subject = Event
    ///     static var name: Notification.Name { Notification.Name("eventDidFinish") }
    ///
    ///     var duration: Int
    ///
    ///     static func makeNotification(_ message: Self) -> Notification {
    ///         return Notification(name: Self.name, userInfo: ["duration": NSNumber(message.duration)])
    ///     }
    ///
    ///     static func makeMessage(_ notification: Notification) -> Self? {
    ///         guard let userInfo = notification.userInfo,
    ///               let duration = userInfo["duration"] as? Int
    ///         else {
    ///             return nil
    ///         }
    ///
    ///         return Self(duration: duration)
    ///     }
    /// }
    /// ```
    ///
    /// With this definition, an observer for this `MainActorMessage` type receives information even if the poster used the ``Notification`` equivalent, and vice versa.
    public protocol MainActorMessage : SendableMetatype {

        /// A type which you can optionally post and observe along with this `MainActorMessage`.
        associatedtype Subject

        /// A optional name corresponding to this type, used to interoperate with notification posters and observers.
        static var name: Notification.Name { get }

        /// Converts a posted notification into this main actor message type for any observers.
        ///
        /// To implement this method in your own `MainActorMessage` conformance, retrieve values from the ``Notification``'s ``Notification/userInfo`` and set them as properties on the message.
        /// - Parameter notification: The posted ``Notification``.
        /// - Returns: The converted `MainActorMessage` or `nil` if conversion is not possible.
        @MainActor static func makeMessage(_ notification: Notification) -> Self?

        /// Converts a posted main actor message into a notification for any observers.
        ///
        /// To implement this method in your own `MainActorMessage` conformance, use the properties defined by the message to populate the ``Notification``'s ``Notification/userInfo``.
        /// - Parameters:
        ///   - message: The posted `MainActorMessage`.
        /// - Returns: The converted ``Notification``.
        @MainActor static func makeNotification(_ message: Self) -> Notification
    }
}

@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *)
extension NotificationCenter {
    /// For example, if there exists a ``Notification`` posted on an arbitrary isolation identified by the ``Notification/Name`` `"eventDidFinish"` with a ``Notification/userInfo``
    /// dictionary containing the key `"duration"` as an ``NSNumber``, an app could post and observe the notification with the following ``AsyncMessage``:
    ///
    /// ```swift
    /// struct EventDidFinish: NotificationCenter.AsyncMessage {
    ///     typealias Subject = Event
    ///     static var name: Notification.Name { Notification.Name("eventDidFinish") }
    ///
    ///     var duration: Int
    ///
    ///     static func makeNotification(_ message: Self) -> Notification {
    ///         return Notification(name: Self.name, userInfo: ["duration": NSNumber(message.duration)])
    ///     }
    ///
    ///     static func makeMessage(_ notification: Notification) -> Self? {
    ///         guard let userInfo = notification.userInfo,
    ///               let duration = userInfo["duration"] as? Int
    ///         else {
    ///             return nil
    ///         }
    ///
    ///         return Self(duration: duration)
    ///     }
    /// }
    /// ```
    ///
    /// With this definition, an observer for this `AsyncMessage` type receives information even if the poster used the ``Notification`` equivalent, and vice versa.
    public protocol AsyncMessage : Sendable {

        /// A type which you can optionally post and observe along with this `AsyncMessage`.
        associatedtype Subject

        /// A optional name corresponding to this type, used to interoperate with notification posters and observers.
        static var name: Notification.Name { get }

        /// Converts a posted notification into this asynchronous message type for any observers.
        ///
        /// To implement this method in your own `AsyncMessage` conformance, retrieve values from the ``Notification``'s ``Notification/userInfo`` and set them as properties on the message.
        /// - Parameter notification: The posted ``Notification``.
        /// - Returns: The converted `AsyncMessage`, or `nil` if conversion is not possible.
        static func makeMessage(_ notification: Notification) -> Self?

        /// Converts a posted asynchronous message into a notification for any observers.
        ///
        /// To implement this method in your own `AsyncMessage` conformance, use the properties defined by the message to populate the ``Notification``'s ``Notification/userInfo``.
        /// - Parameters:
        ///   - message: The posted `AsyncMessage`.
        /// - Returns: The converted ``Notification``.
        static func makeNotification(_ message: Self) -> Notification
    }
}

实现一个 Message 结构体 = 定义通知 Schema,编译器自动生成转换逻辑。

实战:把 Document 更新通知变成类型安全

  1. 定义消息结构体
class Document {
    var title: String = "title"
    var content: String = "content"
}

extension Document {
    struct Update: NotificationCenter.MainActorMessage {
        typealias Subject = Document          // 关联对象类型
        // 这里的name依然使用了字符串,这个不能避免
        static var name: Notification.Name { .init("DocumentDidUpdate") }
        
        let title: String                     // 强类型字段
        let content: String
    }
}
  • MainActorMessage → 回调保证在主线程
  • 若无需主线程 → 改遵 AsyncMessage 即可
  1. 发送通知:一行代码
let doc = Document()
doc.title = "New Title"
NotificationCenter.default.post(
    Document.Update(title: doc.title, content: doc.content),
    subject: doc
)

→ 无字符串 key、无 userInfo、无强转。

  1. 观察通知:返回类型就是消息结构体
struct ContentView: View {
    @State private var token: NotificationCenter.ObservationToken?
    @State private var text = ""
    @State private var doc = Document(title: "Hi", content: "Text")

    var body: some View {
        VStack {
            Text(text)
            Button("Update") {
                doc.title = "Another Title"
                NotificationCenter.default.post(
                    Document.Update(title: doc.title, content: doc.content),
                    subject: doc
                )
            }
        }
        .onAppear {
            token = NotificationCenter.default.addObserver(
                of: doc,
                for: Document.Update.self
            ) { message in   // 👈 消息就是结构体
                text = "标题: \(message.title)  长度: \(message.content.count)"
            }
        }
    }
}

亮点:

  • 回调参数 message 已是 Document.Update → 字段类型自动对
  • 拼写错误 → 编译期报错
  • 观察令牌 ObservationToken 生命周期随 View → 自动移除监听

并发安全:Sendable 一步到位

extension Document.Update: AsyncMessage { }   // 空实现即可
  • 因为结构体里所有字段都遵守 Sendable → 自动满足
  • 回调在后台线程执行也无数据竞争

若字段含非 Sendable 类型,编译器会立刻报错,防止“带病上线”。

向后兼容 & 迁移策略

场景 做法
老代码大量字符串通知 先加 @preconcurrency import Foundation 静默警告,再逐步封装 Message
需要支持旧系统 保留原 Notification.Name + userInfo,新旧 API 并存
模块边界 把 Message 定义在 public extension 里,供外部模块使用

迁移口诀: “先封装 Message,再替换 post/observer,最后删除字符串 key。”

常见编译错误对照

错误 原因 修复
Type 'Update' does not conform to protocol 'Sendable' 字段含非 Sendable 类型 给字段加 Sendable 或改用 class + @unchecked Sendable
Call to main actor-isolated instance method in a synchronous context MainActorMessage 但回调里调 UI 确保回调已加 @MainActor 或包进 MainActor.run
Cannot find 'addObserver' in scope Deployment Target < macOS 26/iOS 26 新 API 仅 Swift 6.2 + 系统版本可用,老系统用旧 API

什么时候用 / 不用

✅ 强烈推荐

  • 新工程全部上新 Message
  • 老工程逐步迁移高价值通知(配置、用户状态)
  • 需要跨模块广播且字段较多

❌ 可以暂缓

  • 仅发送一次且字段极少的通知(如 "appDidBecomeActive"
  • 需要支持远古系统(iOS < 18)

一句话总结

“Message 协议 = 把通知变成结构体:字段类型自动对,编译器替你排雷。”

Swift 控制流深度解析(二):模式匹配、并发与真实项目套路

作者 unravel2025
2025年10月13日 11:05

让自定义类型支持 for-in:三分钟实现 Sequence

需求

自己写了一个“分页加载器”,想这样用:

for page in Paginator(pageSize: 20) {
    print("拿到 \(page.items.count) 条数据")
}

实现

struct Page {
    let items: [String]
}

struct PageIterator: IteratorProtocol {
    let pageSize: Int
    private var current = 1
    init(pageSize: Int, current: Int = 1) {
        self.pageSize = pageSize
        self.current = current
    }
    
    mutating func next() -> Page? {
        // 模拟 3 页就结束
        guard current <= 3 else { return nil }
        defer { current += 1 }
        return Page(items: (0..<pageSize).map { "第\($0)条" })
    }
}

struct Paginator: Sequence {
    let pageSize: Int
    // 只要实现 makeIterator() 即可
    func makeIterator() -> PageIterator {
        PageIterator(pageSize: pageSize)
    }
}

// 验证
for (idx, page) in Paginator(pageSize: 5).enumerated() {
    print("第 \(idx + 1) 页:\(page.items)")
}

要点

  • 只要 makeIterator() 返回的对象能满足 IteratorProtocol,就能享受所有 Sequence 的“语法糖”:for-inmapfilterstrideenumerated()
  • 想支持“倒序”?再写个 ReversedCollection 即可,零侵入。

Switch 模式匹配进阶:写个“小型解析器”

JSON 节点建模

indirect enum JSON {
    case null, bool(Bool), number(Double), string(String)
    case array([JSON])
    case object([String: JSON])
}

一行代码计算“叶子节点数”

func leafCount(_ node: JSON) -> Int {
    switch node {
    case .null, .bool, .number, .string:      // 原子节点
        return 1
    case .array(let arr):
        return arr.map(leafCount).reduce(0, +)
    case .object(let dict):
        return dict.values.map(leafCount).reduce(0, +)
    }
}

利用“值绑定 + where”做校验

func validate(_ json: JSON) -> Bool {
    switch json {
    case .object(let dict) where dict["version"] != nil:
        return true
    case .array(let arr) where !arr.isEmpty:
        return true
    default:
        return false
    }
}

结论:

  • switch 不仅能“匹配值”,还能“解构 + 过滤”,天然适合 AST、路由表、状态机。
  • 配合 indirect enum 可无限嵌套,写编译器前端都够用。

控制流 × 结构化并发

异步循环:逐页下载直到空数据

struct RemoteLoader: AsyncSequence {
    typealias Element = [String]
    let pageSize: Int
    
    struct AsyncIterator: AsyncIteratorProtocol {
        let pageSize: Int
        private var page = 1
        init(pageSize: Int, page: Int = 1) {
            self.pageSize = pageSize
            self.page = page
        }
        
        mutating func next() async throws -> Element? {
            // 模拟网络请求
            try await Task.sleep(nanoseconds: 300_000_000)
            guard page <= 3 else { return nil }   // 第 4 页开始空数据
            defer { page += 1 }
            return (0..<pageSize).map { "Item-\(page)-\($0)" }
        }
    }
    
    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(pageSize: pageSize)
    }
}

// 使用
Task {
    for try await batch in RemoteLoader(pageSize: 5) {
        print("下载到 \(batch.count)\(batch)")
    }
    print("全部下载完成")
}

要点

  • AsyncSequenceSequence 语义完全一致,只是“迭代”变成异步。
  • for try await 就能像同步世界一样写“循环”,避免了回调地狱。
  • 早期退出:break 可直接离开异步循环;Task.checkCancellation() 配合 try Task.sleep() 实现可取消的下载器。

defer 实战:数据库事务模板方法

问题

事务套路重复:

  1. begin
  2. try 块里做 SQL
  3. 成功 commit / 失败 rollback
  4. 还要处理连接关闭

封装

enum DBError: Error { case rollback, commit }

func inTransaction<T>(_ work: (Connection) throws -> T) rethrows -> T {
    let conn = try dbPool.acquire()
    try conn.execute("BEGIN")
    var needRollback = true
    
    // 无论正常 return 还是抛错,都保证最后释放连接
    defer {
        dbPool.release(conn)
    }
    
    // 第二个 defer:只要 work 不崩溃,就一定 rollback(除非被显式覆盖)
    defer {
        if needRollback {
            try? conn.execute("ROLLBACK")
        }
    }
    
    let result = try work(conn)
    try conn.execute("COMMIT")
    
    // 如果走到这里,说明 commit 成功;把 rollback defer 取消掉
    func cancelRollback() {
        needRollback = false
    }   // 空函数,仅用于语法占位
    cancelRollback()           // 调用后,前一个 defer 不再执行(⚠️ 技巧:Swift 没有“撤销 defer”语法,这里通过“提前覆盖”实现)
    
    return result
}

使用方零心智负担:

let newId = try inTransaction { conn in
    try conn.run("INSERT INTO user(name) VALUES (?)", "Alice")
    return conn.lastInsertRowID
}
// 离开作用域:commit 已做,连接已还池。

综合案例:用控制流写个“命令行扫雷”骨架

展示如何把“循环 + switch + where + defer”全部串起来。

enum Cell: String {
    case mine = "💣", empty = "⬜️", flag = "🚩"
}

struct Board {
    let size: Int
    private(set) var cells: [[Cell]]
    init(size: Int) {
        self.size = size
        cells = Array(repeating: Array(repeating: .empty, count: size), count: size)
    }
    
    subscript(x: Int, y: Int) -> Cell {
        get { cells[y][x] }
        set { cells[y][x] = newValue }
    }
    
    func isValid(x: Int, y: Int) -> Bool {
        x >= 0 && y >= 0 && y < size && x < size
    }
    
    mutating func randomSetMines() {
        for x in 0..<size {
            for y in 0..<size {
                if Bool.random() {
                    self[x, y] = Cell.mine
                }
            }
        }
    }
    
    func printSelf(visible: Bool) {
        cells.forEach { cs in
            for c in cs {
                if visible {
                    print(c.rawValue, terminator: " ")
                } else {
                    if case .mine = c  {
                        print(Cell.empty.rawValue, terminator: " ")
                    } else {
                        print(c.rawValue, terminator: " ")
                    }
                }
            }
            print()
        }
        print()
    }
}

func parseInput(_ s: String) -> [Int]? {
    let sChars = s.split(separator: " ")
    if sChars.count < 2 {
        return nil
    }
    return sChars.map { ss in
        Int(ss) ?? 0
    }
}

// 游戏主循环
func game() {
    var board = Board(size: 9)
    var firstStep = true
    
    board.randomSetMines()
    board.printSelf(visible: true)
    print("游戏开始啦")
    
    gameLoop: while true {
        board.printSelf(visible: false)
        
        // 读坐标
        print("输入 x y [f](f 代表插旗):", terminator: "")
        
        guard let line = readLine(),
              let ints = parseInput(line), ints.count >= 2,
              board.isValid(x: ints[0], y: ints[1]) else {
            print("格式或越界,重试")
            continue gameLoop
        }
        let (x, y, isFlag) = (ints[0], ints[1], ints.count == 3)
        
        switch (board[x, y], isFlag) {
        case (.mine, false) where !firstStep:      // 第一次不炸
            print("Game Over")
            break gameLoop
            
        case (.empty, true):
            board[x, y] = .flag
            
        case (.flag, true):                       // 再按一次取消
            board[x, y] = .empty
            
        default:
            break
        }
        firstStep = false
    }
    
    // defer 保证打印结束语
    defer { print("欢迎再来!") }
}

// 运行
game()

控制流技巧复盘:

  • gameLoop 标签精准跳出多重嵌套。
  • switch 用 tuple + where 一次性判断“状态 + 动作”。
  • defer 做“扫尾”,即使未来加 return 也不会漏掉结束语。

避坑指南:最容易踩的 5 个暗礁

现象 官方一句话 正确姿势
switch 空 case 编译报错 case 必须至少一条语句 break 占位或合并
fallthrough 误解 还想绑定值 fallthrough 不能带值绑定 把共用逻辑抽函数
defer 顺序 后注册的先执行 栈结构 把“先开后关”写在一起
AsyncSequence 取消 死循环不退 不会自动检查 循环内显式 try Task.checkCancellation()
guard let 作用域 外部取不到 guard 绑定才延续 guard let x = x else { return }

结语:把“控制流”变成“业务流”

  1. 任何复杂业务,都能拆成“循环 + 分支 + 提前退出”三种原语。
  2. 先写“快乐路径”,再用 guard 把异常路径平行展开,代码会突然清爽。
  3. switch 的模式匹配,本质是“把 if-else 链压缩成语义表”,让机器帮你查表。
  4. defer 是“把释放逻辑提到申请点旁边”,降低心智距离,比 Java 的 finally 更细粒度。
  5. 结构化并发让“异步循环”与“同步循环”共享同一套关键词,未来是 AsyncSequence 的天下。

Swift 控制流深度解析(一):循环、条件与分支

作者 unravel2025
2025年10月13日 11:03

为什么 Swift 的控制流值得单开一篇?

  1. 语法糖多:区间、stride、tuple、where、guard、defer……
  2. 安全严苛:switch 必须 exhaustive、case 不能空、默认不贯穿。
  3. 表达能力强:if/switch 可以当表达式用,一行赋值即可。
  4. 场景丰富:从日常循环到资源清理、API 可用性检查,全覆盖。

For-In 循环:从“会写”到“写对”

基本形态

// 遍历数组
let fruits = ["apple", "orange", "banana"]
for fruit in fruits {
    print("我喜欢吃\(fruit)")
}

字典遍历注意顺序

let legCount = ["ant": 6, "snake": 0, "cat": 4]
// 字典无序!同一台机器多次运行顺序都可能不同
for (animal, legs) in legCount {
    print("\(animal)\(legs) 条腿")
}

区间与“忽略值”

// 闭区间 ...  包含两端
for i in 1...5 {
    print("5 x \(i) = \(5 * i)")
}

// 半开区间 ..<  忽略最后一项
let minutes = 0..<60          // 0~59
for tick in minutes where tick % 5 == 0 {
    // 只打印 0/5/10/.../55
    print("表盘刻度 \(tick)")
}

// 如果根本不想用下标,用 _ 占位
var base = 1
for _ in 1...10 {            // 3 的 10 次方
    base *= 3
}
print("3^10 = \(base)")     // 59049

stride 灵活跳步

// 开区间 stride(from:to:by:)
for degree in stride(from: 0, to: 360, by: 30) {
    print("旋转 \(degree)°")
}

// 闭区间 stride(from:through:by:)
for rate in stride(from: 0.5, through: 1.5, by: 0.25) {
    print("汇率 \(rate)")
}

While 与 Repeat-While:何时选谁?

场景 推荐
可能一次都不执行 while
至少执行一次 repeat-while

蛇梯棋:同一逻辑两种写法

let finalSquare = 25
var board = Array(repeating: 0, count: finalSquare + 1)
// 梯子
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
// 蛇
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

// ----- while 版 -----
var square = 0, diceRoll = 0
while square < finalSquare {
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }          // 模拟 1~6 骰子
    square += diceRoll
    if square < board.count { square += board[square] }
}
print("while 版到达终点")

// ----- repeat-while 版 -----
square = 0; diceRoll = 0
repeat {
    square += board[square]   // 先结算梯子/蛇
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }
    square += diceRoll
} while square < finalSquare
print("repeat-while 版到达终点")

经验:

  • 必须先“爬梯子/滑蛇”再“掷骰子”时,用 repeat-while 可以省一次越界检查。
  • 其余情况 while 可读性更高。

条件语句:if / guard / switch 全维度对比

if 表达式(Swift 5.9+)

let temp = 26
// 一行赋值,不再需要三目嵌套
let advice = if temp <= 0 { "穿羽绒服" }
             else if temp >= 30 { "短袖+冷饮" }
             else { "正常穿衣" }
print(advice)   // 正常穿衣

注意:

  • 所有分支必须返回同一类型,否则需要显式标注类型。
  • 可以抛错:let level = if temp > 100 { throw TempError.boiling } else { "ok" }

guard:提前退出,减少金字塔

func buy(age: Int, stock: Int) {
    guard age >= 18 else {
        print("未成年禁止购买")
        return
    }
    guard stock > 0 else {
        print("库存不足")
        return
    }
    // 以下代码一定是成年人且有库存
    print("购买成功")
}

guard 与 if 的区别:

  1. 必须带 else
  2. else 内必须中断控制流(return/break/continue/throw/fatalError);
  3. 解绑变量作用域延续到后续代码,避免多层嵌套。

switch:模式匹配大杀器

区间 & 复合值

let score = 87
switch score {
case 90...100: print("优秀")
case 80..<90:  print("良好")
case 60..<80:  print("及格")
default:       print("不及格")
}

tuple + where 条件

let point = (2, 2)
switch point {
case (0, 0):                  print("原点")
case (let x, 0):              print("在 x 轴,x=\(x)")
case (0, let y):              print("在 y 轴,y=\(y)")
case (let x, let y) where x == y:  print("在对角线 x=y 上")
default:                      print("其他")
}

值绑定与复合 case

// 复合 case 共享同一段代码
switch "e" {
case "a", "e", "i", "o", "u": print("小写元音")
case "b", "c", "d", "f", "g": print("部分辅音")
default: print("其他字符")
}

// 复合 case 也支持绑定,但要保证类型一致
switch (2, 0) {
case (let d, 0), (0, let d):   // 两个 pattern 都绑定 d 且类型一致
    print("到轴距离为 \(d)")
default:
    break
}

贯穿:显式 fallthrough

let integerToDescribe = 5
var description = "数字 \(integerToDescribe) 是"
switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
    description += " 质数,且"
    fallthrough          // 继续执行下一个 case
default:
    description += " 是整数。"
}
print(description)   // 数字 5 是 质数,且 是整数。

控制转移语句:continue / break / fallthrough / return / throw

continue & break 的最常见误区

// 去掉小写元音与空格
let puzzleInput = "great minds think alike"
let vowels: Set<Character> = ["a", "e", "i", "o", "u", " "]
var output = ""
for ch in puzzleInput {
    if vowels.contains(ch) {
        continue          // 立即进入下一轮
    }
    output.append(ch)
}
print(output)   // grtmndsthnklk

带标签的语句:精确跳出血腥嵌套

var finalSquare = 25
var square = 0
gameLoop: while true {
    let dice = Int.random(in: 1...6)
    switch square + dice {
    case finalSquare:
        print("刚好到达,游戏胜利")
        break gameLoop      // 跳出 while,不是跳出 switch
    case let n where n > finalSquare:
        print("点数太大,重新掷")
        continue gameLoop   // 继续 while 下一轮
    default:
        square += dice
    }
}

defer:作用域退出时的“扫尾”利器

func updateScore() {
    var score = 10
    if Bool.random() {
        score += 5
        defer { print("本次加分已落地,当前分数:\(score)") }  // 1️⃣ 先写后执行
        defer { score -= 100 }                                // 2️⃣ 再写先执行
        print("离开 if 前 score=\(score)")                      // 15
    }
    print("离开函数前 score=\(score)")                          // -85
}
updateScore()

规则小结:

  • 同一作用域多个 defer 以栈顺序执行(先注册后执行)。
  • 无论正常 return、break、continue、throw 都会执行;进程崩溃除外。
  • 典型场景:文件句柄/锁/数据库事务配对释放。

API 可用性检查:让旧系统安心升级

if #available(iOS 15, macOS 12, *) {
    // 仅 iOS15+/macOS12+ 能走到这里
    print("iOS15+/macOS12+")
} else {
    // 低版本走兼容方案
    print("iOS15以下或者macOS12以下或者其他系统,比如Linex、visionOS")
}

// guard 写法,提前 return
func useNewAPI() {
    guard #available(macOS 12, *) else { return }
    // 以下代码编译器保证只在 macOS12+ 运行
}

总结

控制流元素 关键记忆点
for-in 可遍历任何 Sequence;字典无序;stride 可跳步
while vs repeat-while 是否“先检查”
if 表达式 同类型、可抛错、可函数返回值
guard 必须 else 中断,解绑变量作用域外可用
switch exhaustive、默认不贯穿、支持 tuple/where/值绑定
defer 栈式延迟,资源清理黄金搭档
#available 编译期+运行期双重保险

扩展思考:把知识迁移到日常开发

  1. 日志场景

    defer 在函数出口统一打印耗时,避免早期 return 漏埋点。

  2. 资源池

    文件句柄获取后立刻写 defer { fclose(fp) },再也不怕忘记关文件。

  3. 交互式 UI

    利用 stride(from: 0, to: 360, by: 30) 生成圆形按钮坐标,一行代码搞定数学。

  4. 数据校验

    多层 guard 提前返回,把“非法输入”挡在门外,主流程保持一级缩进。

控制流不是“写得出”,而是“写得对、写得优雅”。

第一个成功在APP store 上架的APP

作者 Pluto538
2025年10月12日 23:18

XunDoc开发之旅:当AI医生遇上家庭健康管家

当我在生活中目睹家人为管理复杂的健康数据、用药提醒而手忙脚乱时,一个想法冒了出来:我能否打造一个App,像一位贴心的家庭健康管家,把全家人的健康都管起来?它不仅要能记录数据,还要够聪明,能解答健康疑惑,能主动提醒。这就是 XunDoc App。

1. 搭建家庭的健康数据中枢

起初,我转向AI助手寻求架构指导。我的构想很明确:一个以家庭为单位,能管理成员信息、记录多种健康指标(血压、血糖等)的系统。AI很快给出了基于SwiftUI和MVVM模式的代码框架,并建议用UserDefaults来存储数据。

但对于一个完整的应用而言,我马上遇到了第一个问题:数据如何在不同视图间高效、准确地共享? 一开始我简单地使用@State,但随着功能增多,数据流变得一团糟,经常出现视图数据不同步的情况。

接着在Claude解决不了的时候我去询问Deepseek,它一针见血地指出:“你的数据管理太分散了,应该使用EnvironmentObject配合单例模式,建立一个统一的数据源。” 这个建议成了项目的转折点。我创建了FamilyShareManagerHealthDataManager这两个核心管家。当我把家庭成员的增删改查、健康数据的录入与读取都交给它们统一调度后,整个应用的数据就像被接通了任督二脉,立刻流畅稳定了起来。

2. 请来AI医生:集成Moonshot API

基础框架搭好,接下来就是实现核心的“智能”部分了。我想让用户能通过文字和图片,向AI咨询健康问题。我再次找到AI助手,描述了皮肤分析、报告解读等四种咨询场景,它很快帮我写出了调用Moonshot多模态API的代码。

然而,每件事都不能事事如意的。文字咨询很顺利,但一到图片上传就频繁失败。AI给出的代码在处理稍大一点的图片时就会崩溃,日志里满是编码错误。我一度怀疑是网络问题,但反复排查后,我询问Deepseek,他告诉我:“多模态API对图片的Base64编码和大小有严格限制,你需要在前端进行压缩和校验。”

我把他给我的建议给到了Claude。claude帮我编写了一个“图片预处理”函数,自动将图片压缩到4MB以内并确保编码格式正确。当这个“关卡”被设立后,之前桀骜不驯的图片上传功能终于变得温顺听话。看着App里拍张照就能得到专业的皮肤分析建议,那种将前沿AI技术握在手中的感觉,实在令人兴奋。

3. 打造永不遗忘的智能提醒系统

健康管理,贵在坚持,难在记忆。我决心打造一个强大的医疗提醒模块。我的想法是:它不能是普通的闹钟,而要像一位专业的护士,能区分用药、复查、预约等不同类型,并能灵活设置重复。

AI助手根据我的描述,生成了利用UserNotifications框架的初始代码。但很快,我发现了一个新问题:对于“每周一次”的重复提醒,当用户点击“完成”后,系统并不会自动创建下一周的通知。这完全违背了“提醒”的初衷。

“这需要你自己实现一个智能调度的逻辑,在用户完成一个提醒时,计算出下一次触发的时间,并重新提交一个本地通知。” 这是deepseek告诉我的,我把这个需求告诉给了Claude。于是,在MedicalNotificationManager中, claude加入了一个“重新调度”的函数。当您标记一个每周的用药提醒为“已完成”时,App会悄无声息地为您安排好下一周的同一时刻的提醒。这个功能的实现,让XunDoc从一个被动的记录工具,真正蜕变为一个主动的健康守护者。

4. 临门一脚:App Store上架“渡劫”指南

当XunDoc终于在模拟器和我的测试机上稳定运行后,我感觉胜利在望。但很快我就意识到,从“本地能跑”到“商店能下”,中间隔着一道巨大的鸿沟——苹果的审核。证书、描述文件、权限声明、截图尺寸……这些繁琐的流程让我一头雾水。

这次,我直接找到了DeepSeek:“我的App开发完了,现在需要上传到App Store,请给我一个最详细、针对新手的小白教程。”

DeepSeek给出的回复堪称保姆级,它把整个过程拆解成了“配置App ID和证书”、“在App Store Connect中创建应用”、“在Xcode中进行归档打包”三大步。我就像拿着攻略打游戏,一步步跟着操作:

  • 创建App ID:在苹果开发者后台,我按照说明创建了唯一的App ID com.[我的ID].XunDoc
  • 搞定证书:最让我头疼的证书环节,DeepSeek指导我分别创建了“Development”和“Distribution”证书,并耐心解释了二者的区别。
  • 设置权限:因为App需要用到相机(拍照诊断)、相册(上传图片)和通知(医疗提醒),我根据指南,在Info.plist文件中一一添加了对应的权限描述,确保审核员能清楚知道我们为什么需要这些权限。

一切准备就绪,我在Xcode中点击了“Product” -> “Archive”。看着进度条缓缓填满,我的心也提到了嗓子眼。打包成功!随后通过“Distribute App”流程,我将我这两天的汗水上传到了App Store Connect。当然不是一次就通过上传的。

image.png

5. 从“能用”到“好用”:三次UI大迭代的觉醒

应用上架最初的兴奋感过去后,我陆续收到了一些早期用户的反馈:“功能很多,但不知道从哪里开始用”、“界面有点拥挤,找东西费劲”。这让我意识到,我的产品在工程师思维里是“功能完备”,但在用户眼里可能却是“复杂难用”。

我决定重新设计UI。第一站,我找到了国产的Mastergo。我将XunDoc的核心界面截图喂给它,并提示:“请为这款家庭健康管理应用生成几套更现代、更友好的UI设计方案。”

Mastergo给出的方案让我大开眼界。它弱化了我之前强调的“卡片”边界,采用了更大的留白和更清晰的视觉层级。它建议将底部的标签栏导航做得更精致,并引入了一个全局的“+”浮动按钮,用于快速记录健康数据。这是我第一套迭代方案的灵感来源:从“功能堆砌”转向“简洁现代”

image.png 然而,Mastergo的方案虽然美观,但有些交互逻辑不太符合iOS的规范。于是,第二站,我请来了Stitch。我将完整的产品介绍、所有功能模块的说明,以及第一版的设计图都给了它,并下达指令:“请基于这些材料,完全重现XunDoc的完整UI,但要遵循iOS Human Interface Guidelines,并确保信息架构清晰,新用户能快速上手。”等到他设计好了后 我将我的设计图UI截图给Claude,让他尽可能的帮我生成。

image.png (以上是我的Stitch构建出来的页面) Claude展现出了惊人的理解力。它不仅仅是在画界面,而是在重构产品的信息架构。它建议将“AI咨询”的四种模式(皮肤、症状、报告、用药)从并列排列,改为一个主导航入口,进去后再通过图标和简短说明让用户选择。同时,它将“首页”重新定义为真正的“健康概览”,只显示最关键的数据和今日提醒,其他所有功能都规整地收纳入标签栏。这形成了我的第二套迭代方案从“简洁现代”深化为“结构清晰”

image.png

拿着Claude的输出,我结合Mastergo和Stitch的视觉灵感,再让Cluade一步一步的微调。我意识到,颜色不仅是美观,更是传达情绪和功能的重要工具。我将原本统一的蓝色系,根据功能模块进行了区分:健康数据用沉稳的蓝色,AI咨询用代表智慧的紫色,医疗提醒用醒目的橙色。图标也设计得更加线性轻量,减少了视觉负担。(其实这是Deepseek给我的建议)这就是最终的第三套迭代方案在清晰的结构上,注入温暖与亲和力

image.png 这次从Stitch到Claude的UI重塑之旅,让我深刻意识到,一个成功的产品不仅仅是代码的堆砌。它是一次与用户的对话,而设计,就是这门对话的语言。通过让不同的AI助手在我的引导下“协同创作”,我成功地让XunDoc从一個工程师的作品,蜕变成一个真正为用户着想的产品。

现在这款app已经成功上架到了我的App store上 大家可以直接搜索下来进行使用和体验,我希望大家可以在未来可以一起解决问题!

昨天以前iOS

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

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

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

老司机 iOS 周报 #354 | 2025-10-13

作者 ChengzhiHuang
2025年10月12日 19:46

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

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

新手推荐

🐎 Understanding Deflate

@xiaofei86:本文通过手工解码一个 gzip 文件,简单探究了其压缩算法 Deflate 的工作机制,Deflate 结合了 LZ77 算法与 Huffman 编码,通过用 “复制指令” 替代重复片段实现无损压缩。作者以字符串 "TOBEORNOTTOBEORTOBEORNOT" 为例,先解析 gzip 文件头尾结构,再根据 Deflate 规范逐位还原压缩块内容,实现了从 24 字节到 16 字节的压缩。感兴趣的同学可以阅读更多文章了解 ~

文章

🐢 Code along with the Foundation Models framework

@Cooper Chen:这篇文章介绍了 Apple 在 WWDC 2025 推出的 Foundation Models 框架,展示了如何在 iOS 与 macOS 应用中直接调用系统内置的大语言模型,实现真正的 on-device AI。通过一个“旅行行程生成器”的示例,作者带你一步步完成从文本生成到结构化输出的全过程,深入展示 Apple Intelligence 的开发潜力。

主要亮点包括:

  • 隐私与安全:模型完全在设备上运行,无需上传数据或调用云端接口。
  • 结构化输出:利用 @Generable 让模型直接生成 Swift 类型的数据,而非普通文本。
  • 提示优化技巧:通过 instructions、示例(one-shot)提升输出质量与稳定性。
  • 流式响应:实时展示生成过程,让用户体验更自然流畅。
  • 工具调用(Tool Calling):让模型能主动调用外部函数或服务,融合实时数据与智能生成。

这篇文章不仅是一份技术指南,更是 Apple 对 AI 未来方向的实践展示。
它强调 隐私优先、系统原生、开发高效 的理念,是每位希望深入了解 Apple Intelligence 的开发者必读之作。

🐎 Enabling enhanced security for your app

@Damien:这篇文章介绍了如何在 Xcode 中为应用启用增强安全性的方法,包括启用地址空间布局随机化(ASLR)、栈保护、堆保护、整数溢出检查和缓冲区溢出检查等编译器安全功能,以防御常见漏洞并提升应用抗攻击能力。

🐕 How to install Xcode 26's Metal Toolchain on CI/CD

@Barney:我来帮您获取并总结这篇文章的内容。这篇文章介绍了 Xcode 26 不再默认包含 Metal 工具链的问题及解决方案。在本地开发时可通过 Xcode 偏好设置安装,但在 CI/CD 环境(包括 Xcode Cloud)中需要使用 xcodebuild 命令行工具手动下载和安装。文章提供了具体的脚本代码,建议在 Xcode Cloud 中作为 post clone 脚本运行。

工具

swift-profile-recorder

@Smallfly:Swift Profile Recorder 是一款「进程内」采样分析器,专为受限容器环境而生:不需要 CAP_SYS_PTRACE,就能在 Linux 与 macOS 上抓取 on-CPU 与 off-CPU 样本,定位真实的性能瓶颈。你只需以 Swift 包集成并启用内置服务器,即可用一次 curl 拿到已符号化的 Linux perf 格式数据,直接拖到 Speedscope / Firefox Profiler 或用 FlameGraph 生成火焰图。集成轻量、开销可控,特别适合线上 Kubernetes/Docker 场景的故障排查与持续优化。

内推

重新开始更新「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)

关于桌游设计大赛的介绍

作者 云风
2025年10月12日 17:57

这一篇是前几个月研究桌游规则期间的另一篇小结。因为最近两个多月都在制作 Deep Future 的数字版,没空整理笔记。现在闲下来,汇总整理这么一篇记录。

今年夏天,我迷上了 DIY 类型的桌游。这类桌游最显设计灵感。商业桌游固然被打磨的更好,但设计/制作周期也更长。通常,规则也更复杂,游戏时间更长。我经常买到喜欢的游戏找不到人开。阅读和理解游戏规则也是颇花精力的事情。所以,我近年更倾向于有单人模式的游戏。这样至少学会了规则就能开始玩。但为单人游玩的商业桌游并不算多(不太好卖),而我对多年前玩过的几款 PnP (打印出来即可玩)类单人桌游印象颇为深刻:比如 Delve 和同期的 Utopia Engine (2010)

在 7 月初我逛 bgg 时,一款叫做 Under Falling Skies 的游戏吸引了我。这是一个只需要 9 张自制卡片加几个骰子就可以玩的单人游戏,规则书很短几分钟就理解了游戏机制,但直觉告诉我在这套规则下会有很丰富的变化。我当即用打印机自制了卡片(普通 A4 纸加 9 个卡套)试玩,果然其乐无穷。尤其是高难度模式颇有挑战。进一步探索,我发现这个游戏还有一个商业版本,添加了更长的战役。当即在淘宝上下了单(有中文版本)。

从这个游戏开始,我了解到了 9 卡微型 PnP 游戏设计大赛。从 2008 年开始,在 bgg (boardgamegeek) 上每年都会举办 PnP 游戏设计大赛。这类游戏不限于单人模式,但显然单人可玩的游戏比例更高。毕竟比赛结果是由玩家票选出来,而单人游戏的试玩成本更低,会有更多玩家尝试。据我观察,历年比赛中,单人游戏可占一半。近几年甚至分拆出来单人游戏和双人游戏,多人游戏不同的设计比赛。

根据使用道具的限制条件,比赛又被细分。从 2016 年开始,开始有专门的 9 卡设计大赛。这是众多比赛中比较热门的一个。我想这是因为 9 张卡片刚好可以排版在一张 A4 纸上,只需要双面打印然后切开就完成了 DIY 制作。加上每个桌游玩家都有的少许米宝和骰子,阅读完说明书就可以游戏了。

如果嫌自己 DIY 麻烦或做出来的卡片不好看,在淘宝上有商家专门收集历年比赛中的优秀作品印出来卖,价格也非常实惠。比赛作品中特别优秀的,也会再完善和充实规则,制作大型的商业版本。例如前面介绍的坠空之下就是一例。我觉得,阅读规则书本身也很有意思。不要只看获奖作品,因为评奖只是少量活跃玩家的票选结果,每个玩家口味不同,你会有自己的喜好。而且我作为研究目的,更爱发现不同创作者的有趣灵感。

如果对这个比赛有兴趣,可以以关键词 2025 9-Card Nanogame Print and Play Design Contest 搜索今年的比赛历程。

我花了几周时间玩了大量的 9 卡桌游。喜欢的非常多,无法一一推荐。除了前面提到的坠空之下,让我推荐的话,我会选择 2023 年的 Survival Park (Dinosaurs game) 。倒不是我自己特别偏爱这款,而是我介绍给云豆后,他也很喜欢。

其实,除了 9 卡游戏,还有 18 卡,54 卡等。卡片数量限制提高后,设计者可以设计出更丰富的玩法。例如著名的 Sprawlopolis (无限都市) 一开始就是一款 18 卡桌游,但后来已经出了相当多的扩展。反过来,也有用更少卡片来设计游戏。比如 1 卡设计大赛就限制设计者只使用一张卡片(的正反面)。


在 bgg 上,你可以在 Design Contests 论坛找到每年举办的各种类型设计大赛。除了传统的 各种 PnP 类型外,我很喜欢的还有传统扑克设计比赛。用 2025 Traditional Deck Game Design Contest 就可以搜索到今年的。这个比赛开始的比较晚,2022 年才开始的第一届。

这个比赛限制设计者围绕传统扑克牌来设计游戏玩法。如果你想玩这些游戏,成本比 PnP 游戏更低:你甚至不需要 DIY 卡片,家中找出 1/2 副扑克就可以玩了。我小时候(1980 年代)特别着迷扑克的各种玩法,在书店买到过一本讲解单人扑克玩法的书,把上面介绍的游戏玩了个遍。所以在多年之后见到 Windows 后,对纸牌游戏的玩法相当亲切。

可以说扑克发展了几百年,单人玩法就没太脱离过“接龙”;多人玩法的核心规则也只有吃墩(桥牌)、爬梯(斗地主)、扑克(Poker 一词在英文中特指德州扑克)等少量原型。

但自从有了这种比赛,设计者的灵感相互碰撞,近几年就涌现出大量依托扑克做道具的新玩法。往往是头一年有人想出一个有趣的点子,后一年就被更多设计者发扬光大。电脑上 2024 年颇为好评的小丑牌也是依托德州扑克的核心玩法,不知道是否受过这个系列比赛作品的启发,但小丑牌的确又启发了这两年的诸多作品:例如我玩过的 River Rats 就特别有小丑牌的味道,同时兼备桌游的趣味。

单人谜题类中,我特别喜欢 2024 年的 Cardbury :它颇有挑战,完成游戏的成功率不太高,但单局游戏时间较短,输了后很容易产生再来一盘的冲动。

多人游戏,我向身边朋友推广比较顺利的有 Chowdah 。它结合了拉米和麻将的玩法。我只需要向朋友介绍这是一款使用扑克牌玩的麻将,就能勾起很多不玩桌游的人的兴趣。而玩起来真的有打麻将的感觉,具备一定的策略深度。

我自己曾经想过怎样用传统扑克来模仿一些经典的卡片类桌游,但设计出来总是不尽人意。比如说多年前我很喜欢的 Condottiere 佣兵队长,如果你没玩过它的话,一定也听过或玩过猎魔人 3 中的 Gwent 昆特牌。昆特牌几乎就沿用了佣兵队长的核心规则。而 2024 年的 Commitment 相当成功的还原了佣兵队长的游戏体验。

还有 MOLE 则很好的发展了 Battle Line 。

如果想体验用扑克牌玩出 RPG 的感觉,可以试试 2022 年的Kni54ts :有探索地图、打怪升级捡装备等元素;多人对抗的则有 Pack kingdoms

有趣的游戏规则还有很多,我自己就记了上千行规则笔记。这里就不再一一列出给出评价,有兴趣的同学可以自己探索。

4.布局系统

作者 JZXStudio
2025年10月10日 23:47

大家好,我是K哥。一名独立开发者,同时也是Swift开发框架【Aquarius】的作者,悦记爱寻车app的开发者。

Aquarius开发框架旨在帮助独立开发者和中小型团队,完成iOS App的快速实现与迭代。使用框架开发将给你带来简单、高效、易维护的编程体验。


Aquarius布局系统简介

Aquarius开发框架提供了一套完整的、极简的布局系统。通过该布局系统,你可以轻松的完成基于代码控制的视图布局。

核心价值

  • 🎯 一行顶多行 - 极简API,大幅减少代码量
  • 动效零成本 - 所有布局变化天然支持动画
  • 🔗 关系式布局 - 直观表达视图间相对关系
  • 📦 批量好帮手 - 一次性操作多个视图
  • 🔄 无缝兼容 - 基于原生frame,即插即用

系统特点

  • 直观的定位和大小调整方法
  • 内置动画支持
  • 多视图批量操作

参见源码

  • UIView+aLayout.swift
  • Array+aLayout.swift

复杂度:

  • 基础布局:提供了对控件的基础设置
  • 高级布局:提供了控件间关系型的动态设置

基础布局:重塑单个视图的操控体验

处理单个视图

位置

使用框架提供的方法,你可以轻松的完成视图位置的获取和设置。

  • 获取x位置

不使用框架的获取方式

let x: CGFloat = myView.frame.origin.x

使用框架的获取方式

let x: CGFloat = myView.x()
//或
let x: CGFloat = myView.left()
  • 设置x位置

不使用框架的获取方式

myView.frame.origin.x = 10.0

使用框架的获取方式

myView.x(x: 10.0)
//或
myView.left(left: 10.0)
  • 获取y位置

不使用框架的获取方式

let y: CGFloat = myView.frame.origin.y

使用框架的获取方式

let y: CGFloat = myView.y()
//或
let y: CGFloat = myView.top()
  • 设置y位置

不使用框架的获取方式

myView.frame.origin.y = 10.0

使用框架的获取方式

myView.y(y: 10.0)
//或
myView.top(top: 10.0)
  • 获取右侧位置

不使用框架的获取方式

let right: CGFloat = myView.frame.origin.x + myView.frame.size.width

使用框架的获取方式

let right: CGFloat = myView.right()
  • 设置右侧位置

不使用框架的获取方式

myView.frame.origin.x = 200.0 - myView.frame.size.width

使用框架的获取方式

myView.right(right: 200)
  • 获取底部位置

不使用框架的获取方式

let bottom: CGFloat = myView.frame.origin.y + myView.frame.size.height

使用框架的获取方式

let bottom: CGFloat = myView.bottom()
  • 设置底部位置

不使用框架的获取方式

myView.frame.origin.y = 200.0 - myView.frame.size.height

使用框架的获取方式

myView.bottom(bottom: 200.0)

大小

使用框架提供的方法,你可以轻松的完成视图大小的获取和设置。

  • 获取宽度

不使用框架的获取方式

let width: CGFloat = myView.frame.size.width

使用框架的获取方式

let width: CGFloat = myView.width()
  • 设置宽度

不使用框架的获取方式

myView.frame.size.width = 100.0

使用框架的获取方式

myView.width(width: 100.0)
  • 获取高度

不使用框架的获取方式

let height: CGFloat = myView.frame.size.height

使用框架的获取方式

let height: CGFloat = myView.height()
  • 设置高度

不使用框架的获取方式

myView.frame.size.height = 100.0

使用框架的获取方式

myView.height(height: 100.0)

point

  • 获取point

不使用框架的获取方式

let point: CGPoint = myView.frame.origin

使用框架的获取方式

let point: CGPoint = myView.point()
  • 设置point

不使用框架的获取方式

myView.frame.origin = CGPoint(x: 10.0, y: 10.0)

使用框架的获取方式

myView.point(x: 10.0, y: 10.0)
//或
myView.point(point: CGPoint(x: 10.0, y: 10.0)
//或
myView.point(points: [10.0, 10.0])
//当x, y值相同时
myView.point(xy: 10.0)

Size

  • 获取Size

不使用框架的获取方式

let size: CGSize = myView.frame.size

使用框架的获取方式

let size: CGSize = myView.size()
  • 设置Size

不使用框架的获取方式

myView.frame.size = CGSize(width: 100.0, height: 100.0)

使用框架的获取方式

myView.size(width: 100.0, height: 100.0)
//或
myView.size(w: 100.0, h: 100.0)
//或
myView.size(size: CGSize(width: 100.0, height: 100.0))
//或
myView.size(sizes: [100.0, 100.0])
//当宽和高相同时
myView.size(widthHeight: 100.0)

frame

  • 获取frame

不使用框架的获取方式

let frame: CGRect = myView.frame

使用框架的获取方式

let frame: CGRect = myView.frame()
  • 设置frame

不使用框架的获取方式

myView.frame = CGRect(x: 10.0, y: 10.0, width: 100.0, height: 100.0)
//或
myView.frame = CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 100.0, height: 100.0))

使用框架的获取方式

myView.frame(frame: CGRect(x: 10.0, y: 10.0, width: 100.0, height: 100.0))
//或
myView.frame(x: 10.0, y: 10.0, w: 100.0, h: 100.0)
//或
myView.frame(frames: [10.0, 10.0, 100.0, 100.0])
//当x, y和宽、高相同时
myView.frame(xy: 10.0, widthHeight: 100.0)

处理多个视图

基础操作

  • 设置多个视图相同的x值

不使用框架的获取方式

view1.frame.origin.x = 10.0
view2.frame.origin.x = 10.0

使用框架的获取方式

UIView.x(x: 10.0, views: [view1, view2])
//或
UIView.left(left: 10.0, views: [view1, view2])
  • 设置多个视图相同的right值

使用框架的获取方式

UIView.right(right: 10.0, views: [view1, view2])
  • 设置多个视图相同的y值

不使用框架的获取方式

view1.frame.origin.y = 10.0
view2.frame.origin.y = 10.0

使用框架的获取方式

UIView.y(y: 10.0, views: [view1, view2])
//或
UIView.top(top: 10.0, views: [view1, view2])
  • 设置多个视图相同的bottom值

使用框架的获取方式

UIView.bottom(bottom: 10.0, views: [view1, view2])
  • 设置多个视图相同的宽度

不使用框架的获取方式

view1.frame.size.width = 100.0
view2.frame.size.width = 100.0

使用框架的获取方式

UIView.width(width: 100.0, views: [view1, view2])
  • 设置多个视图相同的高度

不使用框架的获取方式

view1.frame.size.height = 100.0
view2.frame.size.height = 100.0

使用框架的获取方式

UIView.height(height: 100.0, views: [view1, view2])
  • 设置多个视图相同的point

不使用框架的获取方式

view1.frame.origin = CGPoint(x: 10, y: 10)
view2.frame.origin = CGPoint(x: 10, y: 10)

使用框架的获取方式

UIView.point(x: 10.0, y: 10.0, views: [view1, view2])
//或
UIView.point(point: CGPoint(x: 10.0, y: 10.0), views: [view1, view2])
//或
UIView.point(points: [10.0, 10.0], views: [view1, view2])
//当x, y值相同时
UIView.point(xy: 10.0, views: [view1, view2])
  • 设置多个视图相同的Size

不使用框架的获取方式

view1.frame.size = CGSize(width: 100.0, height: 100.0)
view2.frame.size = CGSize(width: 100.0, height: 100.0)

使用框架的获取方式

UIView.size(width: 100.0, height: 100.0, views: [view1, view2])
//或
UIView.size(w: 100.0, h: 100.0, views: [view1, view2])
//或
UIView.size(size: CGSize(width: 100.0, height: 100.0), views: [view1, view2])
//或
UIView.size(sizes: [100.0, 100.0], views: [view1, view2])
//当宽和高相同时
UIView.size(widthHeight: 100.0, views: [view1, view2])
  • 设置多个视图相同的frame

不使用框架的获取方式

view1.frame = CGRect(x: 10.0, y: 10.0, width: 100.0, height: 100.0)
view2.frame = CGRect(x: 10.0, y: 10.0, width: 100.0, height: 100.0)
//或
view1.frame = CGRect(origin: CGPoint(x: 10.0, y: 10.0), size:CGSize(width: 100.0, height: 100.0))
view2.frame = CGRect(origin: CGPoint(x: 10.0, y: 10.0), size:CGSize(width: 100.0, height: 100.0))

使用框架的获取方式

UIView.frame(x: 10.0, y: 10.0, w: 100.0, h: 100.0, views: [view1, view2])
//或
UIView.frame(frame: CGRect(x: 10.0, y: 10.0, width: 100.0, height: 100.0), views: [view1, view2])
//或
UIView.frame(frame: CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 100.0, height: 100.0)), views: [view1, view2])
//或
UIView.frame(frames: [10.0, 10.0, 100.0, 100.0], views: [view1, view2])
//当x, y和宽, 高相等时
UIView.frame(xy: 10.0, widthHeight: 100.0, views: [view1, view2])

对齐操作

框架支持多个视图的对齐

//顶端对齐
UIView.top(views: [view1, view2, view3])
//底部对齐
UIView.bottom(views: [view1, view2, view3])
//左侧对齐
UIView.left(views: [view1, view2, view3])
//右侧对齐
UIView.right(views: [view1, view2, view3])

支持对齐到某个目标位置

//顶端对齐
UIView.top(views: [view1, view2, view3], position: 50)
//底部对齐
UIView.bottom(views: [view1, view2, view3], position: 50)
//左侧对齐
UIView.left(views: [view1, view2, view3], position: 50)
//右侧对齐
UIView.right(views: [view1, view2, view3], position: 50)

分布操作

均匀分布视图:

// 在第一个和最后一个视图之间水平分布视图
UIView.horizontal(views: [view1, view2, view3, view4]) 

// 在第一个和最后一个视图之间垂直分布视图
UIView.vertical(views: [view1, view2, view3, view4])

组合对齐和分布操作

在一次操作中对齐和分布视图:

// 顶端对齐并且水平分布视图
UIView.topAndHorizontal(views: [view1, view2, view3, view4]) 
// 顶端对齐到某个目标位置并且水平分布视图
UIView.topAndHorizontal(views: [view1, view2, view3, view4], position: 50) 
// 底部对齐并且水平分布视图
UIView.bottomAndHorizontal(views: [view1, view2, view3, view4]) 
// 底部对齐到某个目标位置并且水平分布视图
UIView.bottomAndHorizontal(views: [view1, view2, view3, view4], position: 50) 

// 左侧对齐并且垂直分布视图
UIView.leftVertical(views: [view1, view2, view3, view4])
// 左侧对齐到某个目标位置并且垂直分布视图
UIView.leftVertical(views: [view1, view2, view3, view4], position: 50)
// 右侧对齐并且垂直分布视图
UIView.rightVertical(views: [view1, view2, view3, view4])
// 右侧对齐到某个目标位置并且垂直分布视图
UIView.rightVertical(views: [view1, view2, view3, view4], position: 50)

批量操作

框架支持数组的方式批量操作视图

let views: [UIView] = [view1, view2, view3]

views.width(width: 100.0)
views.width(width: 100.0, 1)//设置数组中第2个UIView的宽度

views.height(height: 100.0)
views.height(height: 100.0, 1)//设置数组中第2个UIView的高度

高级布局:构建智能的、响应式的界面

Aquarius开发框架提供了一个强大且灵活的布局系统,超越了基本的定位功能。通过高级布局技术,帮助你用最少的代码和最大的灵活性创建复杂的UI布局。

Aquarius开发框架通过全面的布局方法扩展了UIView,这些方法建立在原生基于框架的布局系统之上,使其更加直观和强大。与Auto Layout的基于约束的方法不同,Aquarius开发框架提供了直接、命令式的API,易于理解且可以无缝动画。

相对定位

基础操作

Aquarius开发框架最强大的功能之一是它能够将视图相对于彼此定位。这消除了在排列视图时进行复杂计算的需要。

// 将viewB定位在viewA下方,间距为10pt
viewB.alignTop(view: viewA, offset: 10)

// 将viewB定位在viewA右侧,间距为15pt
viewB.alignLeft(view: viewA, offset: 15)

// 将viewB定位在viewA左侧,间距为8pt
viewB.alignRight(view: viewA, offset: 8)

// 将viewB定位在viewA上方,间距为12pt
viewB.alignBottom(view: viewA, offset: 12)

批量操作

let views: [UIView] = [view1, view2, view3]

// 将views数组定位在viewA下方,间距为8pt
views.alignTop(view: viewA, offset: 8)

// 将views数组中第2个视图定位在viewA下方,间距为8pt
views.alignTop(view: viewA, offset: 8, 1)

// 将views数组定位在viewA上方,间距为8pt
views.alignBottom(view: viewA, offset: 8)

// 将views数组中第2个UI视图在viewA上方,间距为8pt
views.alignBottom(view: viewA, offset: 8, 1)

// 将views数组定位在viewA右侧,间距为8pt
views.alignLeft(view: viewA, offset: 8)

// 将views数组中第2个视图视图iewA右侧,间距为8pt
views.alignLeft(view: viewA, offset: 8, 1)

// 将views数组定位在viewA左侧,间距为8pt
views.alignRight(view: viewA, offset: 8)

// 将views数组中第2个UI视图视图wA左侧,间距为8pt
views.alignRight(view: viewA, offset: 8, 1)

这些方法简化了流布局的创建,并能够在处理动态内容时无需手动计算位置。

等同定位

基础操作

当希望视图共享相同的边缘位置时,equal方法提供了一个简洁的语法

// 使viewB的左边缘与viewA相同(可选偏移)
viewB.equalLeft(target: viewA, offset: 5)

// 使viewB的右边缘与viewA相同
viewB.equalRight(target: viewA)

// 使viewB的顶部边缘与viewA相同
viewB.equalTop(target: viewA)

// 使viewB的底部边缘与viewA相同
viewB.equalBottom(target: viewA)

// 使viewB的大小与viewA相同
viewB.equalSize(target: viewA)

// 使viewB的框架与viewA相同
viewB.equalRect(target: viewA)

// 使viewB的左侧边缘设置为0
viewB.equalZeroLeft()

// 使viewB的顶端边缘设置为0
viewB.equalZeroTop()

// 使viewB的左侧边缘和顶端边缘设置为0
viewB.equalZeroTopAndLeft()

批量操作

let views: [UIView] = [view1, view2, view3]

// 使views数组的左边缘与viewA相同,间距为5pt
views.equalLeft(target: viewA, offset: 5)

// 使views数组中第2个视图的左边缘与viewA相同,间距为5pt
views.equalLeft(target: viewA, offset: 5, 1)

// 使views数组的右边缘与viewA相同,间距为5pt
views.equalRight(target: viewA, offset: 5)

// 使views数组中第2个UI视图边缘与viewA相同,间距为5pt
views.equalRight(target: viewA, offset: 5, 1)

// 使views数组的宽度与viewA相同,间距为5pt
views.equalWidth(target: viewA, offset: 5)

// 使views数组中第2个视图视图viewA相同,间距为5pt
views.equalWidth(target: viewA, offset: 5, 1)

// 使views数组的高度与viewA相同,间距为5pt
views.equalHeight(target: viewA, offset: 5)

// 使views数组中第2个UI视图视图ewA相同,间距为5pt
views.equalHeight(target: viewA, offset: 5, 1)

这种方法非常适合创建对齐的UI元素,并在界面中保持视觉和谐。

屏幕感知定位

Aquarius开发框架提供了方便的方法来处理屏幕尺寸和系统UI元素:

// 设置视图宽度与屏幕宽度相同
view.equalScreenWidth()

// 设置视图高度与屏幕高度相同
view.equalScreenHeight()

// 设置视图宽度和高度与屏幕高度相同
view.equalScrenSize()

// 设置视图高度与屏幕高度相同,减去状态栏高度
view.equalScreenHeightNoStatus()

// 设置视图高度与屏幕高度相同,减去导航栏高度
view.screenHeightNoNavigation()

// 设置视图高度与屏幕高度相同,减去状态栏和导航栏高度
view.screenHeightNoStatusNoNavigation()

// 设置视图高度与屏幕高度相同,减去底部安全区高度
view.screenHeightNoSafeAreaFooter()

// 设置视图高度与屏幕高度相同,减去tabBar高度
view.screenHeightNoTabBar()

// 设置视图高度与屏幕高度相同,减去状态栏和底部安全区高度
view.screenHeightNoStatusNoSafeAreaFooter()

// 设置视图高度与屏幕高度相同,减去状态栏和tabBar高度
view.screenHeightNoStatusNoTabBar()

// 设置视图高度与屏幕高度相同,减去状态栏、底部安全区和tabBar高度
view.screenHeightNoStatusNoSafeAreaFooterNoTabBar()

// 设置视图高度与屏幕高度相同,减去导航栏和底部安全区高度
view.screenHeightNoNavigationNoSafeAreaFooter()

// 设置视图高度与屏幕高度相同,减去导航栏和tabBar高度
view.screenHeightNoNavigationNoTabBar()

// 设置视图高度与屏幕高度相同,减去导航栏、底部安全区和tabBar高度
view.screenHeightNoNavigationNoSafeAreaFooterNoTabBar()

// 设置视图高度与屏幕高度相同,减去状态栏、导航栏和底部安全区高度
view.screenHeightNoStatusNoNavigationNoSafeAreaFooter()

// 设置视图高度与屏幕高度相同,减去状态栏、导航栏和tabBar高度
view.screenHeightNoStatusNoNavigationNoTabBar()

// 设置视图高度与屏幕高度相同,减去状态栏、导航栏、底部安全区和tabBar高度
view.screenHeightNoStatusNoNavigationNoSafeAreaFooterNoTabBar()

动画集成:让界面“活”起来

Aquarius开发框架针对布局系统提供简单的动画,所有布局系统相关的方法均提供动画功能

// 在0.3秒内动画改变宽度
view.width(width: 200, animate: true, duration: 0.3)

// 使用默认动画时长改变位置
view.left(left: 50, animate: true)

// 使用自定义时序动画多个视图到相同位置
UIView.bottom(bottom: 500, animate: true, duration: 0.5, views: [view1, view2, view3])
...

这种内置的动画支持使创建布局状态之间的平滑过渡变得简单,而无需额外代码。

实战案例:悦记 NoteView 布局剖析

下面以悦记APPNoteViewa_Layout方法为例,展示Aquarius开发框架布局系统的具体使用案例:

import UIKit
import Foundation

import Aquarius
import CommonFramework

class NoteView: BaseView {
    ...
    override func a_Layout() {
        super.a_Layout()

        searchBar.frame(frames: [
                8,
                8,
                screenWidth()-8*2,
                52
            ])

        noteTableView.size(sizes: [
                screenWidth(),
                screenHeight()-navigationBarHeight()-safeAreaFooterHeight()-tabBarHeight()-8
            ])
        noteTableView.equalTop(target: searchBar)
        noteTableView.equalLeft(target: self)

        footerView.equalWidth(target: noteTableView)
        footerView.height(height: 48.0)

        activityIndicatorView.equalSize(target: footerView)
        activityIndicatorView.equalZeroTopAndLeft()

        leftSpaceView.width(width: NoteCell.distance)
        leftSpaceView.equalHeight(target: noteTableView)
        leftSpaceView.equalZeroLeft()
        leftSpaceView.equalTop(target: noteTableView)
        leftSpaceView.target(rightSpaceView)
        leftSpaceView.equals([.size, .top])

        rightSpaceView.left(left: screenWidth()-NoteCell.distance)

        createNoteButton.size(widthHeight: 60)
        createNoteButton.point(points: [
                screenWidth()-60-16,
                screenHeight()-statusBarHeight()-navigationBarHeight()-tabBarHeight()-safeAreaFooterHeight()-createNoteButton.height()
            ])
    }
    ...
}

代码解读

  • 关系清晰:视图间的依赖关系(如 equalTop, equalWidth)让布局逻辑一目了然。
  • 易于维护:当某个视图的尺寸或位置需要调整时,只需修改一处,相关视图会自动更新。
  • 动态适应:使用 screenHeight(), navigationBarHeight() 等方法,布局能自动适应不同的设备尺寸和系统状态。

为什么选择Aquarius布局系统

特性 Aquarius 原生Frame AutoLayout
代码简洁性 🏆 极致简洁 重复繁琐 冗长复杂
学习成本 🏆 几分钟上手 较低 曲线陡峭
动画支持 🏆 原生内置 需手动实现 实现复杂
运行性能 🏆 原生级性能 最佳 布局计算开销

适用场景

  • 🎯 独立开发者:追求开发效率,希望快速迭代。
  • 🎯 动态UI:界面元素需要频繁变化、动画丰富的应用。
  • 🎯 代码控:喜欢用代码精确控制每一个像素,厌恶Storyboard的臃肿。
  • 🎯 迁移项目:老项目基于Frame,希望用最小成本引入现代布局方式。

总结

Aquarius开发框架的布局系统提供了Auto Layout的强大替代方案,强调可读性、简洁性和动画集成。通过本篇文章介绍的相关技术,你可以用更少的代码创建复杂的布局,同时保持对UI元素定位和动画的完全控制。

直观的相对定位、灵活的分布方法和内置的动画支持相结合,使Aquarius开发框架成为需要创建动态、响应式界面的开发者的绝佳选择,而无需约束布局的复杂性。


立即体验Aquarius:

第一步:探索资源

第二步:体验效果

  • 📱 下载示例APP悦记 | 爱寻车 - 感受真实项目中的流畅体验

第三步:沟通交流

Bluetooth常见问题

2025年10月10日 21:48

iOS 蓝牙开发是面试中经常考察的知识点,尤其是涉及硬件交互、IoT 等领域的岗位。下面我为你整理了常见的面试问题,分为基础概念核心流程进阶问题实战问题几大类。


一、基础概念

1. 蓝牙有几种技术标准?iOS 开发主要用哪种?

  • 经典蓝牙 (Bluetooth Classic): 传输速率高、功耗大,主要用于音频传输、文件传输等场景。iOS 对经典蓝牙的设备间通信支持有限(主要通过 MFI 认证)。
  • 低功耗蓝牙 (Bluetooth Low Energy, BLE): 功耗低、数据量小,主要用于设备状态同步、传感器数据采集等。iOS 蓝牙开发主要围绕 BLE

2. iOS 中处理蓝牙的核心框架是哪个?

  • CoreBluetooth.framework
  • 它提供了与 BLE 设备交互的所有必要类和方法。

3. 解释 BLE 中的中心设备 (Central) 和外设 (Peripheral) 角色

  • 外设 (Peripheral): 广播数据的设备,例如智能手环、心率监测器。它持有数据。
  • 中心设备 (Central): 扫描并连接外设的设备,例如 iPhone。它消费数据。
  • 在 iOS 开发中,我们的 App 通常作为 Central 去连接其他 BLE 设备。 但 iOS 设备也可以作为 Peripheral(通过 CBPeripheralManager)。

二、核心流程与 API

4. 描述一个完整的 Central 端连接和数据交互流程

这是最核心的问题,几乎必问。

  1. 创建中心管理器

    centralManager = CBCentralManager(delegate: self, queue: nil)
    
  2. 监听蓝牙状态 (CBCentralManagerDelegate)

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            // 蓝牙已开启,开始扫描
            central.scanForPeripherals(withServices: nil, options: nil)
        }
    }
    
  3. 发现外设

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        // 检查外设名称或广播数据,找到目标设备
        if peripheral.name == "MyDevice" {
            self.targetPeripheral = peripheral
            central.stopScan()
            central.connect(peripheral, options: nil)
        }
    }
    
  4. 连接外设

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        // 连接成功,设置 Peripheral 的代理并开始发现服务
        peripheral.delegate = self
        peripheral.discoverServices(nil) // 传入 nil 发现所有服务
    }
    
  5. 发现服务 (CBPeripheralDelegate)

    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if let services = peripheral.services {
            for service in services {
                // 根据目标服务的 UUID 进行过滤
                if service.uuid == CBUUID(string: "180D") { // 心率服务
                    peripheral.discoverCharacteristics(nil, for: service)
                }
            }
        }
    }
    
  6. 发现特征

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if let characteristics = service.characteristics {
            for characteristic in characteristics {
                // 根据特征的属性进行操作
                if characteristic.properties.contains(.notify) {
                    peripheral.setNotifyValue(true, for: characteristic)
                }
                if characteristic.properties.contains(.read) {
                    peripheral.readValue(for: characteristic)
                }
                // 如果需要写入
                // if characteristic.properties.contains(.write) { ... }
            }
        }
    }
    
  7. 数据交互

    • 读取数据peripheral.readValue(for: characteristic) 然后在 didUpdateValueFor 回调中获取数据。
    • 订阅通知peripheral.setNotifyValue(true, for: characteristic) 数据更新时,会在 didUpdateValueFor 回调中收到。
    • 写入数据peripheral.writeValue(data, for: characteristic, type: .withResponse) (或 .withoutResponse)
  8. 断开连接

    centralManager.cancelPeripheralConnection(peripheral)
    

5. 解释 BLE 的 GATT 结构

  • GATT (Generic Attribute Profile): 定义了 BLE 设备间数据传输的格式和结构。
  • 层级关系Device -> Service -> Characteristic -> Descriptor
    • Service (服务): 一个设备提供的特定功能,例如电池服务、设备信息服务。用 UUID 标识。
    • Characteristic (特征): 服务下的具体数据点,是数据交互的真正载体。例如电池电量、心率测量值。它有自己的 UUID、值和属性(读、写、通知等)。
    • Descriptor (描述符): 对 Characteristic 的额外描述,例如用户描述、特征格式等。

6. CBCharacteristicproperties 有哪些?各自含义是什么?

  • .read: 可读
  • .write: 可写(需要响应)
  • .writeWithoutResponse: 可写(不需要响应,速度快但不可靠)
  • .notify: 可订阅(外设主动推送数据,无确认)
  • .indicate: 可指示(外设主动推送数据,有确认,更可靠)
  • 等等。在操作一个特征前,必须检查其属性。

三、进阶问题

7. 后台模式下的蓝牙处理

  • 配置: 在 Capabilities 中开启 Background Modes 并勾选 Uses Bluetooth LE accessories
  • 限制
    • 扫描时需要使用 Service UUIDs(scanForPeripherals(withServices: [CBUUID], options: ...)),否则在后台扫描不到设备。
    • App 在后台或被挂起时,所有蓝牙事件都会在后台队列中被缓存,当 App 回到前台时会一并交付。
  • 状态保存与恢复: 通过实现 restoreIdentifiercentralManager(_:willRestoreState:) 方法,可以让系统在 App 被杀死后重新启动时恢复蓝牙连接和状态。

8. 如何保证蓝牙连接的稳定性和重连机制?

  • 监听断开事件: 实现 centralManager(_:didDisconnectPeripheral:error:) 代理方法。
  • 实现重连逻辑: 在断开回调中,根据错误码和业务逻辑进行重试连接。
  • 连接超时处理CoreBluetooth 没有原生超时,需要自己用 DispatchWorkItemTimer 实现。
  • 错误处理: 妥善处理常见的错误,如连接失败、服务发现失败等。

9. 蓝牙配对和绑定 (Bonding) 的区别?

  • 配对 (Pairing): 一个一次性的过程,用于交换密钥、认证身份,建立安全的连接。
  • 绑定 (Bonding): 在配对成功后,将交换的密钥(LTK, Long-Term Key)存储起来,以便后续重新连接时无需再次配对。在 iOS 上,这个过程由系统自动管理。

10. 作为 Peripheral 端 (CBPeripheralManager) 需要做什么?

  • 创建 CBPeripheralManager
  • 设置服务和特征(CBMutableServiceCBMutableCharacteristic)。
  • 发布服务到本地数据库。
  • 开始广播。
  • 处理 Central 的订阅、读请求和写请求。

四、实战与经验

11. 你在项目中遇到的蓝牙难点是什么?如何解决的?

这是一个开放性问题,考察实际经验。可能的答案:

  • 连接不稳定: 实现了指数退避算法的重连机制。
  • 数据分包与组包: BLE 单次传输数据量有限(通常是 20 字节),需要设计协议来处理长数据。
  • 多设备连接管理: 使用字典或数组管理多个 CBPeripheral 实例。
  • 不同厂商设备兼容性: 处理非标准的 UUID 或不符合规范的 GATT 结构。

12. 如何调试蓝牙问题?

  • 使用 LightBluenRF Connect 等第三方 App 模拟 Peripheral 或扫描设备,验证硬件本身是否正常。
  • 在 Xcode 中查看 CoreBluetooth 的日志(有时需要额外的系统日志工具)。
  • 检查所有的 Delegate 回调,特别是错误回调。
  • 使用 Packet Logger(需要苹果开发者账号)进行底层 HCI 日志抓取,这是最强大的调试手段。

13. 了解 Bluetooth 5.0 的新特性吗?

  • 2M PHY: 更高的传输速率。
  • LE Audio: 新一代蓝牙音频标准。
  • Long Range: 更长的传输距离。
  • Advertising Extensions: 更强大的广播数据能力。
  • 注意: 这些特性需要手机硬件和外围设备同时支持,并且 iOS API 可能对部分特性有版本要求。

Swift 函数完全指南(四):从 `@escaping` 到 `async/await`——打通“回调→异步→并发”任督二脉

作者 unravel2025
2025年10月10日 19:17

历史包袱:海量第三方 SDK 仍是回调形态

// 某社交 SDK,2025 年依旧没变
func login(
    _ platform: String,
    completion: @escaping (Result<User, Error>) -> Void
)

痛点:

  • 嵌套地狱(Callback Pyramid)
  • 错误处理分散
  • 难以取消

官方桥接器:withCheckedThrowingContinuation

核心思想

把“回调”转成“Continuation”(续体),让编译器帮你生成 async 上下文。

1 行封装

extension SocialSDK {
    static func login(_ platform: String) async throws -> User {
        try await withCheckedThrowingContinuation { continuation in
            login(platform) { result in
                continuation.resume(with: result)
            }
        }
    }
}

调用方瞬间清爽:

let user = try await SocialSDK.login("WeChat")

取消传播

func loginCancelable(_ platform: String) async throws -> User {
    try await withTaskCancellationHandler(
        operation: {
            try await withCheckedThrowingContinuation { con in
                let task = SocialSDK.login(platform) { con.resume(with: $0) }
                // 注册取消回调
                con.onTermination = { _ in task.cancel() }
            }
        },
        onCancel: { SocialSDK.cancelLogin(platform) }
    )
}

并发安全:让“函数”跨线程也无锁

  1. 数据竞争的本质
var counter = 0
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
    counter += 1   // 未保护 → 结果 < 1_000_000
}
  1. actor 把“状态”封装成“函数”
actor Counter1 {
    private var value = 0
    func increment() -> Int {
        value += 1
        return value
    }
}

Task {
    let counter = Counter1()
    await withTaskGroup(of: Void.self) { group in
        for _ in 0..<1_000_000 {
            group.addTask { _ = await counter.increment() }
        }
    }
    print(await counter.increment()) // 1_000_001
}
  • actor 内部任意函数都是“串行队列”语义,外部调用需 await
  • 编译期保证无锁线程安全。
  1. 纯函数 + Sendable 实现“无锁并行”
/// 纯函数:无副作用,仅依赖参数
func hash(_ x: Int) -> Int { x &* 31 &+ 7 }

Task.detached {
    /// Int 已遵循 Sendable,可安全跨线程
    await withTaskGroup(of: Int.self) { group in
        for i in 0..<1_000_000 {
            group.addTask { hash(i) }
        }
        let hashes = await group.reduce(0, +)
    }
}

规则:

  • 值类型(IntStringArray 等)默认 Sendable
  • 自定义引用类型需显式 final class Box: Sendable 并保证内部不可变或加锁。

函数式高阶算子:并发版 map / filter / reduce

  1. concurrentMap
extension Sequence {
    func concurrentMap<T: Sendable>(
        _ transform: @Sendable @escaping (Element) async throws -> T
    ) async rethrows -> [T] where Element : Sendable {
        try await withThrowingTaskGroup(of: (Int, T).self) { group in
            // 1. 提交所有任务
            for (index, item) in self.enumerated() {
                group.addTask { (index, try await transform(item)) }
            }
            // 2. 收集结果,保持顺序
            var pairs = [(Int, T)]()
            for try await pair in group { pairs.append(pair) }
            return pairs.sorted { $0.0 < $1.0 }.map(\.1)
        }
    }
}
  1. 使用:并发下载 100 张缩略图
Task {
    let urls = (0..<100).map { "https://picsum.photos/200/300?random=\($0)" }
    let _: [Data] = try await urls.concurrentMap { url -> Data in
        let (data, _) = try await URLSession.shared.data(from: URL(string: url)!)
        return data
    }
}
  • 自动利用所有 CPU 核心
  • 顺序与输入一致,无需二次排序
  1. concurrentCompactMap / concurrentFilter 同理,留给读者练习。

终极案例:用“函数”写出可组合的并发管道

需求:

  1. 并发下载 HTML →
  2. 并发解析标题 →
  3. 顺序汇总报告
struct Report {
    let url: String
    let title: String
}

func pipeline(_ urls: [String]) async throws -> [Report] {
    try await urls
        .concurrentMap { url -> (String, Data) in
            let (data, _) = try await URLSession.shared.data(from: URL(string: url)!)
            return (url, data)
        }
        .concurrentMap { (url, data) -> Report in
            let html = String(decoding: data, as: UTF8.self)
            let title = html.replacingOccurrences(
                of: #"<title>(.*?)</title>"#,
                with: "$1",
                options: .regularExpression
            )
            return Report(url: url, title: title.trimmingCharacters(in: .whitespacesAndNewlines))
        }
}

特点:

  • 阶段之间用临时元组传递上下文,避免全局可变状态。
  • 任意阶段加 try 即可自动终止后续任务,异常传播靠 rethrows 机制。

思维导图(文字版)

回调  withContinuation  async/await
actor / Sendable → 无锁并发
高阶函数 → concurrentMap → 函数式并行管道

面试速答卡片

问题 一句话答案
旧 SDK 回调如何转 async withCheckedThrowingContinuation 一键桥接
actor 与类最大区别? 编译期强制串行访问,自动解决数据竞争
Sendable 对函数意味着什么? 闭包及其捕获变量必须线程安全,否则编译报错
concurrentMap 会不会乱序? 内部用索引对还原,保证与输入顺序一致

Swift 函数完全指南(三):`@autoclosure`、`rethrows`、`@escaping` 与内存管理

作者 unravel2025
2025年10月10日 19:06

@autoclosure:把“表达式”包成“闭包”,实现“短路求值”

  1. 场景回顾
/// 自己写的 assert(简化版)
func myAssert(_ condition: Bool, _ message: String) {
    if !condition { print("❌ \(message)") }
}
myAssert(2 > 3, "2 不可能大于 3")   // 无论断言是否成功,message 都会被求值

问题:

  • 字符串先拼接完成,再传进函数——性能浪费。
  • 若拼接代价高("计算成本:\(expensive())"),则每次调用都白算。
  1. @autoclosure 延迟求值
func smartAssert(
    _ condition: Bool,
    _ message: @autoclosure () -> String = ""
) {
    if !condition { print("❌ \(message())") }
}

func expensive() -> String {
    "一个非常昂贵的算法"
}

smartAssert(2 > 3, "2 大于 3 的成本:\(expensive())")
  • 只有当 condition == false 时,message() 才被真正执行。
  • 调用方写法与普通传参一样,无需写大括号——这是 @autoclosure 的语法糖核心。
  1. 官方用法对照
  • assert(condition:)
  • fatalError(message:)
  • os_logmessage 参数

rethrows:让“函数参数”的异常传播出去

  1. 背景
func map<T>( _ array: [Int], _ transform: (Int) throws -> T) rethrows -> [T] {
    try array.map(transform)
}
  • 如果 transform 不抛错,map 也不会抛;
  • 如果 transform 抛错,map 再把异常原路抛回给调用者。
  1. throws 的区别
关键字 谁可能抛错 调用方必须用 try
throws 函数本身
rethrows 仅闭包参数 ✅(但闭包不抛就隐式免 try
  1. 实现 tryMap
extension Array {
    func tryMap<T>(_ transform: (Element) throws -> T) rethrows -> [T] {
        var result: [T] = []
        for e in self { result.append(try transform(e)) }
        return result
    }
}

let nums = ["1", "2", "A"]
let parsed: [Int] = try nums.tryMap { str in
    guard let v = Int(str) else { throw NSError(domain: "NaN", code: 0) }
    return v
}
  1. 坑位
  • rethrows 函数内部只能抛出由闭包参数传来的错误,不能自己 throw 新错误。
  • 如果闭包有多个,只要其中一个会抛,即可标注 rethrows

@escaping:闭包“逃出”函数生命周期

  1. 什么是“逃逸”
var handlers: [() -> Void] = []

func addHandler(_ handler: () -> Void) {
    handlers.append(handler) // 编译错误:闭包可能稍后调用,必须标记 @escaping
}
  • 数组把闭包“留住”→ 函数栈已销毁→ 闭包逃出→ 必须加 @escaping
  1. 正确写法
nonisolated(unsafe) var handlers: [() -> Void] = []

func addHandler(_ handler: @escaping () -> Void) {
    handlers.append(handler)
}

  1. 逃逸闭包的内存管理:循环引用
class Request {
    var onSuccess: (() -> Void)?
    func start() {
        onSuccess?()
    }
}

class ViewController {
    let request = Request()
    var name = "VC"
    
    func setup() {
        // 强引用 self → 循环引用
        request.onSuccess = {
            print(self.name)
        }
    }
}

解决套路:

  1. [weak self]

  2. [unowned self](生命周期确定时)

  3. guard let self else { return } 消除可选链

  4. 逃逸闭包在异步 API 的典型形态

func loadData(
    from url: String,
    completion: @escaping (Data?) -> Void
) {
    DispatchQueue.global().async {
        let data = try? Data(contentsOf: URL(string: url)!)
        DispatchQueue.main.async {
            completion(data) // 再次逃逸,但编译器已允许
        }
    }
}
  1. @nonescaping(默认)对比
特性 默认(非逃逸) @escaping
持有成本 低,可栈分配 高,必须堆分配
捕获策略 无需 self. 限定 必须显式处理 self
使用场景 同步回调 异步、存储、延迟调用

4 个关键字组合实战:写个“线程安全且短路”的缓存加载器

import SwiftUI

final class ImageCache {
    private var storage: [String: Image] = [:]
    private let queue = DispatchQueue(label: "cache", attributes: .concurrent)
    
    /// 仅当 key 不存在时才调用 `factory` 加载
    func load(
        _ key: String,
        factory: @escaping () throws -> Image
    ) rethrows -> Image {
        // 1. 先读缓存(并发读安全)
        if let cached = queue.sync(execute: { storage[key] }) {
            return cached
        }
        
        // 2. 缓存未命中,加锁写
        return try queue.sync(flags: .barrier) {
            if let cached = storage[key] { return cached } // 双检锁
            let image = try factory()
            storage[key] = image
            return image
        }
    }
}

亮点:

  • @escaping:工厂闭包可能异步下载,必须逃逸。
  • rethrows:工厂抛错,缓存器再抛给调用者;若工厂不抛,调用方可免 try
  • @autoclosure 未出现,是因为工厂需要多次调用(双检锁),而 autoclosure 只能一次性求值。

常见面试追问

  1. @autoclosure 与“零成本抽象”冲突吗?

    不冲突。编译器会把包起来的表达式生成匿名闭包,优化级别高时会内联,运行时仍接近零成本。

  2. rethrows 可以标记初始化器吗?

    可以。

   init(_ f: () throws -> Void) rethrows { try f() }
  1. 逃逸闭包为什么默认捕获强引用?

    因为闭包生命周期可能长于当前函数,编译器保守地强引所有外部变量;需要开发者显式 weak/unowned 解除循环。

Swift 函数完全指南(二):泛型函数与可变参数、函数重载、递归、以及函数式编程思想

作者 unravel2025
2025年10月10日 18:57

泛型函数:让代码从“具体类型”升维到“抽象类型”

  1. 场景:写了一个交换两个 Int 的函数,后来又要交换 Double、String、CGPoint……
  2. 泛型版本:
/// 交换任意两个相同类型的值
/// T 是占位符,调用时由编译器推断
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    (a, b) = (b, a)
}

var x = 3.14, y = 2.71
swapTwoValues(&x, &y)        // Double
print(x,y)
var s1 = "A", s2 = "B"
swapTwoValues(&s1, &s2)      // String
print(s1,s2)
  1. 泛型约束:只支持“可比较”类型
/// 返回数组中最大元素,数组为空时返回 nil
/// T 必须实现 Comparable 协议
func maxElement<T: Comparable>(in array: [T]) -> T? {
    guard var max = array.first else { return nil }
    for e in array.dropFirst() where e > max { max = e }
    return max
}

print(maxElement(in: [3, 1, 4, 2]))         // Int?
print(maxElement(in: ["apple", "zebra"]))   // String?
  1. 多占位符 + 多个约束
/// 把字典的 value 映射成新类型,key 不变
func mapValues<K, V, U>(
    _ dict: [K: V],
    _ transform: (V) throws -> U
) rethrows -> [K: U] {
    try dict.mapValues(transform)   // 直接复用标准库
}

易错点:

  • 泛型函数仍然遵循“单态化”(monomorphization) 编译模型,不会带来运行时开销。
  • 如果约束过多,考虑使用 where 子句可读性更好。

函数重载:同名不同参,编译器如何选?

  1. 重载维度
维度 能否作为重载依据
参数个数
参数类型
参数标签
返回类型 ❌(不能单独作为依据)
  1. 示例:看似相同,实则都能共存
/// 1. 只有个数不同
func f(_ x: Int) { }
func f(_ x: Int, _ y: Int) { }

/// 2. 类型不同
func f(_ x: String) { }

/// 3. 标签不同
func f(value x: Int) { }
  1. 歧义爆发点:默认参数 + 可变参
func sum(_ nums: Int...) -> Int { nums.reduce(0, +) }
func sum(_ a: Int, _ b: Int = 0) -> Int { a + b }

// 以下调用会编译失败:编译器无法决定用哪个
// sum(1)

解决:

  • 把“可变参版本”改成内部判断空数组;
  • 或者干脆改名,避免重载。
  1. 泛型 vs 重载

泛型是“横向”抽象,重载是“纵向”展开。当两者冲突时,编译器优先选“更具体”的重载版本:

func printIt<T>(_ x: T) { print("generic: \(x)") }
func printIt(_ x: Int) { print("Int: \(x)") }

printIt(42)      // 输出 Int: 42
printIt("hi")    // 输出 generic: hi

递归:自己调用自己,如何不爆栈?

  1. 经典例子:阶乘
/// 普通递归,深度大时可能栈溢出
func factorial(_ n: Int) -> Int {
    n <= 1 ? 1 : n * factorial(n - 1)
}
  1. 尾递归(Tail Recursion)——让编译器做尾调用优化(TCO)
/// 把中间结果放到 accumulator 参数里,递归调用是最后一条指令
func factorialT(_ n: Int, _ acc: Int = 1) -> Int {
    n <= 1 ? acc : factorialT(n - 1, acc * n)
}

注意:

  • Swift 5 之后不保证在 -O 优化下一定做 TCO,仅“尽力而为”。
  • 真正要防爆栈,请用循环或 Sequence 惰性计算。
  1. 实战:递归遍历嵌套文件夹(简化版)
import Foundation

/// 返回目录下所有 `.swift` 文件
func swiftFiles(in path: String) -> [String] {
    let fm = FileManager.default
    guard let enumerator = fm.enumerator(atPath: path) else { return [] }
    var result: [String] = []
    while let file = enumerator.nextObject() as? String {
        if file.hasSuffix(".swift") {
            result.append((path as NSString).appendingPathComponent(file))
        }
    }
    return result
}

(递归由 FileManagerenumerator 代劳,无需自写)

函数式思维落地:再封装 map / filter / reduce

  1. reduce 的陷阱:初始值类型
let nums = [1, 2, 3]
// 错误:拼接字符串却给 Int 初始值
// let r = nums.reduce(0) { $0 + String($1) } // 编译失败

// 正确
let r = nums.reduce("") { $0 + String($1) } // "123"
print(r)
  1. 封装“管道”算子,让代码像搭积木
infix operator |> : AdditionPrecedence
/// 把值通过函数“管道”传递
func |> <T, U>(value: T, function: (T) -> U) -> U {
    function(value)
}

/// 使用
let result = [1, 2, 3]
    |> { $0.map { $0 * 2 } }   // [2, 4, 6]
    |> { $0.filter { $0 > 3 } } // [4, 6]
    |> { $0.reduce(0, +) }      // 10
print(result)
  1. 自定义“链式”集合封装
struct Chain<Wrapped> {
    private let value: Wrapped
    init(_ value: Wrapped) { self.value = value }
    
    func map<T>(_ transform: (Wrapped) -> T) -> Chain<T> {
        .init(transform(value))
    }
    
    func filter(_ condition: (Wrapped) -> Bool) -> Chain<Wrapped>? {
        condition(value) ? self : nil
    }
    
    func unwrap() -> Wrapped { value }
}

/// 使用
let ans = Chain([1, 2, 3, 4])
    .map { $0.map { $0 * 10 } }   // [10, 20, 30, 40]
    .filter { !$0.isEmpty }!
    .unwrap()
    .reduce(0, +)                 // 100
print(ans)

常见坑位 Top 3

现象 根治方案
1. 可变参 + 默认参重载 编译歧义 改名或删除一个版本
2. 递归无终止条件 运行时崩溃 guard 提前 return
3. 泛型约束过多 编译耗时飙升 把复杂约束拆成 protocol + extension

结语

函数不仅是“代码片段”,更是 Swift 世界里的“乐高积木”。

当你能把“泛型 + 重载 + 递归 + 函数式”自由组合时,就拥有了“用函数生产函数”的元编程能力。

愿我们都能把“函数”玩成“艺术”,而不是“语法”。

Swift 函数完全指南(一)——从入门到嵌套

作者 unravel2025
2025年10月10日 18:45

函数的本质

  1. 自包含的代码片段,完成一个“任务”。
  2. 有名字 → 可被重复“调用”。
  3. 有类型 → 由“参数类型 + 返回类型”组成,可以像 Int、String 一样被赋值、传递、返回
  4. 可嵌套 → 在函数内部再定义函数,实现隐藏实现细节。

语法骨架速记

func 函数名(参数列表) -> 返回类型 {
    // 函数体
}

调用:

函数名(实参)

知识点全景图

无参函数

// 无输入、无输出(返回 Void)
func sayHelloWorld() {
    print("hello, world")
}
sayHelloWorld()

易错点:调用时也必须写空括号 (),否则编译器会当成“函数引用”而非“调用”。

单参 + 单返回值

func greet(person: String) -> String {
    // 字符串插值更 Swifty
    return "Hello, \(person)!"
}
print(greet(person: "Anna"))   // Hello, Anna!

简化写法:单表达式可省 return

func greetAgain(person: String) -> String {
    "Hello again, \(person)!"   // 隐式返回
}

多参数

func greet(person: String, alreadyGreeted: Bool) -> String {
    alreadyGreeted ? greetAgain(person: person) : greet(person: person)
}
print(greet(person: "Tim", alreadyGreeted: true))

注意:函数名相同但参数列表不同 → 函数重载(Swift 支持)。

无返回值

func greet(person: String) {
    print("Hello, \(person)!")
}

细节:

  • 不写 -> Void 与写 -> Void 完全等价。
  • 调用者可使用 _ = 显式忽略返回值,提高代码可读性。

多返回值 → 元组(Tuple)

func minMax(array: [Int]) -> (min: Int, max: Int) {
    var curMin = array[0], curMax = array[0]
    for value in array.dropFirst() {
        if value < curMin { curMin = value }
        if value > curMax { curMax = value }
    }
    return (curMin, curMax)
}
let bounds = minMax(array: [8, -6, 2, 109])
print("最小 \(bounds.min) 最大 \(bounds.max)")

可选元组:当数组可能为空时,返回整个元组为 nil

func minMaxSafe(array: [Int]) -> (min: Int, max: Int)? {
    guard !array.isEmpty else { return nil }
    ...
}

参数标签(Argument Label)与参数名

func greet(person: String, from hometown: String) -> String {
    "Hello \(person), glad you could visit from \(hometown)."
}
// 调用时形成“句子”
print(greet(person: "Bill", from: "Cupertino"))

省略标签:用 _

func add(_ a: Int, _ b: Int) -> Int { a + b }
add(2, 3)   // 不再强制写标签

默认参数值

func power(_ base: Int, _ exponent: Int = 2) -> Int {
    return (0..<exponent).reduce(1) { partialResult, _ in
        partialResult * base
    }
}
print(power(5))        // 25
print(power(5, 3))     // 125

规则:带默认值的参数放最右边;调用时省略就从右往左省略。

可变参数(Variadic Parameter)

func arithmeticMean(_ numbers: Double...) -> Double {
    guard !numbers.isEmpty else { return .nan }
    return numbers.reduce(0, +) / Double(numbers.count)
}
print(arithmeticMean(1, 2, 3, 4, 5))   // 3.0

限制:

  1. 一个函数可以有多个可变参,多个可变参只能有一个可以省略标签,其他的需要使用标签传递实参
  2. 若后面还有参数,必须带标签。

多个可变参

func arithmeticMean(_ numbers: Double..., names: String...) -> Double {
    guard !numbers.isEmpty else { return .nan }
    print(names)
    return numbers.reduce(0, +) / Double(numbers.count)
}
print(arithmeticMean(1, 2, 3, 4, 5, names:"a","b"))   // 3.0

in-out 参数:函数内部修改外部变量

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    (a, b) = (b, a)   // 元组交换,无需临时变量
}
var x = 3, y = 107
swapTwoInts(&x, &y)   // 调用时加 &
print("x=\(x) y=\(y)") // x=107 y=3

注意:

  • 只能传变量(var),不能传 let 常量或字面量。
  • in-out 与并发不兼容,不能跨 actor 使用。

函数类型:一等公民

typealias MathOp = (Int, Int) -> Int
let op: MathOp = (+)   // 系统运算符也是函数
print(op(2,3))         // 5

高阶函数实战:把函数当参数/返回值

func printResult(_ f: (Int,Int)->Int, _ a: Int, _ b: Int) {
    print("结果=\(f(a,b))")
}
printResult(+, 4, 5)

返回函数 → 工厂函数

func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    backward ? { $0 - 1 } : { $0 + 1 }
}
var value = 3
let move = chooseStepFunction(backward: value > 0)
while value != 0 {
    print(value)
    value = move(value)
}

利用闭包简写,省掉嵌套函数写法,但逻辑等价。

嵌套函数(Nested Function)

func chooseStepNested(backward: Bool) -> (Int) -> Int {
    func stepForward(i: Int) -> Int { i + 1 }
    func stepBackward(i: Int) -> Int { i - 1 }
    return backward ? stepBackward : stepForward
}

特点:

  • 默认对外部不可见,封装更彻底。
  • 可捕获外层函数的局部变量(闭包特性)。

思维导图(文字版)

函数
├─ 定义与调用
├─ 参数
│  ├─ 无参 / 多参
│  ├─ 标签与省略
│  ├─ 默认值
│  ├─ 可变参
│  └─ in-out
├─ 返回值
│  ├─ 无返回
│  ├─ 单值
│  ├─ 多值(元组)
│  └─ 可选元组
├─ 函数类型
│  ├─ 作为变量
│  ├─ 作为参数
│  └─ 作为返回值
└─ 嵌套函数

总结与实战建议

  1. 把“函数类型”真正当成类型:

    写网络层时,可把 (Data) -> Void 的回调类型 typealiasCompletion,一处修改处处生效。

  2. 默认参数 + 可变参组合:

    封装日志库时,func log(_ items: Any..., separator: String = " ") 既能接收任意数量,又能自定义分隔符。

  3. in-out 的替代方案:

    多数场景可用返回元组代替,语义更清晰;in-out 仅当“必须原地修改”且“性能敏感”才用。

  4. 嵌套函数是“小范围私有函数”的最佳实践:

    避免全局命名空间污染,尤其在算法题或表格视图控制器里,可把“工具函数”直接嵌套在 viewDidLoad 里。

  5. 函数式思维:

    多利用“函数作为返回值”做策略模式,比传统面向对象的“策略类”更轻量。例如根据用户配置返回不同价格计算器:

enum VipLevel { case normal, silver, gold }
func discount(for level: VipLevel) -> (Double) -> Double {
    switch level {
    case .normal:  { $0 }          // 无折扣
    case .silver:  { $0 * 0.9 }    // 9 折
    case .gold:    { $0 * 0.8 }    // 8 折
    }
}
let finalPrice = discount(for: .gold)(100) // 80
❌
❌