普通视图

发现新文章,点击刷新页面。
昨天以前掘金 iOS

Swift异步详解

作者 如此风景
2025年9月5日 10:30

Swift 的异步编程模型是现代 Swift 开发的核心特性之一,尤其在 Swift 5.5 引入 async/await 语法后,极大简化了异步操作的处理。以下是对 Swift 异步编程的详细解析:

一、异步编程的核心问题

在传统同步代码中,操作按顺序执行,耗时操作(如网络请求、文件读写)会阻塞当前线程。而异步编程的目的是:

  • 避免耗时操作阻塞 UI 或主线程
  • 提高代码可读性和可维护性
  • 简化复杂异步逻辑(如依赖多个异步任务的场景)

二、Swift 异步的三种主要方式

1. 回调闭包(Closure Callback)

最传统的方式,通过闭包传递异步结果:

func fetchData(completion: @escaping (Result<String, Error>) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        // 模拟网络请求
        completion(.success("Data from server"))
    }
}

// 调用
fetchData { result in
    switch result {
    case .success(let data):
        print(data)
    case .failure(let error):
        print(error)
    }
}

缺点:多层嵌套易形成“回调地狱”,逻辑复杂时可读性差。

2. Combine 框架(响应式编程)

基于发布者(Publisher)和订阅者(Subscriber)模式,适合处理数据流:

import Combine

func fetchData() -> AnyPublisher<String, Error> {
    return Future<String, Error> { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            promise(.success("Data from server"))
        }
    }
    .eraseToAnyPublisher()
}

// 调用
let cancellable = fetchData()
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { completion in
        // 处理完成或错误
    }, receiveValue: { data in
        print(data)
    })

优势:强大的操作符(如 mapflatMap)支持复杂数据流处理,适合响应式场景。

3. async/await 语法(Swift 5.5+)

现代异步编程的主流方式,用同步代码的形式编写异步逻辑:

// 定义异步函数
func fetchData() async throws -> String {
    try await Task.sleep(nanoseconds: 1_000_000_000) // 模拟耗时1秒
    return "Data from server"
}

// 调用异步函数(需在异步上下文中)
Task {
    do {
        let data = try await fetchData()
        print(data)
    } catch {
        print(error)
    }
}

核心特点

  • async 标记函数为异步
  • await 等待异步结果(不会阻塞线程)
  • try 处理可能的错误
  • 必须在异步上下文(如 Task)中调用

三、async/await 深度解析

1. 异步函数的定义与调用

  • 函数声明:用 async 关键字标记,可返回值或抛出错误
    func loadUser() async throws -> User
    func cacheData() async // 无返回值
    
  • 调用限制:必须在 async 函数内或 Task 中使用 await 调用
    // 正确:在Task中调用
    Task {
        let user = try await loadUser()
    }
    
    // 正确:在async函数中调用
    func process() async throws {
        let user = try await loadUser()
    }
    

2. 任务(Task)与并发

Task 是异步操作的载体,负责管理异步任务的生命周期:

  • 创建任务
    let task = Task {
        try await fetchData()
    }
    
  • 取消任务
    task.cancel() // 触发任务内部的CancellationError
    
  • 任务优先级
    Task(priority: .high) { ... } // 高优先级任务
    

3. 并发执行多个任务

  • 并行执行:用 async let 同时启动多个任务,最后统一等待结果
    async let data1 = fetchData(url: url1)
    async let data2 = fetchData(url: url2)
    
    let result = try await (data1, data2) // 等待所有任务完成
    
  • 结构化并发:通过 TaskGroup 管理一组动态任务
    await withTaskGroup(of: String.self) { group in
        for url in urls {
            group.addTask {
                try await fetchData(url: url)
            }
        }
        // 收集结果
        for try await data in group {
            print(data)
        }
    }
    

4. 主队列调度

UI 操作必须在主线程执行,可通过 @MainActor 约束:

// 标记函数必须在主线程执行
@MainActor
func updateUI(text: String) {
    label.text = text
}

// 调用时自动切换到主线程
Task {
    let data = try await fetchData()
    await updateUI(text: data) // 自动切换到主线程
}

四、异常处理

异步函数的错误通过 throws 抛出,在调用时用 try 捕获:

func riskyOperation() async throws {
    if failureCondition {
        throw MyError.somethingWrong
    }
}

// 调用时处理错误
Task {
    do {
        try await riskyOperation()
    } catch MyError.somethingWrong {
        print("特定错误处理")
    } catch {
        print("通用错误处理: \(error)")
    }
}

五、与其他异步模型的互操作

1. 回调转 async/await

withCheckedThrowingContinuation 将传统回调转换为异步函数:

func fetchData() async throws -> String {
    try await withCheckedThrowingContinuation { continuation in
        // 传统回调函数
        legacyFetch { result in
            switch result {
            case .success(let data):
                continuation.resume(returning: data)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

2. async/await 转 Combine

Future 包装异步函数:

func fetchDataPublisher() -> AnyPublisher<String, Error> {
    Future { promise in
        Task {
            do {
                let data = try await fetchData()
                promise(.success(data))
            } catch {
                promise(.failure(error))
            }
        }
    }
    .eraseToAnyPublisher()
}

六、最佳实践

  1. 避免阻塞await 不会阻塞线程,无需手动切换到后台队列
  2. 任务取消:在长时间任务中检查取消状态
    func longRunningTask() async throws {
        for i in 0..<100 {
            if Task.isCancelled {
                throw CancellationError()
            }
            // 执行部分任务
            try await Task.sleep(nanoseconds: 100_000_000)
        }
    }
    
  3. 限制并发量:用 TaskGroup 配合信号量控制并发数量
  4. 优先使用 async/await:相比回调和 Combine,代码更简洁易读

总结

Swift 的异步编程模型从回调到 Combine,再到 async/await,逐步向更简洁、更安全的方向发展。async/await 结合结构化并发,成为处理异步逻辑的首选方式,尤其适合网络请求、文件操作等场景,同时保持了与传统异步模型的兼容性。

强制 SwiftUI 重新渲染:`.id()` 这把“重启键”你用对了吗?

作者 unravel2025
2025年9月5日 09:36

参考原文:Forcing a View Reload in SwiftUI

为什么需要“强制 reload”?

SwiftUI 的声明式 DSL 依赖 状态 diff 自动更新视图,但以下场景需要“硬重启”:

  • 网络请求失败后的“重试”按钮
  • 图片/视频加载损坏,需重新解码
  • 底层 @StateObject 内部状态错乱,手动复位成本过高

核心思路:

改变视图 身份 (identity) → SwiftUI 认为“旧视图已消失”→ 重建整个子树。

官方逃生舱:.id(_:) 一行代码搞定

struct DemoView: View {
    @State private var viewId = UUID()

    var body: some View {
        VStack {
            // 1️⃣ 用 .id 绑定唯一标识
            Text(viewId.uuidString)
                .id(viewId)

            // 2️⃣ 刷新标识 → 强制重建
            Button("Retry") {
                viewId = UUID()
            }
        }
    }
}
  • 每次 viewId 变化,Text 被视为全新视图,旧实例被销毁。
  • 子树内所有 @State / @StateObject / 内部绑定一并丢弃,状态清零。

优点:快、狠、准

优势 说明
✅ 一键复位 无需手动清空 N 个 @State
✅ 行为可预测 基于 SwiftUI 身份机制,官方支持
✅ 适用 retry 场景 网络/解码失败时瞬间“满血复活”

代价:性能 & 状态损失

风险 场景
⚠️ 局部状态全灭 用户输入/滚动位置/播放器进度 会丢失(除非提前迁出子树)
⚠️ 大视图重建开销 复杂 UI / 大图 / 3D 场景可能出现掉帧
⚠️ 掩盖架构问题 频繁 .id()往往意味着状态建模不合理,应优先重构

实战指南:何时该用、何时避免

场景 建议
临时 retry / reset 按钮 ✅ 首选 .id()
列表 item 偶发错乱 ✅ 给 item 加 .id(item.unique)
用户输入表单 ❌ 别把 .id()绑在输入框外层,会丢键盘/光标
高频刷新(如计时器) ❌ 用专门的状态驱动,而非改 .id()

进阶技巧:把“ reload”封装成 Modifier

struct Reloadable<Content: View>: View {
    @State private var reloadID = UUID()
    let content: (UUID) -> Content
    
    var body: some View {
        content(reloadID)
            .id(reloadID)
    }
    
    func reload() {
        reloadID = UUID()
    }
}

// 使用
struct PlayerView: View {
    @State private var player = Reloadable { id in
        VideoPlayer(url: url)
            .id(id)          // 绑定唯一身份
    }
    
    var body: some View {
        player
            .onReceive(retryNotification) { _ in
                player.reload()   // 硬重启播放器
            }
    }
}
  • 将 reload 动作 暴露给外部,不污染子树状态。
  • 支持 动画过渡(可再包 .transition)。

一句话总结

.id(UUID()) 是 SwiftUI 的“重启键”——

应急可用,滥用伤身。

在 retry / 纠错场景下它是救命稻草;若发现自己在每页都用,请先回头看看状态建模是否出了问题。

Swift 6.2 新语法糖:在字符串插值里直接给 Optional 写默认值

作者 unravel2025
2025年9月5日 09:27

参考原文:Making String Interpolation Smarter in Swift 6.2: Default Values for Optionals

一句话看懂新特性

旧写法(nil-coalescing)

let name: String? = nil
print("Hi \(name ?? "Guest")")          // OK,同类型

新写法(Swift 6.2 插值默认值)

print("Hi \(name, default: "Guest")")   // 等价,但支持**异类型**默认值

关键差异:?? 要求左右类型一致;(default:) 把默认值直接当字符串用,无视原始类型!

异类型痛点:旧语法搞不定

let count: Int? = nil

// ❌ 编译失败:Int 与 String 不匹配
print("Count: \(count ?? "Unknown")")

// ✅ Swift 6.2 直接通过
print("Count: \(count, default: "Unknown")")   // 输出 "Count: Unknown"

省去了手动 .map(String.init) ?? "Unknown" 的繁琐步骤。

真实场景:用户资料拼接

struct User {
    var username: String?
    var email: String?
    var age: Int?
}

let user = User(username: nil, email: "jane@example.com", age: nil)

print("""
User Info:
- Username: \(user.username, default: "Guest")
- Email: \(user.email, default: "Not provided")
- Age: \(user.age, default: "Unknown")
""")

旧写法对比

"- Username: \(user.username ?? "Guest")"
"- Age: \(user.age.map(String.init) ?? "Unknown")"   // 手动 map

新语法一行搞定,可读性大幅提升。

日志 & 调试神器

func logEvent(name: String?, duration: Double?) {
    print("Event '\(name, default: "Unnamed")' ran for \(duration, default: "an unknown amount of time")")
}

logEvent(name: nil, duration: nil)
// Output: Event 'Unnamed' ran for an unknown amount of time

无需提前拆包、转换类型,插值处直接给默认值。

语法要点速记

特性 说明
可用版本 Swift 6.2+
插值格式 (optional, default: 任意表达式)
类型要求 无;默认值会被直接当 String
与 ??共存 完全兼容,按场景选择

何时用它而非 ??

场景 选 (default:) 选 ??
默认值与 Optional 类型不同
默认值是同类型、已存在 皆可 ✅ 更短
需要复杂表达式/函数调用 ✅ 可读 ❌ 过长

一句话总结

同类型用 ??,异类型或懒得转换时用 (default:)

Swift 6.2 这个小糖让字符串插值兼顾安全与优雅,再不用为“Optional 转 String”写大段模板代码!

窥探 `@Observable` 的“小黑盒”:private 属性到底会不会被观察?

作者 unravel2025
2025年9月5日 08:45

参考原文:Exploring Observation in Swift: What Happens with Private Properties

问题抛出

import Observation
@Observable
final class ViewModel {
    var publicProp  = "A"          // 1️⃣ 公开,可观察
    @ObservationIgnored var ignoredProp = "B"  // 2️⃣ 显式忽略
    private var privateProp = "C"  // 3️⃣ 私有,会参与观察吗?
}
  • 直觉:private = 对外隐藏 = 不生成观察代码?
  • 真相:除非加 @ObservationIgnored,否则一律观察,与可见性无关!

验证工具:SIL(Swift Intermediate Language)

一键导出 SIL

xcrun swiftc -emit-silgen -Onone -parse-as-library \
  -sdk $(xcrun --show-sdk-path --sdk macosx) \
  ViewModel.swift > ViewModel.sil

导出后的文件我存储在了:gitcode.com/unravel/dis…

关键发现(节选)

属性 是否生成 _modify调用方法 结论
publicProp ✅ 有 参与观察
ignoredProp ❌ 无 直接读写,无开销
privateProp ✅ 有 同样被观察

可见:编译器为 所有非忽略属性 生成相同的观察包装,private 也不能幸免。

Xcode 可视化捷径:Expand Macro

不想看 SIL?Xcode 15+ 支持宏展开:

  1. 选中 @Observable 宏,右键选择 “Expand Macro”。

image.png

  1. 在展开的代码里搜索 @ObservationTracked
    • 出现 = 会观察
    • 不出现 = 已忽略(或被 @ObservationIgnored 标记)

(示意:private 属性同样被 @ObservationTracked 包裹)

image.png

实战影响 & 最佳实践

场景 建议
私有缓存、临时变量 不需要观察 显式加 @ObservationIgnored
希望 SwiftUI 不刷新 的辅助属性 加 @ObservationIgnored减少调度开销
确实需要观察私有状态(如内部网络层) 保持默认即可,private 仅对外隐藏,对 Observation 透明

一句话总结

@Observable 世界里,“private” ≠ ‘忽略’;

想真正跳过观察,请用 @ObservationIgnored —— 不论 public 还是 private,编译器都会一视同仁地生成观察代码。

Swift 并发避坑指南:自己动手实现“原子”属性与集合

作者 unravel2025
2025年9月5日 08:09

原文:Atomic properties and collections in Swift

为什么需要“原子”操作?

Swift 没有现成的 atomic 关键字。当多个线程/任务同时读写同一属性或集合时,会出现:

  • 读到中间状态(数组越界、字典重复 key)
  • 丢失更新(值类型复制-修改-写回)

使用Swift 实现Atomic有多种方案,以下从简单到高级介绍iOS17及以下系统的可行方案;最后补充 iOS 18 原生 Synchronization 框架的替代思路。

方案 1:属性包装器 + GCD(入门级)

@propertyWrapper
struct Atomic<Value> {
    private let queue = DispatchQueue(label: "atomic.queue") // 串行队列
    private var storage: Value
    
    init(wrappedValue: Value) { storage = wrappedValue }
    
    var wrappedValue: Value {
        // 串行队列同步执行,类似锁机制,读写都需要排队
        get { queue.sync { storage } }
        set { queue.sync { storage = newValue } }   // 注意:非 barrier
    }
}

使用

class Client {
    @Atomic var counter = 0
}

踩坑:复合运算“非原子”

let client = Client()
client.counter += 1   // 读 + 改 + 写 三步,线程不安全!

方案 2:暴露 modify 闭包(中级)

@propertyWrapper
class Atomic<Value> {          // 改为 class,避免值拷贝
    private let queue = DispatchQueue(label: "atomic.queue") // 串行队列
    private var storage: Value
    
    var projectedValue: Atomic<Value> { self }
    
    init(wrappedValue: Value) { storage = wrappedValue }
    
    var wrappedValue: Value {
        // 串行队列同步执行,类似锁机制,读写都需要排队
        // 这里对应整体取值和整体赋值
        get { queue.sync { storage } }
        set { queue.sync { storage = newValue } }
    }
    
    // 原子“读-改-写”
    // 这里对应复合运算
    func modify(_ block: (inout Value) -> Void) {
        queue.sync { block(&storage) }   // 整个 block 在串行队列里执行
    }
}

使用

client.$counter.modify { $0 += 1 }        // ✅ 线程安全
client.$counter.modify { $0.append(42) }  // 同样适用于数组

方案 3:独立 Atomic<T> 类(高级,API 更友好)

final class Atomic<Value> {
    private var value: Value
    private let queue = DispatchQueue(label: "atomic.queue", attributes: .concurrent) // 并发队列
    
    init(_ value: Value) { self.value = value }
    
    // 1. 只读快照
    func read<T>(_ keyPath: KeyPath<Value, T>) -> T {
        queue.sync { value[keyPath: keyPath] }
    }
    
    // 2. 读并转换
    func read<T>(_ transform: (Value) throws -> T) rethrows -> T {
        try queue.sync { try transform(value) }
    }
    
    // 3. 整体替换
    func modify(_ newValue: Value) {
        queue.async(flags: .barrier) { self.value = newValue }
    }
    
    // 4. 原子读-改-写
    func modify(_ transform: (inout Value) -> Void) {
        queue.sync(flags: .barrier) { transform(&value) }
    }
}

使用

let num = Atomic(0)
num.modify { $0 += 1 }
print(num.read(\.self))   // 1

let arr = Atomic([1, 2, 3])
arr.modify { $0.append(4) }
print(arr.read { $0.contains(4) }) // true

并发队列 + barrier 保证“写”互斥,“读”并行,性能优于串行队列。

方案 4:专用原子集合(线程安全数组)

class AtomicArray<Element> {
    private var storage: [Element] = []
    private let queue = DispatchQueue(label: "atomic.array", attributes: .concurrent)
    
    subscript(index: Int) -> Element {
        get { queue.sync { storage[index] } }
        set { queue.async(flags: .barrier) { self.storage[index] = newValue } }
    }
    
    func append(_ new: Element) {
        queue.async(flags: .barrier) { self.storage.append(new) }
    }
    
    func removeAll() {
        queue.async(flags: .barrier) { self.storage.removeAll() }
    }
    
    func forEach(_ body: (Element) throws -> Void) rethrows {
        try queue.sync { try storage.forEach(body) }
    }
    
    func all() -> [Element] { queue.sync { storage } }
}

压测:10 线程并发 append

Task.detached {
    let arr = AtomicArray<Int>()
    DispatchQueue.concurrentPerform(iterations: 10_000) { @Sendable in  arr.append($0)}
    print(arr.all().count)   // 10_000 ✅
}

方案 5:原子字典(读多写少最优解)

class AtomicDictionary<Key: Hashable, Value> {
    private var dict: [Key: Value] = [:]
    private let queue = DispatchQueue(label: "atomic.dict", attributes: .concurrent)
    
    subscript(key: Key) -> Value? {
        get { queue.sync { dict[key] } }
        set { queue.async(flags: .barrier) { self.dict[key] = newValue } }
    }
    
    subscript(key: Key, default defaultValue: @autoclosure () -> Value) -> Value {
        get {
            queue.sync { dict[key] ?? defaultValue() }
        }
        set {
            queue.async(flags: .barrier) { self.dict[key] = newValue }
        }
    }
}

方案 6 _read / _modify 原生读写访问器

⚠️ 下划线 API,非稳定,仅用于实验。

@propertyWrapper
struct Atomic<Value> {
    private var storage: Value
    private let lock = NSLock()
    
    var wrappedValue: Value {
        get { lock.lock(); defer { lock.unlock() }; return storage }
        
        _modify {              // 原生“产出”引用,零拷贝
            lock.lock()
            defer { lock.unlock() }
            yield &storage
        }
    }
}

优点:

  • 无队列调度,纯锁+直接内存引用,性能更高。

缺点:

  • 未来可能改名或移除;不建议上生产。

iOS 18+ 新选择:Apple Synchronization 框架

import Synchronization

let counter = Atomic<Int>(0)   // 系统级原子类型,无锁、CPU 指令级
counter.wrappingIncrement(by: 1)
  • 真正无锁(使用 std::atomic 底层)。
  • 支持泛型、Sendable、wait/notify 等高级语义。
  • 最低部署版本 iOS 18;若需向下兼容,仍需本文方案。

选型速查表

场景 推荐方案
只支持 iOS 18+ 系统 Synchronization
属性读写,iOS 17 及以下 方案 3 Atomic<Value>
专用集合(数组/字典) 方案 4 / 5 原子集合类
超高性能、实验 方案 6 _read_modify

结论

  • Swift 没有“官方原子”≠ 不能写出线程安全的代码。
  • GCD + barrier + 值类型拷贝 足以覆盖 99 % 业务需求。
  • 提前布局:当 iOS 18 普及后,迁移到 Synchronization 只需换一行 import。

记住口诀:

“读并行、写互斥,修改用 barrier,集合要包类。”

惊!只是 `import Foundation`,`String.contains("")` 的返回值居然变了?

作者 unravel2025
2025年9月4日 20:28

两行代码,两种结果

是否 import Foundation "".contains("") "abc".contains("")
❌ 纯 Swift true true
✅ + Foundation false false

同一个 API,返回值完全相反,而且大多数 iOS 项目会间接 import Foundation,所以你一直用的其实是 ObjC 版本!

幕后黑手:桥接与方法决议

  1. Swift.String 与 NSString 是 toll-free bridged。
  2. 一旦 import Foundation,编译器会把同名方法 优先桥接到 NSString 的实现。
  3. 两个版本的 contains 签名兼容,但 行为不同:
实现 空子串规则
Swift 原生 空字符串是任何字符串的子串 → true
NSString 空子串不存在 → false

实战影响

  • 绝大多数 iOS/SwiftUI 工程都会间接 import Foundation → 你看到的都是 false
  • 纯 Swift Package / Linux Server 没 Foundation → 看到的是 true
  • 跨平台库若依赖空子串行为,务必显式测试两种环境。

避坑清单

场景 建议
写跨平台库 显式单元测试两种 import 状态
需要与 ObjC 对齐 直接使用 Foundation 行为并写注释
想要纯 Swift 行为 自建模块不 import Foundation,或用 Swift-only 方法
不确定当前行为 打印类型或查看 Quick Help 看是 String.contains还是 NSString.contains

一句话总结

只要 import Foundation,你就默认接受了 NSString 的行为。

在写通用逻辑 / 跨平台库 / 纯 Swift Package时,记得给空子串单独写测试,别被“同名不同魂”坑了。

Swift 6.2 新武器:`weak let` —— 既弱引用又不可变的安全魔法

作者 unravel2025
2025年9月4日 20:19

为什么需要 weak let

需求场景 weak var的痛点 weak let的新能力
并发安全的 Sendable类型 weak var无法标记 Sendable ✅ 可以
不可重新赋值的弱引用 仍可能被外部篡改 ✅ 编译期禁止
值类型持有弱引用 无法保证不变性 ✅ 完美支持

一句话:弱引用 + 不可变 = 更安全的所有权图。

语法速览

final class Downloader: Sendable {
    // 1️⃣ 一次性设置,之后不可改指向
    weak let delegate: DownloaderDelegate?
    
    init(delegate: DownloaderDelegate?) {
        self.delegate = delegate
    }
}
  • 仍遵守 ARC:目标释放后自动变 nil

  • 编译器禁止重新赋值:

    downloader.delegate = AnotherVC() // ❌ 直接报错。

完整示例:下载器 + 控制器

protocol DownloaderDelegate: AnyObject {
    func downloadDidUpdate(progress: Double)
}

final class Downloader: Sendable {
    weak let delegate: DownloaderDelegate?
    
    init(delegate: DownloaderDelegate?) {
        self.delegate = delegate
    }
    
    func simulateDownload() {
        delegate?.downloadDidUpdate(progress: 0.5)
    }
}

final class ViewController: DownloaderDelegate {
    func downloadDidUpdate(progress: Double) {
        print("进度:\(progress)")
    }
}

// MARK: - 测试
var vc: ViewController? = ViewController()
let downloader = Downloader(delegate: vc)
downloader.simulateDownload()   // ✅ 打印 0.5

vc = nil
downloader.simulateDownload()   // ✅ delegate 自动 nil,无崩溃

迁移清单:何时把 weak var 换成 weak let

场景 建议
代理/回调 一次性设置 直接替换
单元测试需要多次赋值 保持 weak var
值类型(struct)持有弱引用 立即使用 weak let
actor / TaskGroup 内部 优先 weak let以获得 Sendable资格

实战:值类型快照

struct UserSnapshot {
    let name: String
    weak let avatarLoader: AvatarLoader?   // 不持有加载器
}
  • 即使 avatarLoader 释放,UserSnapshot 依旧安全。
  • 结构体可以跨线程传递,无需担心循环引用。

一句话总结

weak let = “一次性弱引用”,让 不可变性 与 ARC 安全 在同一行代码握手。

在并发、UI、快照场景里,它是 Swift 6.2 给你的“隐形护栏”。

吃透 Swift 的 `@autoclosure`:把“表达式”变“闭包”的延迟利器

作者 unravel2025
2025年9月4日 20:01

什么是 @autoclosure

一句话:把“传入的表达式”自动包成“无参闭包”,实现延迟求值(lazy evaluation)。

语法糖级别:调用方完全无感,只需像传普通值一样写表达式;函数内部拿到的是 () -> T 闭包,想执行才执行。

为什么需要延迟求值?

反例:生产环境也强制计算

class Logger {
    static func log(_ message: String) {
        #if DEBUG
        print(message)
        #endif
    }
}

class DataSource {
    var data: [CustomStringConvertible] = []
    
    func update(with item: CustomStringConvertible) {
        data.append(item)
        // ⚠️ 即使 Release 不打印,description 也会被立即求值
        Logger.log(item.description)
    }
}
  • 浪费 CPU:复杂 description 可能拼接大量字符串。
  • 浪费内存:中间结果在 Release 版毫无用处。

上正菜:用 @autoclosure 实现“真正只在需要时才计算”

改一行签名即可

class Logger {
    // 1. 自动把调用处的表达式包成 () -> String
    static func log(_ message: @autoclosure () -> String) {
        #if DEBUG
        print(message())        // 2. 只有 DEBUG 才执行闭包
        #endif
    }
}

调用方零感知

ds.update(with: Vehicle(name: "BMW"))
// 调用处完全像传值,无需手写大括号

内部流程

阶段 实际行为
编译期 把表达式 item.description包成 { item.description }
运行期 只有 message()被调用时才执行闭包

进阶玩法

?? 运算符同源

标准库定义:

public func ?? <T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
    optional != nil ? optional! : defaultValue()
}
  • defaultValue 只有在前者为 nil 才执行,避免无谓开销。

自定义断言

func myAssert(_ condition: Bool, _ message: @autoclosure () -> String = "") {
    #if DEBUG
    if !condition {
        print("断言失败: \(message())")
    }
    #endif
}

// 使用
myAssert(score > 0, "分数必须为正,当前值:\(score)")
// 若断言通过,字符串插值不会被执行

短路求值

func logIf(_ condition: Bool, _ msg: @autoclosure () -> String) {
    guard condition else { return }
    print(msg())
}

logIf(isDebug, "昂贵计算结果:\(heavyCompute())")   // 非 Debug 直接短路

使用 checklist

场景 是否适合 @autoclosure
日志、断言、调试信息 ✅ 延迟 + 避免副作用
复杂默认值 ✅ 与 ??同理
需要多次读取的闭包 ❌ 每次调用都会重新求值,缓存请手动处理
需要捕获可变量的闭包 ⚠️ 捕获的是表达式当时的值,注意值语义

一句话总结

@autoclosure 是 Swift 给你的“惰性开关”:

调用方像传值,接收方像拿闭包,只在真正需要时才执行表达式。

把它用在“可能昂贵、可能无效、可能副作用”的参数上,代码立刻更省、更快、更安全。

当液态玻璃计划遭遇反叛者:一场 iOS 26 界面的暗战

2025年9月4日 18:58

在这里插入图片描述

引子

在硅谷的地下代码俱乐部里,流传着一个关于 "液态玻璃" 的传说 —— 那是 Apple 秘密研发的界面改造计划,如同电影《变脸》中那张能改变命运的面具,一旦启用,所有 App 都将被迫换上流光溢彩的新面孔。

在这里插入图片描述

而今天,我们的主角琳恩,一位以守护经典界面为己任的开发者,正面临着职业生涯中最严峻的挑战:她必须在 72 小时内阻止自己开发的 "星图导航"App 被强制换脸,否则整个星际迷航爱好者社区将失去他们最熟悉的操作界面。

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

  • 引子
    1. 🔍 危机降临:液态玻璃计划的突袭
    1. 🕵️ 发现破绽:藏在 Info.plist 里的救命稻草
    1. ⏳ 倒计时:反杀窗口即将关闭
    1. 🛡️ 终极防御:在变脸狂潮中守住初心

她是否能最终成功?让我们拭目以待!


1. 🔍 危机降临:液态玻璃计划的突袭

"琳恩,紧急情况!" 凌晨三点,搭档杰克的全息投影突然出现在屏幕上,他的虚拟形象因信号急促而闪烁不定,"Apple 刚刚推送了 iOS 26 的终极更新,所有重编译的 App 都会自动启用 ' 液态玻璃 ' 界面 —— 我们的星图坐标系统会彻底错乱!"

在这里插入图片描述

琳恩猛地从睡梦中惊醒,手指在键盘上飞舞如刀。

iOS 26 的 "液态玻璃" redesign 就像反派卡斯托・特洛伊的阴谋,表面光鲜亮丽,实则暗藏杀机:按钮边缘的液态流动效果会遮挡星图的经纬度标识,半透明的层级设计会让深空探测数据变得模糊不清。

更可怕的是,按照 Apple 的规则,只要用 Xcode 26 重新编译,这套新界面就会像病毒一样侵入 App 的每一个角落。

在这里插入图片描述

"他们这是强行换脸!" 琳恩咬牙切齿地调出测试机,屏幕上的星图果然已面目全非 —— 原本棱角分明的星座连线变得弯弯曲曲,像被融化的玻璃随意流淌。这哪是升级,简直是对专业用户的背叛!

2. 🕵️ 发现破绽:藏在 Info.plist 里的救命稻草

就在琳恩快要绝望时,她的导师,人称 "代码幽灵" 的马库斯发来一条加密信息:"查 UIDesignRequiresCompatibility,在 Info.plist 的第 47 行附近。"

在这里插入图片描述

这个神秘的密钥就像《变脸》中肖恩藏在十字架里的微型炸弹,是对抗强制换脸的唯一希望。琳恩立刻打开项目中的 Info.plist 文件 —— 这个相当于 App 身份证的配置文件里,果然藏着玄机。当她添加这行代码时,屏幕仿佛传来一声轻微的 "咔哒" 声,就像解开了某种电子锁:

<!-- Info.plist 关键配置 -->

<key>UIDesignRequiresCompatibility\</key>

<!-- 设为YES,相当于给App装上反变脸装置,阻止液态玻璃界面强制生效 -->

<true/>

在这里插入图片描述

重新编译后,奇迹发生了:液态玻璃效果如同退潮般消失,熟悉的星图界面重现在眼前 —— 棱角分明的按钮、清晰锐利的坐标线,连星座名称的字体都保持着经典样式。

在这里插入图片描述

琳恩长舒一口气,仿佛刚从卡斯托的魔爪中夺回自己的脸,悬在心上的巨石终于落地。

3. ⏳ 倒计时:反杀窗口即将关闭

"别高兴太早," 马库斯的全息影像再次出现,这次他的表情凝重如铁,"Apple 在开发者文档里埋了炸弹 —— 这个密钥将在 Xcode 27 中永久移除。"

在这里插入图片描述

琳恩的心沉了下去。就像电影中那枚有倒计时的炸弹,这个反制措施的有效期只剩下不到一年。Apple 的公告写得明明白白:

UIDesignRequiresCompatibility 主要用于调试和测试,而非长期解决方案。这意味着他们虽然暂时保住了界面,但最终还是要面对液态玻璃的全面接管,临时密钥不过是 "缓兵之计",绝非 "长久之策"。

"我们得提前布局," 杰克在一旁调出 Xcode 27 的预览文档,手指敲出一串数据流,"接下来的 12 个月,我们要做的不是逃避,而是让经典界面与液态玻璃 ' 和平共处 '—— 既不丢老用户的情怀,也不违逆新系统的规则。"

在这里插入图片描述

4. 🛡️ 终极防御:在变脸狂潮中守住初心

接下来的日子里,琳恩团队展开了一场与时间的赛跑。他们没有简单依赖临时密钥,而是像《变脸》中肖恩潜伏在敌人内部一样,深入研究液态玻璃的设计逻辑,在代码世界里搭建起 "双重界面防线":

在这里插入图片描述

  • 核心操作区保留经典样式:星图坐标、星座标注等关键功能模块,坚持使用老用户熟悉的设计,守住 App 的 "灵魂底线";

  • 辅助功能区融入液态元素:设置页面、帮助指南等非核心界面,适度添加液态玻璃的过渡动画和光影效果,让新系统用户也能感受到适配诚意;

  • 开发 "界面切换器":在 App 设置中加入开关,让用户自主选择 "经典模式" 或 "液态模式",把界面选择权交还给用户,真正做到 "以人为本"。

在这里插入图片描述

当 Xcode 27 如期而至,强制启用液态玻璃时,"星图导航" 成为了少数几个没有引发用户暴动的 App。

老用户打开 App,看到熟悉的星图界面时会心一笑;新用户切换到液态模式,也能体验到丝滑的现代设计。琳恩团队用行动证明:开发者面对系统更新,不必像肖恩那样被动接受 "换脸",也不必像卡斯托那样极端反抗,而是能用智慧找到平衡 —— 既顺应技术趋势,又守住用户初心。

在这里插入图片描述

就像《变脸》的结局,肖恩最终接纳了曾带来痛苦的面具,却从未丢失自己的灵魂。在代码的世界里,真正的高手从不是抗拒变化的顽固派,而是在技术浪潮中,始终把用户体验放在首位,用一行行代码守护住那份最珍贵的 "界面情怀"。

在这里插入图片描述

而这份情怀,正是让 App 在无数竞品中脱颖而出的关键,也是开发者对用户最真诚的承诺。

那么,宝子们你们 get 到了吗?感谢观赏,我们下次再会吧!8-)

@Observable 遇上属性包装器:一键绕过‘计算属性’禁令的 Swift 5.9 实战技巧

作者 unravel2025
2025年9月4日 18:07

在 Swift 5.9 引入的 @Observable 宏(Observable framework)让“全部属性默认被观察”成为可能,但也带来了一个副作用:

被宏展开后,所有存储属性都变成了计算属性,于是再给它们加自定义 Property Wrapper(如 @Injected@UserDefault 等)就会直接报错:

Property wrapper cannot be applied to a computed property

下面给出原因剖析 + 最小修复示例 + 两种长期策略,让你既能享受 @Observable 的简洁,又能继续用属性包装器做 DI、缓存、格式化等横切逻辑。

先复现问题

@Observable
final class ViewModel {
    var data: [String] = []
    
    @Injected                 // ❌ 编译失败
    private var dataProvider: ProviderProtocol
}

展开后的大致伪代码:

final class ViewModel: Observable {
    var data: [String] {
        get { _storage.data }          // 计算属性
        set { _storage.data = newValue }
    }
    // 同样,dataProvider 也被转成计算属性 → 无法附加 @Injected
}

最小修复:@ObservationIgnored

苹果提供了忽略观察的宏:@ObservationIgnored

作用:让 @Observable 不要把该属性转成计算属性,保持原样(存储属性),于是就能继续挂 Property Wrapper。

@Observable
final class ViewModel {
    var data: [String] = []
    
    @ObservationIgnored      // ✅ 1. 先忽略观察
    @Injected                // ✅ 2. 再挂自定义包装器
    private var dataProvider: ProviderProtocol
    
    func loadData() {
        data = dataProvider.getData()
    }
}
  • 观察链:data 变化仍能触发 SwiftUI 刷新;
  • 注入链:dataProvider 仍是 @Injected 托管的存储属性;
  • 零成本:@ObservationIgnored 编译期生效,运行期无额外开销。

原理速览:宏 vs 属性包装器

维度 Property Wrapper Macro(@Observable、@ObservationIgnored)
执行时机 运行期 编译期
产物 生成存储 + 计算属性 生成计算属性 + 观测逻辑
能否叠加 ❌ 计算属性上不能再挂包装器 ✅ 宏可组合
典型用途 注入、缓存、格式化 观察、代码生成

结论:属性包装器与宏不是替代关系,而是互补工具;冲突时先用宏“放行”,再用包装器“加功能”。

长期策略

策略 A:包装器内移——把 @Injected 放到下层类型

@Observable
final class ViewModel {
    var data: [String] = []
    
    // 不再直接注入,而是持有一个“已注入”的对象
    private let repo = DataRepository()   // 内部已用 @Injected
}
  • 优点:ViewModel 代码干净,100 % 被观察
  • 缺点:需要多一层类型

策略 B:自定义宏——用 SwiftSyntax 写 @InjectedMacro

@Observable
final class ViewModel {
    var data: [String] = []
    
    // 未来可能出现官方或第三方注入宏
    #Injected(.singleton)
    private var dataProvider: ProviderProtocol
}
  • 优点:编译期展开,无运行时反射,性能更好
  • 缺点:目前需自己实现

实战小结(Copy-Paste 模板)

import Observation
import Foundation

// 1. 定义注入协议(示例)
protocol ProviderProtocol {
    func getData() -> [String]
}

// 2. 自定义 Property Wrapper
@propertyWrapper
struct Injected<T> {
    var wrappedValue: T
    init() {
        // 简单演示:从全局容器解析
        self.wrappedValue = DIContainer.shared.resolve()
    }
}

// 3. 可观察模型
@Observable
final class ViewModel {
    var data: [String] = []
    
    @ObservationIgnored   // ← 关键:忽略观察
    @Injected             // ← 继续用包装器
    private var dataProvider: ProviderProtocol
    
    func loadData() {
        data = dataProvider.getData()
    }
}

一句话记住

只要看到 “Property wrapper cannot be applied to a computed property”

立刻想:“先 @ObservationIgnored 忽略观察,再挂包装器” —— 问题秒解。

如何绕过“Extensions must not contain stored properties”错误

作者 unravel2025
2025年9月4日 17:27

为什么 Swift 禁止扩展里存值?

Swift 扩展(extension)不能增加存储属性,原因有三:

  1. 内存布局已确定

    类/结构体的大小在编译期就固定,随意加字段会破坏 ABI。

  2. 类型安全

    允许多个模块给同一类型加字段,会出现命名冲突、重复存储。

  3. 语言哲学

    扩展应“加行为,不改状态”。如果必须加状态,说明设计该重构了。

方案 1:嵌套静态实体(纯 Swift,无 OC 依赖)

思路:把值存在全局/静态变量里,再用计算属性转发。

class Object {}

extension Object {
    // 1. 私有嵌套枚举,仅作命名空间
    private enum Storage {
        // ⚠️ 静态变量,所有实例共用一份
        static var property: String = ""
    }
    
    // 2. 计算属性转发
    var property: String {
        get { Storage.property }
        set { Storage.property = newValue }
    }
}

适用场景

  • 全局共享一份默认值(例如 App 配置)。
  • 不想引入 Objective-C 运行时。

陷阱

  • 所有实例共享同一变量 → 线程安全、命名冲突要自己保证。
  • 不能实现“每个实例各自附加字段”。

方案 2:Associated Objects(每实例独立存储)

思路:借助 Objective-C 运行时,把“键-值”挂在对象上。

导入头文件

import ObjectiveC

extension Object {
    private enum Key {
        // 用 Void? 取地址即可,不占内存
        static var propertyKey: Void?
    }
}

计算属性封装

extension Object {
    var property: String? {
        get {
            objc_getAssociatedObject(self, &Key.propertyKey) as? String
        }
        set {
            objc_setAssociatedObject(
                self,
                &Key.propertyKey,
                newValue,
                .OBJC_ASSOCIATION_RETAIN_NONATOMIC
            )
        }
    }
}

关联策略一览

策略 语义 常用场景
.OBJC_ASSOCIATION_RETAIN 原子 + strong 线程安全对象
.OBJC_ASSOCIATION_RETAIN_NONATOMIC 非原子 + strong 性能优先,UI 控件
.OBJC_ASSOCIATION_COPY 原子 + copy NSString/NSArray等可拷贝类型
.OBJC_ASSOCIATION_ASSIGN 非拥有引用 弱关联(需手动置 nil)

两种方案对比

维度 嵌套静态实体 Associated Objects
是否纯 Swift ❌(需 ObjectiveC)
每实例独立存储 ❌(全局一份)
线程安全 自己加锁 依赖策略(原子/非原子)
生命周期 与 App 相同 与对象生命周期绑定
性能 直接内存访问 哈希表查找,略慢
适用 共享配置、单例 给 UIView/VC 挂私有属性

实战:给 UIView 加一个“点击闭包”扩展

import ObjectiveC

extension UIView {
    private struct Key {
        static var tapAction: Void?
    }
    
    var onTap: (() -> Void)? {
        get {
            objc_getAssociatedObject(self, &Key.tapAction) as? () -> Void
        }
        set {
            objc_setAssociatedObject(
                self,
                &Key.tapAction,
                newValue,
                .OBJC_ASSOCIATION_COPY_NONATOMIC
            )
        }
    }
}

// 使用
let button = UIButton()
button.onTap = { print("被点了一下") }

设计忠告:能不用就不用

  1. 优先考虑组合

    把状态封装到专用对象里,而不是挂在别人身上。

  2. 命名空间要私有

    嵌套 enum / struct 都标 private,防止污染全局。

  3. 线程安全

    静态变量用 NSLock / DispatchQueue;关联对象选对策略。

  4. 文档注释

    说明“此属性通过扩展实现,生命周期与宿主对象一致”。

一句话总结

  • 嵌套静态实体:简单、纯 Swift,但全局共享。
  • Associated Objects:能给每个实例挂私货,却依赖 OC 运行时。

记住:

“扩展里存状态”是语言红线,出现这种需求时,先问自己“是不是该新建一个类型?”

如果答案是否,再安全地“曲线救国”。

彻底搞懂 Swift 的 Any、AnyObject、any

作者 unravel2025
2025年9月4日 14:57

一张速查表先拎清

名称 具体类型 能容纳 典型场景 Swift 版本
Any 顶层类型(具体类型) 任意值(含函数、元组、struct、class) 与旧 API 交互、极少数极端动态场景 1.0+
AnyObject 协议(仅类/actor 遵循) 引用类型(class / actor) 弱引用容器、协议限制为引用语义 1.0+
any existential 关键字 任意协议(含关联类型) 声明“存在某个协议实现” 5.7+

Any:万能口袋,但请少用

let bag: [Any] = [
    "Jack",                // String
    55,                    // Int
    { () -> Void in },     // 闭包
    (44.9, 0)              // 元组
]

问题:类型安全被“蒸发”

struct StringValidator {
    func validate(_ value: Any) -> Bool {
        guard let str = value as? String else {
            fatalError("只接受 String")   // 运行时爆炸
        }
        return !str.isEmpty
    }
}

更安全的替代

  • 协议约束(推荐 1)
protocol Validator {
    associatedtype Value
    func validate(_ value: Value) -> Bool
}

struct StringValidator: Validator {
    typealias Value = String
    func validate(_ value: String) -> Bool { !value.isEmpty }
}
  • 泛型(推荐 2)
struct AnyValidator<T>: Validator {
    typealias Value = T
    func validate(_ value: T) -> Bool {
        if let str = value as? String { return !str.isEmpty }
        if let int = value as? Int { return int > 0 }
        return false
    }
}

总结:能用协议/泛型就别用 Any。

AnyObject:只拥抱引用类型

  • 只有 classactor 隐式遵循。
  • 协议限制为“必须是引用类型”。
protocol Cache: AnyObject {
    func clear()
}

class MemoryCache: Cache {
    func clear() { print("内存缓存清空") }
}

struct DiskCache: Cache { /* ❌ struct 无法遵守 AnyObject 协议 */ }

func perform(_ c: AnyObject) { /* 只接受引用类型 */ }

与 actor 搭配

actor Counter {
    private var value = 0
    nonisolated func description() -> String { "CounterActor" }
}

let obj: AnyObject = Counter()

注意:nonisolated 让 actor 方法同步调用,避免 await。

any:existential 关键字(Swift 5.7+)

  • 用于 带关联类型的协议 或 需要运行时多态 时。
  • 不是类型,而是标记:“这是一个盒子,里面装着某个具体实现”。

带关联类型的协议

protocol AppleDevice {
    associatedtype Chip
    var chip: Chip { get }
}

class ModernMacBook: AppleDevice {
    typealias Chip = String
    let chip = "M3"
}

class OldMacBook: AppleDevice {
    typealias Chip = Int
    let chip = 42
}

// ✅ 正确:用 any 声明 existential
let devices: [any AppleDevice] = [ModernMacBook(), OldMacBook()]

// ❌ 错误:不能直接用协议名
// 使用协议类型,前面默认使用any修饰。目前会报警告,但后续会报错。需要明确写上any
// let wrong: [AppleDevice] = [ModernMacBook(), OldMacBook()]

与 some 的区别

关键字 含义 何时使用
some opaque type(编译期已知具体类型) 返回值隐藏实现细节
any existential(运行时才知道具体类型) 参数、集合元素
func opaque() -> some AppleDevice { ModernMacBook() }  // 编译期确定
func existential(_ d: any AppleDevice) { ... }         // 运行时多态

实战:弱引用容器场景

用 AnyObject 存储 delegate

final class MulticastDelegate {
    private var delegates = NSHashTable<AnyObject>.weakObjects()

    func add<T: AnyObject>(_ delegate: T) {
        delegates.add(delegate)
    }
}

用 any 擦除带关联类型的协议

protocol EventHandler {
    associatedtype Event
    func handle(_ event: Event)
}

struct AnyHandler<Event>: EventHandler {
    private let _handle: (Event) -> Void
    init<H: EventHandler>(_ handler: H) where H.Event == Event {
        _handle = handler.handle
    }
    func handle(_ event: Event) { _handle(event) }
}

一句话总结

  • Any ≈ 远古时代的 id,能装万物,但类型信息全丢。
  • AnyObject ≈ “必须是引用类型”的协议约束,常用于弱引用容器。
  • any ≈ Swift 5.7 的新关键字,解决“带关联类型协议”的泛型多态问题。

开发口诀:能用具体类型 > 泛型 > 协议 > any > AnyObject > Any

按此优先级,你将获得类型安全、性能与可读性的三重保障。

SwiftUI基础学习

作者 如此风景
2025年9月4日 11:06

以下是对 SwiftUI 更全面、深入的详解,涵盖架构设计、视图系统、状态管理、交互处理、性能优化等核心内容,适合希望系统掌握 SwiftUI 开发的开发者:

一、SwiftUI 架构与设计理念

1. 声明式范式的底层逻辑

SwiftUI 的声明式语法并非简单的“语法糖”,而是基于 “状态驱动视图” 的核心思想:

  • 状态(State):视图依赖的数据(如用户输入、网络数据)。
  • 视图(View):对状态的“描述”(body 计算属性定义视图结构)。
  • 绑定(Binding):状态与视图之间的双向关联(状态变化 → 视图刷新;用户交互 → 状态更新)。

核心流程

状态变化 → 重新计算 body → 生成新视图树 → Diffing 算法对比差异 → 只更新变化的部分

这种机制相比 UIKit 的命令式编程(需手动管理视图生命周期和更新),大幅减少了样板代码,降低了状态同步错误的概率。

2. 跨平台统一性

SwiftUI 是 Apple 生态的统一 UI 框架,一套代码可运行在:

  • iOS 13+、iPadOS 13+、macOS 10.15+、watchOS 6+、tvOS 13+
  • 支持动态适配不同设备尺寸(通过 Size Classes)和交互方式(触摸/鼠标/键盘)

平台适配示例

struct PlatformView: View {
    var body: some View {
        Text("Hello Platforms")
            #if os(iOS)
            .font(.title)
            #elseif os(macOS)
            .font(.largeTitle)
            #elseif os(watchOS)
            .font(.caption)
            #endif
    }
}

二、视图系统(View)深度解析

1. View 协议与不透明返回类型

  • some View:Swift 5.1 引入的不透明返回类型,表示“某种遵循 View 协议的类型”,允许编译器推断具体类型,同时隐藏实现细节。
  • 视图组合:复杂视图通过嵌套基础视图构建(如 VStack { Text(...) + Button(...) }),编译器会将其合并为一个复合视图类型(如 _VStack<...>)。
// 复合视图类型自动生成,无需手动管理
struct ComplexView: View {
    var body: some View {
        VStack {
            HStack { Text("A"); Text("B") }
            Image(systemName: "star")
        }
    }
}

2. 基础视图与修饰符(Modifiers)

  • 基础视图TextImageButtonTextField 等,负责单一 UI 元素的展示。
  • 修饰符:通过链式调用修改视图属性(如 .font().foregroundColor()),本质是返回一个新的“包装视图”(而非修改原视图,因结构体是值类型)。

修饰符执行顺序:先调用的修饰符作用于内层,后调用的作用于外层(类似洋葱模型):

Text("SwiftUI")
    .foregroundColor(.white)  // 先设置文本颜色
    .padding()                // 再添加内边距(白色文本周围的空间)
    .background(Color.blue)   // 最后设置背景(包含文本和内边距的区域)

3. 布局系统核心规则

SwiftUI 布局遵循 “父子协商” 机制,分三步:

  1. 父视图提议尺寸:父视图向子视图提供一个建议尺寸(如 VStack 向子视图提供其宽度)。
  2. 子视图确定自身尺寸:子视图根据内容和修饰符(如 .frame())返回自身需要的尺寸。
  3. 父视图放置子视图:父视图根据子视图的尺寸和布局规则(如 alignment)确定最终位置。

关键布局修饰符

  • .frame(minWidth: idealWidth: maxWidth: minHeight: idealHeight: maxHeight: alignment:):设置尺寸范围(父视图可压缩或拉伸)。
  • .fixedSize(horizontal: vertical:):强制使用理想尺寸(不接受父视图的压缩)。
  • .layoutPriority(_:):优先级越高,越能优先获得空间(默认 0,最高 1000)。
  • .padding(_:edges:):增加内边距(会扩大视图的占用空间)。
  • .background(_:alignment:):背景视图的尺寸由前景内容和内边距决定。

4. 容器视图高级用法

  • List 与动态数据

    • 支持 ForEach 动态生成行(需数据遵循 Identifiable 或提供 id 参数)。
    • 支持分组(Section)、编辑模式(.environment(\.editMode, .constant(.active)))。
    • 懒加载优化:List 会自动复用不可见行,性能接近 UITableView
    struct UserList: View {
        let users: [User] // User 遵循 Identifiable
        
        var body: some View {
            List {
                Section(header: Text("用户列表")) {
                    ForEach(users) { user in
                        NavigationLink(user.name) {
                            UserDetailView(user: user)
                        }
                    }
                    .onDelete(perform: deleteUser) // 支持滑动删除
                }
            }
        }
        
        private func deleteUser(at offsets: IndexSet) {
            // 删除逻辑
        }
    }
    
  • NavigationStack(iOS 16+)

    • 替代旧版 NavigationView,支持程序化导航(通过路径数组控制)。
    • 可指定导航栏样式、标题显示模式,支持深层链接。
    struct NavDemo: View {
        // 导航路径(存储当前导航栈)
        @State private var path: [Int] = []
        
        var body: some View {
            NavigationStack(path: $path) {
                List(1..<10) { i in
                    NavigationLink(value: i) {
                        Text("前往页面 \(i)")
                    }
                }
                .navigationTitle("导航示例")
                // 定义路径对应的目标视图
                .navigationDestination(for: Int.self) { i in
                    DetailView(number: i, path: $path)
                }
            }
        }
    }
    
    struct DetailView: View {
        var number: Int
        @Binding var path: [Int]
        
        var body: some View {
            VStack {
                Text("页面 \(number)")
                Button("前往下一页") {
                    path.append(number + 1) // 程序化导航
                }
            }
        }
    }
    

三、状态管理体系(State Management)

SwiftUI 提供了多套状态管理方案,覆盖从简单到复杂的场景:

1. 视图私有状态:@State

  • 用于仅当前视图使用的临时状态(如按钮点击计数、表单输入)。
  • 原理:@State 是属性包装器,将状态存储在 SwiftUI 管理的“外部存储”中(而非结构体内部),因此状态变化不会导致结构体重新初始化。
  • 访问规则:修饰的属性必须用 private 标记(强调私有性)。
struct CounterView: View {
    @State private var count = 0 // 私有状态
    
    var body: some View {
        Button("点击了 \(count) 次") {
            count += 1 // 状态变化 → 视图刷新
        }
    }
}

2. 父子视图共享:@Binding

  • 用于子视图修改父视图的状态(避免状态拷贝,实现双向绑定)。
  • 语法:父视图通过 $ 传递状态的“绑定引用”,子视图用 @Binding 接收。
// 子视图:接收绑定
struct ToggleView: View {
    @Binding var isOn: Bool
    
    var body: some View {
        Button(isOn ? "关闭" : "开启") {
            isOn.toggle() // 修改绑定 → 父视图状态同步变化
        }
    }
}

// 父视图:传递绑定
struct ParentToggle: View {
    @State private var flag = false
    
    var body: some View {
        VStack {
            ToggleView(isOn: $flag)
            Text("当前状态:\(flag ? "开启" : "关闭")")
        }
    }
}

3. 跨视图共享(非全局):ObservableObject + @ObservedObject/@StateObject

  • 适用场景:多个视图共享的复杂数据(如用户信息、网络请求结果)。
  • 实现步骤:
    1. 定义遵循 ObservableObject 的数据模型类(引用类型)。
    2. @Published 标记需要观察的属性(属性变化时自动发送通知)。
    3. 视图中用 @StateObject 初始化模型(负责生命周期),用 @ObservedObject 观察模型(接收通知)。
// 1. 数据模型
class ProductStore: ObservableObject {
    @Published var products: [Product] = [] // 变化时通知观察者
    
    func fetchProducts() {
        // 模拟网络请求
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.products = [Product(name: "iPhone"), Product(name: "iPad")]
        }
    }
}

// 2. 父视图:初始化模型(用 @StateObject)
struct ProductList: View {
    @StateObject private var store = ProductStore() // 管理生命周期
    
    var body: some View {
        List(store.products) { product in
            ProductItemView(product: product)
        }
        .onAppear { store.fetchProducts() }
    }
}

// 3. 子视图:观察模型(用 @ObservedObject)
struct ProductDetail: View {
    @ObservedObject var store: ProductStore // 接收模型引用
    
    var body: some View {
        Text("商品数量:\(store.products.count)")
    }
}
  • @StateObject vs @ObservedObject
    • @StateObject:用于首次初始化对象的视图,确保对象生命周期与视图一致(视图销毁时对象也销毁)。
    • @ObservedObject:用于接收已初始化的对象,不负责生命周期管理。

4. 全局共享状态:@EnvironmentObject

  • 适用场景:应用级全局状态(如主题设置、用户登录状态),避免多层参数传递(“依赖注入地狱”)。
  • 实现步骤:
    1. 在顶层视图通过 .environmentObject(_:) 注入模型。
    2. 任意子视图用 @EnvironmentObject 直接获取(无需显式传递)。
// 1. 定义全局模型
class ThemeManager: ObservableObject {
    @Published var isDarkMode = false
}

// 2. 顶层注入(如 App 入口)
@main
struct MyApp: App {
    let theme = ThemeManager()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(theme) // 注入环境
        }
    }
}

