普通视图

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

30 Apps 第 1 天:待办清单 App —— 数据层完整设计

作者 冰凌时空
2026年4月21日 09:33

专栏:iOS功能实战30Days
编号:B01 · 系列第 1 篇
字数:约 5500 字
标签:iOS / SwiftUI / SQLite / Repository 模式 / 数据持久化


前言

从今天开始,我们开启一个新的系列:30 Apps,30 天,30 个真实可上线的 iOS 功能

第一天,我们从一个最简单的 App 入手:待办清单(Todo List)。但别被「待办清单」这个名字骗了——这个 App 的数据层设计,足以应对一个中等规模 App 的所有持久化需求。

我们将完成:

  1. 持久化方案选型:SQLite.swift / Realm / Core Data 对比
  2. 数据模型设计:Task 的完整结构
  3. Repository 模式:解耦数据层与业务层
  4. 数据库迁移策略:Schema 演进的最佳实践
  5. 完整的 SQLite.swift 封装:可直接复用到任何项目

一、项目概述与功能需求

1.1 我们要做什么

待办清单 App 的核心功能:

  • 创建、编辑、删除待办事项
  • 标记完成/未完成
  • 按优先级排序
  • 按分类筛选
  • 搜索功能
  • 数据持久化存储

1.2 数据模型

struct Task: Identifiable, Codable, Equatable {
    var id: UUID
    var title: String
    var content: String          // 详细描述
    var priority: Priority       // 优先级
    var status: Status           // 完成状态
    var category: Category       // 分类
    var dueDate: Date?           // 截止日期
    var createdAt: Date
    var updatedAt: Date
    var completedAt: Date?       // 完成时间
    var isPinned: Bool           // 置顶

    enum Priority: Int, Codable, CaseIterable {
        case low = 0
        case medium = 1
        case high = 2
    }

    enum Status: Int, Codable {
        case pending = 0
        case completed = 1
    }

    enum Category: String, Codable, CaseIterable {
        case work = "work"
        case life = "life"
        case study = "study"
        case health = "health"
    }
}

二、持久化方案选型

2.1 主流方案对比

维度 SQLite.swift Realm Core Data UserDefaults
适用数据量 10万+ 条 10万+ 条 10万+ 条 < 1000 条
关系查询 支持 JOIN 支持 支持 不支持
线程安全 需要小心处理 自动线程安全 需要小心处理 主线程
学习曲线
包体积 ~2MB ~30MB 内置
Swift 友好度 极高 简单
Schema 迁移 手动 自动 复杂
实时通知 无(需手动轮询) 有(NSFetchedResultsController)

2.2 我们的选择:SQLite.swift

选择 SQLite.swift 的理由:

  1. 包体积小:2MB,对 App 大小影响可忽略
  2. 性能优秀:原生 C 实现,比 ORM 快 10x
  3. 灵活性强:SQL 查询解决所有复杂查询场景
  4. Swift 原生:API 设计风格接近 Swift 标准库
  5. 无供应商绑定:纯 SQLite,不依赖任何框架
  6. 学习价值:理解 SQL 是每个工程师的必修课

三、项目搭建

3.1 创建项目

使用 Xcode 创建新的 SwiftUI 项目,命名为 TodoApp

3.2 添加依赖

使用 Swift Package Manager 添加 SQLite.swift:

// 在 Xcode 中:File → Add Package Dependencies
// 输入:https://github.com/stephencelis/SQLite.swift
// 选择最新版本(>= 0.15.0)

3.3 项目结构

TodoApp/
├── App/
│   └── TodoAppApp.swift
├── Models/
│   └── Task.swift
├── Data/
│   ├── Database/
│   │   ├── DatabaseManager.swift      // 数据库初始化
│   │   └── TaskTable.swift            // Task 表定义
│   └── Repositories/
│       └── TaskRepository.swift        // 数据访问层
├── ViewModels/
│   └── TaskListViewModel.swift
├── Views/
│   ├── ContentView.swift
│   ├── TaskRowView.swift
│   └── TaskEditorView.swift
└── Extensions/
    └── Date+Extensions.swift

四、数据库层实现

4.1 数据库管理器

import Foundation
import SQLite

final class DatabaseManager {
    static let shared = DatabaseManager()

    private(set) var db: Connection!

    private init() {}

    func setup() throws {
        let path = try FileManager.default
            .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            .appendingPathComponent("todo.sqlite3")
            .path

        db = try Connection(path)

        // 启用外键约束
        try db.execute("PRAGMA foreign_keys = ON;")

        // 初始化表
        try TaskTable.create(db: db)
    }

