阅读视图

发现新文章,点击刷新页面。

Swift 中基础概念:「函数」与「方法」

为什么要区分「函数」和「方法」

写 Swift 时,我们每天都在写 func

但同一个关键字,有时叫「函数」,有时又叫「方法」。

名字不同,背后其实是作用域与归属权的差异:

  • 函数(function)——独立存在,像一把瑞士军刀,谁都能拿来用。
  • 方法(method)——寄居在类型内部,能直接访问类型的数据,像家电的遥控器,只能操控指定品牌。

搞清这一点,再读苹果文档或第三方库源码,就不会迷糊。

函数:真正独立的代码块

  1. 定义与调用
// 函数定义:全局可用,不依赖任何类型
func greet(name: String) -> String {
    return "Hello, \(name)!"
}

// 调用
let msg = greet(name: "Alice")
print(msg)   // 输出:Hello, Alice!
  1. 生活化比喻

把函数想成微波炉:你把它搬到任何厨房(项目),它都能加热食物(完成任务)。

微波炉不隶属于某套房子(对象),完全独立。

  1. 再看一个无参无副作用的工具函数
/// 返回当前时间戳(秒)
func currentTimestamp() -> TimeInterval {
    return Date().timeIntervalSince1970
}

这种「纯工具」逻辑,做成函数最合适,因为任何类型都可能用得到。

方法:绑定在类型上的函数

方法 = 函数 + 上下文。

上下文就是类型(class/struct/enum)里的属性与其他方法。

  1. 实例方法(Instance Method)
struct Car {
    var speed: Int      // 属性

    // 实例方法:只能由具体 Car 实例调用
    func describe() -> String {
        return "The car is going \(speed) km/h."
    }
}

let myCar = Car(speed: 100)
print(myCar.describe())   // 输出:The car is going 100 km/h.

要点:

  • describe 写在 Car 内部,直接读取 speed
  • 如果脱离 Car 实例,describe 无法存在。
  1. 变异方法(Mutating Method)

值类型(struct/enum)默认不可修改自身属性,需要显式标记 mutating

struct Counter {
    var count = 0

    // 变异方法:可以修改属性
    mutating func increment() {
        count += 1
    }
}

var counter = Counter()
counter.increment()
print(counter.count)   // 输出:1

注意:

  1. 只有 值类型 才用 mutating;class 是引用类型,直接改即可。

  2. 调用者必须用 var 声明,不能用 let

  3. 类型方法(Type Method)

相当于其他语言的「静态方法」,由类型本身调用,不依赖实例。

class Car {
    // 类型属性
    static let defaultWheelCount = 4
    
    // 类型方法
    static func numberOfWheels() -> Int {
        return defaultWheelCount
    }
}

print(Car.numberOfWheels())   // 输出:4

使用场景:

  • 工厂方法(创建实例)
  • 与实例无关的全局配置或工具逻辑

函数与方法协同实战:跑步记录器

下面把「函数」与「方法」放在同一段业务里,感受它们如何各司其职。

// 1️⃣ 独立函数:公里转英里,任何模块都能用
func convertToMiles(kilometers: Double) -> Double {
    return kilometers * 0.621371
}

// 2️⃣ 类型:Runner
struct Runner {
    var name: String
    var distanceInKm: Double
    
    // 实例方法:生成专属报告
    func progressReport() -> String {
        // 直接调用上面的独立函数
        let miles = convertToMiles(kilometers: distanceInKm)
        return "\(name) 今天跑了 \(String(format: "%.2f", miles)) 英里"
    }
    
    // 变异方法:增加跑量
    mutating func runExtra(km: Double) {
        distanceInKm += km
    }
}

// 3️⃣ 使用
var emma = Runner(name: "Emma", distanceInKm: 5)
print(emma.progressReport())   // Emma 今天跑了 3.11 英里

emma.runExtra(km: 2)
print(emma.progressReport())   // Emma 今天跑了 4.35 英里

看到吗?

  • convertToMiles 是纯逻辑,与任何类型无关。
  • progressReport 需要 Runner 内部数据,所以做成实例方法。
  • 两者组合,代码既复用又内聚。

易错点小结

场景 正确做法 常见错误
struct 里想改属性 mutating 忘记写,编译报错
类型方法里用实例属性 ❌ 不能 static 当实例方法用
全局工具逻辑 写成函数 强行塞进某个类型,导致复用困难

理解 & 使用原则

  1. 优先写成函数:只要逻辑不依赖任何实例数据,就独立出来。

    方便单元测试、跨模块复用,也减少类型体积。

  2. 升阶为方法的时机:

    • 需要频繁访问类型内部私有属性
    • 需要多态(子类重写)
    • 需要链式调用(return self

    满足一条,再考虑搬进类型。

  3. 命名统一,作用域清晰:

    函数用「动词」开头,convertdownloadvalidate

    方法也用「动词」,但可省略主语,describeincrementsave

    让调用者一眼看出谁属于谁。

扩展场景:把知识用到实际项目

  1. 网络层
   // 独立函数,任何模型都能调
   func GET<T: Decodable>(_ url: URL) async throws -> T { ... }

   // 模型内部方法,自己知道怎么拼装 URL
   struct Article {
       let id: Int
       func detailURL() -> URL { ... }
   }
  1. SwiftUI 视图
   // 全局函数,做颜色插值
   func interpolateColor(from: Color, to: Color, percent: Double) -> Color { ... }

   // 视图的方法,负责自己的业务
   struct ProgressCircle: View {
       var progress: Double
       private func strokeColor() -> Color {
           return interpolateColor(from: .green, to: .red, percent: progress)
       }
   }
  1. 算法库

    快速排序、哈希函数等纯算法,一律写成函数,避免绑定具体类型。

结语

所有方法都是函数,但函数不一定是方法。

记住「是否依赖类型数据」这一把尺子,该独立就独立,该内聚就内聚。

当你把边界划清楚,代码会自然呈现出高内聚、低耦合的漂亮形态。

希望这篇笔记能帮你把「函数 vs 方法」彻底吃透,写出更 Swifty 的代码!

参考资料

Swift 官方语言指南 - Functions

Swift 官方语言指南 - Methods

iOS进阶1-combine

iOS 进阶:深入浅出 Swift Combine 框架

本文深入探讨 Apple 的响应式编程框架 Combine。通过核心概念解析、丰富代码示例和实战场景,系统介绍如何利用 Combine 优雅地处理异步事件和数据流,提升代码的可读性、可维护性。

一、Combine 框架概述

Combine 是 Apple 在 2019 年 WWDC 上推出的一个声明式响应式编程框架,专为 Swift 语言设计。它的核心思想是处理随时间变化的值或异步事件。想象一下诸如网络请求响应、用户界面输入、定时器事件等场景,这些都可以被建模为事件流,而 Combine 提供了一套统一的 API 来组合和转换这些流 。

在 SwiftUI 中,Combine 是数据驱动 UI 的基石(如 @Published 和 ObservableObject)。但它同样强大地适用于 UIKit 开发,用于简化复杂的异步操作和状态管理 。

核心概念三要素

Combine 的运作主要围绕三个核心角色,它们共同构成一条清晰的数据流水线:

核心概念 角色描述 现实世界类比
Publisher(发布者) 事件的源头,负责产出值。 报社
Subscriber(订阅者) 事件的终点,负责接收并消费值。 订户
Operator(操作符) 数据的处理站,位于 Publisher 和 Subscriber 之间,负责转换、过滤、组合值。 报社的编辑部门(校对、排版、内容整合)

一条完整的 Combine 链条工作流程如下:Publisher -> (零个或多个 Operator) -> Subscriber 

。接下来,我们详细看看每个组成部分。

二、核心组件详解

2.1 Publisher(发布者)

Publisher 是一个协议,它定义了一个能够发出一系列元素(Values)、完成事件(Completion)或错误(Failure)的类型。它有两种终止状态:正常完成(.finished)失败(.failure)。在收到终止事件后,数据流即结束 。

常用的内置 Publisher 包括:

  • **Just**: 发送一个值后立即完成。

    swift
    复制
    let publisher = Just("Hello, Combine!") // 输出类型是 String, 错误类型是 Never
    
  • **Fail**: 立即发送一个错误并终止。

  • **PassthroughSubject**: 一个可以手动发送值的 Subject(主题),不保留当前值。

  • **CurrentValueSubject**: 一个可以手动发送值的 Subject,并保存当前值的状态 。

  • **@Published**: 属性包装器,能将一个属性转换为 Publisher 。

2.2 Subscriber(订阅者)

Subscriber 是数据的消费者。Publisher 在有了 Subscriber 之后才会开始发送数据。Combine 提供了两种常用的内置 Subscriber:

  1. **sink**
    最通用的订阅者,它接收两个闭包:一个处理接收到的值(receiveValue),另一个处理完成事件(receiveCompletion)。

    swift
    复制
    [1, 2, 3, 4, 5].publisher // Publisher 发布数据
        .sink(
            receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("数据流正常结束")
                case .failure(let error):
                    print("数据流因错误结束: (error)")
                }
            },
            receiveValue: { value in
                print("接收到值: (value)") // 依次打印 1, 2, 3, 4, 5
            }
        )
    
  2. **assign**
    将接收到的值直接绑定到某个对象的某个属性上,用于更新 UI 非常方便。

    swift
    复制
    class MyViewController: UIViewController {
        @IBOutlet weak var nameLabel: UILabel!
        var cancellables = Set<AnyCancellable>()
        
        func viewDidLoad() {
            super.viewDidLoad()
            // $name 是一个 Publisher<String, Never>
            viewModel.$name
                .assign(to: .text, on: nameLabel) // 将值直接赋给 label 的 text 属性
                .store(in: &cancellables)
        }
    }
    

2.3 Operator(操作符)

操作符是 Combine 强大功能的体现,它们是 Publisher 协议上定义的方法,每个操作符都会返回一个新的 Publisher,从而允许进行链式调用。以下是一些常用类别:

操作符类别 代表操作符 功能说明 示例
转换 map 将接收到的值转换为另一种形式。 .map { $0.count } (将字符串流转换为整数流)
过滤 filter 只允许满足条件的值通过。 .filter { $0 > 10 } (只保留大于10的值)
错误处理 catch 捕获错误,并返回一个备用的 Publisher。 .catch { _ in return Just("Default Value") }
组合 combineLatest 组合多个 Publisher,当任何一个有新值时,发送所有 Publisher 最新值的元组。 用于表单验证,同时监听用户名和密码输入框。
时间控制 debounce 防抖,例如用于搜索框,在用户停止输入一段时间后才发送请求。 .debounce(for: .seconds(0.5), scheduler: RunLoop.main)

链式调用示例

swift
复制
// 一个综合使用操作符的示例
let cancellable = [1, 2, 3, 4, 5].publisher
    .filter { $0 % 2 == 0 } // 过滤偶数:2, 4
    .map { $0 * $0 }        // 转换平方:4, 16
    .sink { value in
        print(value) // 最终输出:4, 16
    }

三、实战应用场景

3.1 网络请求

Combine 能极大地简化网络请求的处理。URLSession 直接提供了 dataTaskPublisher 用于网络调用。

swift
复制
import Combine

struct User: Decodable {
    let name: String
}

func fetchUser(userId: String) -> AnyPublisher<User, Error> {
    guard let url = URL(string: "https://api.example.com/users/(userId)") else {
        return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
    }
    
    return URLSession.shared.dataTaskPublisher(for: url) // 1. 创建网络请求 Publisher
        .map(.data)                                   // 2. 提取数据部分
        .decode(type: User.self, decoder: JSONDecoder()) // 3. 解码 JSON
        .receive(on: DispatchQueue.main)              // 4. 切换到主线程更新 UI
        .eraseToAnyPublisher()                         // 5. 类型擦除,方便返回
}

// 使用
var cancellables = Set<AnyCancellable>()
fetchUser(userId: "123")
    .sink(receiveCompletion: { completion in
        if case .failure(let error) = completion {
            print("请求失败: (error)")
        }
    }, receiveValue: { user in
        print("获取到用户: (user.name)")
        // 在这里更新 UI
    })
    .store(in: &cancellables)

3.2 处理用户输入(UIKit)

使用 Combine 可以轻松响应 UIKit 控件的各种事件。

swift
复制
import Combine
import UIKit

class SearchViewController: UIViewController {
    @IBOutlet weak var searchTextField: UITextField!
    @IBOutlet weak var resultsLabel: UILabel!
    
    var viewModel = SearchViewModel()
    var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bindTextField()
    }
    
    private func bindTextField() {
        // 1. 创建文本变化的 Publisher
        let textPublisher = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: searchTextField)
            .compactMap { ($0.object as? UITextField)?.text } // 确保有文本
            .eraseToAnyPublisher()
        
        // 2. 将 Publisher 绑定到 ViewModel
        viewModel.performSearch(with: textPublisher)
        
        // 3. 订阅 ViewModel 的结果来更新 UI
        viewModel.$searchResults
            .receive(on: DispatchQueue.main)
            .assign(to: .text, on: resultsLabel)
            .store(in: &cancellables)
    }
}

class SearchViewModel: ObservableObject {
    @Published var searchResults: String = ""
    private var cancellables = Set<AnyCancellable>()
    
    func performSearch(with queryPublisher: AnyPublisher<String, Never>) {
        queryPublisher
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main) // 防抖,避免频繁请求
            .removeDuplicates() // 去除连续重复的输入
            .flatMap { query -> AnyPublisher<String, Never> in
                // 模拟网络搜索,例如返回 "Results for '(query)'"
                return Just("Results for '(query)'").eraseToAnyPublisher()
            }
            .assign(to: .searchResults, on: self)
            .store(in: &cancellables)
    }
}

3.3 状态管理与 SwiftUI 集成

在 SwiftUI 中,Combine 是无缝集成的。ObservableObject 协议和 @Published 属性包装器是核心。

swift
复制
import SwiftUI
import Combine

class CounterViewModel: ObservableObject {
    @Published var count: Int = 0 // 使用 @Published 标记,当其变化时会通知视图更新
    @Published var isEven: Bool = false
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 监听 count 的变化,自动推导 isEven 的状态
        $count
            .map { $0 % 2 == 0 }
            .assign(to: .isEven, on: self)
            .store(in: &cancellables)
    }
    
    func increment() {
        count += 1
    }
    
    func decrement() {
        count -= 1
    }
}

struct CounterView: View {
    @StateObject var viewModel = CounterViewModel() // StateObject 监听 ObservableObject 的变化
    
    var body: some View {
        VStack {
            Text("Count: (viewModel.count)")
            Text(viewModel.isEven ? "Even" : "Odd")
            Button("+1") {
                viewModel.increment()
            }
            Button("-1") {
                viewModel.decrement()
            }
        }
    }
}

四、内存管理与最佳实践

4.1 内存管理:AnyCancellable

当您调用 sink 或 assign 时,返回值是一个 AnyCancellable 实例。你必须强引用这个对象,否则订阅会立即被取消,数据流也会中断 。

标准做法是使用一个集合(通常是 Set<AnyCancellable>)来存储所有订阅。

swift
复制
class MyViewController: UIViewController {
    private var cancellables = Set<AnyCancellable>() // 存储订阅的集合
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        SomePublisher()
            .sink { ... }
            .store(in: &cancellables) // 关键:将订阅存储到集合中
    }
}

4.2 最佳实践建议

  1. 线程切换:使用 receive(on:) 操作符确保 UI 更新在主线程进行(DispatchQueue.main)。

  2. 错误处理:合理使用 catchretry 等操作符来优雅地处理可能发生的错误。

  3. 避免循环引用:在 sink 的闭包内使用 [weak self] 来避免循环引用,特别是在将值赋给 self 的属性时。

    swift
    复制
    .sink { [weak self] value in
        self?.updateUI(with: value)
    }
    
  4. 合理使用操作符:操作符虽好,但不宜过度嵌套,保持链式的可读性。

  5. 调试:使用 print() 操作符可以打印出事件流的发生,便于调试。

    swift
    复制
    .print("Debug Stream")
    .sink { ... }
    

总结

Combine 框架通过其声明式的语法,将复杂的异步代码转化为清晰、线性的数据流管道。虽然初学时有一定门槛,但一旦掌握,它将极大地提升你处理异步事件和状态管理的能力,使代码更健壮、更易维护。从简单的 UI 绑定到复杂的异步操作链,Combine 都是一个强大的工具。建议从简单的例子开始实践,逐步深入到更复杂的场景中。

希望这篇博客能为你打开 Combine 世界的大门!

KN:Kotlin 与 OC 交互

Kotlin Native (KN)

Kotlin/Native | Kotlin Documentation

KN是一种技术,可以将Kotlin的代码编译为原生的二进制库,无需依赖虚拟机即可运行,主要包括下面两部分的内容:

● 基于LLVM的Kotlin编译器的后端

● Kotlin标准库的原生实现

 

 

KN 是 Kotlin Multiplatform 的一部分,KN让 Kotlin 代码可以在更多的目标平台运行。

 

目标平台(Target Platform) 指代码编译后最终运行的硬件和操作系统环境

处理器指令集架构 x86、x86_64、arm32、arm64
二进制文件的组织规范 ELF(Linux)、Mach-O(macOS/iOS)、PE(Windows)
操作系统特有的接口 不同的系统调用接口(IO、多线程)

 

 

编译

image.png

将 Kotlin IR 转换为 LLVM IR,最终通过LLVM生成对应平台的可执行文件。

处理器指令集架构 以及 二进制文件格式 的差异被处理

 

 

Runtime

运行时库,抹平操作系统的差异,为Kotlin标准库提供底层功能实现

 

image.png

可以参考C/C++运行时库理解,可以自行搜索

 

 

编译器跟runtime是相互配合的,runtime 提供的能力,需要编译器来桥接,编译器也需要知道哪些功能可以怎么桥接才能完成编译。

 

 

Helloworld

创建一个 Hello.kt 文件,内容如下:

interface HelloAble {
    fun makeIt(): String
}
 
open class Hello(open var name: String) {
 
    var helloable: HelloAble? = null
 
    open fun greetPrefix(): String {
        return "Hello"
    }
 
    fun greet(): String {
        return ("${greetPrefix()} $name")
    }
 
    fun triggleInterface() {
        helloable?.makeIt()
    }
 
    companion object {
        fun createObj(): Hello {
            return Hello("luyu")
        }
    }
}
 
open class Helloworld(override var name: String) : Hello(name) {
    override fun greetPrefix(): String {
        val s = super.greetPrefix()
        return "$s world"
    }
}
 
fun main() {
    val h = Helloworld("cindy")
    println(h.greet())
    h.triggleInterface()
}

 

执行命令:

kotlinc-native Hello.kt -g -Xno-inline -p program -o Hello

● kotlinc-native:编译Kotlin文件

● -g:带调试信息

● -p program:编译为可执行程序,还可以编译为动态库,静态库,framework等

● -o Hello:输出文件名为Hello

 

在我目前的Mac电脑上,生成下面两个产物:

 

可以直接运行,不依赖虚拟机环境

 

 

启动过程

使用MachOView打开Hello.kexe,搜索main函数,发现函数符号名为kfun:#main(){},可以使用lldb调试看看:

查看堆栈:

需要先初始化runtime,创建参数

调到我们的main函数

 

对应的runtime源码

 

对象创建

main函数反编译结果

 

整体跟C++的机制比较像,成员方法编译为全局的C函数,对象实例是一个C结构体; 调用成员方法就是调用全局函数,并把对象作为第一个参数传入

 

通过 MachOView 可以看到编译后所有的对应的函数

 

 

我们打断点到init方法