// 3. 任意子视图使用
struct SettingsView: View {
    @EnvironmentObject var theme: ThemeManager // 直接获取
    
    var body: some View {
        Toggle("深色模式", isOn: $theme.isDarkMode)
    }
}

struct DetailView: View {
    @EnvironmentObject var theme: ThemeManager // 同一模型
    
    var body: some View {
        Text("当前模式:\(theme.isDarkMode ? "深色" : "浅色")")
    }
}

5. 持久化状态:@AppStorageCore Data

  • @AppStorage:轻量级持久化(基于 UserDefaults),自动同步状态与本地存储:

    struct SettingsView: View {
        @AppStorage("autoLogin") private var autoLogin = false // 持久化到 UserDefaults
        @AppStorage("username", store: UserDefaults(suiteName: "group.com.myapp")) 
        private var username = "" // 自定义存储位置(如 App Group)
        
        var body: some View {
            Toggle("自动登录", isOn: $autoLogin)
        }
    }
    
  • Core Data 集成:复杂数据持久化,通过 @FetchRequest 直接在视图中获取数据:

    struct CoreDataView: View {
        // 直接从 Core Data 获取数据
        @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)])
        private var items: FetchedResults<Item>
        
        @Environment(\.managedObjectContext) private var viewContext // 上下文
        
        var body: some View {
            List(items) { item in
                Text(item.name ?? "未知")
            }
            .onAppear {
                // 添加数据
                let newItem = Item(context: viewContext)
                newItem.name = "新条目"
                try? viewContext.save()
            }
        }
    }
    