    func resetDatabase() throws {
        try db.execute("DELETE FROM tasks")
        try db.execute("VACUUM")
    }
}

4.2 表定义

import Foundation
import SQLite

enum TaskTable {
    static let table = Table("tasks")

    // 列定义
    static let id = Expression<String>("id")
    static let title = Expression<String>("title")
    static let content = Expression<String?>("content")
    static let priority = Expression<Int>("priority")
    static let status = Expression<Int>("status")
    static let category = Expression<String>("category")
    static let dueDate = Expression<Double?>("due_date")
    static let createdAt = Expression<Double>("created_at")
    static let updatedAt = Expression<Double>("updated_at")
    static let completedAt = Expression<Double?>("completed_at")
    static let isPinned = Expression<Bool>("is_pinned")

    static func create(db: Connection) throws {
        try db.run(table.create(ifNotExists: true) { t in
            t.column(id, primaryKey: true)
            t.column(title)
            t.column(content)
            t.column(priority, defaultValue: 1)
            t.column(status, defaultValue: 0)
            t.column(category, defaultValue: "work")
            t.column(dueDate)
            t.column(createdAt)
            t.column(updatedAt)
            t.column(completedAt)
            t.column(isPinned, defaultValue: false)
        })

        // 创建索引,加速常见查询
        try db.run(table.createIndex(status, ifNotExists: true))
        try db.run(table.createIndex(priority, ifNotExists: true))
        try db.run(table.createIndex(category, ifNotExists: true))
        try db.run(table.createIndex(createdAt, ifNotExists: true))
        try db.run(table.createIndex(isPinned, ifNotExists: true))
    }

    // 从数据库行映射到模型
    static func rowToTask(_ row: Row) -> Task {
        Task(
            id: UUID(uuidString: row[id]) ?? UUID(),
            title: row[title],
            content: row[content],
            priority: Task.Priority(rawValue: row[priority]) ?? .medium,
            status: Task.Status(rawValue: row[status]) ?? .pending,
            category: Task.Category(rawValue: row[category]) ?? .work,
            dueDate: row[dueDate].map { Date(timeIntervalSince1970: $0) },
            createdAt: Date(timeIntervalSince1970: row[createdAt]),
            updatedAt: Date(timeIntervalSince1970: row[updatedAt]),
            completedAt: row[completedAt].map { Date(timeIntervalSince1970: $0) },
            isPinned: row[isPinned]
        )
    }
}

五、Repository 模式实现

5.1 为什么需要 Repository

┌─────────────┐     ┌──────────────┐     ┌────────────────┐
│   Views     │────▶│  ViewModels  │────▶│  Repository    │
│  (SwiftUI)  │     │ (Combine)    │     │  (TaskRepository) │
└─────────────┘     └──────────────┘     └───────┬────────┘
                                                  │
                                           ┌──────▼────────┐
                                           │  DatabaseManager│
                                           │  (SQLite.swift) │
                                           └───────┬────────┘
                                                   │
                                           ┌──────▼────────┐
                                           │   todo.sqlite3 │
                                           └───────────────┘

Repository 模式的优势:

  1. 数据源可替换:可以从 SQLite 切换到 Core Data,不需要修改任何业务代码
  2. 测试方便:Mock Repository 不需要真实的数据库
  3. 职责单一:ViewModel 只关心业务逻辑,Repository 只关心数据访问
  4. 复用性:同一个 Repository 可被多个 ViewModel 使用

5.2 Repository 接口设计

import Foundation
import Combine

protocol TaskRepositoryProtocol {
    // CRUD 操作
    func fetchAllTasks() async throws -> [Task]
    func fetchTask(by id: UUID) async throws -> Task?
    func insertTask(_ task: Task) async throws
    func updateTask(_ task: Task) async throws
    func deleteTask(by id: UUID) async throws
    func deleteAllCompletedTasks() async throws -> Int

    // 高级查询
    func fetchTasks(
        status: Task.Status?,
        category: Task.Category?,
        searchQuery: String?,
        sortBy: SortOption
    ) async throws -> [Task]

    // 聚合查询
    func countTasks(status: Task.Status?) async throws -> Int
    func fetchOverdueTasks() async throws -> [Task]
}