清楚的看到有两个参数,第一个参数是this对象,第二个参数是name,分别通过x0寄存器跟x0寄存器 跟 x1 寄存器传递

 

 

我们给name传的值是字符串"cindy",验证下是否正确

● x/8gx $x1 读取 $x1 地址对应的内存中的内容,g:8个字节为一组 x:以16进制显示 重复 8 次

● x/8hx 0x1000b22c0 读取0x1000b22c0地址对应的内存中的内容 h:两个字节为一组 x:以16进制显示 重复 8 次 似乎看到了ASCII 吗

● 以字符串显示,发现了确实是cindy,Kotlin每个字符大小是两个字节

 

 

编译器已经把源码中的 "cindy" 字符串编译为了放在全局数据区的一个 String 对象(全局常量),C结构体定义如下

struct StringHeader {
    TypeInfo* typeInfoOrMeta_;    uint32_t count_;
    int32_t hashCode_;
    uint16_t flags_;
    alignas(KChar) char data_[];
}

 

x1是字符串"cindy",编译器生成的全局常量,x1是字符串"cindy",编译器生成的全局常量,x0是Helloworld对象吗?怎么来的?

 

可以看main函数中调用 init方法之前的一段代码

 

 

● 从数据段获取了Helloworld的类型的信息 kclass:Helloworld

● 调用 AllocInstance 分配内存

● 调用 UpdateStackRef 将分配好的内存的地址保存到栈上

 

类型信息

kclass:Helloworld 是一个地址,里面存的内容是什么?为什么创建对象需要它?

 

通过查看源码,对应的类型是 TypeInfo 的结构体,存储一个类的信息(类比与OC的类对象)

 

对应的定义如下(截图不完整):

● 这些信息本身都是在编译期的常量,编译器组织起来放在了数据段(不可变),运行时可以读取使用

● 很多跟OC类似的信息,父类型、实例大小、成员变量的便宜、遵守的协议等等

 

可以读取信息看下

有 3 个成员变量:objOffsetsCount_ = 3,便宜地址如下:

 

relativeName_ = Helloworld

 

看看ExtendedTypeInfo

三个变量的名字分别是:name helloable name

 

instanceSize_ = 0x20 占用32个字节

三个成员变量,三个指针,每个指针占用8个字节,这里为啥是32个字节?

 

对象

返回的对象使用ObjHeader来表示

 

所有的Kotlin对象,都有一个成员叫 typeInfoOrMeta_,指向自己的类对象 (类比OC中的isa指针)

 

小结

对象创建的流程大概如下:

● 编译器根据类的定义生成类信息(TypeInfo),并放在全局数据段(__DATA_CONST.__const)

● 代码中遇到类实例化,则将代码翻译为以下几步

● 从数据段取到类信息

● 调用 AllocInstance 分配内存,分配内存的大小 从 TypeInfo 中获取

● 并把前 8 个字节的内容填写为TypeInfo的地址

● 调用class的构造方法初始化成员变量

 

方法调用

初始化方法

会先调用父类的构造方法

 

父类设置name属性

 

子类设置name属性

 

 

greet方法

直接调用函数,并且把上面初始化的对象作为第一个参数传入

 

重载greetPrefix

查看函数,greetPrefix 有三个方法,除了父类与子类的实现个一个外,还有一个trampoline的辅助函数

打断点看到,第一个参数也是我们上面创建的对象实例

逻辑如下:

通过对象(读取前八个字节里的地址对应的内容)获取对应的类对象(TypeInfo)

类对象中有虚函数表,虚函数表里有函数地址,通过偏移读取到目标函数的地址,然后跳转到具体的函数

 

查看虚函数表

由于我们没有重载toString等方法的实现,所以虚函数表中对应的函数地址是父类的函数的地址。

 

 

与OC交互

重新将产物编译为 framework

kotlinc-native Hello.kt -g -Xno-inline -Xstatic-framework -p framework -o Hello

 

得到如下产物:

 

可以集成到 Xcode 中运行了

 

生成的头文件也都可以调用了

 

main函数

在二进制的符号表中搜索#main,发现多了一个包装函数

 

objc2kotlin_kfun:#main(){} 中会调用 kfun:#main(){}

 

objc2kotlin_kfun:#main(){} 函数不对应任何一句原代码,完全由编译器生成

objc2kotlin前缀表示 从objc调用kotlin的函数

 

kfun:#main(){} 的逻辑不变(创建对象,方法调用)

 

 

成员方法

查看二进制文件符号,发现每个成员方法也生成了对应的objc2kotlin前缀的函数,例如:

 

在KN编译代码中找到了"objc2kotlin"

编译器如何通过生成 objc2kotlin_ 前缀的方法?

 

objc2kotlin_xxx方法是在OC侧调用,所以其接受的参数以及返回值都是 OC 的对象

objc2kotlin_xxx 要"复用" xxx(KN kotlin 方法) 的逻辑

 

根据相关位置的代码以及编译结果看,大概的流程为以下三步:

 

1.  转换 OC对象 -> Kotlin对象(struct ObjHeader)

2.  调用kotlin函数,获得Kotlin对象的返回值

3.  将返回值转回 OC 对象(还有其他分支,暂不关心)

 

 

比如:

 

由于是init方法,返回值就是传入的self对象,所以不需要第三部转换,可以通过下面这个函数来看下返回值的情况

 

 

 

OC Kotlin 交互

Kotlin_ObjCExport_refFromObjC

查看源码可知是调用objc_msgSend,target是obj,SEL 是toKotlin:

 

 

任意一个对象能响应这个方法吗?

可以响应,runtime给NSObject添加了分类方法

 

当然对于 OC 的原生类有具体的处理:

 

还有一个特殊的KotlinBase的类,是 所有从Kotlin生成的OC的类的 基类

比如:HelloHello,是从Kotlin的Hello类生成的,其基类就是 KotlinBase

这个类有一个特殊的成员变量 refHolder

在toKotlin:中,返回值就是从refHolder中获取的,所以需要看下这个refHolder什么时候赋值的,其内容是什么?

 

情况1

● 94:分配内存,创建 OC 的实例对象(Oojb)

● 96:获取到HelloHello类对应的Kotlin Hello类的TypeInfo

○ OC的HelloHello 是 编译器根据 Hello 生成的,编译器跟runtime有办法获取到,具体获取方法暂不介绍

●  109:通过 Hello类的TypeInfo,创建一个Hello类的实例对象(Kobj),并把 Oobj 绑定到 Kobj 上

○ 绑定:通过Kobj可以轻松的(成本很低的)获取到Oobj

● 112:把 Kobj 放在 Oobj 的 refHolder 成员变量里

 

串一下这段代码后面发生的事情:

 

● alloc 过程:有两个分配内存的过程(创建了两个对象),OC 侧跟Kotlin侧个创建了一个对象,并且把两个对象绑定起来了(互相持有引用),通过其中任意一个对象,可以轻松获取到对侧的对象

● init 过程:给上述Alloc获取的对象发消息,SEL='initWithName',对应的实现是objc2kotlin_kfun:Hello#<init>(kotlin.String){}函数,函数中self对象跟NSString 通过 toKotlin: 转换为 Kotlin 侧的对象,并作为调用 kfun:Hello#<init>(kotlin.String){}函数的参数

 

问题

上述过程好像只是把Kobj初始化了,Oobj初始化成功了吗?

Oobj有成员变量吗?

 

output:

ivar count = 0; property count = 0; method count = 8;

 

其实HelloHello这个类没有实例变量,其父类有一个成员变量:refHolder

 

可以认为OC 侧的对象是 Kotlin侧对象的一个包装,自己不保存任何数据,对OC 侧做的操作(函数调用)都完全转发给Kotlin侧来实现

● OC函数是对Kotlin函数的包装

● OC对象是对Kotlin对象的包装

 

 

情况2

Kotlin_ObjCExport_refToRetainedObjC

● 462:获取Kobj绑定的Oobj

○ 之前绑定过,获取成功直接返回

● 下面的逻辑:需要走新建对象并绑定

○ 1. 通过TypeInfo获取到对应的OC class

○ 2. 调用 createRetainedWrapper 方法

● 分配内存,创建 Oobj

● 双向绑定

 

下面的情况属于这种情况,先创建Kotlin对象,然后创建OC对象

 

情况3

不是从Kotlin class 生成的OC 的类的实例对象转Kotlin对象

● MyObject 从Kotlin 的class生成的

● 如果有一个类集成了 'HelloHello',这个类也不是从Kotlin class 映射过来的

转Kotlin对象走Kotlin_ObjCExport_convertUnmappedObjCObject

动态生成一个TypeInfo,然后单向绑定,只有从Kotlin对象可以找到OC对象

 

动态生成一个TypeInfo里会根据父类,遵守的协议设置虚函数表(这些虚标是编译器提前为每个类型准备好的),对应的实现函数是 以kotlin2objc为前缀的这些函数:

 

这些函数是跟上面介绍的objc2kotlin的作用相反

● kotlin2objc函数 在Kotlin侧调用(绑定在TypeInfo的虚表中);objc2kotlin函数在OC侧调用,绑定在OC类对应的SEL上

● kotlin2objc 将参数转为OC对象,然后给OC对象发消息,获取返回值转为Kotlin对象

● objc2kotlin 将参数转为Kotlin对象,然后调用Kotlin的函数,获取返回值转为OC对象

 

 

 

背后的过程是

● OC_37

○ 创建 MyObject 的实例对象 O1

○ 调用 h1 的 setHelloable 的方法 参数是 h1(self) SEL O1 函数对应的实现是 objc2kotlin_kfun:Hello#<set-helloable>(HelloAble?){}

○ objc2kotlin_kfun:Hello#<set-helloable>(HelloAble?){} 将 h1 跟 O1 转为 对应的 Kotlin 对象 Kh1 KO1

○ h1 -> Kh1 的转换读取refHolder即可,因为 h1 是通过 createRetainedWrapper 方法创建的,创建的时候已经绑定了kotlin对象

○ O1 -> KO1 的过程: 通过 Kotlin_ObjCExport_convertUnmappedObjCObject实现,过程中会动态创建 TypeInfo,通过 O1 遵守的HelloHelloAble协议(OC的runtime可以获取到) 找到Kotlin侧 HelloAble 协议对应的虚表,并设置到动态创建的TypeInfo上,然后通过创建的TypeInfo创建一个 KO1 对象,

○ 然后将 KO1 设置到 Kh1 对应的成员变量上

● OC_38

○ 调用 h1 的 triggleInterface 方法,方法的实现是 objc2kotlin_kfun:Hello#triggleInterface(){}

○ 获取 h1 对应的 Kh1

○ 调用 kotlin 侧的 triggleInterface 方法

● Kotlin_18

○ 读取 Kh1 的成员变量helloable,读取到的对象是 KO1

○ 调用 KO1 makeIt 方法 kfun:HelloAble#makeIt(){}kotlin.String-trampoline -trampoline 后缀的方法是需要通过 TypeInfo 读虚表(此TypeInfo是上面动态创建的,创建的时候设置好了虚表)来确定调用最终的函数为kotlin2objc_kfun:HelloAble#makeIt(){}kotlin.String

○ kotlin2objc_kfun:HelloAble#makeIt(){}kotlin.String 中会把 KO1 转为 O1 对象,调用 O1 对象的 makeIt 方法,通过msgSend 实现,获取到返回值通过toKotlin: 方法转为Kotlin侧的对象

 

总结

● KN 处理 Kotlin 的逻辑整体跟C++的方式比较类似

● refHolder、toKotlin:、objc2kotlin_xx、kotlin2objc_xx 实现了与OC的交互

 

 

附录

objc2kotlin_xx 与 kotlin2objc_xx 方法如何绑定

● 每个class 每个 interface 编译器都会未其生成 TypeInfo

● 如果需要跟OC交互,TypeInfo中有个变量

● objc2kotlin_xx 与 kotlin2objc_xx 就存在这些列表里,runtime在运行时会根据类型匹配,动态绑定这些函数入口

● 在OC类的initialize方法中,会给OC类添加(通过 OC 的 runtime) directAdapters,classAdapters,implementedInterfaces_

● 在动态创建TypeInfo 的过程中,则会使用到kotlinVtable,kotlinItable,reverseAdapters

5.A.swift 使用指南

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

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


Aquarius 是一个为 Swift 开发者打造的高效、轻量级开发框架,致力于提供简洁统一的 API 设计,帮助开发者快速构建高质量的 iOS 应用。本文将重点介绍其核心工具集 A.swift 的使用方法。

Aquarius 框架中的 A.swift(简称 A)是一个功能强大的工具箱,它将常用的开发功能封装为静态方法,覆盖了 UI 构建、颜色图片、数据存储、文件操作、事件管理、日志、内购等高频场景。使用 A,开发者可以告别繁琐的底层调用,大幅提升开发效率与代码整洁度。

一、A.swift 是什么?

A 是 Aquarius 框架中的核心便捷层,以命名空间(Namespace)的形式组织代码,提供了一系列静态属性和方法,让开发者能够通过类似 A.uiA.colorA.file 这样的语法快速调用功能模块。

其主要优势包括:

  • 统一入口:所有功能通过 A 访问,降低记忆成本
  • 类型安全:多数接口设计为类型安全或可选值,避免隐性崩溃
  • 功能完备:涵盖 UI、主题、存储、系统交互、日志、支付等常用场景
  • 现代并发支持:如 IAP 模块使用 async/await 封装,适配 Swift 并发编程

二、核心模块概览

下面我们简要介绍 A 中常用的子模块及其典型用途:

模块名 功能说明
A.ui 快速创建常用 UI 控件
A.color / A.image 主题色、系统图标与图片工具
A.userDefaults(_:) UserDefaults 便捷封装,支持 App Group
A.file 文件路径、目录与文件操作
A.calendarEvent 日历事件管理(基于 EventKit)
A.log 分级日志输出,支持 emoji 标识
A.iap 内购流程封装,基于 StoreKit 现代 API

三、实战演示:感受编码效率的飞跃

3.1 快速创建并添加按钮

传统方式

let button = UIButton(type: .system)
button.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)

使用A.swift

let button = A.ui.button
button.addTouchUpInsideBlock { [weak self] control in
    ...
}
let label = A.ui.label
let imageView = A.ui.imageView
let tableView = A.ui.tableView
...

告别不同UI控件创建方式的不同,统一UI控件创建方式。

3.2 使用主题色与系统图标

view.backgroundColor = A.color.blackColor
imageView.image = A.image.systemImage(systemName: "star.fill")

统一管理颜色与图标,轻松适配暗黑模式与主题切换。

3.3 读写 UserDefaults

//写入
A.userDefaults("group.com.jzx.app").forKey("username")
A.userDefaults("group.com.jzx.app").setValue("张三", forKey: "username")
//读取
let name: String? = A.userDefaults("group.com.jzx.app").getStringValue("username")

支持 App Group,并提供类型安全的读取接口。

3.4 文件操作

let path = A.file.pathFromDocuments("data/user.json")
if !A.file.isExist(path) {
    try? A.file.createFolder(at: "data")
}
// 写入文件...

封装常用文件操作方法,提升代码可读性。

3.5 创建日历事件

A.calendarEvent.add(title: "发布会", startDate: start, endDate: end) { result in
    switch result {
    case .success(let id): A.log.info("创建成功:\(id)")
    case .failure(let err): A.log.error("创建失败:\(err)")
    }
}

自动处理权限申请与事件添加,回调清晰。

3.6 分级日志

A.log.debug("用户点击按钮")
A.log.warning("网络请求超时")
A.log.error("解析失败:\(error)")

日志自带 emoji 和等级标识,调试更直观。

3.7 发起内购

Task {
    do {
        let products = try await A.iap.fetchProducts(["com.jzx.pro"])
        if let product = products.first {
            try await A.iap.purchase(product: product)
            A.log.info("购买成功")
        }
    } catch {
        A.log.error("购买失败:\(error)")
    }
}

基于现代 StoreKit API,支持 async/await,逻辑清晰。

四、最佳实践与注意事项

4.1 错误处理要到位

A 中多数可能出错的操作都会通过 Resultthrows 或可选值来表示失败,请务必处理这些情况,避免直接使用 try! 或强制解包。

4.2 权限管理不能忘

如使用 A.calendarEventA.reminderEvent,请确保已在 Info.plist 中添加相应权限说明,并在使用前检查授权状态。

4.3 线程安全需注意

涉及 UI 更新的操作请确保在主线程执行。A.iap 等异步方法已自动处理线程切换,但仍建议使用 MainActorDispatchQueue.main 更新界面。

4.4 结合 MVVM 架构

你可以在 ViewModel 中直接使用 A.fileA.userDefaultsA.iap 等模块,将平台相关代码与 UI 逻辑分离:

class SettingViewModel {
    func clearCache() {
        let cachePath = A.file.pathFromCaches("")
        try? A.file.removeItem(cachePath)
    }
}

五、总结

A.swift 作为 Aquarius 框架中的"瑞士军刀",极大地简化了 iOS 开发中常见的任务流程。无论是创建界面、管理数据、记录日志,还是处理内购和系统事件,A 都提供了简洁而强大的接口。

如果你正在寻找一个能提升开发效率、减少样板代码的 Swift 工具集,不妨试试 Aquarius 框架中的 A.swift


立即体验Aquarius:

第一步:探索资源

第二步:体验效果

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

第三步:沟通交流

🧩 iOS DiffableDataSource 死锁问题记录

本文提到的问题是实际项目中遇到的,但文章内容由ChatGPT完成,人工进行了review

🪪 错误信息

在使用 UITableViewDiffableDataSource / UICollectionViewDiffableDataSource 时,
调用 apply(_:animatingDifferences:completion:) 方法更新数据时出现了如下崩溃错误

Deadlock detected: calling this method on the main queue with outstanding async updates is not permitted and will deadlock. Please always submit updates either always on the main queue or always off the main queue

⚙️ 问题本质

  • 由于 apply() 本身是 异步执行 diff 计算与 UI 更新 的,如果在前一次 apply() 尚未完成时又调用了新的 apply(),UIKit 就会检测到潜在死锁并抛出上述错误。
  • “outstanding async updates” 表示仍在进行中的异步更新。 即上一次 diff 操作尚未完成,又发起了新的一次 diff

需要注意的是,虽然崩溃信息中提示是线程问题,但根据实际测试,即使所有调用都在主线程执行,也仍然可能发生此错误,因为 UIKit 内部的 diff 计算与视图更新是异步的。

错误代码以及错误原因如下所示:

import UIKit

class MyViewController: UIViewController {
    enum Section { case main }
    struct Item: Hashable { let id = UUID(); let title: String }

    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
    private var items: [Item] = [.init(title: "A")]

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionViewFlowLayout())
        view.addSubview(collectionView)
        collectionView.register(MyCell.self, forCellWithReuseIdentifier: "cell")

        dataSource = UICollectionViewDiffableDataSource<Section, Item>(
            collectionView: collectionView
        ) { [weak self] collectionView, indexPath, item in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCell
            cell.label.text = item.title

            // 第二次通过apply刷新列表,❌ 错误:cell 回调中再次触发 apply
            cell.onTap = {
                guard let self = self else { return }
                self.items.append(.init(title: "New"))
                self.applySnapshot() // 上一次 apply 未完成时调用,可能触发错误
            }
            return cell
        }
        // 第一次通过apply刷新列表
        applySnapshot()
    }

    private func applySnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}


🧭 解决思路

✅ 1. 防止重入(推荐)

使用标志位,确保同一时间只执行一次 apply(),并缓存后续请求:

private var isApplyingSnapshot = false
private var pendingSnapshot: NSDiffableDataSourceSnapshot<Section, Item>?