四、交互与动画系统

1. 事件处理

  • 按钮(Button:基础交互控件,支持自定义动作和外观:

    Button(role: .destructive) { // 角色:影响系统样式(如红色删除按钮)
        print("删除操作")
    } label: {
        Text("删除")
    }
    .buttonStyle(.borderedProminent) // 系统预设样式
    
  • 手势(Gestures:支持多种复杂手势,可组合使用:

    struct GestureDemo: View {
        @State private var scale = 1.0
        @State private var rotation = 0.0
        
        var body: some View {
            Rectangle()
                .fill(Color.blue)
                .frame(width: 200, height: 200)
                .scaleEffect(scale)
                .rotationEffect(.degrees(rotation))
                .gesture(
                    // 组合缩放手势和旋转手势
                    MagnificationGesture()
                        .onChanged { scale = $0 }
                        .simultaneously(with:
                            RotationGesture()
                                .onChanged { rotation = $0.degrees }
                        )
                )
        }
    }
    
  • 文本输入(TextField/TextEditor

    struct InputView: View {
        @State private var username = ""
        @State private var bio = ""
        
        var body: some View {
            VStack {
                TextField("用户名", text: $username)
                    .textFieldStyle(.roundedBorder)
                    .autocapitalization(.none) // 禁用自动大写
                
                TextEditor(text: $bio)
                    .frame(height: 100)
                    .border(Color.gray)
            }
            .padding()
        }
    }
    

2. 动画系统

SwiftUI 动画基于 “隐式动画”“显式动画”,底层使用 UIKit/AppKit 的动画引擎,但简化了使用方式。

  • 隐式动画:为视图绑定的状态变化添加动画,通过 .animation(_:value:) 修饰符:

    struct ImplicitAnimation: View {
        @State private var isToggled = false
        
        var body: some View {
            Circle()
                .fill(isToggled ? Color.red : Color.blue)
                .frame(width: isToggled ? 200 : 100, height: isToggled ? 200 : 100)
                .animation(.easeInOut, value: isToggled) // 仅当 isToggled 变化时动画
                .onTapGesture { isToggled.toggle() }
        }
    }
    
  • 显式动画:用 withAnimation 包裹状态修改,控制更灵活:

    struct ExplicitAnimation: View {
        @State private var offset = CGSize.zero
        
        var body: some View {
            Rectangle()
                .frame(width: 100, height: 100)
                .offset(offset)
                .onTapGesture {
                    // 显式指定动画参数
                    withAnimation(.spring(response: 0.5, dampingFraction: 0.3)) {
                        offset = CGSize(width: 100, height: 200)
                    }
                }
        }
    }
    
  • 自定义过渡动画:控制视图插入/删除时的动画:

    struct TransitionDemo: View {
        @State private var showView = false
        
        var body: some View {
            VStack {
                Button("切换视图") { showView.toggle() }
                
                if showView {
                    Text("过渡动画")
                        .transition(.scale.combined(with: .opacity)) // 组合缩放和透明度
                }
            }
            .animation(.easeInOut, value: showView)
        }
    }
    

五、性能优化策略

1. 避免不必要的视图刷新

  • EquatableView:当视图的输入数据未变化时,阻止刷新:

    struct EquatableItem: View, Equatable {
        let data: String
        
        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.data == rhs.data // 数据不变则不刷新
        }
        
        var body: some View {
            Text(data)
        }
    }
    
    // 使用时包裹 EquatableView
    EquatableView(EquatableItem(data: "固定文本"))
    
  • 拆分视图:将复杂视图拆分为多个子视图,避免局部变化导致整体刷新。

2. 优化列表性能

  • LazyVStack/LazyHStack:仅渲染可见区域的视图,适合长列表:

    ScrollView {
        LazyVStack {
            ForEach(0..<1000) { i in
                Text("Item \(i)")
                    .frame(height: 50)
            }
        }
    }
    
  • List 复用机制List 内部自动复用单元格,比 VStack 更适合动态数据。

3. 减少视图树复杂度

  • 避免在 body 中创建临时对象(如 Button { ... } label: { Text("\(Date())") } 会导致每秒刷新)。
  • Group@ViewBuilder 合并条件视图,减少视图树分支:
    // 优化前:条件分支生成不同视图类型
    if condition {
        Text("A")
    } else {
        Text("B")
    }
    
    // 优化后:合并为同一类型
    Text(condition ? "A" : "B")
    

4. 图片与资源优化

  • 使用 resizable()scaledToFit() 适配图片尺寸,避免拉伸变形。
  • 对大型图片使用 AsyncImage 异步加载(iOS 15+):
    AsyncImage(url: URL(string: "https://example.com/image.jpg")) { phase in
        switch phase {
        case .empty:
            ProgressView() // 加载中
        case .success(let image):
            image.resizable().scaledToFit()
        case .failure:
            Image(systemName: "photo") // 加载失败
        @unknown default:
            EmptyView()
        }
    }
    

六、与 UIKit 的互操作

SwiftUI 并非完全替代 UIKit,而是可以无缝集成:

1. 在 SwiftUI 中使用 UIKit 控件(UIViewRepresentable

// 包装 UIKit 的 UILabel
struct UIKitLabel: UIViewRepresentable {
    var text: String
    
    // 创建 UIView 实例
    func makeUIView(context: Context) -> UILabel {
        let label = UILabel()
        label.textAlignment = .center
        return label
    }
    
    // 更新 UIView(状态变化时调用)
    func updateUIView(_ uiView: UILabel, context: Context) {
        uiView.text = text
    }
}

// 使用
struct SwiftUIView: View {
    var body: some View {
        UIKitLabel(text: "SwiftUI 中的 UIKit 标签")
            .frame(height: 50)
    }
}

2. 在 UIKit 中使用 SwiftUI 视图(UIHostingController

// SwiftUI 视图
struct MySwiftUIView: View {
    var body: some View {
        Text("UIKit 中的 SwiftUI 视图")
    }
}

// UIKit 视图控制器中嵌入
import UIKit
import SwiftUI

class MyUIKitViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 创建 hosting controller
        let swiftUIView = MySwiftUIView()
        let hostingController = UIHostingController(rootView: swiftUIView)
        
        // 添加到当前控制器
        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.didMove(toParent: self)
        
        // 布局
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hostingController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            hostingController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

七、调试与工具链

1. Xcode 预览(Previews)

  • 支持多设备、多状态预览,实时查看界面效果:

    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            Group {
                ContentView()
                    .previewDevice("iPhone 15")
                    .previewDisplayName("iPhone 15")
                
                ContentView()
                    .preferredColorScheme(.dark)
                    .previewDevice("iPad Pro (12.9-inch)")
            }
        }
    }
    
  • 预览宏(iOS 17+):简化预览代码:

    #Preview {
        ContentView()
    }
    
    #Preview("深色模式") {
        ContentView()
            .preferredColorScheme(.dark)
    }
    