enum SortOption: String, CaseIterable {
    case createdDesc = "最新创建"
    case createdAsc = "最早创建"
    case priorityDesc = "优先级最高"
    case dueDateAsc = "截止日期最近"
    case dueDateDesc = "截止日期最远"
}

5.3 Repository 实现

final class TaskRepository: TaskRepositoryProtocol {
    private let db: Connection

    init(db: Connection = DatabaseManager.shared.db) {
        self.db = db
    }

    // MARK: - CRUD

    func fetchAllTasks() async throws -> [Task] {
        let rows = try db.prepare(TaskTable.table)
        return rows.map { TaskTable.rowToTask($0) }
    }

    func fetchTask(by id: UUID) async throws -> Task? {
        let query = TaskTable.table.filter(TaskTable.id == id.uuidString)
        guard let row = try db.pluck(query) else {
            return nil
        }
        return TaskTable.rowToTask(row)
    }

    func insertTask(_ task: Task) async throws {
        try db.run(TaskTable.table.insert(
            TaskTable.id <- task.id.uuidString,
            TaskTable.title <- task.title,
            TaskTable.content <- task.content,
            TaskTable.priority <- task.priority.rawValue,
            TaskTable.status <- task.status.rawValue,
            TaskTable.category <- task.category.rawValue,
            TaskTable.dueDate <- task.dueDate?.timeIntervalSince1970,
            TaskTable.createdAt <- task.createdAt.timeIntervalSince1970,
            TaskTable.updatedAt <- task.updatedAt.timeIntervalSince1970,
            TaskTable.completedAt <- task.completedAt?.timeIntervalSince1970,
            TaskTable.isPinned <- task.isPinned
        ))
    }

    func updateTask(_ task: Task) async throws {
        let target = TaskTable.table.filter(TaskTable.id == task.id.uuidString)
        try db.run(target.update(
            TaskTable.title <- task.title,
            TaskTable.content <- task.content,
            TaskTable.priority <- task.priority.rawValue,
            TaskTable.status <- task.status.rawValue,
            TaskTable.category <- task.category.rawValue,
            TaskTable.dueDate <- task.dueDate?.timeIntervalSince1970,
            TaskTable.updatedAt <- task.updatedAt.timeIntervalSince1970,
            TaskTable.completedAt <- task.completedAt?.timeIntervalSince1970,
            TaskTable.isPinned <- task.isPinned
        ))
    }

    func deleteTask(by id: UUID) async throws {
        let target = TaskTable.table.filter(TaskTable.id == id.uuidString)
        try db.run(target.delete())
    }

    func deleteAllCompletedTasks() async throws -> Int {
        let query = TaskTable.table.filter(TaskTable.status == Task.Status.completed.rawValue)
        return try db.run(query.delete())
    }

    // MARK: - 高级查询

    func fetchTasks(
        status: Task.Status? = nil,
        category: Task.Category? = nil,
        searchQuery: String? = nil,
        sortBy: SortOption = .createdDesc
    ) async throws -> [Task] {
        var query = TaskTable.table

        // 动态添加过滤条件
        if let status {
            query = query.filter(TaskTable.status == status.rawValue)
        }
        if let category {
            query = query.filter(TaskTable.category == category.rawValue)
        }
        if let searchQuery, !searchQuery.isEmpty {
            let pattern = "%\(searchQuery)%"
            query = query.filter(TaskTable.title.like(pattern) || TaskTable.content.like(pattern))
        }

        // 排序:置顶任务始终在最前
        query = query.order(
            TaskTable.isPinned.desc,
            sortExpression(for: sortBy)
        )

        return try db.prepare(query).map { TaskTable.rowToTask($0) }
    }

    private func sortExpression(for option: SortOption) -> Expression<Double> {
        switch option {
        case .createdDesc: return TaskTable.createdAt.desc
        case .createdAsc: return TaskTable.createdAt.asc
        case .priorityDesc: return TaskTable.priority.desc
        case .dueDateAsc: return TaskTable.dueDate.asc
        case .dueDateDesc: return TaskTable.dueDate.desc
        }
    }

    // MARK: - 聚合查询

    func countTasks(status: Task.Status?) async throws -> Int {
        var query = TaskTable.table
        if let status {
            query = query.filter(TaskTable.status == status.rawValue)
        }
        return try db.scalar(query.count)
    }

    func fetchOverdueTasks() async throws -> [Task] {
        let now = Date().timeIntervalSince1970
        let query = TaskTable.table
            .filter(TaskTable.status == Task.Status.pending.rawValue)
            .filter(TaskTable.dueDate < now)
            .order(TaskTable.dueDate.asc)

        return try db.prepare(query).map { TaskTable.rowToTask($0) }
    }
}