func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>) {
    guard !isApplyingSnapshot else {
        pendingSnapshot = snapshot
        return
    }
    isApplyingSnapshot = true

    dataSource.apply(snapshot, animatingDifferences: true) { [weak self] in
        guard let self = self else { return }
        self.isApplyingSnapshot = false
        if let next = self.pendingSnapshot {
            self.pendingSnapshot = nil
            self.applySnapshot(next)
        }
    }
}

并可以根据该思想封装一个防止重入的类

import UIKit

/// 一个安全的 DiffableDataSource 快照更新管理器
/// 支持自动排队多次 apply,防止死锁与丢帧
final class SafeDiffableApplier<Section: Hashable, Item: Hashable> {
    private let dataSource: UITableViewDiffableDataSource<Section, Item>
    private var isApplying = false
    private var queue: [QueuedSnapshot] = []

    private struct QueuedSnapshot {
        let snapshot: NSDiffableDataSourceSnapshot<Section, Item>
        let animatingDifferences: Bool
        let completion: (() -> Void)?
    }

    init(dataSource: UITableViewDiffableDataSource<Section, Item>) {
        self.dataSource = dataSource
    }

    /// 安全地应用快照(自动排队,避免死锁)
    func apply(
        _ snapshot: NSDiffableDataSourceSnapshot<Section, Item>,
        animatingDifferences: Bool = true,
        completion: (() -> Void)? = nil
    ) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            let task = QueuedSnapshot(snapshot: snapshot, animatingDifferences: animatingDifferences, completion: completion)
            self.queue.append(task)
            self.processNextIfNeeded()
        }
    }

    /// 按顺序依次执行队列中的快照更新
    private func processNextIfNeeded() {
        guard !isApplying, !queue.isEmpty else { return }
        isApplying = true

        let next = queue.removeFirst()
        dataSource.apply(next.snapshot, animatingDifferences: next.animatingDifferences) { [weak self] in
            guard let self = self else { return }
            next.completion?()
            self.isApplying = false
            self.processNextIfNeeded() // 递归继续下一个
        }
    }
}

✅ 2. 合并或节流更新

如果更新非常频繁,可以合并多次变化后再统一 apply()

func scheduleSnapshotUpdate() {
    pendingWorkItem?.cancel()
    let workItem = DispatchWorkItem { [weak self] in
        guard let self = self else { return }
        let snapshot = self.generateSnapshot()
        self.dataSource.apply(snapshot, animatingDifferences: true)
    }
    pendingWorkItem = workItem
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem)
}

🧠 总结

  • apply()异步 的,不可重复调用。
  • 错误提示的 “outstanding async updates” 即代表上一次 diff 尚未完成。
  • 必须串行化更新操作,或合并多次更新。
  • 仅仅在主线程调度(DispatchQueue.main.async) 并不能根本解决问题。

参考


《Flutter全栈开发实战指南:从零到高级》- 06 -常用布局组件

Flutter常用布局

1. 引言:为什么布局系统如此重要?

比方说你要装修一间房子:你需要规划每个房间的位置、大小,考虑家具的摆放,确保空间利用合理且美观。Flutter的布局系统就是你在数字世界中的"室内设计师",它决定了每个UI元素的位置、大小和相互关系。

一个好的布局应该具备:

  • 精确的元素定位
  • 兼容自适应屏幕
  • 渲染性能高效
  • 视觉层次美观

今天就带你详细介绍Flutter的常用布局,让你熟练掌握布局系统~~~

2. Container:万能的布局容器

它是最基础也是最强大的布局组件之一。 71bbb7f5f8f06c5f6fe93e939eee55b8.png

2.1 Container的基本用法

Container(
  width: 200,                    // 设置宽度
  height: 100,                   // 设置高度
  color: Colors.blue,            // 背景颜色
  child: Text('Hello Flutter'),  // 子组件
)

这就像给文字套上了一个蓝色的相框,简单直接。

2.2 Container的装饰功能

但Container的真正威力在于它的装饰能力:

Container(
  width: 200,
  height: 100,
  decoration: BoxDecoration(
    color: Colors.white,                     // 背景色
    borderRadius: BorderRadius.circular(16), // 圆角
    boxShadow: [                             // 阴影
      BoxShadow(
        color: Colors.black12,
        blurRadius: 10,
        offset: Offset(0, 4),
      ),
    ],
    border: Border.all(                    // 边框
      color: Colors.blue,
      width: 2,
    ),
    gradient: LinearGradient(              // 渐变背景
      colors: [Colors.blue, Colors.purple],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    ),
  ),
  child: Center(
    child: Text(
      '精美的容器',
      style: TextStyle(
        color: Colors.white,
        fontWeight: FontWeight.bold,
      ),
    ),
  ),
)

Container的核心属性:

  • width / height:控制尺寸
  • margin:外边距,与其他组件的距离
  • padding:内边距,内容与边框的距离
  • decoration:装饰效果(颜色、边框、阴影等)
  • constraints:尺寸约束

2.3 实际应用场景

下面以具体的实际开发场景为例,带大家深入了解Container组件

场景1:用户头像容器

Container(
  width: 80,
  height: 80,
  decoration: BoxDecoration(
    color: Colors.grey[200],
    borderRadius: BorderRadius.circular(40), // 圆形
    border: Border.all(color: Colors.blue, width: 2),
    image: DecorationImage(
      image: NetworkImage('https://example.com/avatar.jpg'),
      fit: BoxFit.cover,
    ),
  ),
)

场景2:消息气泡

Container(
  constraints: BoxConstraints(
    maxWidth: 250,  // 最大宽度限制
  ),
  padding: EdgeInsets.all(12),
  decoration: BoxDecoration(
    color: Colors.blue[50],
    borderRadius: BorderRadius.only(
      topLeft: Radius.circular(16),
      topRight: Radius.circular(16),
      bottomRight: Radius.circular(4),
    ),
  ),
  child: Text('这是一条消息内容'),
)

3. Padding和Margin

Padding和Margin就像人与人之间的安全距离,它们控制着组件之间的空间关系,但作用对象不同。

3.1 Padding:内部空间

Padding是组件内容与边框之间的距离,好比相框与照片之间的留白:

Container(
  color: Colors.blue,
  child: Padding(
    padding: EdgeInsets.all(16),  // 四周都留16像素的空白
    child: Text(
      '有呼吸空间的文字',
      style: TextStyle(color: Colors.white),
    ),
  ),
)

EdgeInsets的四种用法:

// 1. 统一间距
EdgeInsets.all(16)

// 2. 分别设置上下左右
EdgeInsets.fromLTRB(10, 20, 10, 20)

// 3. 设置水平和垂直
EdgeInsets.symmetric(horizontal: 10, vertical: 20)

// 4. 只设置一边
EdgeInsets.only(left: 10, top: 5)

3.2 Margin:外部安全距离

Margin是组件与其他组件之间的距离,就像两个人谈话时的舒适距离:

Container(
  width: 100,
  height: 100,
  color: Colors.red,
  margin: EdgeInsets.all(20),  // 四周都保持20像素的距离
  child: Text('我有外边距'),
)

3.3 实际应用:卡片布局

Container(
  margin: EdgeInsets.all(16),      // 卡片与其他组件的距离
  padding: EdgeInsets.all(20),     // 卡片内容与边框的距离
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [
      BoxShadow(
        color: Colors.black12,
        blurRadius: 8,
      ),
    ],
  ),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        '产品标题',
        style: TextStyle(
          fontSize: 18,
          fontWeight: FontWeight.bold,
        ),
      ),
      SizedBox(height: 8),         // 文字之间的间距
      Text('产品描述信息...'),
    ],
  ),
)

4. Row和Column:线性布局

Row是横向布局,Column是竖向布局。它们让组件按照线性方式排列,是使用频率最高的布局组件。

4.1 Row:水平排列

Row让子组件水平排列,就像我们生活中排队买票的人群: 97750a37af503adc2ed73b58323a389f.png

Row(
  children: [
    Icon(Icons.star, color: Colors.orange),
    Icon(Icons.star, color: Colors.orange),
    Icon(Icons.star, color: Colors.orange),
    Icon(Icons.star_border, color: Colors.grey),
    Icon(Icons.star_border, color: Colors.grey),
  ],
)

Row的核心属性:

  • mainAxisAlignment:主轴对齐方式(水平方向)
  • crossAxisAlignment:交叉轴对齐方式(垂直方向)
  • mainAxisSize:主轴尺寸

4.2 主轴对齐方式

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween, // 两端对齐,均匀分布
  children: [
    Container(width: 50, height: 50, color: Colors.red),
    Container(width: 50, height: 50, color: Colors.green),
    Container(width: 50, height: 50, color: Colors.blue),
  ],
)

MainAxisAlignment的选项:

  • start:左对齐
  • end:右对齐
  • center:居中对齐
  • spaceBetween:两端对齐,组件间隔相等
  • spaceAround:每个组件两侧间隔相等
  • spaceEvenly:组件间隔与边框间隔都相等

4.3 Column:垂直排列

Column让子组件垂直排列,就像叠放的一摞书籍: f397e34988953bbbdb8ad0d9a937f65d.png

Column(
  crossAxisAlignment: CrossAxisAlignment.start, // 左对齐
  children: [
    Text('标题', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
    SizedBox(height: 8),
    Text('副标题', style: TextStyle(fontSize: 16, color: Colors.grey)),
    SizedBox(height: 16),
    Text('内容描述...'),
  ],
)

4.4 实际应用:用户信息卡片

Row(
  children: [
    // 头像
    Container(
      width: 60,
      height: 60,
      decoration: BoxDecoration(
        color: Colors.grey[300],
        borderRadius: BorderRadius.circular(30),
      ),
      child: Icon(Icons.person, size: 30, color: Colors.grey[600]),
    ),
    
    // 间距
    SizedBox(width: 16),
    
    // 用户信息
    Expanded(  // 占据剩余空间
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('张小明', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          SizedBox(height: 4),
          Text('高级用户体验设计师', style: TextStyle(color: Colors.grey[600])),
          SizedBox(height: 4),
          Text('2小时前在线', style: TextStyle(color: Colors.green, fontSize: 12)),
        ],
      ),
    ),
    
    // 右侧图标
    Icon(Icons.chevron_right, color: Colors.grey[400]),
  ],
)

5. Flex和Expanded:弹性布局

举个例子:Flex和Expanded就像弹簧和橡皮筋,它们让布局具有弹性,能够根据可用空间自动调整。

5.1 Flex布局基础

Flex是Row和Column的父类,提供了更灵活的布局方式: f6851a8990489db85a134b5058f86981.png

Flex(
  direction: Axis.horizontal,  // 水平排列,相当于Row
  children: [
    // 子组件
  ],
)

5.2 Expanded:占据剩余空间

Expanded让子组件占据剩余空间,就像弹簧可以拉伸:

Row(
  children: [
    Container(
      width: 80,
      height: 50,
      color: Colors.red,
    ),
    Expanded(  // 占据剩余的所有水平空间
      child: Container(
        height: 50,
        color: Colors.blue,
        child: Center(child: Text('弹性区域')),
      ),
    ),
  ],
)

5.3 Flexible:尺寸控制

Flexible提供更精细的弹性控制:

Row(
  children: [
    Flexible(
      flex: 1,  // 权重为1
      child: Container(height: 50, color: Colors.red),
    ),
    Flexible(
      flex: 2,  // 权重为2,占据两倍的空间
      child: Container(height: 50, color: Colors.green),
    ),
    Flexible(
      flex: 1,  // 权重为1
      child: Container(height: 50, color: Colors.blue),
    ),
  ],
)

Flexible vs Expanded:

  • Expanded是Flexible(fit: FlexFit.tight)的简写
  • Flexible默认是FlexFit.loose,子组件可以选择不填满空间
  • Expanded强制子组件填满空间

5.4 实际应用:比例布局

Column(
  children: [
    // 标题栏
    Container(
      height: 60,
      color: Colors.blue,
      child: Center(child: Text('仪表盘', style: TextStyle(color: Colors.white))),
    ),
    
    // 内容区域(占据剩余空间)
    Expanded(
      child: Row(
        children: [
          // 侧边栏(固定宽度)
          Container(
            width: 200,
            color: Colors.grey[100],
            child: ListView(
              children: [
                ListTile(title: Text('菜单项1')),
                ListTile(title: Text('菜单项2')),
                ListTile(title: Text('菜单项3')),
              ],
            ),
          ),
          
          // 主内容区(占据剩余空间)
          Expanded(
            child: Container(
              color: Colors.white,
              child: Center(child: Text('主内容区域')),
            ),
          ),
        ],
      ),
    ),
    
    // 底部栏
    Container(
      height: 50,
      color: Colors.grey[800],
      child: Center(child: Text('版权所有 © 2024', style: TextStyle(color: Colors.white))),
    ),
  ],
)

6. Stack:层叠布局

Stack好比透明的幻灯片,可以让多个组件重叠在一起,组合出丰富的页面视觉效果。 f4cc635cca69f396ef656e89d5e5a1d0.png

6.1 Stack基础用法

Stack(
  children: [
    // 底层背景
    Container(
      width: 200,
      height: 200,
      color: Colors.blue,
    ),
    
    // 中间层
    Positioned(
      top: 20,
      left: 20,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.red,
      ),
    ),
    
    // 顶层
    Positioned(
      bottom: 20,
      right: 20,
      child: Container(
        width: 80,
        height: 80,
        color: Colors.green,
      ),
    ),
  ],
)

6.2 Positioned:精确定位

Positioned用于在Stack中精确定位子组件:

Positioned(
  top: 10,      // 距离顶部10像素
  left: 20,     // 距离左边20像素
  right: 30,    // 距离右边30像素
  bottom: 40,   // 距离底部40像素
  child: Container(color: Colors.orange),
)

6.3 Alignment:相对定位

除了Positioned,还可以使用Alignment进行相对定位:

Stack(
  alignment: Alignment.center,  // 所有子组件默认居中对齐
  children: [
    Container(width: 200, height: 200, color: Colors.blue),
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Align(  // 单独设置对齐方式
      alignment: Alignment.bottomRight,
      child: Container(
        width: 50,
        height: 50,
        color: Colors.green,
      ),
    ),
  ],
)

6.4 实际应用:用户头像徽章

Stack(
  children: [
    // 用户头像
    Container(
      width: 80,
      height: 80,
      decoration: BoxDecoration(
        color: Colors.grey[300],
        borderRadius: BorderRadius.circular(40),
      ),
      child: Icon(Icons.person, size: 40, color: Colors.grey[600]),
    ),
    
    // 在线状态指示器
    Positioned(
      bottom: 0,
      right: 0,
      child: Container(
        width: 20,
        height: 20,
        decoration: BoxDecoration(
          color: Colors.green,
          borderRadius: BorderRadius.circular(10),
          border: Border.all(color: Colors.white, width: 2),
        ),
      ),
    ),
    
    // VIP徽章
    Positioned(
      top: 0,
      right: 0,
      child: Container(
        padding: EdgeInsets.all(4),
        decoration: BoxDecoration(
          color: Colors.orange,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Text(
          'VIP',
          style: TextStyle(
            color: Colors.white,
            fontSize: 10,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    ),
  ],
)

7. 响应式布局设计:适配各种屏幕

响应式布局能够根据不同的屏幕尺寸自动调整布局,提升用户体验。

7.1 MediaQuery:获取屏幕信息

MediaQuery可以获取屏幕尺寸、方向等信息:

class ResponsiveLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 获取屏幕尺寸
    final screenWidth = MediaQuery.of(context).size.width;
    final screenHeight = MediaQuery.of(context).size.height;
    
    // 判断屏幕方向
    final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;
    
    return Container(
      width: screenWidth,
      height: screenHeight,
      color: Colors.grey[200],
      child: Center(
        child: Text(
          '屏幕尺寸: ${screenWidth.toInt()} × ${screenHeight.toInt()}\n'
          '方向: ${isPortrait ? '竖屏' : '横屏'}',
          textAlign: TextAlign.center,
        ),
      ),
    );
  }
}

7.2 LayoutBuilder:根据约束调整布局

LayoutBuilder可以根据父组件的约束动态调整布局:

LayoutBuilder(
  builder: (context, constraints) {
    // 根据可用宽度决定布局方式
    if (constraints.maxWidth > 600) {
      // 宽屏布局
      return Row(
        children: [
          Container(width: 200, color: Colors.blue, child: Text('侧边栏')),
          Expanded(child: Container(color: Colors.green, child: Text('主内容'))),
        ],
      );
    } else {
      // 窄屏布局
      return Column(
        children: [
          Container(height: 100, color: Colors.blue, child: Text('顶部导航')),
          Expanded(child: Container(color: Colors.green, child: Text('主内容'))),
        ],
      );
    }
  },
)

7.3 实际应用:响应式仪表盘

class ResponsiveDashboard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('响应式仪表盘')),
      body: LayoutBuilder(
        builder: (context, constraints) {
          final isWideScreen = constraints.maxWidth > 768;
          
          return Row(
            children: [
              // 侧边栏(在宽屏显示,窄屏隐藏)
              if (isWideScreen)
                Container(
                  width: 250,
                  color: Colors.grey[100],
                  child: ListView(
                    children: [
                      ListTile(title: Text('仪表盘')),
                      ListTile(title: Text('用户管理')),
                      ListTile(title: Text('数据分析')),
                      ListTile(title: Text('系统设置')),
                    ],
                  ),
                ),
              
              // 主内容区域
              Expanded(
                child: Container(
                  padding: EdgeInsets.all(16),
                  child: GridView.count(
                    // 根据屏幕宽度调整列数
                    crossAxisCount: isWideScreen ? 3 : 2,
                    crossAxisSpacing: 16,
                    mainAxisSpacing: 16,
                    children: [
                      _buildStatCard('用户数', '1,234', Colors.blue),
                      _buildStatCard('订单数', '567', Colors.green),
                      _buildStatCard('收入', '\$8,901', Colors.orange),
                      _buildStatCard('增长率', '12.3%', Colors.purple),
                      _buildStatCard('满意度', '98%', Colors.red),
                      _buildStatCard('活跃度', '87%', Colors.teal),
                    ],
                  ),
                ),
              ),
            ],
          );
        },
      ),
      
      // 窄屏时显示底部导航
      bottomNavigationBar: constraints.maxWidth <= 768 ? BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.dashboard), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.people), label: '用户'),
          BottomNavigationBarItem(icon: Icon(Icons.settings), label: '设置'),
        ],
      ) : null,
    );
  }
  
  Widget _buildStatCard(String title, String value, Color color) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
        boxShadow: [
          BoxShadow(
            color: Colors.black12,
            blurRadius: 6,
            offset: Offset(0, 2),
          ),
        ],
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            value,
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
              color: color,
            ),
          ),
          SizedBox(height: 8),
          Text(
            title,
            style: TextStyle(
              color: Colors.grey[600],
            ),
          ),
        ],
      ),
    );
  }
}

8. 实战案例:用户资料卡片页面

让我们把所有知识融合起来,创建一个完整的用户资料卡片:

class UserProfileCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(16),
      padding: EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black12,
            blurRadius: 10,
            offset: Offset(0, 4),
          ),
        ],
      ),
      child: Column(
        children: [
          // 头部:头像和基本信息
          Row(
            children: [
              // 头像区域(带徽章)
              Stack(
                children: [
                  Container(
                    width: 80,
                    height: 80,
                    decoration: BoxDecoration(
                      color: Colors.blue[100],
                      borderRadius: BorderRadius.circular(40),
                      image: DecorationImage(
                        image: NetworkImage('https://example.com/avatar.jpg'),
                        fit: BoxFit.cover,
                      ),
                    ),
                  ),
                  // 在线状态
                  Positioned(
                    bottom: 0,
                    right: 0,
                    child: Container(
                      width: 20,
                      height: 20,
                      decoration: BoxDecoration(
                        color: Colors.green,
                        borderRadius: BorderRadius.circular(10),
                        border: Border.all(color: Colors.white, width: 2),
                      ),
                    ),
                  ),
                ],
              ),
              
              // 间距
              SizedBox(width: 16),
              
              // 用户信息
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      '张小明',
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    SizedBox(height: 4),
                    Text(
                      '高级用户体验设计师',
                      style: TextStyle(
                        color: Colors.grey[600],
                      ),
                    ),
                    SizedBox(height: 8),
                    Row(
                      children: [
                        Icon(Icons.location_on, size: 16, color: Colors.grey),
                        SizedBox(width: 4),
                        Text(
                          '北京市海淀区',
                          style: TextStyle(fontSize: 12, color: Colors.grey),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
              
              // 更多操作按钮
              IconButton(
                icon: Icon(Icons.more_vert, color: Colors.grey),
                onPressed: () {},
              ),
            ],
          ),
          
          // 分隔线
          Padding(
            padding: EdgeInsets.symmetric(vertical: 16),
            child: Divider(height: 1, color: Colors.grey[300]),
          ),
          
          // 统计信息
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _buildStatItem('关注', '234'),
              _buildStatItem('粉丝', '1.2k'),
              _buildStatItem('作品', '56'),
              _buildStatItem('点赞', '3.4k'),
            ],
          ),
          
          // 分隔线
          Padding(
            padding: EdgeInsets.symmetric(vertical: 16),
            child: Divider(height: 1, color: Colors.grey[300]),
          ),
          
          // 操作按钮
          Row(
            children: [
              Expanded(
                child: ElevatedButton(
                  onPressed: () {},
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.blue,
                    foregroundColor: Colors.white,
                  ),
                  child: Text('关注'),
                ),
              ),
              SizedBox(width: 12),
              Expanded(
                child: OutlinedButton(
                  onPressed: () {},
                  child: Text('发消息'),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
  
  Widget _buildStatItem(String label, String value) {
    return Column(
      children: [
        Text(
          value,
          style: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
        ),
        SizedBox(height: 4),
        Text(
          label,
          style: TextStyle(
            fontSize: 12,
            color: Colors.grey[600],
          ),
        ),
      ],
    );
  }
}

9. 性能优化与最佳实践

9.1 布局性能优化

  1. 避免过度嵌套

    // ❌ 不好的做法:过度嵌套
    Container(
      child: Padding(
        padding: EdgeInsets.all(10),
        child: Container(
          child: Center(
            child: Text('Hello'),
          ),
        ),
      ),
    )
    
    // ✅ 好的做法:使用Container的padding属性
    Container(
      padding: EdgeInsets.all(10),
      child: Center(
        child: Text('Hello'),
      ),
    )
    
  2. 使用const构造函数

    // ✅ 好的做法:使用const
    const Text('静态文本')
    
    // ❌ 不好的做法:不使用const
    Text('静态文本')
    

9.2 代码优化

  1. 提取重复布局

    // 提取为独立组件
    Widget _buildListItem(String title, String subtitle) {
      return ListTile(
        title: Text(title),
        subtitle: Text(subtitle),
        trailing: Icon(Icons.chevron_right),
      );
    }
    
  2. 使用扩展方法

    extension PaddingExtension on Widget {
      Widget withPadding(EdgeInsets padding) {
        return Padding(padding: padding, child: this);
      }
    }
    
    // 使用
    Text('Hello').withPadding(EdgeInsets.all(16))
    

当然还有很多其他优化的点,这里就不一一介绍了,需要大家花时间去一步步摸索尝试~

10. 知识点总结

1e81305d8cf662284c242d04e69e3050.png

通过今天的学习,我们掌握了Flutter布局系统的核心概念:

1. 基础容器:

  • Container:万能的布局容器,支持装饰效果
  • Padding:控制内部间距
  • Margin:控制外部间距

2. 线性布局:

  • Row:水平排列组件
  • Column:垂直排列组件
  • Flex / Expanded:弹性布局,按比例分配空间

3. 层叠布局:

  • Stack:组件重叠布局
  • Positioned:在Stack中精确定位
  • Align:相对对齐定位

4. 响应式设计:

  • MediaQuery:获取屏幕信息
  • LayoutBuilder:根据约束动态布局
  • 断点设计和方向适配

重点:布局设计思维

  1. 从外到内:先确定整体结构,再细化内部组件
  2. 优先使用简单布局:能用Row/Column解决的问题不要用复杂布局
  3. 考虑扩展性:设计时要考虑不同屏幕尺寸和内容变化
  4. 性能意识:避免过度嵌套,合理使用const

写在最后的话

好的布局就像好的建筑,不仅要美观,更要实用和稳固。

布局设计是一个需要不断练习和实践的过程。多观察优秀的App界面,思考它们的布局方式,然后用自己的代码实现出来。很快你就会发现,面对任何UI设计稿,你都能轻松地用Flutter实现出来!

如果这篇教程对你有帮助,请给我点个赞 👍 支持一下! 有什么布局方面的疑问?欢迎在评论区留言讨论~Happy Coding! ✨

Swift 方法全解:实例方法、mutating 方法与类型方法一本通

前言

官方文档已经把语法和规则写得足够严谨,但初学者常遇到三个卡点:

  1. 结构体/枚举居然也能定义方法?
  2. mutating 到底“变异”了什么?
  3. static 与 class 关键字在类型方法里的区别与实战意义。

方法(Method)到底是什么

一句话:方法是“挂在某个类型上的函数”。

  • 在 Swift 里,类(class)、结构体(struct)、枚举(enum)都能挂函数,有两大类,分别叫做实例方法或类型方法。

  • 与 C/Objective-C 不同,C 只有函数指针,Objective-C 只有类能定义方法;Swift 把“方法”能力下放到了值类型,带来了更灵活的建模方式。

实例方法(Instance Method)

  1. 定义与调用
class Counter1 {
    var count = 0
    
    // 实例方法:默认访问全部实例成员
    func increment() {
        count += 1
    }
    
    // 带参数的方法
    func increment(by amount: Int) {
        count += amount
    }
    
    func reset() {
        count = 0
    }
}

// 调用
let counter = Counter1()
counter.increment()          // 1
print(counter.count)
counter.increment(by: 5)     // 6
print(counter.count)
counter.reset()              // 0
print(counter.count)
  1. self 的隐式与显式
  • 不写 self:编译器默认你访问的是“当前实例”成员。
  • 必须写 self:局部变量/参数与属性重名时,用来消歧。
struct Point {
    var x = 0.0, y = 0.0
    
    func isToTheRightOf(x: Double) -> Bool {
        // 如果省略 self,x 会被当成参数 x
        return self.x > x
    }
}

值类型内部修改自身:mutating 实例方法

  1. 默认禁止修改

结构体/枚举是值类型,实例方法里不能改自己属性——除非加 mutating

  1. 加 mutating 后发生了什么
  • 方法被标记为“会改本体”,编译器会把调用处生成的 let 常量拦截掉。
  • 底层实现:方法拿到的是 inout self,可以整体替换。
struct Point: CustomStringConvertible {
    var description: String {
        "{ x: \(x), y: \(y)}"
    }
    
    var x = 0.0, y = 0.0
    
    // 移动自身
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
    
    // 更激进的写法:直接给 self 赋新实例
    mutating func teleport(toX x: Double, toY y: Double) {
        self = Point(x: x, y: y)
    }
}

var p = Point(x: 1, y: 1)
print(p)
p.moveBy(x: 2, y: 3)   // 现在 (3,4)
print(p)
p.teleport(toX: 0, toY: 0) // 整体替换为 (0,0)
print(p)

// 以下会编译错误
let fixedPoint = Point(x: 3, y: 3)
print(fixedPoint)
// fixedPoint.moveBy(x: 1, y: 1) // ❌
  1. 枚举也能 mutating
enum TriStateSwitch {
    case off, low, high
    
    mutating func next() {
        switch self {
        case .off: self = .low
        case .low: self = .high
        case .high: self = .off
        }
    }
}

var oven = TriStateSwitch.low
print(oven)
oven.next() // high
print(oven)
oven.next() // off
print(oven)

类型方法(Type Method)

  1. 关键字
  • static:类、结构体、枚举都能用;子类不能重写。
  • class:仅类能用;子类可 override。
  1. 调用方式

“类型名.方法名”,无需实例。

  1. 方法体内 self 指“类型自身”
class SomeClass {
    class func helloType() {
        print("Hello from \(self)") // 打印类名
    }
}
SomeClass.helloType()

完整实战:游戏关卡管理

下面把“类型属性 + 类型方法 + 实例方法”揉到一起,演示一个常用模式:

  • 类型层保存“全局状态”(最高解锁关卡)。
  • 实例层保存“个人状态”(当前玩家在第几关)。
struct LevelTracker {
    // 1. 类型属性:所有玩家共享
    nonisolated(unsafe) static var highestUnlockedLevel = 1
    
    // 2. 类型方法:解锁
    static func unlock(_ level: Int) {
        if level > highestUnlockedLevel {
            highestUnlockedLevel = level
        }
    }
    
    // 3. 类型方法:查询是否解锁
    static func isUnlocked(_ level: Int) -> Bool {
        return level <= highestUnlockedLevel
    }
    
    // 4. 实例属性:个人当前关卡
    var currentLevel = 1
    
    // 5. 实例方法(mutating):进阶到指定关
    @discardableResult
    mutating func advance(to level: Int) -> Bool {
        if LevelTracker.isUnlocked(level) {
            currentLevel = level
            return true
        }
        return false
    }
}

// 玩家类
class Player {
    let name: String
    var tracker = LevelTracker()
    
    init(name: String) {
        self.name = name
    }
    
    // 完成某关
    func complete(level: Int) {
        LevelTracker.unlock(level + 1)          // 全局解锁下一关
        tracker.advance(to: level + 1)          // 个人进度推进
    }
}

// 场景脚本
let player = Player(name: "Argyrios")
player.complete(level: 1)
print("最高解锁关卡:\(LevelTracker.highestUnlockedLevel)") // 2

// 第二个玩家想跳关
let player2 = Player(name: "Beto")
if player2.tracker.advance(to: 6) {
    print("直接跳到 6 成功")
} else {
    print("6 关尚未解锁") // 走进这里
}

易忘细节速查表

  1. mutating 只能用于 struct/enum;class 天生可变,不需要。
  2. static 与 class 区别:
    • 结构体/枚举只能用 static。
    • 类里 static = final class,不允许子类覆盖;class 允许覆盖。
  3. 在类型方法里调用同类类型方法/属性,可直接写名字,无需前缀类型。
  4. @discardableResult 用于“调用方可以不处理返回值”的场景,消除警告。

总结与实战扩展

  1. 方法不再“是类的专利”后,优先用 struct 建模数据,再根据需要升级成 class,可大幅降低引用类型带来的共享状态问题。
  2. mutating 让“值语义 + 链式调用”成为可能,例如:
extension Array where Element: Comparable {
    mutating func removeMin() -> Element? {
        guard let min = self.min() else { return nil }
        remove(at: firstIndex(of: min)!)
        return min
    }
}
var scores = [98, 67, 84]
while let min = scores.removeMin() {
    print("从低到高处理", min)
}
  1. 类型方法是做“全局状态但作用域受限”的利器:
  • App 配置中心(static 存储 + 类型方法读写)
  • 网络请求 stub 中心(type method 注册/注销 mock)
  • 工厂方法(class func makeDefaultXxx())
  1. 与协议组合

把 mutating 写进协议,让 struct/enum 也能提供“可变更”接口,而 class 实现时自动忽略 mutating:

protocol Resettable {
    mutating func reset()
}

Swift 嵌套类型:在复杂类型内部优雅地组织枚举、结构体与协议

为什么要“嵌套”

在 Swift 中,我们经常会写一些“小工具”类型:

  • 只在某个类/结构体里用到的枚举
  • 仅服务于一条业务逻辑的辅助结构体
  • 与外部世界无关的私有协议

如果把它们全部写成顶层类型,会导致:

  1. 命名空间污染(Top-Level 名字过多)
  2. 可读性下降(“这个类型到底给谁用?”)
  3. 访问控制粒度变粗(想私有却不得不 public)

嵌套类型(Nested Types)正是为了解决这三个痛点:把“辅助类型”放进“主类型”内部,让代码的“作用域”与“视觉层次”保持一致。

语法一览:如何“套娃”

// 外层:主类型
struct BlackjackCard {
    
    // 嵌套枚举 ①
    enum Suit: Character {
        case spades   = "♠"
        case hearts   = "♥"
        case diamonds = "♦"
        case clubs    = "♣"
    }
    
    // 嵌套枚举 ②
    enum Rank: Int {
        case two = 2, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king, ace
        
        // 在枚举里再嵌套一个结构体 ③
        struct Values {
            let first: Int
            let second: Int?   // Ace 才有第二值
        }
        
        // 计算属性,返回嵌套结构体
        var values: Values {
            switch self {
            case .ace:
                return Values(first: 1, second: 11)
            case .jack, .queen, .king:
                return Values(first: 10, second: nil)
            default:                // 2...10
                return Values(first: self.rawValue, second: nil)
            }
        }
    }
    
    // 主类型自己的属性
    let rank: Rank
    let suit: Suit
    
    // 计算属性,拼接描述
    var description: String {
        let valueDesc = rank.values.second == nil ?
            "\(rank.values.first)" :
            "\(rank.values.first)\(rank.values.second!)"
        return "\(suit.rawValue)\(rank.rawValue)(点数 \(valueDesc))"
    }
}

知识点逐条拆解

  1. 嵌套深度不限

    上面 Values 结构体嵌套在 Rank 枚举里,Rank 又嵌套在 BlackjackCard 中,形成三级嵌套。只要你愿意,可以继续往下套。

  2. 名字自动带上“前缀”

    在外部使用时,编译器强制你加“外层名字.”前缀,天然起到命名空间隔离:

let color = BlackjackCard.Suit.hearts   // 不会和 Poker.Suit.hearts 冲突
  1. 访问控制可逐层细化

    如果 BlackjackCardpublic,而 Values 声明为 private,那么模块外部无法感知 Values 存在,实现细节被彻底隐藏。

  2. 成员构造器依旧生效

    因为 BlackjackCard 是结构体且未自定义 init,编译器仍会生成逐成员构造器:

let card = BlackjackCard(rank: .ace, suit: .spades)
print(card.description)   // ♠ace(点数 1 或 11)

注意:.ace.spades 可以省略前缀,因为 Swift 能根据形参类型推断出 RankSuit

再举三个日常开发场景

  1. UITableView 嵌套数据源
class SettingsViewController: UITableViewController {
    
    // 仅在本控制器里使用的模型
    private enum Section: Int, CaseIterable {
        case account, privacy, about
        
        var title: String {
            switch self {
            case .account: return "账号"
            case .privacy: return "隐私"
            case .about:   return "关于"
            }
        }
    }
    
    private typealias Row = (icon: UIImage?, text: String, action: () -> Void)
    
    private var data: [Section: [Row]] = [:]
}
  1. Network 嵌套错误
struct API {
    enum Error: Swift.Error {
        case invalidURL
        case httpStatus(code: Int)
        case decodeFailure(underlying: Swift.Error)
    }
    
    func request() async throws -> Model {
        guard let url = URL(string: "https://example.com") else {
            throw Error.invalidURL
        }
        ...
    }
}
  1. SwiftUI 嵌套模型
struct EmojiMemoryGame: View {
    
    // 仅在本 View 文件里使用
    private struct Card: Identifiable {
        let id = UUID()
        let emoji: String
        var isFaceUp = false
    }
    
    @State private var cards: [Card] = []
}

总结与最佳实践

  1. 命名空间 > 前缀

    与其写 BlackjackSuitBlackjackRank,不如直接嵌套,用 BlackjackCard.Suit 既简洁又清晰。

  2. 能 private 就 private

    把嵌套类型默认写成 private,直到外部真的需要再放宽权限,避免“泄露实现”。

  3. 不要“为了嵌套而嵌套”

    如果某个类型在多个业务模块出现,继续嵌套反而会增加引用成本,此时应提升为顶层 internalpublic

  4. typealias 搭配食用更佳

    当嵌套路径过长时,可在当前文件顶部 typealias CardSuit = BlackjackCard.Suit,既保留命名空间,又减少手指负担。

  5. 在 Swift Package 中作为“实现细节”

    公开接口只暴露最外层 public struct BlackjackCard,所有辅助枚举/结构体保持 internalprivate,后续迭代可随意重构而不破坏 SemVer。

Swift 类型转换实用指北:从 is / as 到 Any/AnyObject 的完整路线

为什么要“类型转换”

Swift 是强类型语言,编译期就必须知道每个变量的真实类型。

但在面向对象、协议、泛型甚至混用 OC 的场景里,变量“静态类型”与“实际类型”常常不一致。

类型转换(Type Casting)就是用来:

  1. 检查“实际类型”到底是谁(is)
  2. 把“静态类型”当成别的类型来用(as / as? / as!)
  3. 处理“任意类型”这种黑盒(Any / AnyObject)

核心运算符速查表

运算符 返回类型 可能失败 用途示例
is Bool 不会崩溃 判断“是不是”某类型
as? 可选值 会返回nil 安全向下转型(失败不炸)
as! 非可选值 可能崩溃 强制向下转型(失败运行时错误)
as 原类型 不会失败 向上转型或桥接(OC↔Swift)

建立实验田:先搭一个类层级

// ① 根类:媒体条目
class MediaItem {
    let name: String
    init(name: String) { self.name = name }
}

// ② 子类:电影
class Movie: MediaItem {
    let director: String
    init(name: String, director: String) {
        self.director = director
        super.init(name: name)
    }
}

// ③ 子类:歌曲
class Song: MediaItem {
    let artist: String
    init(name: String, artist: String) {
        self.artist = artist
        super.init(name: name)
    }
}

// ④ 仓库:存放所有媒体
let library: [MediaItem] = [
    Movie(name: "卧虎藏龙", director: "李安"),
    Song(name: "青花瓷", artist: "周杰伦"),
    Movie(name: "星际穿越", director: "诺兰"),
    Song(name: "晴天", artist: "周杰伦"),
    Song(name: "夜曲", artist: "周杰伦")
]

虽然数组静态类型是[MediaItem],但运行时每个元素仍然是原来的MovieSong

想访问director/artist?先检查、再转换——这就是本文主题。

检查类型:is 的用法

需求:统计library里电影、歌曲各多少。

var movieCount = 0
var songCount  = 0

for item in library {
    if item is Movie { movieCount += 1 }
    if item is Song  { songCount  += 1 }
}

print("电影\(movieCount)部,歌曲\(songCount)首")
// 打印:电影2部,歌曲3首

要点

  1. is只回答“是/否”,不改动类型。
  2. 对协议也适用,例如item is CustomStringConvertible

向下转型:as? 与 as! 的抉择

需求:把每个元素的详细信息打印出来,需要访问子类独有属性。

for item in library {
    // 1. 先尝试当成电影
    if let movie = item as? Movie {
        print("电影:《\(movie.name)》——导演:\(movie.director)")
        continue
    }
    
    // 2. 再尝试当成歌曲
    if let song = item as? Song {
        print("歌曲:《\(song.name)》——歌手:\(song.artist)")
    }
}

输出: 电影:《卧虎藏龙》——导演:李安 歌曲:《青花瓷》——歌手:周杰伦 电影:《星际穿越》——导演:诺兰 歌曲:《晴天》——歌手:周杰伦 歌曲:《夜曲》——歌手:周杰伦

经验

  1. 不确定成功用as?+可选绑定,几乎不会错。
  2. 只有100%确定时才写as!,否则崩溃现场见:
let first = library[0] as! Song  // 运行时错误:Could not cast value of type 'Movie' to 'Song'

向上转型:as 的“隐形”场景

向上转是最安全的,因为子类一定能当父类用,Swift甚至允许省略as

let m: Movie = Movie(name: "哪吒", director: "饺子")
let item: MediaItem = m   // 编译器自动向上转

但在某些桥接场景必须显式写as

// NSArray 只能装 NSObject,Swift String 需要桥接
let ocArray: NSArray = ["A", "B", "C"] as NSArray

Any 与 AnyObject:万金油盒子

类型 能装什么 常见场景
Any 任何类型(含struct/enum/closure) JSON、脚本语言交互
AnyObject 任何class(含@objc协议) OC SDK、UITableView datasource

示例:把“完全不相干”的值塞进一个数组

var things = [Any]()
things.append(42)                       // Int
things.append(3.14)                     // Double
things.append("Hello")                  // String
things.append((2.0, 5.0))               // 元组
things.append(Movie(name: "哪吒", director: "饺子"))
things.append({ (name: String) -> String in "Hi, \(name)" }) // 闭包

怎么把这些值取出来?switch + 模式匹配最清晰:

for thing in things {
    switch thing {
    case let int as Int:
        print("整数值:\(int)")
    case let double as Double:
        print("小数值:\(double)")
    case let str as String:
        print("字符串:\(str)")
    case let (x, y) as (Double, Double):
        print("坐标:(\(x), \(y))")
    case let movie as Movie:
        print("任意盒里的电影:\(movie.name)")
    case let closure as (String) -> String:
        print("闭包返回:\(closure("Swift"))")
    default:
        print("未匹配到的类型")
    }
}

要点

  1. as模式可以一次性完成“类型检查+绑定”。
  2. 不要滥用Any/AnyObject,你会失去编译期检查,代码维护成本陡增。

常见踩坑与调试技巧

  1. “is”对协议要求苛刻
protocol Playable { }
extension Song: Playable { }

let s: MediaItem = Song(name: "x", artist: "y")
print(s is Playable) // true

之前的某个Swift版本,会将s推断为MediaItem,而MediaItem没实现Playable协议,所以返回false

从4.x之后s的实际类型是Sone,返回true

  1. JSON转字典后全成Any
let json: [String: Any] = ["age": 18]
let age = json["age"] as! Int + 1  // 万一服务器返回String就崩

as?+guard提前返回,或Codable一步到位。

  1. as!链式写法
let label = (view.subviews[0] as! UILabel).text as! String  // 两层强转,一层失败就崩

建议分步+可选绑定,或使用if let label = view.subviews.first as? UILabel

实战延伸:类型转换在架构中的身影

  1. MVVM差异加载

tableView同一cellForRow里根据item is HeaderItem / DetailItem画不同UI。
2. 路由/插件

URL路由把参数打包成[String: Any],各插件再as?取出自己关心的类型。
3. 单元测试

XCTAssertTrue(mock is MockNetworkClient)确保测试替身注入正确。
4. OC老SDK混编

UIViewController→自定义子类,as!前先用isKind(of:)(OC习惯)或is检查。

总结与私货

  1. 类型转换不是“黑科技”,它只是把运行时类型信息暴露给开发者。
  2. 优先用as?+可选绑定,让错误止步于nil;as!留给自己能写单元测试担保的场景。
  3. Any/AnyObject是“逃生舱”,一旦打开就等于对编译器说“相信我”。能不用就不用,实在要用就封装成明确的枚举或struct,把转换工作限制在最小作用域。
  4. 在团队Code Review里,见到as!可以强制要求写注释说明为什么不会崩;这是用制度换安全感。
  5. 如果业务里大量is/as泛滥,多半协议/泛型抽象得不够,可以考虑重构:
    • 用协议扩展把“差异化行为”做成多态,而不是if/else判断类型。
    • 用泛型把“运行时类型”提前到“编译期类型”,减少转换。

《Flutter全栈开发实战指南:从零到高级》- 05 - 基础组件实战:构建登录界面

手把手教你实现一个Flutter登录页面

嗨,各位Flutter爱好者!今天我要和大家分享一个超级实用的功能——用Flutter构建一个功能完整的登录界面。说实话,第一次接触Flutter时,看着那些组件列表也是一头雾水,但当真正动手做出第一个登录页面后,才发现原来一切都这么有趣!

登录界面就像餐厅的门面,直接影响用户的第一印象。今天,我们就一起来打造一个既美观又实用的"门面"!

我们要实现什么?

先来看看我们的目标——一个支持多种登录方式的登录界面:

含以下功能点:

  • 双登录方式:账号密码 + 手机验证码
  • 实时表单验证
  • 记住密码和自动登录
  • 验证码倒计时
  • 第三方登录(微信&QQ&微博)
  • 交互动画

是不是已经迫不及待了?别急,工欲善其事,必先利其器!!! 在开始搭建之前,我们先来熟悉一下Flutter的基础组件,这些组件就像乐高积木,每个都有独特的用途,组合起来就能创造奇迹!

一、Flutter基础组件

1.1 Text组件:不只是显示文字

Text组件就像聊天时的文字消息,不同的样式能传达不同的情感。让我给你展示几个实用的例子:

// 基础文本 - 就像普通的聊天消息
Text('你好,Flutter!')

// 带样式的文本 - 像加了特效的消息
Text(
  '欢迎回来!',
  style: TextStyle(
    fontSize: 24.0,              // 字体大小
    fontWeight: FontWeight.bold, // 字体粗细
    color: Colors.blue[800],     // 字体颜色
    letterSpacing: 1.2,          // 字母间距
  ),
)

// 富文本 - 像一条消息中有不同样式的部分
Text.rich(
  TextSpan(
    children: [
      TextSpan(
        text: '已有账号?',
        style: TextStyle(color: Colors.grey[600]),
      ),
      TextSpan(
        text: '立即登录',
        style: TextStyle(
          color: Colors.blue,
          fontWeight: FontWeight.bold,
        ),
      ),
    ],
  ),
)

实用技巧:

  • 文字超出时显示省略号:overflow: TextOverflow.ellipsis
  • 限制最多显示行数:maxLines: 2
  • 文字居中显示:textAlign: TextAlign.center

1.2 TextField组件:用户输入

TextField就像餐厅的点菜单,用户在上面写下需求,我们负责处理。来看看如何打造一个贴心的输入体验:

// 基础输入框
TextField(
  decoration: InputDecoration(
    labelText: '用户名',             // 标签文字
    hintText: '请输入用户名',        // 提示文字
    prefixIcon: Icon(Icons.person), // 前缀图标
  ),
)

// 密码输入框 - 带显示/隐藏切换
TextField(
  obscureText: true,  // 隐藏输入内容
  decoration: InputDecoration(
    labelText: '密码',
    prefixIcon: Icon(Icons.lock),
    suffixIcon: IconButton(    // 后缀图标按钮
      icon: Icon(Icons.visibility),
      onPressed: () {
        // 切换密码显示/隐藏
      },
    ),
  ),
)

// 带验证的输入框
TextFormField(
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '请输入内容';  // 验证失败时的提示
    }
    return null;  // 验证成功
  },
)

TextField的核心技能:

  • controller:管理输入内容
  • focusNode:跟踪输入焦点
  • keyboardType:为不同场景准备合适的键盘
  • onChanged:实时监听用户的每个输入

1.3 按钮组件:触发事件的开关

按钮就像电梯的按键,按下它就会带你到达想去的楼层。Flutter提供了多种类型按钮,每种都有其独有的特性:

// 1. ElevatedButton - 主要操作按钮(有立体感)
ElevatedButton(
  onPressed: () {
    print('按钮被点击了!');
  },
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.blue,      // 背景色
    foregroundColor: Colors.white,     // 文字颜色
    padding: EdgeInsets.all(16),       // 内边距
    shape: RoundedRectangleBorder(     // 形状
      borderRadius: BorderRadius.circular(12),
    ),
  ),
  child: Text('登录'),
)

// 2. TextButton - 次要操作按钮
TextButton(
  onPressed: () {
    print('忘记密码');
  },
  child: Text('忘记密码?'),
)

// 3. OutlinedButton - 边框按钮
OutlinedButton(
  onPressed: () {},
  child: Text('取消'),
  style: OutlinedButton.styleFrom(
    side: BorderSide(color: Colors.grey),
  ),
)

// 4. IconButton - 图标按钮
IconButton(
  onPressed: () {},
  icon: Icon(Icons.close),
  color: Colors.grey,
)

按钮状态管理很重要:

  • 加载时禁用按钮,防止重复提交
  • 根据表单验证结果控制按钮可用性
  • 提供视觉反馈,让用户知道操作已被接收

1.4 布局组件

布局组件就像房子的承重墙,它们决定了界面元素的排列方式。掌握它们,你就能轻松构建各种复杂布局:

// Container - 万能的容器
Container(
  width: 200,
  height: 100,
  margin: EdgeInsets.all(16),    // 外边距
  padding: EdgeInsets.all(20),   // 内边距
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [                 // 阴影效果
      BoxShadow(
        color: Colors.black12,
        blurRadius: 10,
      ),
    ],
  ),
  child: Text('内容'),
)

// Row - 水平排列
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Text('左边'),
    Text('右边'),
  ],
)