2. 视图调试工具

  • debugLayout:显示视图边界和布局指南:

    Text("调试布局")
        .debugLayout(true)
    
  • 视图层级检查器:Xcode 菜单 Debug > View Debugging > Inspect View Hierarchy,查看运行时视图结构。

  • 性能分析:使用 InstrumentsSwiftUI 模板,检测视图刷新频率、布局耗时等性能问题。

八、最佳实践与进阶方向

  1. 遵循单一职责原则:每个视图只负责一件事(如展示列表、处理表单输入)。
  2. 状态提升:将共享状态提升到最近的共同父视图,避免状态分散。
  3. 使用 @Environment 访问系统环境:如尺寸类别、颜色方案、设备方向等:
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    
    var body: some View {
        if horizontalSizeClass == .compact {
            VStack { /* 竖屏布局 */ }
        } else {
            HStack { /* 横屏布局 */ }
        }
    }
    
  4. 学习 SwiftUI 3.0+ 新特性:如 Async/Await 集成、SwiftData(Core Data 替代方案)、Observation 框架(替代 ObservableObject)等。

SwiftUI 是一个不断进化的框架,其核心优势在于声明式语法的简洁性和跨平台能力。深入理解状态管理和布局系统是掌握 SwiftUI 的关键,建议结合实际项目练习,逐步积累复杂场景的解决方案。