六、数据库迁移策略

6.1 为什么需要迁移策略

App 发布后,用户会升级到新版本。如果新版本修改了数据库结构(增加列、修改类型、创建新表),直接升级会导致老用户的数据丢失或崩溃。

6.2 轻量级迁移方案

final class DatabaseMigration {
    private let db: Connection
    private let versionKey = "database_version"
    private let currentVersion = 1

    init(db: Connection) {
        self.db = db
    }

    func migrate() throws {
        let storedVersion = UserDefaults.standard.integer(forKey: versionKey)

        guard storedVersion < currentVersion else { return }

        if storedVersion < 1 {
            try migrateToV1()
        }

        // 未来新版本的迁移写在这里
        // if storedVersion < 2 {
        //     try migrateToV2()
        // }

        UserDefaults.standard.set(currentVersion, forKey: versionKey)
    }

    private func migrateToV1() throws {
        // V1: 添加 isPinned 列(如果不存在)
        // 注意:SQLite 不支持 ADD COLUMN IF NOT EXISTS 语法
        // 我们用 PRAGMA table_info 来检查列是否存在
        let columns = try db.prepare("PRAGMA table_info(tasks)").map { $0[1] as! String }
        if !columns.contains("is_pinned") {
            try db.execute("ALTER TABLE tasks ADD COLUMN is_pinned INTEGER DEFAULT 0")
        }

        // V1: 添加 completedAt 列
        if !columns.contains("completed_at") {
            try db.execute("ALTER TABLE tasks ADD COLUMN completed_at REAL")
        }
    }
}

6.3 集成迁移

DatabaseManager.setup() 中集成迁移:

func setup() throws {
    let path = try FileManager.default
        .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent("todo.sqlite3")
        .path

    db = try Connection(path)
    try db.execute("PRAGMA foreign_keys = ON;")
    try TaskTable.create(db: db)

    // 添加迁移
    try DatabaseMigration(db: db).migrate()
}

七、完整的使用示例

7.1 App 入口集成