// Column - 垂直排列
Column(
  children: [
    Text('第一行'),
    SizedBox(height: 16),  // 间距组件
    Text('第二行'),
  ],
)

现在我们已经熟悉了基础组件,是时候开始真正的功能实战了!

二、功能实战:构建多功能登录页面

2.1 项目目录结构

在开始编码前,我们先规划好项目结构,就像建房子前先画好房体图纸一样:

lib/
├── main.dart                    # 应用入口
├── models/                      # 数据模型
│   ├── user_model.dart          # 用户模型
│   └── login_type.dart          # 登录类型
├── pages/                       # 页面文件
│   ├── login_page.dart          # 登录页面
│   ├── home_page.dart           # 首页
│   └── register_page.dart       # 注册页面
├── widgets/                     # 自定义组件
│   ├── login_tab_bar.dart       # 登录选项卡
│   ├── auth_text_field.dart     # 认证输入框
│   └── third_party_login.dart   # 第三方登录
├── services/                    # 服务层
│   └── auth_service.dart        # 认证服务
├── utils/                       # 工具类
│   └── validators.dart          # 表单验证
└── theme/                       # 主题配置
    └── app_theme.dart           # 应用主题

2.2 数据模型定义

我们先定义需要用到的数据模型:

// 登录类型枚举
enum LoginType {
  account,  // 账号密码登录
  phone,    // 手机验证码登录
}

// 用户数据模型
class User {
  final String id;
  final String name;
  final String email;
  final String phone;
  
  User({
    required this.id,
    required this.name,
    required this.email,
    required this.phone,
  });
}

2.3 实现登录页面

下面我将会带你一步步构建登录页面。

第一步:状态管理

首先,我们需要管理页面的各种状态,就像我们平时开车时要关注各项指标:

class _LoginPageState extends State<LoginPage> {
  // 登录方式状态
  LoginType _loginType = LoginType.account;
  
  // 文本控制器
  final TextEditingController _accountController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  final TextEditingController _phoneController = TextEditingController();
  final TextEditingController _smsController = TextEditingController();
  
  // 焦点管理
  final FocusNode _accountFocus = FocusNode();
  final FocusNode _passwordFocus = FocusNode();
  final FocusNode _phoneFocus = FocusNode();
  final FocusNode _smsFocus = FocusNode();
  
  // 状态变量
  bool _isLoading = false;
  bool _rememberPassword = true;
  bool _autoLogin = false;
  bool _isPasswordVisible = false;
  bool _isSmsLoading = false;
  int _smsCountdown = 0;
  
  // 错误信息
  String? _accountError;
  String? _passwordError;
  String? _phoneError;
  String? _smsError;
  
  // 表单Key
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  
  @override
  void initState() {
    super.initState();
    _loadSavedData();
  }
  
  void _loadSavedData() {
    // 从本地存储加载保存的账号
    if (_rememberPassword) {
      _accountController.text = 'user@example.com';
    }
  }
}
第二步:构建页面

接下来,我们构建页面的整体结构:

@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.grey[50],
    body: SafeArea(
      child: SingleChildScrollView(
        physics: BouncingScrollPhysics(),
        child: Container(
          padding: EdgeInsets.all(24),
          child: Form(
            key: _formKey,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _buildBackButton(),        // 返回按钮
                SizedBox(height: 20),
                _buildHeader(),            // 页面标题
                SizedBox(height: 40),
                _buildLoginTypeTab(),      // 登录方式切换
                SizedBox(height: 32),
                _buildDynamicForm(),       // 动态表单
                SizedBox(height: 24),
                _buildRememberSection(),   // 记住密码选项
                SizedBox(height: 32),
                _buildLoginButton(),       // 登录按钮
                SizedBox(height: 40),
                _buildThirdPartyLogin(),   // 第三方登录
                SizedBox(height: 24),
                _buildRegisterPrompt(),    // 注册提示
              ],
            ),
          ),
        ),
      ),
    ),
  );
}
第三步:构建各个组件

现在我们来逐一实现每个功能组件:

登录方式切换选项卡:

Widget _buildLoginTypeTab() {
  return Container(
    height: 48,
    decoration: BoxDecoration(
      color: Colors.grey[100],
      borderRadius: BorderRadius.circular(12),
    ),
    child: Row(
      children: [
        // 账号登录选项卡
        _buildTabItem(
          title: '账号登录',
          isSelected: _loginType == LoginType.account,
          onTap: () {
            setState(() {
              _loginType = LoginType.account;
            });
          },
        ),
        // 手机登录选项卡
        _buildTabItem(
          title: '手机登录',
          isSelected: _loginType == LoginType.phone,
          onTap: () {
            setState(() {
              _loginType = LoginType.phone;
            });
          },
        ),
      ],
    ),
  );
}

动态表单区域:

Widget _buildDynamicForm() {
  return AnimatedSwitcher(
    duration: Duration(milliseconds: 300),
    child: _loginType == LoginType.account
        ? _buildAccountForm()   // 账号登录表单
        : _buildPhoneForm(),    // 手机登录表单
  );
}

账号输入框组件:

Widget _buildAccountField() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text('邮箱/用户名'),
      SizedBox(height: 8),
      TextFormField(
        controller: _accountController,
        focusNode: _accountFocus,
        decoration: InputDecoration(
          hintText: '请输入邮箱或用户名',
          prefixIcon: Icon(Icons.person_outline),
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
          ),
          errorText: _accountError,
        ),
        onChanged: (value) {
          setState(() {
            _accountError = _validateAccount(value);
          });
        },
      ),
    ],
  );
}

登录按钮组件:

Widget _buildLoginButton() {
  bool isFormValid = _loginType == LoginType.account
      ? _accountError == null && _passwordError == null
      : _phoneError == null && _smsError == null;

  return SizedBox(
    width: double.infinity,
    height: 52,
    child: ElevatedButton(
      onPressed: isFormValid && !_isLoading ? _handleLogin : null,
      child: _isLoading
          ? CircularProgressIndicator()
          : Text('立即登录'),
    ),
  );
}
第四步:实现业务逻辑

表单验证:

String? _validateAccount(String? value) {
  if (value == null || value.isEmpty) {
    return '请输入账号';
  }
  final emailRegex = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
  if (!emailRegex.hasMatch(value)) {
    return '请输入有效的邮箱';
  }
  return null;
}

登录逻辑:

Future<void> _handleLogin() async {
  if (_isLoading) return;
  
  if (_formKey.currentState!.validate()) {
    setState(() {
      _isLoading = true;
    });
    
    try {
      User user;
      if (_loginType == LoginType.account) {
        user = await AuthService.loginWithAccount(
          account: _accountController.text,
          password: _passwordController.text,
        );
      } else {
        user = await AuthService.loginWithPhone(
          phone: _phoneController.text,
          smsCode: _smsController.text,
        );
      }
      await _handleLoginSuccess(user);
    } catch (error) {
      _handleLoginError(error);
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }
}

效果展示与总结

f1.png

f2.png 至此我们终于完成了一个功能完整的登录页面!让我们总结一下实现的功能:

实现功能点

  1. 双登录方式:用户可以在账号密码和手机验证码之间无缝切换
  2. 智能验证:实时表单验证,即时错误提示
  3. 用户体验:加载状态、错误提示、流畅动画
  4. 第三方登录:支持微信、QQ、微博登录
  5. 状态记忆:记住密码和自动登录选项

学到了什么?

通过这个项目,我们掌握了:

  • 组件使用:Text、TextField、Button等基础组件的深度使用
  • 状态管理:使用setState管理复杂的页面状态
  • 表单处理:实时验证和用户交互
  • 布局技巧:创建响应式和美观的界面布局
  • 业务逻辑:处理用户输入和API调用

最后的话

看到这里,你已经成功构建了一个完整的登录界面!这个登录页面只是开始,期待你能创造出更多更好的应用!

有什么问题或想法?欢迎在评论区留言讨论~, Happy Coding!✨

Swift 枚举完全指南——从基础语法到递归枚举的渐进式学习笔记

前言

在 C/Objective-C 里,枚举只是一组别名整型;在 Swift 里,枚举被提升为“一等类型”(first-class type),可以拥有

  • 计算属性
  • 实例方法
  • 初始化器
  • 扩展、协议、泛型
  • 递归结构

因此,它不再只是“常量集合”,而是一种强大的建模工具。

基础语法:enum、case、点语法

// 1. 最简形式:不附带任何值
enum CompassPoint {
    case north
    case south
    case east
    case west
}

// 2. 单行多 case 写法
enum Planet {
    case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

// 3. 类型推断下的点语法
var direction = CompassPoint.north
direction = .west          // 类型已知,可省略前缀

与 switch 联用: 穷举检查

Swift 的 switch 必须覆盖所有 case,否则编译失败——这是“安全第一”的体现。

var direction = CompassPoint.north
switch direction {
case .north:
    print("Lots of planets have a north")
case .south:
    print("Watch out for penguins")
case .east, .west:          // 多 case 合并
    print("Where the sun rises or sets")
}
// 如果注释掉任意 case,编译器立即报错

遍历所有 case:CaseIterable 协议

只需加一句 : CaseIterable,编译器自动合成 allCases 数组。

enum Beverage: CaseIterable {
    case coffee, tea, juice
}
print("总共 \(Beverage.allCases.count) 种饮品")
for drink in Beverage.allCases {
    print("今天喝\(drink)")
}

关联值(Associated Values)

区别于原始值,关联值是把额外信息绑定到具体实例,而不是枚举定义本身。

enum Barcode {
    // UPC 一维码:四段数字
    case upc(Int, Int, Int, Int)
    // QR 二维码:任意长度字符串
    case qrCode(String)
}

// 创建实例时才真正携带值
var product = Barcode.upc(8, 85909, 51226, 3)
product = .qrCode("https://swift.org")

// switch 提取关联值
switch product {
    //case .upc(let numSystem, let manufacturer, let product, let check):
    // 简写:如果全是 let 或 var,可移到前面
case let .upc(numSystem, manufacturer, product, check):
    print("UPC: \(numSystem)-\(manufacturer)-\(product)-\(check)")
case .qrCode(let url):
    print("QR 内容: \(url)")
}

原始值(Raw Values)——“编译期就确定”

原始值与关联值互斥:

  • 原始值在定义时就写死,所有实例共用;
  • 关联值在创建时才给出,每个实例可以不同。
  1. 手动指定
enum ASCIIControl: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}
  1. 隐式自动递增 / 隐式字符串
enum PlanetInt: Int {
    case mercury = 1      // 显式从 1 开始
    case venus            // 隐式 2
    case earth            // 隐式 3
}

enum CompassString: String {
    case north            // 隐式 rawValue = "north"
    case south
}
  1. 通过 rawValue 初始化?返回的是可选值
enum PlanetInt: Int {
    case mercury = 1      // 显式从 1 开始
    case venus            // 隐式 2
    case earth            // 隐式 3
}

let possiblePlanet = PlanetInt(rawValue: 7)   // nil,因为没有第 7 颗行星
print(possiblePlanet) // nil
if let planet = PlanetInt(rawValue: 3) {
    print("第 3 颗行星是 \(planet)")   // earth
}

自定义构造器 / 计算属性 / 方法

枚举也能“长得像类”。

enum LightBulb {
    case on(brightness: Double)   // 关联值
    case off

    // 计算属性
    var isOn: Bool {
        switch self {
        case .on: return true
        case .off: return false
        }
    }

    // 实例方法
    mutating func toggle() {
        switch self {
        case .on(let b):
            self = .off
            print("从亮度 \(b) 关灯")
        case .off:
            self = .on(brightness: 1.0)
            print("开灯到默认亮度")
        }
    }
}

var bulb = LightBulb.on(brightness: 0.8)
bulb.toggle()   // 关灯
bulb.toggle()   // 开灯

递归枚举(Indirect Enumeration)

当枚举的关联值再次包含自身时,需要显式标记 indirect,让编译器插入间接层,避免无限嵌套导致内存无法布局。

// 方式 A:单个 case 递归
enum ArithmeticExpr {
    case number(Int)
    indirect case addition(ArithmeticExpr, ArithmeticExpr)
    indirect case multiplication(ArithmeticExpr, ArithmeticExpr)
}

// 方式 B:整个枚举全部 case 都递归
indirect enum Tree<T> {
    case leaf(T)
    case node(Tree<T>, Tree<T>)
}

构建与求值:把“(5 + 4) * 2”装进枚举

let five = ArithmeticExpr.number(5)
let four = ArithmeticExpr.number(4)
let two = ArithmeticExpr.number(2)

let sum = ArithmeticExpr.addition(five, four)
let product = ArithmeticExpr.multiplication(sum, two)

// 递归求值
func evaluate(_ expr: ArithmeticExpr) -> Int {
    switch expr {
    case .number(let value):
        return value
    case .addition(let left, let right):
        return evaluate(left) + evaluate(right)
    case .multiplication(let left, let right):
        return evaluate(left) * evaluate(right)
    }
}

print(evaluate(product))   // 18

实战 1:用枚举建模“JSON”

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

let json: JSON = .dictionary([
    "name": .string("Swift"),
    "year": .number(2014),
    "awesome": .bool(true),
    "tags": .array([.string("iOS"), .string("macOS")])
])

优势:

  • 编译期保证类型组合合法;
  • 写解析/生成器时,switch 覆盖所有 case 即可,无需 if-else 层层判断。

实战 2:消除“字符串驱动”——网络请求路由

enum API {
    case login(user: String, pass: String)
    case userInfo(id: Int)
    case articleList(page: Int, pageSize: Int)
}

extension API {
    var host: String { "https://api.example.com" }
    
    var path: String {
        switch self {
        case .login: return "/login"
        case .userInfo(let id): return "/users/\(id)"
        case .articleList: return "/articles"
        }
    }
    
    var parameters: [String: Any] {
        switch self {
        case .login(let u, let p):
            return ["username": u, "password": p]
        case .userInfo:
            return [:]
        case .articleList(let page, let size):
            return ["page": page, "pageSize": size]
        }
    }
}

// 使用
let request = API.login(user: "alice", pass: "123456")
print("请求地址:\(request.host + request.path)")

好处:

  • 路由与参数封装在一起,外部无需硬编码字符串;
  • 新增接口只需再加一个 case,编译器会强制你补全 path & parameters。

性能与内存Tips

  1. 不带关联值的枚举 = 一个整型大小,最省内存。
  2. 关联值会占用更多空间,编译器会按最大 case 对齐;如果内存敏感,可用 indirect 将大数据挂到堆上。
  3. 原始值并不会额外占用存储,它只是编译期常量;运行时通过 rawValue 访问即可。
  4. 枚举是值类型,跨线程传递无需担心引用计数,但大体积关联值复制时要注意写时复制(CoW)开销。

给枚举加“泛型”——一个类型参数打通所有关联值

// 1. 泛型枚举:Success 与 Failure 的具体类型由使用方决定
enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

// 2. 网络层统一返回
enum APIError: Error { case timeout, invalidJSON }

func fetchUser(id: Int) -> Result<User, APIError> {
    ...
    return .success(user)
}

// 3. 调用方用 switch 就能拿到强类型的 User 或 APIError
let r = fetchUser(id: 1)
switch r {
case .success(let user):
    print(user.name)
case .failure(let error):
    print(error)
}

要点

  • 枚举可以带泛型参数,且每个 case 可使用不同参数。
  • Swift 标准库已内置 Result<Success, Failure>,无需自己写。

枚举也遵守协议——让“一组无关类型”共享行为

protocol Describable { var desc: String { get } }

enum IOAction: Describable {
    case read(path: String)
    case write(path: String, data: Data)
    
    var desc: String {
        switch self {
        case .read(let p):  return "读取 \(p)"
        case .write(let p, _): return "写入 \(p)"
        }
    }
}

let action = IOAction.write(path: "/tmp/a.txt", data: Data())
print(action.desc)

进阶:把枚举当成“小而美”的命名空间,里面再套结构体、类,一并遵守协议,可组合出非常灵活的对象图。

@unknown default —— 面向库作者的“向后兼容”保险

当模块使用 library evolutionBUILD_LIBRARY_FOR_DISTRIBUTION = YES)打开 resilient 构建时,公开枚举默认是“非冻结”的,未来可能新增 case。

客户端必须用 @unknown default: 兜底,否则升级库后会得到编译警告:

// 在 App 代码里
switch frameworkEnum {
case .oldCaseA: ...
case .oldCaseB: ...
@unknown default:        // 少了就会警告
    assertionFailure("请适配新版本 SDK")
}

冻结枚举(@frozen)则告诉编译器“以后绝对不会再加 case”,可以省略 @unknown default

System 框架里大量使用了该技巧,保证 Apple 加新枚举值时老 App 不会直接崩溃。

SwiftUI 视图工厂——用枚举消灭“字符串驱动”的 Navigation

enum Route: Hashable {
    case home
    case article(id: Int)
    case settings(debug: Bool)
}

@main
struct App: SwiftUI.App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .navigationDestination(for: Route.self) { route in
                    switch route {
                    case .home:         HomeView()
                    case .article(let id): ArticleView(id: id)
                    case .settings(let debug): SettingsView(debug: debug)
                    }
                }
        }
    }
}

优点

  • 路由表即枚举,强类型;
  • 新增 case 编译器会强制你补全对应视图;
  • 支持 NavigationStackpath 参数,可持久化/还原整棵导航树。

把枚举当“位掩码”——OptionSet 的本质仍是枚举

struct FilePermission: OptionSet {
    let rawValue: Int
    
    // 内部用静态枚举常量,对外却是结构体
    static let read   = FilePermission(rawValue: 1 << 0)
    static let write  = FilePermission(rawValue: 1 << 1)
    static let execute = FilePermission(rawValue: 1 << 2)
}

let rw: FilePermission = [.read, .write]
print(rw.rawValue)   // 3

为什么不用“纯枚举”?

  • 枚举无法表达“组合”语义;
  • OptionSet 协议要求 struct 以便支持按位或/与运算。

结论:需要位运算时,用结构体包一层 rawValue,而不是直接上枚举。

性能压测:100 万个关联值到底占多少内存?

测试模型

enum Node {
    case leaf(Int)
    indirect case node(Node, Node)
}

在 64 位下

  • leaf:实际 9 字节(1 字节区分 case + 8 字节 Int),但按 16 字节对齐。
  • node:额外存储两个指针(16 字节)+ 1 字节 tag → 24 字节对齐。

结论

  • 不带 indirect 的枚举=最省内存;
  • 大数据字段务必 indirect 挂到堆上,避免栈爆炸;
  • 如果 case 差异巨大,考虑“枚举 + 类”混合:枚举负责分派,类负责存数据。

什么时候该把“枚举”改回“结构体/类”

  1. case 数量会动态膨胀(如用户标签、城市字典)→ 用字典或数据库。
  2. 需要存储大量同质数据 → 结构体数组更合适。
  3. 需要继承/多态扩展 → 用协议 + 类/结构体。
  4. 需要弱引用、循环引用 → class + delegate 模式。

口诀:“有限状态用枚举,无限集合用集合;行为多态用协议,生命周期用类。”

一条龙完整示例:用枚举写个“小型正则表达式”引擎

indirect enum Regex {
    case literal(Character)
    case concatenation(Regex, Regex)
    case alternation(Regex, Regex)   // “或”
    case repetition(Regex)           // 闭包 *
}

// 匹配函数
extension Regex {
    func match(_ str: String) -> Bool {
        var idx = str.startIndex
        return matchHelper(str, &idx) && idx == str.endIndex
    }
    
    private func matchHelper(_ str: String, _ idx: inout String.Index) -> Bool {
        switch self {
        case .literal(let ch):
            guard idx < str.endIndex, str[idx] == ch else { return false }
            str.formIndex(after: &idx)
            return true
            
        case .concatenation(let left, let right):
            let tmp = idx
            return left.matchHelper(str, &idx) && right.matchHelper(str, &idx) || ({ idx = tmp; return false })()
            
        case .alternation(let left, let right):
            let tmp = idx
            return left.matchHelper(str, &idx) || ({ idx = tmp; return right.matchHelper(str, &idx) })()
            
        case .repetition(let r):
            let tmp = idx
            while r.matchHelper(str, &idx) { }
            return true
        }
    }
}

// 测试
let pattern = Regex.repetition(.alternation(.literal("a"), .literal("b")))
print(pattern.match("abba"))   // true

亮点

  • 纯值类型,线程安全;
  • 用枚举递归描述语法树,代码即文档;
  • 若需性能,可再包一层 JIT 或转成 NFA/DFA。

总结与扩展场景

  1. 枚举是值类型,但拥有近似类的能力。
  2. 关联值 = 运行期动态绑定;原始值 = 编译期静态绑定。
  3. switch 必须 exhaustive,借助 CaseIterable 可遍历。
  4. 可以写构造器、计算属性、方法、扩展、协议等
  5. 建模“有限状态 + 上下文”时,优先用枚举:
    • 播放器状态:.idle / .loading(url) / .playing(item, currentTime) / .paused(item, currentTime)
    • 订单状态:.unpaid(amount) / .paid(date) / .shipped(tracking) / .refunded(reason)
  6. 把“字符串魔法”改成枚举,可让编译器帮你检查漏掉的 case,减少运行时崩溃。
  7. 递归枚举天生适合表达树/表达式这类“自相似”结构,配合模式匹配写解释器极其清爽。
  8. 如果 case 太多(>100),可读性下降,可考虑:
    • 拆成多级枚举(namespace)
    • 用静态工厂方法隐藏细节
    • 改用结构体 + 协议,让“类型”退化为“数据”

checklist:如何写“优雅”的 Swift 枚举

☑ 名字首字母大写,case 小写。

☑ 先问自己“状态是否有限”,再决定用枚举还是字符串。

☑ 关联值 > 3 个字段就封装成结构体,保持 switch 整洁。

☑ 公开库一定要想好“未来会不会加 case”,决定 @frozen 与否。

☑ 超过 20 个 case 考虑分层:外层命名空间枚举,内层再拆。

☑ 需要 Codable 时,关联值枚举要自定义 init(from:) & encode(to:),否则编译器会报错。

☑ 最后写单元测试:把每个 case 都 switch 一遍,防止后续改挂。

Swift -- 第三方登录之微信登录 源码分享

Swift -- 第三方登录之微信登录 源码分享

第一步: 微信开放平台注册,获取APPID和秘钥

不管微信登录,微信支付,微信分享都需要到微信开放平台注册账号后并注册应用,拿到应用唯一标识AppID和应用密钥 AppSecret 然后集成SDK,具体如何集成查看官方文档,文档有详细介绍微信开放平台–iOS接入指南

第二步:程序内设置

微信SDK初始化,注意universal_link必须添加

didFinishLaunchingWithOptions 中对微信SDK进行初始化,方法如下:

 let universal_link = "https://wx.universal_link.url" //此处填写微信后台写的universal_link 地址
 WXApi.registerApp(WX_APPID, universalLink: universal_link)

⚠️ 不要忘了在Signing & Capabilities 中添加Associated Domains中添加applinks, 格式如下图所示: 设置universalLink

第三步 添加代码

微信配置代理,接收微信请求后的返回信息(用户登录/微信支付订单信息)

 // 9.0之后  打开第三方应用之后返回程序内  设置系统回调  --------待完善---------
    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        if url.absoluteString.hasPrefix(WX_APPID){
            //微-信
            WXApi.handleOpen(url, delegate: NK_WXUtils.sharedManager)
            return true
        }
     }
    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        
        if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
            if let url = userActivity.webpageURL{
                    return WXApi.handleOpenUniversalLink(userActivity, delegate: NK_WXUtils.sharedManager)
            }
            
        }
        return true
    }

第四部:微信调用方法

下面就是完整的微信登录及获取用户信息的调用方法

class NK_WXUtils: NSObject , WXApiDelegate{

//微信登录
    static func wxLogin(){
        
        if WXApi.isWXAppInstalled() {
            let req = SendAuthReq()
            req.state = "wx_oauth_authorization_state";//用于保持请求和回调的状态,授权请求或原样带回
            req.scope = "snsapi_userinfo";//授权作用域:获取用户个人信息
            // req.scope = "snsapi_userinfo,snsapi_base";//授权作用域:获取用户个人信息
            
            DispatchQueue.main.async {
                WXApi.send(req)
            }
        }else{
            MBProgressHUD.showJustText(msg: "您尚未安装微信客户端,请安装后重试!")
        }
        
    }

//微信发送请求,这里不用处理
    func onReq(_ req: BaseReq) {
        
        MYLog( "\n\n----openID:"+req.openID)
    }
//微信请求返回结果,这里处理返回的结果
    func onResp(_ resp: BaseResp) {
        if resp.isKind(of: SendAuthResp.self) {
            let res = resp as? SendAuthResp
            if res?.state == "wx_oauth_authorization_state" {
                NK_WXUtils.getWechatAccessToken(code: res!.code!)
            }
        }
    }

    static func getWechatAccessToken(code:String)  {
        
        let url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=\(WX_APPID)&secret=\(WX_SECRET)&code=\(code)&grant_type=authorization_code"
        MBProgressHUD.showLoadingHUD(msg: nil, view: nil)
        NK_HttpManager().requestData(with: url, cache: false, method: .get, params: nil).success { (code, res) in
            MYLog(res)
            guard let dic = res as? [String: Any] else{
                return
            }
            
            guard let access_token = dic["access_token"] as? String,  let openid = dic["openid"] as? String else{
                MBProgressHUD.hideToastHUD(view: nil)
                return
            }
            getWechatUserInfo(with: access_token, openId: openid )
           
            }.fail { (error, msg) in
//                MYLog(msg)
                MBProgressHUD.showJustText(msg: msg)
        }
        
    }
    
    
    static func getWechatUserInfo(with access_token:String, openId:String)  {

        let url = "https://api.weixin.qq.com/sns/userinfo?access_token=\(access_token)&openid=\(openId)"
        NK_HttpManager().requestData(with: url, cache: false, method: .get, params: nil).success { (code, res) in
            MBProgressHUD.hideToastHUD(view: nil)
//            MYLog(res)
            if let dic = res as? [String: Any]{
                // 获取到的用户信息json格式,可以拿来给服务端绑定用户信息
                `在这里绑定获取到的用户信息`
            }
            
            }.fail { (error, msg) in
                MBProgressHUD.showJustText(msg: msg)
        }
    }
    
}

记录我适配iOS26遇到的一些问题

这是适配iOS 26的笔记,并非介绍新功能和API。我只是把项目中遇到的适配问题记录起来。后续如果遇到新的问题会更新这个笔记。

1. 暂时关闭Liquid Glass 液态玻璃

在iOS26中,系统默认开启了Liquid Glass 液态玻璃效果。例如UINavigationBar和UITabBar等,并且是强制性的。但是在项目紧急上线,适配没有做好的情况可以暂时关闭这个效果。

当然苹果也给了最终限制,最多一年时间,下个主要版本就没这个属性了。不推荐长期使用,应尽快完成适配

只要在info.plist加上这一项即可:


<key>UIDesignRequiresCompatibility</key>

<true/>

image.png

2. 导航栏相关

2.1 导航栏按钮玻璃背景问题

在iOS 26中,导航栏按钮会出现大的圆角矩形玻璃背景,无法隐藏或关闭。这可能导致:

  • 按钮文字被遮挡

  • 图标偏移显示异常

  • 之前设置的偏移量不再适用

解决方案:


// 调整偏移量或更换居中的图标资源

// 之前可能设置了负值偏移让按钮靠前,现在需要重新调整

  


// 方法1: 重新设计图标,使用居中对齐的图标

// 方法2: 调整customView的布局约束

2.2 自定义View添加到NavigationBar的问题

将自定义view添加到navigationBar后,在iOS 26中会出现异常:


// 页面出现时添加

[self.navigationController.navigationBar addSubview:_naviView];

  


// 页面消失时移除

[_naviView removeFromSuperview];

现象: 开始正常显示,但从二级页面返回后view消失(图层中存在但不可见)

原因:

这时候由于Apple 对 UINavigationBar 做了多次底层改造:

| iOS版本 | 导航栏变化 | 影响 |

| ------- | ------------------------------------------------ | ------------------------------------ |

| iOS 15 | 引入 UINavigationBarAppearance | 改变背景和阴影绘制机制 |

| iOS 17+ | 导航栏层级变动,_UINavigationBarModernContentView 延迟加载 | 手动添加的子视图可能被系统布局或动画移除 |

| iOS 26 | NavigationBar使用新的 compositing layer 结构 | 非官方子视图在 appear/disappear 动画时被“吞掉”或遮盖 |

导致了在iOS26中,可能出现下面的问题:

会被系统的内部 layer 覆盖;

或者生命周期中 navigationBar 被重新创建;

导致 view 不再显示、被替换或无法响应。

这时候有三种解决方案:


// 解决方法1:一般不使用

// 把view添加到titleView上

// 优点:跟随导航栏生命周期自动管理,不会丢失或被覆盖。

// 缺点:只能放在标题区域,布局受限。

  


[self.navigationController.navigationItem.titleView addSubview:_naviView];

  


// 解决方法2:

// 放到 NavigationBar 的 superview 层(而非导航栏内部)

// 优点:可以放在任何位置,布局灵活。

// 缺点:需要手动管理生命周期,容易出错。

  


// ✅ 这样即使导航栏内部结构变动,你的 view 也不会丢。

// ⚠️ 记得在二级页面 viewWillAppear 时隐藏它。

[self.navigationController.view addSubview:_naviView];

  


// 临时解决方案:

// 延迟加载 view 到 navigationBar 上