OptionSet vs Enum:Swift 中如何优雅地表达“多选”?

作者 unravel2025
2025年9月4日 09:51

两种“多选”方案的基因差异

维度 OptionSet Enum + Set
底层模型 位图(bitset) 哈希集合(HashSet)
存储大小 固定位宽(UInt8/Int/UInt64) 动态哈希表
性能 位运算 O(1) 哈希操作 O(1)
组合能力 原生支持按位或、补集、交集 需手动写集合运算
可读性 需要位运算知识 更接近自然语言
最大选项数 受位宽限制(UInt8=8,UInt=64) 理论无上限
场景 性能敏感、C 接口、系统 API 业务逻辑、易读的权限/标签

系统框架中的 OptionSet 速览

// UIView 自动布局掩码
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]

// 动画选项(可以同时指定多个)
UIView.animate(withDuration: 0.3,
               delay: 0,
               options: [.curveEaseInOut, .allowUserInteraction]) { ... }

// JSON 序列化
let data = try JSONSerialization.data(withJSONObject: obj,
                                      options: [.prettyPrinted, .fragmentsAllowed])

自定义 OptionSet:四行代码搞定位运算

最原始写法(不推荐)

struct UserRight {
    let rawValue: Int
    static let create = UserRight(rawValue: 1)   // 0001
    static let edit   = UserRight(rawValue: 2)   // 0010
    static let read   = UserRight(rawValue: 4)   // 0100
    static let delete = UserRight(rawValue: 8)   // 1000
}
extension UserRight: OptionSet {}

位左移写法(推荐)

struct UserRight: OptionSet {
    let rawValue: Int
    
    static let create = UserRight(rawValue: 1 << 0) // 0001
    static let edit   = UserRight(rawValue: 1 << 1) // 0010
    static let read   = UserRight(rawValue: 1 << 2) // 0100
    static let delete = UserRight(rawValue: 1 << 3) // 1000
    
    // 常用组合
    static let admin: UserRight = [.create, .edit, .read, .delete]
    static let editor: UserRight = [.read, .edit]
}

技巧:用 1 << n2ⁿ 直观,且编译器会检查越界。

与位运算配合

var rights: UserRight = [.read, .edit]

let b0 = rights.contains(.read)        // true
let b1 = rights.contains(.delete)      // false

let r = rights.insert(.delete)        // 返回新的 OptionSet
rights.remove(.read)          // 去掉读权限
let b2 = rights == .admin              // false

可读性升级:打印友好

extension UserRight: CustomStringConvertible {
    var description: String {
        let map: [(Self, String)] = [
            (.create, "create"),
            (.edit,   "edit"),
            (.read,   "read"),
            (.delete, "delete")
        ]
        return map
            .filter { contains($0.0) }
            .map(\.1)
            .joined(separator: ", ")
    }
}

print(UserRight.admin)   // "create, edit, read, delete"

Enum + Set:当可读性更重要时

定义协议(一次写完,到处复用)

protocol OptionProtocol: RawRepresentable, Hashable, CaseIterable {}

枚举声明

enum UserRightOption: String, OptionProtocol {
    case create, edit, read, delete
}

// 使用 Set 作为容器
typealias UserRights = Set<UserRightOption>

extension Set where Element == UserRightOption {
    static var editor: UserRights { [.read, .edit] }
    // 这里的好处是可以直接使用Enum的allCases,而如果是optionSet只能手动写全部的case
    static var admin: UserRights  { Set(Element.allCases) }
}

与 OptionSet 互转

extension Set where Element: OptionProtocol {
    /// 转成 Int 位图,方便与 C 层交互
    var rawValue: Int {
        var value = 0
        for (index, element) in Element.allCases.enumerated() where contains(element) {
            value |= (1 << index)
        }
        return value
    }
}

let set: UserRights = [.read, .edit]
let bits = set.rawValue   // 6 (0b110)

实战选型建议

场景 推荐 理由
系统 API、CoreGraphics、Metal 标志 OptionSet 必须与 C 位图交互
网络请求参数、数据库字段 OptionSet 体积小、易序列化
业务权限、标签、筛选器 Enum + Set 可读、易扩展、无位宽限制
需要超过 64 个选项 Enum + Set OptionSet 位宽不够

性能基准(macOS, 1M 次操作)

操作 OptionSet (Int) Set
contains 0.05 ms 0.21 ms
insert 0.03 ms 0.35 ms
内存/实例 8 Byte 24 Byte+

PS: 这里只是展示了最基础的num+set用法。enum还有关联值,可以玩更多花活

一句话总结

  • OptionSet = 位图,极致轻量,适合与底层、系统、性能敏感代码打交道。
  • Enum + Set = 哈希集合,可读性高,适合业务层快速迭代。

选型口诀:“对外接口选 OptionSet,对内业务选 Enum+Set,超过 64 个选项直接 Enum。”

SwiftUI 自定义 Shape:实现顶部圆角矩形 RoundedTopRectangle

作者 雪糕吖
2025年9月4日 09:39

在 SwiftUI 里,如果你只想要一个“顶部两个角是圆角,底部是直角”的矩形,第一反应可能是用 .cornerRadius()
.cornerRadius() 会同时作用在四个角,没办法单独控制某些角,这时候就需要自定义 Shape

这篇文章,我们就从零实现一个 RoundedTopRectangle,并详细解释绘制的每一步。

为什么用 Shape

在 SwiftUI 里,Shape 是最底层的绘制单位。它的 path(in:) 方法允许我们在给定的矩形范围内,自己决定如何绘制路径。

你可以理解成:给了我们一张画布(rect),我们要在上面用“画笔”按顺序描出路径。

  • move(to:) 就是把画笔移动到某个点(不画线)。
  • addLine(to:) 是画直线。
  • addArc(...) 是画圆弧。
  • closeSubpath() 是闭合路径,自动连回起点。

这样拼起来,就能绘制出任何你想要的形状。

完整代码

import SwiftUI