@main
struct TodoAppApp: App {
    init() {
        do {
            try DatabaseManager.shared.setup()
        } catch {
            fatalError("Database setup failed: \(error)")
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

7.2 ViewModel 调用

@MainActor
class TaskListViewModel: ObservableObject {
    @Published var tasks: [Task] = []
    @Published var isLoading = false
    @Published var error: Error?

    @Published var selectedStatus: Task.Status?
    @Published var selectedCategory: Task.Category?
    @Published var searchQuery = ""
    @Published var sortOption: SortOption = .createdDesc

    private let repository: TaskRepositoryProtocol

    init(repository: TaskRepositoryProtocol = TaskRepository()) {
        self.repository = repository
    }

    func loadTasks() async {
        isLoading = true
        defer { isLoading = false }

        do {
            tasks = try await repository.fetchTasks(
                status: selectedStatus,
                category: selectedCategory,
                searchQuery: searchQuery.isEmpty ? nil : searchQuery,
                sortBy: sortOption
            )
        } catch {
            self.error = error
        }
    }

    func createTask(_ task: Task) async {
        do {
            try await repository.insertTask(task)
            await loadTasks()
        } catch {
            self.error = error
        }
    }

    func toggleTaskStatus(_ task: Task) async {
        var updated = task
        updated.status = task.status == .pending ? .completed : .pending
        updated.completedAt = updated.status == .completed ? Date() : nil
        updated.updatedAt = Date()

        do {
            try await repository.updateTask(updated)
            await loadTasks()
        } catch {
            self.error = error
        }
    }

    func deleteTask(_ task: Task) async {
        do {
            try await repository.deleteTask(by: task.id)
            await loadTasks()
        } catch {
            self.error = error
        }
    }

    func deleteAllCompleted() async {
        do {
            let count = try await repository.deleteAllCompletedTasks()
            print("Deleted \(count) completed tasks")
            await loadTasks()
        } catch {
            self.error = error
        }
    }
}

八、今天的代码架构总结

数据层架构
│
├── DatabaseManager (单例,数据库连接管理)
│   └── DatabaseMigration (版本迁移管理)
│
├── TaskTable (表定义与行映射)
│   ├── create(): 建表 + 索引
│   └── rowToTask(): Row → Task
│
└── TaskRepository (数据访问层,异步接口)
    ├── fetchAllTasks()
    ├── fetchTasks(status:category:search:sort:)
    ├── insertTask()
    ├── updateTask()
    ├── deleteTask()
    ├── countTasks()
    └── fetchOverdueTasks()

关键设计原则

  1. Repository 抽象数据源:ViewModel 不知道数据来自 SQLite 还是网络
  2. 异步所有 IO 操作:数据库操作绝不阻塞主线程
  3. 类型安全的 SQL:SQLite.swift 的 Expression 防止 SQL 注入
  4. 显式迁移:每个 Schema 变更都有对应的迁移函数
  5. 索引加速查询:status、priority、category、createdAt、isPinned 都有索引

下篇预告

明天我们将完成这个待办清单 App 的 UI 层:使用 SwiftUI + MVVM + Combine 实现完整的响应式界面,包括列表展示、滑动操作、筛选排序等交互。


往期回顾:无(系列第一篇)


如果你完成了今天的代码编写,欢迎在评论区分享你遇到的问题或优化思路。30 天,我们一起坚持。

Swift vs Objective-C:语言设计哲学的全面对比

作者 冰凌时空
2026年4月20日 00:33

专栏:Swift语言精进之路
编号:A01 · 系列第 1 篇
字数:约 5000 字
标签:Swift / Objective-C / iOS / 语言对比 / 底层原理


前言

Swift 和 Objective-C 都是构建 Apple 平台应用的核心语言。前者诞生于 2014 年,后者则从 1980 年代一路走来,支撑了 macOS 和 iOS 生态的黄金二十年。

但很多 iOS 开发者对这两门语言的理解,仅停留在「Swift 更现代、Objective-C 更老」的表面认知上。实际上,两者的差异远比语法更深——它们代表着两种截然不同的语言设计哲学

理解这种哲学差异,不仅能帮助你更好地理解 Swift 的设计决策,更能让你在写代码时做出更明智的选择。


一、历史背景:从两个时代的需求出发

1.1 Objective-C 的诞生

Objective-C 诞生于 1980 年代初,由 Brad Cox 和 Tom Love 基于 Smalltalk 的面向对象思想嫁接到 C 语言之上。

选择这条道路的原因是务实的:

  • 兼容 C:在 Unix 生态中,C 是绝对的主流。Objective-C 可以直接调用任何 C 函数,零成本复用所有 C 库。
  • Smalltalk 的消息机制:借鉴自 Smalltalk 的运行时消息传递,带来了强大的动态特性。
  • 工业级稳定:诞生于军工和电信领域,要求极高的稳定性。

这解释了为什么 Objective-C 的语法看起来如此「奇怪」——[object method:arg] 而不是 object.method(arg),以及 @selector@interface 这些 @ 符号标记,都是历史路径依赖的产物。

1.2 Swift 的诞生

Swift 诞生于 2014 年 WWDC,由 Chris Lattner 领导的 Apple 团队设计。

此时的背景完全不同:

  • 移动互联网时代:App 的安全性、性能、开发效率成为核心矛盾。
  • 多核和并行计算普及:传统消息传递在多核时代暴露出效率问题。
  • 竞争对手的压力:Google 的 Go、JetBrains 的 Kotlin、Facebook 的 Hack 都在快速演进。
  • Apple 生态的统一:需要一个能同时服务于 iOS、macOS、watchOS、tvOS 的语言。

Swift 的设计目标明确:快、安全、现代。这里的「快」不仅指运行时性能,还包括开发速度。「安全」则涵盖了内存安全、类型安全和并发安全三个维度。


二、语法层面的哲学差异

2.1 消息传递 vs 函数调用

这是两者最核心的差异。

Objective-C 使用消息传递

// 实际执行的是 [obj message] 这一行代码
// 编译器将其转换为 objc_msgSend(obj, @selector(message))
// 如果对象没有实现 message,运行时不会崩溃,而是返回 nil 或抛异常
id result = [object doSomethingWith:param];

消息传递的本质是运行时决策。编译器不需要知道 object 的真实类型,方法分派发生在运行时。这意味着:

  • 可以向 nil 发送消息,不会崩溃(返回 0 或 nil)
  • 可以动态替换方法的实现(Method Swizzling)
  • 可以在运行时创建新类、添加方法

Swift 使用函数调用(更接近传统编译型语言)

// 编译器在编译时就决定了方法的调用地址
// 如果类型不匹配,编译直接失败
let result = object.doSomething(with: param)

Swift 的函数调用是编译时决策。编译器通过类型推导确定调用哪个方法,在编译阶段就生成直接的函数调用指令。这意味着:

  • 方法调用没有消息查找的开销
  • 编译器可以做更多优化
  • 类型不匹配会在编译期暴露,而不是运行时

2.2 代码对比:同一个功能

Objective-C 版本

// ViewController.m
@interface ViewController ()
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSArray<NSString *> *items;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 可变字典,键值都必须是对象
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];

    // 语法啰嗦,每个语句都需要分号和括号
    for (NSString *item in self.items) {
        if (item.length > 0) {
            [dict setObject:item forKey:@(dict.count).stringValue];
        }
    }

    // nil 可以安全地参与运算
    NSString *result = [self processData:nil];
    NSLog(@"Result: %@", result); // 输出 "Result: (null)",不会崩溃
}

- (NSString *)processData:(NSString *)input {
    if (input == nil) {
        return nil;
    }
    return [input stringByAppendingString:@"_processed"];
}

@end

Swift 版本

// ViewController.swift
class ViewController: UIViewController {
    var name: String = ""
    var items: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        // 字典有明确的类型约束
        var dict: [String: String] = [:]