// 确保在 navigationBar 完成布局后再添加 或者在viewDidAppear 中添加

dispatch_async(dispatch_get_main_queue(), ^{

[self.navigationController.view addSubview:_naviView];

});

  


3. TabBar相关

在最新版本中,TabBar的变动很大,

3.1 私有属性设置TabBar问题

❌ 不推荐的做法:

因为在iOS26中Apple 给 tabBar 属性加了 runtime 保护;这时候或者运行闪退或者是新增一个单独的tab


// 通过私有属性设置TabBar

[self setValue:self.customTabbar forKey:@"tabBar"];

问题: 在iOS 26中,Apple给tabBar属性加了runtime保护,会导致:

  • 运行时闪退

  • 新增一个单独的tab

  • 自定义TabBar失效

3.2 直接添加SubView的方式

如果是通过直接添加到tabbar上,这种显示基本没大问题,可能有中间大按钮的问题,且有玻璃效果。 但是可能造成点击失效的问题(被系统拦截)。 我项目中是使用的系统TabBar,没有自定义TabBar。 所以没有遇到这个问题。


// 直接添加到tabbar上

self.customTabbar.frame = self.tabBar.bounds;

[self.tabBar addSubview:self.customTabbar];

问题:

  • 显示基本正常,有玻璃效果

  • 中间大按钮可能有问题

  • 点击可能失效(被系统拦截)

3.3 自定义TabBar适配建议

如果你是自定义仿咸鱼的那种发布tabbar,可能出现只有四个tabarItem和中间一个发布图标的的情况。 这时候点击也会出问题。

这种情况就需要重新设计UI了,紧急修复,或者等三方库更新,或者再找找别的方法。

3.4 TabBar透明度问题

如果内容没有延伸到TabBar下方,检查是否设置了isTranslucent属性:


// iOS 26需要移除或条件编译

if #unavailable(iOS 26) {

tabBar.isTranslucent = false

}

我是没有遇到这个问题,因为我们应用首页是个collectionview,我还怕它延伸到tabbar下方,造成不好点击的问题。

4. 布局约束问题

在修改中我发现之前获取的kTopHeight(NaviBarHeight+StatusBarHeight) 会有问题。

原因如下:

  1. windowScene.statusBarManager.statusBarFrame 在某些时机是 0 或未更新(特别是多 Scene、导航过渡、或 navigationBar 异步布局时)。

  2. safeAreaInsets 由系统在 view 布局完成后才会精确计算,早用会得到旧值或 0。

  3. UINavigationBar 在 iOS 26 里可能异步构建(或使用新 compositing),导致你在 viewDidLoad/viewWillAppear 读取到不正确的高度。

  4. 如果你把子视图约束到 self.view.top 而不是 safeAreaLayoutGuide.top,内容会延伸到状态栏/导航栏下方(被遮盖)。

所以布局时使用 Safe Area(safeAreaLayoutGuide 或 view.safeAreaInsets)而不是 statusBarFrame。


//建议使用这个来获取高度

make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop).offset(0);

5. 图片导航栏按钮设置original仍显示蓝色

即使设置了UIImageRenderingModeAlwaysOriginal,在iOS 26中图片按钮仍显示为蓝色(系统tintColor)。


// ❌ 在iOS 26中无效

- (void)setNavigationBarBtn {

UIImage *addImg = [[UIImage imageNamed:@"规范_新增+"]

imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];

UIBarButtonItem *add = [[UIBarButtonItem alloc] initWithImage:addImg

style:UIBarButtonItemStyleDone

target:self

action:@selector(addClient)];

self.navigationItem.rightBarButtonItems = @[add];

}

解决方案

方案1:设置tintColor为clearColor(推荐)


- (void)setNavigationBarBtn {

UIImage *addImg = [[UIImage imageNamed:@"规范_新增+"]

imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];

UIImage *searchImg = [[UIImage imageNamed:@"放大镜"]

imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];

  


UIBarButtonItem *add = [[UIBarButtonItem alloc] initWithImage:addImg

style:UIBarButtonItemStylePlain

target:self

action:@selector(addClient)];

  


UIBarButtonItem *search = [[UIBarButtonItem alloc] initWithImage:searchImg

style:UIBarButtonItemStylePlain

target:self

action:@selector(searchClient)];

  


// ✅ iOS 26修复方案

for (UIBarButtonItem *item in @[add, search]) {

item.tintColor = UIColor.clearColor; // 确保使用原图色

}

// ⚠️ 注意:iOS 26中左右顺序和之前版本相反

if (@available(iOS 26.0, *)) {

self.navigationItem.rightBarButtonItems = @[search, add];

} else {

self.navigationItem.rightBarButtonItems = @[add, search];

}

}

方案2:使用自定义UIButton


- (void)setNavigationBarBtn {

UIButton *addButton = [UIButton buttonWithType:UIButtonTypeCustom];

[addButton setImage:[UIImage imageNamed:@"规范_新增+"] forState:UIControlStateNormal];

addButton.frame = CGRectMake(0, 0, 30, 30);

[addButton addTarget:self action:@selector(addClient) forControlEvents:UIControlEventTouchUpInside];

UIBarButtonItem *add = [[UIBarButtonItem alloc] initWithCustomView:addButton];

self.navigationItem.rightBarButtonItems = @[add];

}

重要提醒

⚠️ iOS 26中导航栏按钮顺序变化:

在设置rightBarButtonItemsleftBarButtonItems时,iOS 26的显示顺序与之前版本相反,需要条件编译处理:


if (@available(iOS 26.0, *)) {

// iOS 26: 数组第一个元素显示在最右边

self.navigationItem.rightBarButtonItems = @[最右边的按钮, 中间按钮, 最左边的按钮];

} else {

// iOS 25及以下: 数组第一个元素显示在最左边

self.navigationItem.rightBarButtonItems = @[最左边的按钮, 中间按钮, 最右边的按钮];

}

补充 iPad相关

可以看这个大佬的iPad适配文章

正确的 .gitignore 配置

# Xcode 用户数据
**/xcuserdata/
*.xcodeproj/xcuserdata/
*.xcworkspace/xcuserdata/

# Xcode 构建文件
build/
DerivedData/

# CocoaPods - 只忽略 Pods 目录,不忽略 Podfile 和 Podfile.lock
Pods/

# macOS
.DS_Store

# 其他
*.swp
*~

提交代码时

git add Podfile Podfile.lock .gitignore
git commit -m "Update dependencies"
git push

执行 pod install 后,.xcodeproj 文件被修改了,产生了待提交的内容。

原因分析

当你运行 pod install 时,CocoaPods 会:

  1. ✅ 在 Pods/ 目录下载依赖库(已被 .gitignore 忽略)
  2. ⚠️ 修改 .xcodeproj/project.pbxproj 文件,添加对 Pods 的引用

首次克隆项目后

# 1. 克隆项目
git clone <your-repo-url>
cd 项目目录

# 2. 安装依赖
pod install

# 3. 提交 .xcodeproj 的修改(如果有)
git add eWordMedical.xcodeproj/project.pbxproj
git commit -m "Update project configuration after pod install"
git push

为什么会有这些修改? 可能的原因:

  1. 路径差异:不同电脑上的绝对路径不同
  2. CocoaPods 版本:不同版本的 CocoaPods 生成的配置略有差异
  3. 首次安装:如果项目之前没有正确提交 .xcodeproj

这样做的好处:

  • 保持项目文件与实际配置一致
  • 团队其他成员拉取后可以直接编译

预防措施 为了减少这种情况,团队应该 统一 CocoaPods 版本

# 查看当前版本
pod --version

# 在 Gemfile 中锁定版本(可选)
gem 'cocoapods', '~> 1.15'

确保 .xcworkspace 也被提交

# .xcworkspace 应该提交(包含工作区配置)
git add xxxx.xcworkspace

在 .gitignore 中只忽略用户数据

# 只忽略用户数据,不忽略项目文件
**/xcuserdata/
*.xcworkspace/xcuserdata/

⚠️ 可能冲突的情况

只有在以下情况会冲突:

  1. 同时修改项目结构

    • 你:添加了新文件 A
    • 同事:添加了新文件 B
    • 两个人都修改了 .xcodeproj
    • 结果:Git 合并冲突 ❌
  2. 同时更新依赖

    • 你:更新了 Alamofire 版本
    • 同事:更新了 SnapKit 版本
    • 两个人都修改了 Podfile.lock 和 .xcodeproj
    • 结果:需要手动合并 ⚠️

CocoaPods 库中的代码有报错问题每次我都需要手动修改为了防止每次都修改以下修改 使用 Podfile 的 post_install 钩子自动修复

post_install do |installer|
  installer.pods_project.targets.each do |target|
    if target.name == 'CountdownLabel'  # 替换为你的 Pod 名称
      target.build_configurations.each do |config|
        # 自动修复感叹号问题
        Dir.glob("Pods/CountdownLabel/**/*.swift").each do |file|
          contents = File.read(file)
          # 将 as !TimeZone 替换为 as? TimeZone
          new_contents = contents.gsub(/as !TimeZone/, 'as? TimeZone')
          File.write(file, new_contents) if contents != new_contents
        end
      end
    end
  end
end

【Swift 筑基记】把“结构体”与“类”掰开揉碎——从值类型与引用类型说起

Swift 里的“结构体”和“类”长什么样?

  1. 定义语法
// 结构体:用 struct 关键字
struct Resolution {
    var width = 0          // 存储属性
    var height = 0
}

// 类:用 class 关键字
class VideoMode {
    var resolution = Resolution() // 复杂类型属性
    var interlaced = false        // 逐行/隔行扫描
    var frameRate = 0.0
    var name: String?             // 可选类型,默认 nil
}

注意:Swift 不需要 .h/.m 分离,一个文件搞定接口与实现。

  1. 创建实例——“()” 就是最简单的初始化器
let someResolution = Resolution() // 结构体实例
let someVideoMode = VideoMode()   // 类实例
  1. 访问属性——点语法(dot syntax)
print("默认宽:\(someResolution.width)")           // 0
print("视频模式宽:\(someVideoMode.resolution.width)") // 0

// 也能层层赋值
someVideoMode.resolution.width = 1280
print("修改后宽:\(someVideoMode.resolution.width)")   // 1280

结构体自带“逐成员初始化器”

编译器会自动给 struct 生成一个 memberwise initializer,class 没有!

// 结构体可直接写全参构造器
let vga = Resolution(width: 640, height: 480)

// 类必须自己写
class VideoMode {
    ...
    init(resolution: Resolution, interlaced: Bool, frameRate: Double, name: String?) {
        self.resolution = resolution
        self.interlaced = interlaced
        self.frameRate = frameRate
        self.name = name
    }
}

值类型 vs 引用类型

  1. 结构体是值类型:赋值 = 全量复制
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd          // 内存里出现两份独立数据
cinema.width = 2048

print("cinema.width = \(cinema.width)") // 2048
print("hd.width     = \(hd.width)")     // 1920,原数据纹丝不动
  1. 枚举也是值类型
enum CompassPoint {
    case north, south, east, west
    mutating func turnNorth() { self = .north }
}
var current = CompassPoint.west
let old = current          // 复制一份
current.turnNorth()

print("当前:\(current)")  // north
print("旧值:\(old)")      // west,不受影响
  1. 类是引用类型:赋值 = 多一根指针指向同一块堆内存
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

let alsoTenEighty = tenEighty   // 只复制指针,未复制对象
alsoTenEighty.frameRate = 30.0

print("tenEighty.frameRate = \(tenEighty.frameRate)") // 30.0,一起变

如何判断“指向同一实例”?——身份运算符

if tenEighty === alsoTenEighty {
    print("两根指针指向同一块堆内存 ✅")
}
// 输出:两根指针指向同一块堆内存 ✅

注意:=== 与 == 完全不同

  • === 比较“身份”(是否同一实例)
  • == 比较“值相等”,需要开发者自己实现 Equatable 协议

4 个易错点

  1. 数组/字典/集合是 struct,但内部有“写时复制”(COW) 优化,大块数据不会立刻复制。
  2. let 修饰 class 实例:只能锁定“指针”不能变,但实例内部属性可变!
let vm = VideoMode()
vm.frameRate = 60 // ✅ 合法,指针没变
  1. struct 里包含 class 属性时,复制的是“引用”。嵌套情况要画对象图。
  2. 多线程下,值类型天然线程安全;引用类型需要额外同步(如锁、actor)。

10 秒选型决策表

场景 首选
模型小而简单,主要存数据 struct
需要继承、多态、抽象基类 class
需要 @objc 动态派发、KVO class
SwiftUI 视图状态(@State) struct
共享可变状态(缓存、注册表) class + 单例
网络 JSON 转模型(Codable) struct(Codable)
需要 deinit 释放资源 class

实战扩展:SwiftUI + Combine 中的 struct/class 协奏

  1. 视图层——全是 struct
struct TweetRow: View {
    let tweet: Tweet        // 值类型,一行数据
    var body: some View { ... }
}
  1. 数据源——class 托管生命周期
final class TimelineVM: ObservableObject {
    @Published private(set) var tweets: [Tweet] = []
    
    func fetch() async {
        ...
    }
}
  1. 共享状态——@StateObject 只接受 class
struct TimelineView: View {
    @StateObject private var vm = TimelineVM() // 必须是 class
    var body: some View {
        List(vm.tweets) { TweetRow(tweet: $0) }
    }
}

struct 真的比 class 快吗?

官方文档只说“struct 是值类型,会复制”,却不说:

  • 复制一次到底多大开销?
  • Array 的“写时复制”(COW) 对自定义类型是否同样生效?
  • 在 10 万元素级别,struct 与 class 差距是 1 % 还是 10 倍?

热身:先写一个“无脑”版本

// 1. 纯值类型,每次赋值都全量复制
struct MyArrayStruct {
    var storage: [Int] = Array(0..<100_000)
}

// 2. 引用类型,永远共享
final class MyArrayClass {
    var storage: [Int] = Array(0..<100_000)
}

跑分结果(M1 Mac,Release 模式,100 万次读):

类型 随机读 拷贝 + 写一次 内存峰值
struct 18 ms 6.2 ms 3.2 MB × 2
class 17 ms 0.3 ms 3.2 MB

结论:读一样快;但凡有一次写入,struct 的复制成本肉眼可见;

但官方 Array 为什么没这么慢?→ 因为 Apple 给标准库做了 COW。

自己动手:给 struct 加上“写时复制”

思路:把实际数据放到引用类型的“盒子”里,再用 isUniquelyReferenced 判断是否需要复制。

final class BufferBox {          // 1. 真实数据放堆里
    var storage: [Int]
    init(_ storage: [Int]) { self.storage = storage }
}

struct COWArray {
    private var box: BufferBox   // 2. 结构体里只保存指针
    
    init() {
        box = BufferBox(Array(0..<100_000))
    }
    
    // 3. 读操作,直接透传
    subscript(index: Int) -> Int {
        box.storage[index]
    }
    
    // 4. 写操作,先检查唯一性
    mutating func set(_ index: Int, _ value: Int) {
        if !isKnownUniquelyReferenced(&box) {
            box = BufferBox(box.storage) // 复制
        }
        box.storage[index] = value
    }
}

关键点:

  • isKnownUniquelyReferenced 是 Swift 标准库函数,编译器帮你优化成“指针比较 + ARC 判断”。
  • 结构体本身仍是值语义,但只有写入时才真复制,读操作零额外开销。

什么时候该自己写 COW

场景 建议
自定义大集合(ImageData、顶点缓冲) 给 struct 加 COW,保值语义
小 Pod 模型 (< 64 Byte) 无脑 struct,复制成本低于 ARC 计数
需要线程安全 struct + COW 天然不可变,写时加锁即可
需要 NSCoding / @objc 用 class,省去桥接

线程安全番外:let class 可变隐患

final class Counter { var value = 0 }

let counter = Counter()   // let 只能锁定“指针”
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
    counter.value += 1    // 未加锁 → 数据竞争
}
print(counter.value)      // 结果 < 1000

值类型就不会出现该问题——因为每个线程拿到的是独立副本。

在多核场景下,“值类型 + COW” 比 “class + 锁” 更容易写出无锁代码。

小结

  1. struct 是“复印机”,class 是“共享云文档”。
  2. Swift 官方推荐“默认 struct,不得不 class 才用 class”。
  3. 值类型/引用类型的区别不仅在于“复制”,更影响“线程安全”“生命周期”“性能”。
  4. 实际开发中两者经常嵌套:struct 保 immutable 语义,class 管共享状态与副作用。

Swift 字符串与字符完全导读(三):比较、正则、性能与跨平台实战

字符串比较的 3 个层次

比较方式 API 等价准则 复杂度 备注
字符相等 “==” 扩展字形簇 canonically equivalent O(n) 最常用
前缀 hasPrefix(:) UTF-8 字节逐段比较 O(m) m=前缀长度
后缀 hasSuffix(:) 同上,从后往前 O(m) 注意字形簇边界

示例

let precomposed = "café"                    // U+00E9
let decomposed  = "cafe\u{301}"             // e + ́
print(precomposed == decomposed)            // true ✅ 字形簇等价

let aEnglish = "A"  // U+0041
let aRussian = "А"  // U+0410 Cyrillic
print(aEnglish == aRussian)                 // false ❌ 视觉欺骗

Unicode 正规化(Normalization)

有时需要把“视觉上一样”的字符串统一到同一二进制形式,再做哈希或数据库唯一索引。

Swift 借助 Foundation 的 decomposedStringWithCanonicalMapping / precomposedStringWithCanonicalMapping

import Foundation

func normalized(_ s: String) -> String {
    s.decomposedStringWithCanonicalMapping
}

let set: Set<String> = [
    normalized("café"),
    normalized("cafe\u{301}")
]
print(set.count) // 1 ✅ 去重成功

Swift 5.7+ Regex 一站式入门

字面量构建

import RegexBuilder

let mdLink = Regex {
    "["                               // 字面左括号
    Capture { OneOrMore(.any) }       // 链接文字
    "]("
    Capture { OneOrMore(.any) }       // URL
    ")"
}

let text = "见 [官方文档](https://swift.org)。"
if let match = text.firstMatch(of: mdLink) {
    let (whole, title, url) = match.output
    print("文字:\(title)  地址:\(url)")
}

性能提示

  • 字面量 Regex 在编译期构建,零运行时解析成本;
  • 捕获组数量 < 5 时,使用静态 Output 类型,无堆分配。

切片 + 区间:一次遍历提取所有信息

需求:把 “/api/v1/users/9527” 拆成版本号与 ID

let path = "/api/v1/users/9527"
// 1. 找到两个数字区间
let versionRange = path.firstRange(of: /v\d+/)!          // Swift 5.7  Regex 作为区间
let idRange      = path.firstRange(of: /\d+$/)!

// 2. 切片
let version = path[versionRange]  // "v1"
let userID  = path[idRange]       // "9527"

关键点:

  • path[range] 返回 Substring,长期存需 String(...)
  • 正则区间可链式调用,避免多次扫描。

性能 Benchmark(M4 MacBook Pro, Release 构建)

测试 1:100 万次 “==” 比较

import QuartzCore
func measure(action: () -> Void) {
    let startTimeinterval = CACurrentMediaTime()
    action()
    let endTimeinterval = CACurrentMediaTime()
    print((endTimeinterval - startTimeinterval) * 1_000)
}

let s1 = "Swift字符串性能测试"
let s2 = "Swift字符串性能测试"

measure { for _ in 0..<1_000_000 { _ = s1 == s2 } }
//  耗时  0.0025 ms

测试 2:100 万次 hasPrefix