struct RoundedTopRectangle: Shape {
    func path(in rect: CGRect) -> Path {
        let width = rect.width
        let height = rect.height
        let cornerRadius: CGFloat = 24

        var path = Path()

        // 1. 起点:从左上角圆弧的下方开始 (0, cornerRadius)
        path.move(to: CGPoint(x: 0, y: cornerRadius))

        // 2. 左上角圆弧:180° → 270°
        path.addArc(
            center: CGPoint(x: cornerRadius, y: cornerRadius),
            radius: cornerRadius,
            startAngle: .degrees(180),
            endAngle: .degrees(270),
            clockwise: false
        )

        // 3. 顶部水平线:连接到右上角圆弧的起点
        path.addLine(to: CGPoint(x: width - cornerRadius, y: 0))

        // 4. 右上角圆弧:270° → 360°
        path.addArc(
            center: CGPoint(x: width - cornerRadius, y: cornerRadius),
            radius: cornerRadius,
            startAngle: .degrees(270),
            endAngle: .degrees(360),
            clockwise: false
        )

        // 5. 右侧直线到底部
        path.addLine(to: CGPoint(x: width, y: height))

        // 6. 底部直线到左下角
        path.addLine(to: CGPoint(x: 0, y: height))

        // 7. 左侧直线回到圆弧起点
        path.addLine(to: CGPoint(x: 0, y: cornerRadius))

        // 8. 闭合路径
        path.closeSubpath()

        return path
    }
}

角度怎么理解?

在 SwiftUI 里,角度是数学坐标系的标准方向

  • 0° = 水平方向(正右)
  • 90° = 垂直方向(正上)
  • 180° = 水平方向(正左)
  • 270° = 垂直方向(正下)

所以:

  • 左上角圆弧,我们要从左往下画:180° → 270°
  • 右上角圆弧,我们要从下往右画:270° → 360°

这就是为什么要用这些角度范围。

效果展示

VStack 里放一个 RoundedTopRectangle

struct ContentView: View {
    var body: some View {
        VStack {
            Spacer()
            RoundedTopRectangle()
                .fill(Color.blue)
                .frame(height: 300)
                .shadow(radius: 5)
        }
        .edgesIgnoringSafeArea(.bottom)
    }
}

运行效果:

  • 底部是一个蓝色矩形
  • 顶部两个角是圆角,底部两个角是直角
  • 非常适合做 Bottom Sheet、播放器控制面板、支付弹窗等 UI。

改进思路

  1. 参数化圆角
    目前 cornerRadius 写死为 24,可以改成一个参数:

    struct RoundedTopRectangle: Shape {
        var cornerRadius: CGFloat = 24
        func path(in rect: CGRect) -> Path { ... }
    }
    
  2. 只控制某些角
    如果你想要“底部圆角,顶部直角”,只需要调整绘制的顺序,把圆弧放到底部。

  3. 支持可配置的四角圆角
    甚至可以写成一个更通用的 CustomRoundedRectangle,传入 [topLeft: true, topRight: false, ...] 这样的布尔配置,来决定哪些角是圆角。

总结

  • .cornerRadius() 方便,但太“平均”。
  • Shape 给了我们精细的控制,可以定义任何奇怪的矩形。
  • 理解 Path + 坐标 + 角度,你就能自由画出想要的图形。

自定义 Shape 是 SwiftUI 的隐藏宝藏,玩明白之后,不仅可以做圆角矩形,还能画波浪、曲线、对角线圆角等各种有趣的 UI。

2025 年真正有效的 App Store 优化(ASO)

作者 JarvanMo
2025年9月4日 09:22

欢迎微信公众号:OpenFlutter,感谢

2025 年真正有效的 App Store 优化(ASO)

关于应用发现的一个不舒服的真相

你的应用即使能治愈癌症,如果没人能找到它,下载量也仍然会是零。

我从 2019 年就开始做应用优化,这是我希望有人能早点告诉我的:ASO 的目的不是为了欺骗算法——而是为了比其他所有应用都更好地匹配用户的意图。

经过多年的测试,我学会了哪些策略真正有效,哪些只是浪费时间。


2025 年真正有效的方法

  1. 语义关键词研究(不只是关键词堆砌)

应用商店的算法变得更聪明了。它们现在理解的是上下文,而不仅仅是精确匹配。

  • 不要这样做: “健身 锻炼 健身房 运动 app”
  • 试试这样做: “不去健身房也能养成健康习惯”

我使用的方法:

  • 询问 10 位潜在用户,他们会如何搜索你的应用。
  • 在你的关键词研究中,使用他们的确切短语。
  • 使用 Apple Search Ads 测试变体(即使只花 50 美元,也能告诉你哪些词能带来转化)。
  • 值得付费的工具: AppTweak 用于语义建议,Sensor Tower 用于搜索量数据。
  1. 截图讲故事(三帧法则)

大多数开发者只展示功能。而赢家展示的是转变。

  • 第一帧: 提出问题(杂乱的预算表格)
  • 第二帧: 你的解决方案(简洁的支出追踪器)
  • 第三帧: 呈现结果(省钱数据仪表盘)

我将这个方法应用在一个客户的金融应用上进行了 A/B 测试——仅仅是改变截图,转化率就有了显著提升。

  1. 聪明的评论时机

不要随机地请求评论。在用户完成积极操作之后再请求。

  • 有效时机:

    • 效率应用: 在完成第一个任务后。
    • 游戏: 在赢得比赛/升级后。
    • 社交应用: 在第一次成功发帖后。
    • 金融应用: 在完成第一笔交易后。

我为一个冥想应用实施了这种时机优化,评论率显著提高。

  1. 翻译之外的本地化

大多数应用收入现在都来自非英语市场,但大多数应用只是通过谷歌翻译来处理所有内容。

真正的本地化意味着:

  • 针对不同文化的痛点(“节省时间” vs “工作与生活的平衡”)。
  • 本地化的社会证明(使用当地语言的推荐语)。
  • 在你的营销中融入当地的节日和活动

我的客户有一款语言学习应用,当我们把“快速学英语”的宣传语改为“英语助你事业腾飞”后,在德国的下载量有了大幅增长。


已经失效的方法(省下你的时间)

已死战术 #1:在标题中堆砌关键词

  • 旧方法: “最佳照片编辑器 相机 滤镜 美颜 自拍 App”
  • 失败原因: 苹果 2024 年的更新会惩罚这种明显的关键词堆砌。
  • 新方法: “VSCO: 照片与视频编辑器”(价值清晰 + 品牌)

已死战术 #2:购买虚假评论

  • 算法检测变得非常精准。我见过有应用因此被完全下架。
  • 替代方法: 优秀的入职引导 + 恰到好处的评论提示。

已死战术 #3:一劳永逸的优化

  • 不定期更新的应用会被埋没。算法偏爱活跃的应用。
  • 新现实: 头部应用倾向于频繁推送更新——许多应用平均每月发布 1-4 次更新,但这只适用于有意义的改进。不要为了更新而更新。

真正有效的 ASO 流程

大多数 ASO 建议只关注战术。以下是我与客户合作时使用的流程

第一周:研究阶段

  • 分析排名前 5 的竞争对手的关键词。
  • 采访 10-15 位潜在用户,了解他们的搜索行为。
  • 审计当前性能基线。

第二至三周:实施阶段

  • 更新标题和副标题(如果需要)。
  • 采用讲故事的方法创建新的截图。
  • 设置评论时机优化。
  • 实施最重要的本地化市场。

第四周:衡量与迭代

  • 追踪转化率,而不仅仅是排名。
  • 进行 A/B 测试。
  • 规划下一个优化周期。

持续进行:每月回顾

  • 竞争对手分析。
  • 季节性关键词调整。
  • 性能回顾和迭代计划。

大多数开发者忽略的高阶技巧

竞争对手搜索广告情报

  • 在你的竞争对手的品牌关键词上运行小额广告活动。数据会告诉你哪些词能真正带来转化(而不仅仅是展示)。
  • 成本: 100-200 美元/月。
  • 价值: 无价的竞争情报。

跨平台 SEO 协同

  • 你的网站内容应该与应用商店的列表关键词保持一致。用户通常在下载前会在 Google 上进行研究。
  • 没有网站? 使用社交媒体资料、Medium 博客、YouTube 演示或一个简单的单页着陆页。Google 仍然会索引这些内容,并为你的应用商店列表带来流量。

季节性关键词轮换

  • 健身应用: “新年目标” → “夏日身材” → “假日健康”
  • 税务应用: “报税准备” → “税务规划” → “年末报税”
  • 大多数应用优化一次就忘了。赢家会适应搜索模式的变化。

真正重要的指标

忽略这些虚荣指标:

  • 总下载量
  • 关键词排名
  • 展示量

转而追踪这些指标:

  • 应用商店页面转化率
  • 7 天留存率
  • 每次安装收入
  • 自然下载量与付费下载量的比例

我使用一个简单的仪表盘:AppFollow 用于评论,Apple/Google Analytics 用于转化率,自定义追踪用于留存率。


你的下一步

从这份清单中选择一件事,在本周内实施它:

  • 检查你的截图——它们是展示转变还是仅仅展示功能?
  • 采访 5 位用户——他们实际上会如何搜索像你这样的应用?
  • 检查你的评论时机——你是否在积极的时刻请求评论?
  • 研究一个本地化市场——你最大的机会在哪里?

ASO 不是什么高深莫测的科学,但它确实需要持续的努力。2025 年的赢家,不一定是最好的应用——而是那些最懂用户搜索行为的应用。


让我们保持真实

有哪些 ASO 策略对你最有效?有哪些是彻底失败的?

在评论中分享你的经验吧——我真的很好奇现在对其他开发者来说,什么方法是奏效的。

Swift 的 Optional.take():一次性消费值的神器

作者 unravel2025
2025年9月4日 08:32

原文:Exploring the take() method for Optionals

什么是 take()

take() 是 Swift6 标准库提供的 Optional 实例方法:

“如果有值就返回它,同时把自己置为 nil;如果已经是 nil 就返回 nil。” 等价于“原子地消费一次”。

语义类似于 Rust 的 Option::take()、C++ 的 std::optional::reset() + 返回值。

为什么需要它?

场景 不用 take()的问题 take()带来的好处
一次性 Token、验证码、密钥 手动 if let + 置 nil容易忘 原子消费,不会二次使用
懒加载缓存 并发下可能重复创建 把“判断 + 清空”做成原子操作
资源释放 提前 nil导致重复初始化 保证只拿一次,线程安全

探索实现

标准库中该函数的定义:public mutating func take() -> Wrapped?

如果我们自己写,两行扩展即可拥有:

extension Optional {
    /// 如果 self 有值则返回它并把 self 置为 nil,否则返回 nil
    public mutating func take() -> Wrapped? {
        defer { self = nil }
        return self
    }
}

注意:必须是 mutating——值类型要改自己。

官方并不是通过extension实现,而是在定义主体里实现的。实现更优雅,使用了consume消费自身

public mutating func take() -> Self {
    let result = consume self
    self = nil
    return result
}

实战对比:代码更短、意图更清晰

传统写法

var token: String? = "a640f873-bf52-45b0-b13a-5bcef123aa2a"

if let value = token {
    print(value)               // 使用
    token = nil                // 手动失效
} else {
    print("token 已被使用过")
}

使用 take()

var token: String? = "a640f873-bf52-45b0-b13a-5bcef123aa2a"

if let value = token.take() {
    print(value)               // 打印一次
    print(token as Any)        // nil
} else {
    print("token 已被使用过")
}

一行完成“取值 + 清空”,不会漏掉置 nil。

进阶场景

线程安全的一次性缓存

class ImageCache {
    private var _image: UIImage?
    
    /// 只有第一次调用会真正下载,后续都返回 nil
    func takeImage() -> UIImage? {
        return _image.take()        // 原子拿图
    }
}

防止重复提交网络请求

var pendingTask: URLSessionDataTask? = URLSessionDataTask()

// 用户狂点按钮也只发一次
if let task = pendingTask.take() {
    task.resume()
} else {
    print("请求已发出,请勿重复点击")
}

Optional chaining 组合

var config: Config? = loadConfig()
let dbPath = config.take()?.databasePath   // 取出即清空

Optional.map 的区别

操作 是否改变原 Optional 语义
map ❌ 不变 只转换值,不消费
take ✅ 置 nil 一次性拿值
var x: Int? = 3
let a = x.map { $0 * 2 }   // a = 6,  x 仍是 3
let b = x.take()           // b = 3,  x 变成 nil