        for item in items {
            if !item.isEmpty {
                dict[String(dict.count)] = item
            }
        }

        // nil 必须显式处理,编译器强制要求
        if let result = processData(input: nil) {
            print("Result: \(result)")
        } else {
            print("Result: nil")
        }
    }

    func processData(input: String?) -> String? {
        guard let input else { return nil }
        return input + "_processed"
    }
}

2.3 语法差异一览

特性 Objective-C Swift
方法调用语法 [obj method:arg] obj.method(arg)
空值处理 [obj method] 对 nil 无害 obj.method() 编译期类型检查
字符串 NSString * String(值类型)
数组 NSArray *(引用类型) [Any](值类型,可选泛型)
字典 NSDictionary * [Key: Value]
属性声明 @property (nonatomic, strong) var / let
继承语法 @interface Foo : Bar class Foo: Bar
协议 @protocol Foo <Bar> protocol Foo: Bar
泛型 几乎不支持(NSArray<NSString *> 是特例) 完整泛型支持
枚举 整数或 NS_ENUM 完整类型安全枚举
Block void (^handler)(int) = ^(int x){ } { x in print(x) }

三、类型系统的哲学差异

3.1 Objective-C:编译时宽松,运行时灵活

Objective-C 的类型系统是名义类型系统(Nominal Typing),但约束非常宽松:

// 完全合法的 Objective-C
id anything = @"Hello";  // id 可以指向任何对象
[anything length];       // 编译器信任你,运行时才检查

NSInteger count = 5;    // 基本类型和对象类型是分开的

id 类型的广泛使用,使得 Objective-C 具有极强的动态能力——但代价是大量运行时错误:

// 这样的代码,编译器不会报错,运行时才崩溃
id data = [[NSData alloc] init];
NSString *str = (NSString *)data;
NSLog(@"Length: %lu", (unsigned long)str.length);
// 运行结果:可能 crash,可能输出垃圾值

3.2 Swift:编译时严格,运行时安全

Swift 采用结构化类型系统结合类型推导,编译器尽可能在编译期发现问题:

// Swift 会在这里直接报错,无法编译
let data: Any = "hello"
let str: String = data  // Error: Cannot convert 'Any' to 'String' explicitly

// 必须使用可选绑定或强制转换(都要显式处理)
if let str = data as? String {
    print(str)
}

Swift 还引入了协议组合泛型约束,在保持灵活性的同时不牺牲安全性:

// 泛型约束:T 必须同时遵守 Codable 和 Hashable
func encodeAndHash<T: Codable & Hashable>(_ value: T) -> String {
    let encoder = JSONEncoder()
    let data = try! encoder.encode(value)
    return String(data: data, encoding: .utf8)!
}

// 协议组合:既可以序列化又可以比较
func process<T: Codable & Comparable>(items: [T]) -> [T] {
    return items.sorted()
}

3.3 值类型 vs 引用类型

这是 Swift 最重要的设计决策之一。

Objective-C 几乎一切皆引用

// 数组是引用类型
NSMutableArray *arr1 = [NSMutableArray arrayWithObject:@1];
NSMutableArray *arr2 = arr1;  // 引用拷贝,两个变量指向同一个对象
[arr2 addObject:@2];
NSLog(@"%@", arr1); // 输出 (1, 2) — arr1 也被改了