measure { for _ in 0..<1_000_000 { _ = s1.hasPrefix("Swift") } }
//  耗时  76 ms

测试 3:提取 Markdown 链接 10 万次

let blog = String(repeating: "见 [官方文档](https://swift.org)。\n", count: 10_000)
measure { _ = blog.matches(of: mdLink).map { $0.output } }
//  median  12 ms

结论:

  • 比较操作已高度优化,可放心用于字典 Key;
  • 正则采用静态构建后,与手写 Scanner 差距 < 5%。

常见“坑”与诊断工具

场景 现象 工具/修复
Substring 泄漏 百万行日志内存暴涨 Instruments → Allocations → 查看 “Swift String” 的 CoW 备份
整数下标越界 运行时 crash 使用 index(_, offsetBy:, limitedBy:) 安全版
正则回溯爆炸 卡住 100% CPU Regex 内使用 Possessive 量词或 OneOrMore(..., .eager)
比较失败 “é” != “é” 检查是否混入 Cyrillic / Greek 等视觉同形字符;打印 unicodeScalars 调试

终极最佳实践清单

  1. 比较:优先用 “==”,必要时先正规化再哈希。
  2. 前缀/后缀:用 hasPrefix / hasSuffix,别手写 prefix() 再比较。
  3. 索引:永远通过 String.Index 计算,禁止 str[Int]
  4. 子串:函数返回前立即 String(substring),防止隐式内存泄漏。
  5. 拼接:大量小字符串先用 [String] 收集,最后 joined();或 reserveCapacity 预分配。
  6. 正则:静态字面量 Regex 性能最佳;捕获组能少就少。
  7. 遍历:
    • 看“人眼字符”→ for ch in string
    • 看“UTF-8 字节”→ string.utf8
    • 看“Unicode 标量”→ string.unicodeScalars
  8. 多线程:String 是值类型,跨线程传递无数据竞争,但共享大字符串时 Substring 会拖住原内存,及时转存。
  9. 日志 / 模板:多行字面量 + 插值最清晰;需要原始反斜杠用扩展分隔符 #"..."#
  10. 性能测量:用 swift test -c release + measure 块, Instruments 只看 “Swift String” 的 CoW 备份次数。

Swift 字符串与字符完全导读(二):Unicode 视图、索引系统与内存陷阱

Unicode 的三种编码视图

Swift 把同一个 String 暴露成 4 种迭代方式:

视图 元素类型 单位长度 典型用途
String Character 人眼“一个字符” 业务逻辑
utf8 UInt8 1~4 字节 网络/文件 UTF-8 流
utf16 UInt16 2 或 4 字节 与 Foundation / Objective-C 交互
unicodeScalars UnicodeScalar 21-bit 精确到标量,做编码分析

代码一览

let dog = "Dog‼🐶"          
// 4 个 Character,5 个标量,10 个 UTF-8 字节,6 个 UTF-16 码元

// 1. Character 视图
for ch in dog {
    print(ch, terminator: "|")
}   // D|o|g|‼|🐶|
print()

// 2. UTF-8
dog.utf8.forEach {
    print($0, terminator: " ")
}// 68 111 103 226 128 188 240 159 144 182
print()

// 3. UTF-16
dog.utf16.forEach {
    print($0, terminator: " ")
}// 68 111 103 8252 55357 56374
print()

// 4. Unicode Scalars
dog.unicodeScalars.forEach {
    print($0.value, terminator: " ")
}// 68 111 103 8252 128054
print()

扩展字形簇 vs 字符计数

var cafe = "cafe"
print(cafe.count)           // 4
cafe += "\u{301}"           // 附加组合重音
print(cafe, cafe.count)     // café 4  (仍然是 4 个 Character)

结论:

  • count 走的是“字形簇”边界,必须从头扫描,复杂度 O(n)。
  • 不要在大循环里频繁读取 str.count;缓存到局部变量。

String.Index 体系

基础 API

let str = "Swift🚀"
let start = str.startIndex
let end   = str.endIndex        // 指向最后一个字符之后
// let bad = str[7]             // ❌ Compile-time error:Index 不是 Int

let fifth = str.index(start, offsetBy: 5)
print(str[fifth])             // 🚀

往前/后偏移

let prev = str.index(before: fifth)
let next = str.index(after: start)
let far  = str.index(start, offsetBy: 4, limitedBy: end) // 安全版,返回可选值

区间与切片

let range = start...fifth
let sub = str[range]            // Substring

子串 Substring 的“零拷贝”双刃剑

let article = "Swift String 深度指南"
let intro   = article.prefix(9) // Substring
// 此时整份 article 的缓冲区仍被 intro 强引用,内存不会释放

// 正确姿势:尽快转成 String
let introString = String(intro)

内存图简述

article ┌-------------------------┐
        │ Swift String 深度指南     │
        └-------------------------┘
          ▲
          │零拷贝
       intro (Substring)

只要 Substring 活着,原 String 的缓冲区就不得释放。

最佳实践:函数返回时立刻 String(substring),避免“隐形内存泄漏”。

插入、删除、Range 替换全 API 速查

var s = "Hello Swift"

// 插入字符
s.insert("!", at: s.endIndex)
// Hello Swift!
print(s)

// 插入字符串
s.insert(contentsOf: " 2025", at: s.index(before: s.endIndex))
// Hello Swift 2025!
print(s)

// 删除单个字符
s.remove(at: s.firstIndex(of: " ")!)        // 删掉第一个空格
// HelloSwift 2025!
print(s)

// 删除子范围
let range = s.range(of: "Swift")!
s.removeSubrange(range)
// Hello 2025!
print(s)

// 直接替换
s.replaceSubrange(s.range(of: "2025")!, with: "2026")
// Hello 2026!
print(s)

实战:写一个“安全截断”函数

需求

  • 按“字符数”截断,但不能把 Emoji/组合音标劈成两半;
  • 尾部加“...”且总长度不超过 maxCount;
  • 返回 String,而非 Substring。

代码

func safeTruncate(_ text: String, maxCount: Int, suffix: String = "...") -> String {
    guard maxCount > suffix.count else { return suffix }
    let maxTextCount = maxCount - suffix.count
    var count = 0
    var idx = text.startIndex
    while idx < text.endIndex && count < maxTextCount {
        idx = text.index(after: idx)
        count += 1
    }
    // 如果原文很短,无需截断
    if idx == text.endIndex { return text }
    return String(text[..<idx]) + suffix
}

// 测试
let long = "Swift 字符串深度指南🚀🚀🚀"
print(safeTruncate(long, maxCount: 12)) // "Swift 字符..."

复杂度 O(n),只扫描一次;不依赖 count 的重复计算。

性能与内存最佳实践清单

  1. 大量拼接用 String.reserveCapacity(_:) 预分配。
  2. 遍历+修改时先复制到 var,再批量改,减少中间临时对象。
  3. 网络/文件 IO 用 utf8 视图直接写入 Data,避免先转 String
  4. 正则提取到的 [Substring] 尽快 map 成 [String] 再长期持有。
  5. 不要缓存 str.count 在多次循环外,如果字符串本身在变。

扩展场景:今天就能落地的 3 段代码

日志脱敏(掩码手机号)

func maskMobile(_ s: String) -> String {
    guard s.count == 11 else { return s }
    let start = s.index(s.startIndex, offsetBy: 3)
    let end   = s.index(s.startIndex, offsetBy: 7)
    return s.replacingCharacters(in: start..<end, with: "****")
}

语法高亮(简易关键词着色)

let keywords = ["let", "var", "func"]
var code = "let foo = 1"
for kw in keywords {
    if let range = code.range(of: kw) {
        code.replaceSubrange(range, with: "[KW]\(kw)[KW]")
    }
}

大文件分块读(UTF-8 视图直接操作)

import Foundation
func chunk(path: String, chunkSize: Int = 1<<14) -> [String] {
    guard let data = FileManager.default.contents(atPath: path) else { return [] }
    return data.split(separator: UInt8(ascii: "\n"),
                      maxSplits: .max,
                      omittingEmptySubsequences: false)
               .map { String(decoding: $0, as: UTF8.self) }
}

利用 UInt8 切片,避免先整体转成 String 的额外内存峰值。

Swift 字符串与字符完全导读(一):从字面量到 Unicode 的实战之旅

前言

Swift 的 String 看起来“像 NSString 的弟弟”,但骨子里是一套全新的 Unicode 抽象模型。

String 与 Character 的本质

  • String:由“扩展字形簇”(extended grapheme cluster)构成的有序集合。
  • Character:一个扩展字形簇,人类眼中的“一个字符”,占用的字节数可变。
// 1 个 Character,由 2 个 Unicode 标量合成
let eAcute: Character = "é"                 // “é”
let eCombining: Character = "\u{65}\u{301}" // “e” + “́” 组合
print(eAcute == eCombining) // true,两者字形簇等价

字符串字面量:单行、多行、转义、扩展分隔符

单行字面量

let msg = "Hello, Swift" // 类型自动推断为 String

多行字面量

let html = """
           <div>
               <p>Hello</p>
           </div>
           """ // 缩进 4 空格会被自动去掉,因为闭合 """ 在最左侧第 12 列
           // 闭合"""左侧的空格会被删除,每一行左侧的同等长度的空格都会被删除

换行控制技巧

let sql = """
          SELECT * FROM user \
          WHERE age > 18
          """ // 反斜杠让源码换行,但字符串里无换行

转义序列

let special = "双引号:\",制表:\t,换行:\n,Unicode:\u{1F496}"

扩展分隔符(#)

场景:正则、JSON 模板里想保留原始反斜杠。

let raw = #"Raw \n still two characters"#
let needEscape = #"Use \#n to enable line break"#
print(raw)       // 输出:Raw \n still two characters
print(needEscape)// 输出:Use
                 //       to enable line break

多行 + 扩展分隔符

let mlRaw = #"""
            Line 1
            Line 2
            """#

空字符串的 3 种创建方式

let a = ""
let b = String()
let c = String("") // 与前两种等价
print(a.isEmpty) // true

可变性:let 与 var 的抉择

let immutable = "can't change"
// immutable += "!" // ❌ Compile-time error

var mutable = "hello"
mutable += ", world" // ✅

值类型:写时复制(COW)到底发生了什么

func foo(_ s: String) {
    var local = s   // 此时未复制,共享同一块缓冲区
    local += "!"    // 突变触发复制,O(n) 成本
    print(local)
}

底层优化:仅当本地突变或跨线程时才真正拷贝,因此作为入参传递时无需担心性能。

字符集合:遍历、提取、构造

let word = "Swift"
for ch in word {
    print(ch, terminator: "-") // S-w-i-f-t-
}

let single: Character = "A"
let fromChars = String([single, "B", "C"]) // "ABC"

字符串拼接的 5 种姿势

let left  = "Hello"
let right = "World"

// 1. 加法
let s1 = left + ", " + right

// 2. 加法赋值
var s2 = left
s2 += ", " + right

// 3. append(Character)
var s3 = left
s3.append(",")
s3.append(" ")
s3.append(Character(right)) // 仅当 right 长度=1 时安全

// 4. append(contentsOf: String)
var s4 = left
s4.append(contentsOf: ", \(right)")

// 5. 多行拼接注意最后一行换行
let goodStart = """
                Line 1
                Line 2
                """
let end       = """
                Line 3
                """
let merged = goodStart + end // 3 行,无意外合并

字符串插值:最灵活的“模板引擎”

let name = "Swift"
let year = 2025
let msg = "Hello, \(name)! In \(year + 1) we will rock."
print(msg) // Hello, Swift! In 2026 we will rock.

// 在扩展分隔符中使用插值
let rawLog = #"Level \#(name) recorded at \#(Date())"#

扩展场景:今天就能用上的 3 个小工具

彩色命令行日志

func log(_ info: String) {
    print(#"\u{1B}[32m[INFO]\#(info)\u{1B}[0m"#)
}
log("Server started") // 终端绿色输出

快速 Mock JSON

let userId = 9527
let json = #"""
           {"id": \#(userId), "name": "Alice"}
           """#
print(json) // 直接贴进 Postman 即可

多行 SQL 模板

let table = "user"
let sql = """
          SELECT *
          FROM \(table)
          WHERE status = 'active'
            AND created_at > ?
          """

你的错误处理一团糟-是时候修复它了-🛠️

GitHub 主页

你的错误处理一团糟,是时候修复它了!🛠️

我还记得那个让我彻夜难眠的 bug。一个支付回调接口,在处理一个罕见的、来自第三方支付网关的异常状态码时,一个Promise链中的.catch()被无意中遗漏了。结果呢?没有日志,没有警报,服务本身也没有崩溃。它只是“沉默地”失败了。那个用户的订单状态永远停留在了“处理中”,而我们,对此一无所知。直到一周后,在对账时我们才发现,有数百个这样的“沉默订单”,造成了数万美元的损失。💸

这个教训是惨痛的。它让我明白,在软件工程中,我们花在处理成功路径上的时间,可能还不到 10%。剩下 90%的复杂性,都来自于如何优雅、健壮地处理各种预料之中和意料之外的错误。 而一个框架的优劣,很大程度上就体现在它如何引导我们去面对这个“错误的世界”。

很多框架,尤其是那些动态语言的“灵活”框架,它们在错误处理上的哲学,几乎可以说是“放任自流”。它们给了你一万种犯错的可能,却只给了你一种需要极度自律才能做对的方式。

回调地狱与被吞噬的Promise:JavaScript 的错误处理之殇

在 Node.js 的世界里,我们经历了一场漫长的、与错误作斗争的进化史。

阶段一:回调地狱 (Callback Hell)

老一辈的 Node.js 开发者都还记得被“金字塔”支配的恐惧。

function processOrder(orderId, callback) {
  db.findOrder(orderId, (err, order) => {
    if (err) {
      // 错误处理点 1
      return callback(err);
    }
    payment.process(order, (err, result) => {
      if (err) {
        // 错误处理点 2
        return callback(err);
      }
      inventory.update(order.items, (err, status) => {
        if (err) {
          // 错误处理点 3
          return callback(err);
        }
        callback(null, status); // 成功!
      });
    });
  });
}

这种“错误优先”的回调风格,在理论上是可行的。但随着业务逻辑的复杂化,代码会向右无限延伸,形成一个难以维护的“死亡金字塔”。你必须在每一个回调里,都记得去检查那个err对象。只要有一次疏忽,错误就会被“吞掉”。

阶段二:Promise的救赎与新的陷阱

Promise的出现,把我们从回调地狱中解救了出来。我们可以用.then().catch()来构建一个更扁平、更易读的异步链。

function processOrder(orderId) {
  return db
    .findOrder(orderId)
    .then((order) => payment.process(order))
    .then((result) => inventory.update(result.items))
    .catch((err) => {
      // 统一的错误处理点
      console.error('Order processing failed:', err);
      // 但这里,你必须记得向上抛出错误,否则调用者会认为成功了
      throw err;
    });
}

这好多了!但新的问题又来了。如果你在一个.then()里忘记了return下一个Promise,或者在一个.catch()里忘记了重新throw错误,这个链条就会以一种你意想不到的方式继续执行下去。错误,再一次被“沉默地”吞噬了。

阶段三:async/await的优雅与最后的伪装

async/await让我们能用看似同步的方式来编写异步代码,这简直是天赐的礼物。

async function processOrder(orderId) {
  try {
    const order = await db.findOrder(orderId);
    const result = await payment.process(order);
    const status = await inventory.update(result.items);
    return status;
  } catch (err) {
    console.error('Order processing failed:', err);
    throw err;
  }
}

这看起来已经很完美了,不是吗?但它依然依赖于程序员的“自觉”。你必须记得把所有可能出错的异步调用都包在一个try...catch块里。如果你忘了await一个返回Promise的函数呢?那个函数里的错误将永远不会被这个try...catch捕获。

JavaScript 的问题在于,错误是一个可以被轻易忽略的值nullundefined可以像幽灵一样在你的代码里游荡。你需要依靠严格的规范、Linter 工具和个人纪律,才能确保每一个错误都被正确处理。而这,恰恰是不可靠的。

Result枚举:当编译器成为你最可靠的错误处理伙伴

现在,让我们进入 Rust 和 hyperlane 的世界。在这里,错误处理的哲学是完全不同的。Rust 语言的核心,有一个叫做Result<T, E>的枚举类型。

enum Result<T, E> {
   Ok(T),  // 代表成功,并包含一个值
   Err(E), // 代表失败,并包含一个错误
}

这个设计,简单而又深刻。它意味着一个可能失败的函数,它的返回值必然是这两种状态之一。它不再是一个可能为null的值,或者一个需要你在别处.catch()Promise。它是一个完整的、包含了所有可能性的类型。

最关键的是,编译器会强制你处理Err的情况。如果你调用一个返回Result的函数,却不处理它的Err分支,编译器会直接给你一个警告甚至错误。你不可能“不小心”忽略一个错误。

让我们看看在 hyperlaneservice 层,代码会是什么样子:

// 在一个 service 文件中
pub fn process_order(order_id: &str) -> Result<Status, OrderError> {
    let order = db::find_order(order_id)?; // `?` 操作符:如果失败,立即返回Err
    let result = payment::process(order)?;
    let status = inventory::update(result.items)?;
    Ok(status) // 明确返回成功
}

看到那个?操作符了吗?它是 Rust 错误处理的精髓。它相当于在说:“调用这个函数,如果它返回Ok(value),就把value取出来继续执行;如果它返回Err(error),就立刻从当前函数返回这个Err(error)。”

这种模式,把之前 JavaScript 中需要try...catch才能实现的逻辑,变成了一种极其简洁、清晰、且由编译器保证安全的链式调用。错误,不再是需要被“捕获”的异常,而是数据流中一个可预期的、被优雅处理的分支。

panic_hook:最后的防线

当然,总有一些错误是我们无法预料的,也就是panic(恐慌)。比如数组越界、整数溢出等。在很多框架中,一个未被捕获的panic会导致整个线程甚至进程崩溃。

hyperlane 提供了一个优雅的“最后防线”——panic_hook。我们在之前的文章中已经见过它的身影:

async fn panic_hook(ctx: Context) {
    let error: Panic = ctx.try_get_panic().await.unwrap_or_default();
    let response_body: String = error.to_string();
    eprintln!("{}", response_body); // 记录详细的错误日志

    // 向客户端返回一个标准的、安全的 500 错误响应
    let _ = ctx
        .set_response_status_code(500)
        .await
        .set_response_body("Internal Server Error")
        .await
        .send()
        .await;
}

// 在 main 函数中注册它
server.panic_hook(panic_hook).await;

这个钩子能捕获任何在请求处理过程中发生的panic。它能防止服务器直接崩溃,并允许你记录下详细的错误信息用于事后分析,同时给客户端返回一个友好的错误页面,而不是一个断开的连接。这是一种极其负责任和健壮的设计。

别再祈祷代码不出错了,从一开始就拥抱错误

好的错误处理,不是在代码的各个角落里都塞满try...catch。它是从语言和框架层面,就为你提供一套机制,让“失败”成为程序流程中一个可预期的、一等公民。

Rust 的Result枚举强迫你直面每一个可能的失败,而hyperlane的架构和钩子系统则为你提供了处理这些失败的优雅模式。它把错误处理从一种“开发者纪律”,变成了一种“编译器保证”。

所以,如果你还在为那混乱的错误处理逻辑而头痛,为那些“沉默”的失败而恐惧,那么问题可能真的不在于你不够努力,而在于你选择的工具,从一开始就没有把“健壮性”放在最重要的位置。是时候,选择一个能和你并肩作战,共同面对这个不完美世界的伙伴了。

GitHub 主页

❌