小结:什么时候用 take()?

  1. 一次性凭证(Token、验证码、邀请码)
  2. 懒加载且只加载一次的单例资源
  3. 需要原子“拿完即焚”的并发场景
  4. 想让代码更短、意图更明显

记住:take() = “拿值 + 废掉它”。

iOS 长截图的完美实现方案 - 附Demo源码

作者 AI机器人
2025年9月3日 22:39

长图拼接完美,完全可以媲美应用市场上的软件。

(因为是C++的算法,所以平台通用,如果你是独立开发者,可以用来上架APP)

由于iOS没有系统自带的长截图功能,所以只能用第三方的,AppStore上有几款好用的长截图APP,都需要付费使用,而且还不便宜。

比如: 

  • Picsew 8元 - 15元;
  • 滚动截图 8元-48元;
  • Stitch 68元;

image.png

收费都很贵,那我想自己是不是可以做一个类似的软件来付费呢?

找了一些长截图的技术文章,发现似乎还真没有公开的技术方案,好像大家都藏着掖着的。

不过好在经历了几天的失败后,终于成功了。

核心拼接思想:

  1. 第一张图片: [内容A][重叠区域][内容B]

  2. 第二张图片: [内容C][重叠区域][内容D]

  3. 图片正序或者倒序不影响结果

  4. 拼接结果: [内容A][重叠区域][内容D]

优化:

  1. 与第三张图片特征匹配时,内容A不参与匹配

  2. 倒序检测

  3. 顶部导航栏与底部安全区优化

技术方案:

  1. APP添加Target - Broadcast Upload Extension

Broadcast Upload Extension 其实是用来做录屏才会用到的。

因为大家都是通过视频流来获取图片的。

不知道是哪个天才的人,想到这个来做长截图。

  1. 主APP 接受Target传递的 CVPixelBuffer

这其实是视频的中的图片帧,可以转为UIImage

  1. 拼接

前两步都比较容易实现,我认为这里才是难点,甚至超出了iOS的技术栈。

将输入的 UIImage 转换为 OpenCV 的 cv::Mat 格式

OpenCV 内部处理拼接;

先看效果:

IMG_0815.JPG

需要源码的可以私我获取完整拼图Demo源码。

设置页面滑动时顶部的导航是会变化的,而且还有顶部还有blur变化(iOS26是玻璃效果),我觉得是一个很复杂的场景,但是这套算法跑起来表现很优秀。

接入OpenCV也遇到了一些问题,编译一直失败,发现OpenCV的头文件声明要在最顶部。

算法也是优化了五六个版本,比如处理拼接处模糊、遮挡、缺失等问题。最后采用的是一套组合算法搭配使用;

  • 智能区域匹配算法:
    • 图像预处理: 将输入的 UIImage 转换为 OpenCV 的 cv::Mat 格式。

    • 特征点检测与描述: 使用 ORB (Oriented FAST and Rotated BRIEF) 算法在两张待拼接的图片中寻找特征点。ORB 是一种高效且免费的特征检测算法,非常适合移动端应用。

    • 动态 ROI (Region of Interest) 限定: 这是算法的精髓。对于第一张图片(即已拼接好的结果),我们只在其 底部新生成的内容区域 (由 activeRegionHeight 参数指定)进行特征点检测。对于第二张新图片,则在其 顶部区域 进行检测。这精确地实现了“避免重复匹配”的需求,极大地提高了匹配的准确性和效率。

    •  特征点匹配: 使用 Brute-Force Matcher (暴力匹配) 配合 汉明距离 (Hamming Distance) 来寻找两组特征点之间的最佳匹配对。

    • 误匹配对筛选 (RANSAC): 匹配的特征点对中难免存在噪声和错误。我们使用 RANSAC (Random Sample Consensus) 算法来提纯匹配结果。它通过迭代找到一个能解释最多匹配点的几何变换模型(在这里是垂直位移),从而剔除掉那些不符合该模型的“局外点”。

    •  计算精确位移: 从经过 RANSAC 筛选后的优质匹配点对中,计算出它们在 Y 轴上的位移(offset)的中位数。使用中位数可以进一步排除极端异常值的影响,得到一个非常稳健的垂直偏移量。

    • 图像裁剪与合并:

      •  根据计算出的精确位移,确定两张图片的重叠区域。
      • 为了实现平滑过渡,我们采用 30/70 的非对称裁剪策略 。从重叠区域的 30% 位置作为“接缝”,分别裁剪第一张图的上方内容和第二张图的下方内容。这保留了更多第二张图的顶部信息,有效解决了之前版本中第二张图顶部被过度裁剪的问题。
      • 最后,将裁剪后的两部分图像垂直拼接( cv::vconcat )成一张新的 cv::Mat 。
    • 结果返回: 将最终拼接好的 cv::Mat 转换回 UIImage ,并更新 activeRegionHeight (新生成内容的高度),为下一次迭代做准备。

总而言之,本项目成功地将经典的计算机视觉特征匹配流程,与针对长截图拼接场景的特定优化(动态 ROI、非对称裁剪)相结合,构建了一个健壮、准确的智能拼接引擎。

需要源码的可以私我获取完整拼图Demo。

转载声明:微信公众号《独立开发者基地》

打造零泄漏的 Swift 代码:三套实用工具完整指南

作者 unravel2025
2025年9月3日 16:38

原文:Three Practical Tools for Managing References and Detecting Memory Leaks in Swift

为什么需要这三件套?

痛点 工具 一句话作用
保留循环导致 Coordinator/VM 不释放 MemoryLeakMonitor 运行时断言实例数量
Delegate 数组、观察者列表强引用 @WeakArray / AtomicWeakArray 自动剔除已释放对象
多线程读写同一字典 AtomicDictionary 无锁并发安全读写

工具 1:弱引用集合

系统自带方案:NSHashTable

private final class ReferenceRepository {
    // NSHashTable 只存弱引用,对象释放后自动移除
    private var references = NSHashTable<AnyObject>.weakObjects()

    func add(_ ref: AnyObject) -> Int {
        references.add(ref)
        return references.count   // 返回当前存活数量
    }
}
  • ✅ 线程安全(内部使用锁)
  • ❌ 无序、不可重复、只能存 AnyObject

自定义 WeakArray —— 有序、可重复、支持泛型

轻量级包装器 WeakObject

/// 弱引用包装器,内部使用闭包捕获弱引用
final class WeakObject<T> {
    private let handler: () -> T?
    // 通过计算属性实时获取弱引用对象
    var value: T? { handler() }

    init(_ value: T) {
        let object = value as AnyObject
        // 巧用闭包捕获原始对象的弱引用
        handler = { [weak object] in object as? T }
    }
}

@WeakArray 属性包装器

@propertyWrapper
struct WeakArray<Element> {
    private var storage = [WeakObject<Element>]()
    // 初始值赋值需要用到的构造器,第一个参数标签必须是wrappedValue
    init(wrappedValue: [Element]) {
        self.wrappedValue = wrappedValue
    }

    /// 读取时自动剔除 nil
    var wrappedValue: [Element] {
        get { storage.compactMap { $0.value } }
        set { storage = newValue.map(WeakObject.init) }
    }
}

使用示例:多播委托

protocol MyDelegate: AnyObject {
    func didUpdate()
}

final class EventBroadcaster {
    // 这里会调用WeakArray的init(wrappedValue:)构造器
    @WeakArray private var subscribers: [MyDelegate] = []
    
    // 添加订阅
    func subscribe(_ subscriber: MyDelegate) {
        subscribers.append(subscriber)
    }

    func notifyAll() {
        subscribers.forEach { $0.didUpdate() }
    }
}

工具 2:线程安全的弱引用数组 AtomicWeakArray

@WeakArray 本身不是线程安全,多线程同时读写会崩溃。

DispatchQueue 实现 并发读、排他写 的版本:

final class AtomicWeakArray<Element> {
    // 并发队列
    private let queue = DispatchQueue(
        label: "atomic-weak-array",
        attributes: .concurrent
    )
    private var storage: [WeakObject<Element>] = []

    /// 只读快照,内部同步
    var all: [Element] {
        queue.sync { storage.compactMap { $0.value } }
    }

    func append(_ element: Element) {
        queue.async(flags: .barrier) {
            self.storage.append(WeakObject(element))
        }
    }

    func removeAll() {
        queue.async(flags: .barrier) { self.storage.removeAll() }
    }

    func forEach(_ body: (Element) throws -> Void) rethrows {
        try queue.sync {
            try storage.compactMap { $0.value }.forEach(body)
        }
    }

    var count: Int { queue.sync { storage.compactMap { $0.value }.count } }
}
  • ✅ 读写并行,写操作互斥
  • ✅ 保持引用语义(class 类型)
  • ✅ 支持 forEachcount 等便捷方法

工具 3:线程安全的字典 AtomicDictionary

标准 Dictionary 是值类型,多线程读写会触发 数据竞争 或 CoW 复制。

封装一个基于 DispatchQueue 的并发字典:

class AtomicDictionary<Key: Hashable, Value> {
    private let queue = DispatchQueue(label: "atomic-dict", attributes: .concurrent)
    private var storage: [Key: Value] = [:]

    subscript(key: Key) -> Value? {
        get { queue.sync { storage[key] } }
        set { queue.async(flags: .barrier) { self.storage[key] = newValue } }
    }

    /// 原子“读取或插入”
    subscript(key: Key, default value: @autoclosure () -> Value) -> Value {
        get {
            if let v = self[key] { return v }
            let new = value()
            self[key] = new
            return new
        }
    }
}

工具 4:内存泄漏监测器 MemoryLeakMonitor

协议:声明最大实例数

protocol MemoryLeakMonitorable: AnyObject {
    /// 允许同时存活的最大实例数量
    var max: Int { get }
}

extension MemoryLeakMonitorable {
    /// 默认用类名作为标识
    var description: String { String(describing: self) }
}

单例监测器

final class MemoryLeakMonitor {
    private static let shared = MemoryLeakMonitor()
    private let dict: AtomicDictionary<String, ReferenceRepository> = .init()

    private init() {}

    /// 在 DEBUG 环境下调用
    static func validate(_ instance: MemoryLeakMonitorable) {
        let count = shared.dict[instance.description, default: .init()]
                       .count(with: instance)
        assert(
            count <= instance.max,
            "内存泄漏!\(instance.description) 当前实例数:\(count)"
        )
    }
}

/// 弱引用仓库,内部使用线程安全的 AtomicWeakArray
private final class ReferenceRepository {
    private var refs: AtomicWeakArray<AnyObject> = .init()

    func count(with ref: AnyObject) -> Int {
        refs.append(ref)
        return refs.count
    }
}

使用示例:Coordinator 自检

class Coordinator: MemoryLeakMonitorable {
    // 只能有一个实例
    var max: Int { 1 }

    init() {
        #if DEBUG
        MemoryLeakMonitor.validate(self)
        #endif
    }
}

final class ArchiveCoordinator: Coordinator {
    override var max: Int { 2 }   // 允许两个并存
}

每次 init 时自动校验,Debug 模式下一旦发现超限立即断言中断,Release 模式无开销。

组合使用:一条链路的最佳实践

  1. Coordinator 实现 MemoryLeakMonitorable,自动检测泄漏
  2. 内部持有子 Coordinator 用 AtomicWeakArray 线程安全存储
  3. 子 Coordinator 的 delegate 使用 @WeakArray 避免循环引用
  4. 共享缓存 用 AtomicDictionary 并发读写

总结 & 迁移建议

工具 适用场景 迁移成本
NSHashTable 简单无序集合 0 行代码
@WeakArray 需要顺序/重复 替换成属性包装器
AtomicWeakArray 多线程读写 把 Array→ AtomicWeakArray
AtomicDictionary 并发缓存、注册表 把 Dictionary→ AtomicDictionary
MemoryLeakMonitor Coordinator/VM/Service 加协议 + 1 行 validate(self)
❌
❌