Swift 大量使用值类型

// 数组是值类型
var arr1 = [1, 2, 3]
var arr2 = arr1  // 值拷贝
arr2.append(4)
print(arr1)      // 输出 [1, 2, 3] — arr1 不受影响

// Swift 字符串也是值类型(Copy-on-Write 优化)
var s1 = "Hello"
var s2 = s1
s2 += " World"
print(s1)  // 输出 "Hello" — s1 不受影响

Swift 选择值类型的原因:

  1. 多线程安全:值类型天然不可变快照,不需要锁
  2. 语义清晰:赋值即拷贝,行为可预测
  3. 优化空间:Copy-on-Write 机制保证只有真正修改时才拷贝

四、安全性的哲学差异

4.1 Objective-C:信任开发者

Objective-C 的哲学是「给开发者最大的自由」。这带来了灵活性,但也埋下了安全隐患:

// 数组越界访问——运行时才崩溃
NSArray *arr = @[@1, @2, @3];
id obj = arr[10];  // 运行时崩溃

// 内存泄漏——完全合法
@implementation MemoryLeaker
+ (instancetype)shared {
    static MemoryLeaker *instance = nil;
    if (!instance) {
        instance = [[self alloc] init];
    }
    return instance; // 如果 init 里产生了循环引用,这里不会被释放
}
@end

// 野指针——释放后继续使用
NSObject *obj = [[NSObject alloc] init];
[obj release];      // MRC 手动释放
[obj description];  // 野指针访问,可能崩溃或返回垃圾值

4.2 Swift:强制安全边界

Swift 通过语言特性消除整类安全问题:

// 数组越界——编译期或运行时明确错误
let arr = [1, 2, 3]
// arr[10]  // 编译不报错,但运行时抛出 Index out of range
// 正确做法:
if arr.indices.contains(10) {
    _ = arr[10]
} else {
    print("索引越界")
}

// ARC 自动管理引用计数,无需手动 retain/release
class Foo {
    var bar: Bar?
}
class Bar {
    weak var foo: Foo?  // 弱引用打破循环,ARC 自动处理
}
// ARC 在编译时计算引用计数,运行时自动插入 retain/release

// 内存安全默认开启
// 使用未初始化的变量——编译错误
var x: Int
print(x)  // Error: Variable 'x' not initialized

4.3 安全性对比表

安全类型 Objective-C Swift
空指针访问 对 nil 发消息无害 ! 强制解包会崩溃,可选类型强制显式处理
数组越界 运行时崩溃 运行时抛明确异常,可选安全下标访问
内存泄漏 MRC 需手动管理,ARC 仍有循环引用问题 ARC + weak/unowned 自动处理
类型转换 隐式转换,运行时风险 显式 as/as?/as!Any 到具体类型需安全转换
整数溢出 默认截断(UB) Debug 模式崩溃,Release 可配置 wrapping

五、并发模型的演进

5.1 Objective-C 的 GCD

Objective-C(通过 libobjc runtime 和 libdispatch)解决了基本的并发问题,但模型本身存在缺陷:

// GCD 的陷阱:retain cycle in block
@implementation MyViewController
- (void)configure {
    // self 持有 block,block 捕获 self —— retain cycle
    self.completionHandler = ^{
        [self doSomething];  // 隐式 strong retain
    };
}
@end

// 必须用 __weak 打破循环
__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf doSomething];
    }
};

5.2 Swift 的结构化并发

Swift 5.5 引入了 async/await 和 Actor 模型,从根本上解决并发安全问题:

// Swift 结构化并发
actor DataManager {
    private var cache: [String: Data] = [:]

    // Actor 自动保证线程安全,无需锁
    func data(for key: String) async -> Data? {
        if let cached = cache[key] {
            return cached
        }
        let data = await fetchFromNetwork(key)
        cache[key] = data
        return data
    }
}

// 调用方:清晰的异步调用链
func loadImage() async throws -> UIImage {
    let data = try await DataManager().data(for: "profile")
    return UIImage(data: data)!
}

六、运行时能力的差异

6.1 Objective-C 的完全动态运行时

Objective-C 的 runtime 几乎是全开放的:

// 运行时创建新类
Class MyClass = objc_allocateClassPair([NSObject class], "MyRuntimeClass", 0);
class_addMethod(MyClass, @selector(greet), (IMP)greetIMP, "v@:");
objc_registerClassPair(MyClass);

// 运行时替换方法实现
Method original = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swizzled = class_getInstanceMethod([NSString class], @selector(customLowercase));
method_exchangeImplementations(original, swizzled);

// 运行时获取/设置 ivar
object_setIvar(obj, ivar, newValue);

6.2 Swift 的受限运行时

Swift 的 runtime 能力受限,主要出于安全考虑:

// Swift 可以使用 Mirror 进行反射,但能力有限
let mirror = Mirror(reflecting: someObject)
for child in mirror.children {
    print("\(child.label ?? ""): \(child.value)")
}

// 无法像 Objective-C 那样动态创建类或替换方法
// 这被视为安全特性而非限制

七、互操作性:混合编程

7.1 在 Swift 中调用 Objective-C

// 只需导入 bridging header
// Swift 自动将 Objective-C API 转换为更 Swift 风格
let view = UIView(frame: .zero)  // CGRect.zero 是 struct
view.backgroundColor = .systemBlue  // UIColor.systemBlue

7.2 在 Objective-C 中使用 Swift

// Swift 类暴露给 Objective-C
// 需要继承自 NSObject 并标记 @objc
@objc class NetworkManager: NSObject {
    @objc func fetchData(completion: @escaping (Data?) -> Void) {
        // ...
    }
}

// Objective-C 调用
NetworkManager *manager = [[NetworkManager alloc] init];
[manager fetchDataWithCompletion:^(NSData * _Nullable data) {
    // ...
}];

八、为什么 Swift 要这样设计

理解了 Objective-C 的哲学之后,Swift 的设计决策就有了清晰的脉络:

Swift 决策 对应 Objective-C 问题
统一值类型和引用类型 Objective-C 的基本类型/对象类型割裂
可选类型而非 nil 对象 Objective-C 的 nil 语义模糊
严格的类型安全 Objective-C 的 id 类型导致的运行时崩溃
guard letif let Objective-C 的 nil 检查冗长易漏
闭包捕获列表 Objective-C block 的 retain cycle 陷阱
async/await 结构化并发 GCD 的回调地狱和线程安全问题
Actor 隔离模型 GCD 无法保证的数据竞争安全
编译时确定方法调用 消息传递的运行时开销

Swift 的目标不是推翻一切,而是在保留 Objective-C 生态兼容性的同时,通过编译期检查消除最常见的安全隐患,同时在运行时性能上不妥协


九、实战选型建议

什么时候继续用 Objective-C?

  1. 维护旧项目:已有大量 Objective-C 代码,且无重构计划
  2. 需要极致动态能力:Method Swizzling、AOP、运行时类替换
  3. 与老旧 C/Objective-C 库深度集成:某些底层库只有 Objective-C 接口
  4. 团队 Objective-C 积累深厚:技术债转移成本过高

什么时候全面转向 Swift?

  1. 新项目:从零开始,Swift 是绝对首选
  2. 需要高安全性:金融、医疗等对安全要求极高的领域
  3. 需要现代并发:涉及大量异步 I/O 的场景
  4. 团队具备 Swift 能力:学习曲线已被团队消化
  5. 需要完整的泛型系统:库作者或框架开发者

推荐的混合模式

新功能模块 → Swift
需要运行时 hook → Objective-C + Swift
核心业务逻辑 → Swift(安全优先)
底层 SDK 封装 → Objective-C(兼容老库)

总结

维度 Objective-C Swift
设计哲学 信任开发者,极致动态 编译期安全,性能不妥协
类型系统 宽松,依赖运行时 严格,依赖编译期检查
空值处理 nil 无害,运行时决定 可选类型,显式处理
并发模型 GCD,共享内存,锁 async/await + Actor,隔离模型
运行时 完全开放 受限(安全优先)
性能 消息传递有开销 直接调用,零成本抽象
学习曲线 陡峭(语法古怪) 平缓(语法现代)
生态 极其成熟,库丰富 快速成熟,SwiftUI 等新框架原生支持

没有最好的语言,只有适合场景的技术选择。 理解两者的设计哲学,才能在 Apple 生态中做出最优的技术决策。


下篇预告

下一篇我们将进入 Swift 类型系统的入门:从 IntString 这些基础类型,到自定义类型的完整设计。点击关注系列更新,不错过任何一篇。

往期回顾:无(这是系列第一篇)


如果这篇文章对你有帮助,欢迎点赞、评论、转发。你的支持是我持续输出的最大动力。

❌
❌