96SEO 2026-04-25 20:40 29
从今天开始,我们开启一个新的系列:30 Apps,30天30个真实可上线的iOS功Neng。这不仅仅是一个代码练习,geng是一场关于架构、性Neng与用户体验的深度探索。

第一天我们从一个Zui简单的App入手:待办清单。但别被“待办清单”这个名字骗了——这个App的数据层设计,足以应对一个中等规模App的所有持久化需求。hen多开发者随手写个UserDefaults就完事了等到数据量一上来或者需要多线程读写时那就是灾难的开始。我们要Zuo的,是一个坚固、可 、且优雅的地基。
一、持久化方案选型:为何钟情SQLite.swift?在iOS开发中,存数据的方式五花八门。选错了工具,就像用勺子挖地基,累死还不出活。我们来对比一下主流选手:
| 维度 | SQLite.swift | Realm | Core Data | UserDefaults |
|---|---|---|---|---|
| 适用数据量 | 10万+ 条 | 10万+ 条 | 10万+ 条 | <100 条 |
| 关系查询 | 支持 JOIN | 支持 | 支持 | 不支持 |
| 线程安全 | 需要小心处理 | 自动线程安全 | 需要小心处理 | 主线程 |
| 学习曲线 | 低 | 中 | 高 | 无 |
| 包体积 | ~2MB | ~30MB | 内置 | 无 |
| Swift 友好度 | 高 | 极高 | 中 | 简单 |
| Schema 迁移 | 手动 | 自动 | 复杂 | 无 |
| 实时通知 | 无 | 有 | 有 | 无 |
为什么Zui终选择了 SQLite.swift?理由hen充分:它足够轻量,不会像Realm那样让App体积膨胀;它类型安全,写起来像Swift代码而不是SQL字符串;Zui重要的是它把控制权交到了开发者手里。Core Data虽然强大,但那复杂的初始化和堆栈配置,对于快速迭代来说简直是噩梦。SQLite.swift刚刚好,既保留了SQL的强大查询Neng力,又屏蔽了C API的繁琐。
二、数据模型定义:不仅仅是几个属性在设计数据层之前,先得搞清楚我们要存什么。一个简单的“任务”其实包含了hen多信息。我们定义一个 Task 结构体,它需要遵循 Identifiable, Codable, Equatable,方便在SwiftUI中使用和测试。
import Foundation
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"
}
}
这里有个细节:时间戳我们用了 Date 类型,但在存入数据库时会转换为 Double。这种转换逻辑我们稍后会在表映射中处理。另外isPinned 这个字段虽然小,但对于用户体验至关重要——谁不想把重要的事情顶在头上呢?
在开始写代码前,先规划好文件夹结构。一个混乱的项目结构是维护地狱的开始。我们采用MVVM架构,并专门划分出Data层。
使用 Xcode 创建新的 SwiftUI 项目,命名为 TodoApp。然后通过 Swift Package Manager 引入 SQLite.swift:
// 在 Xcode 中:File → Add Package Dependencies
// 输入:https://github.com/stephencelis/SQLite.swift
// 选择Zui新版本
建议的项目结构如下:
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
四、数据库层实现:打好地基
1. 数据库管理器
我们需要一个单例来管理数据库连接。这不仅Neng避免重复打开文件造成的资源浪费,还Neng方便全局调用。在这里我们还要处理一个关键点:外键约束。虽然我们现在的表比较简单,但为了未来 ,开启外键约束是必须的。
import Foundation
import SQLite
final class DatabaseManager {
static let shared = DatabaseManager
private var db: Connection!
private init {}
func setup throws {
// 获取沙盒中的数据库文件路径
let path = try FileManager.default
.url
.appendingPathComponent
.path
db = try Connection
// 启用外键约束,保证数据一致性
try db.execute
// 初始化表结构
try TaskTable.create
}
// 用于测试或重置数据
func resetDatabase throws {
try db.execute
try db.execute
}
}
2. 表定义与映射
这是SQLite.swiftZui迷人的地方。我们Ke以用Swift代码来定义表结构,类型安全,编译器会帮我们检查错误。注意kan rowToTask 方法,它负责把数据库里干巴巴的行数据转换成我们漂亮的 Task 模型。
另外别忘了索引!hen多新手App慢,就是因为没建索引。我们要对经常查询的字段建立索引,这Neng带来数量级的查询速度提升。
import Foundation
import SQLite
enum TaskTable {
static let table = Table
// 列定义
static let id = Expression
static let title = Expression
static let content = Expression
static let priority = Expression
static let status = Expression
static let category = Expression
static let dueDate = Expression
static let createdAt = Expression
static let updatedAt = Expression
static let completedAt = Expression
static let isPinned = Expression
static func create throws {
try db.run { t in
t.column
t.column
t.column
t.column
t.column
t.column
t.column
t.column
t.column
t.column
t.column
})
// 创建索引,加速常见查询,这是性Neng优化的关键
try db.run)
try db.run)
try db.run)
try db.run)
try db.run)
}
// 从数据库行映射到模型
static func rowToTask -> Task {
Task(
id: UUID ?? UUID,
title: row,
content: row,
priority: Task.Priority ?? .medium,
status: Task.Status ?? .pending,
category: Task.Category ?? .work,
dueDate: row.map { Date },
createdAt: Date,
updatedAt: Date,
completedAt: row.map { Date },
isPinned: row
)
}
}
五、Repository 模式实现:解耦的艺术
1. 为什么需要 Repository?
直接在ViewModel里写SQL?千万别这么Zuo!那会让你的业务逻辑和数据存储逻辑纠缠在一起,像一团乱麻。Repository模式充当了中间人的角色。ViewModel只关心“给我任务列表”或“保存这个任务”,至于底层是SQLite、Core Data还是网络请求,ViewModel根本不需要知道。
这种分层带来的好处是巨大的:代码geng易读,测试geng方便,未来换数据库也容易。
┌─────────────┐ ┌──────────────┐ ┌────────────────┐
│ Views │────▶│ ViewModels │────▶│ Repository │
│ │ │ │ │ │
└─────────────┘ └──────────────┘ └───────┬────────┘
│
┌──────▼────────┐
│DatabaseManager│
│ │
└───────┬────────┘
│
┌──────▼────────┐
│ todo.sqlite3 │
└───────────────┘
2. 定义协议
先定义接口,再实现功Neng。这是面向协议编程的精髓。
import Foundation
import Combine
protocol TaskRepositoryProtocol {
// 基础 CRUD 操作
func fetchAllTasks async throws ->
func fetchTask async throws -> Task?
func insertTask async throws
func updateTask async throws
func deleteTask async throws
func deleteAllCompletedTasks async throws -> Int
// 高级查询:筛选、搜索、排序
func fetchTasks(
status: Task.Status?,
category: Task.Category?,
searchQuery: String?,
sortBy: SortOption
) async throws ->
// 聚合查询
func countTasks async throws -> Int
func fetchOverdueTasks async throws ->
}
enum SortOption: String, CaseIterable {
case createdDesc = "Zui新创建"
case createdAsc = "Zui早创建"
case priorityDesc = "优先级Zui高"
case dueDateAsc = "截止日期Zui近"
case dueDateDesc = "截止日期Zui远"
}
3. 具体实现
这里是真正干活的地方。我们实现了协议里的所有方法。注意kan fetchTasks 方法,它展示了动态构建SQL查询的强大之处。根据用户传入的参数,我们动态拼接 filter 和 order。特别是排序逻辑,我们强制要求“置顶”的任务始终排在Zui前面这是一个非常实用的交互细节。
final class TaskRepository: TaskRepositoryProtocol {
private let db: Connection
init {
self.db = db
}
// MARK: - CRUD
func fetchAllTasks async throws -> {
let rows = try db.prepare
return rows.map { TaskTable.rowToTask }
}
func fetchTask async throws -> Task? {
let query = TaskTable.table.filter
guard let row = try db.pluck else { return nil }
return TaskTable.rowToTask
}
func insertTask 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 async throws {
let target = TaskTable.table.filter
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 async throws {
let target = TaskTable.table.filter
try db.run)
}
func deleteAllCompletedTasks async throws -> Int {
let query = TaskTable.table.filter
return try db.run)
}
// MARK: - 高级查询
func fetchTasks(
status: Task.Status? = nil,
category: Task.Category? = nil,
searchQuery: String? = nil,
sortBy: SortOption = .createdDesc
) async throws -> {
var query = TaskTable.table
// 动态添加过滤条件
if let status {
query = query.filter
}
if let category {
query = query.filter
}
if let searchQuery, !searchQuery.isEmpty {
let pattern = "%\%"
query = query.filter || TaskTable.content.like)
}
// 排序:置顶任务始终在Zui前,然后才是用户选择的排序方式
query = query.order(
TaskTable.isPinned.desc,
sortExpression
)
return try db.prepare.map { TaskTable.rowToTask }
}
private func sortExpression -> Expression {
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 async throws -> Int {
var query = TaskTable.table
if let status {
query = query.filter
}
return try db.scalar
}
func fetchOverdueTasks async throws -> {
let now = Date.timeIntervalSince1970
let query = TaskTable.table
.filter
.filter
.order
return try db.prepare.map { TaskTable.rowToTask }
}
}
六、数据库迁移策略:未雨绸缪
1. 为什么需要迁移策略?
App发布后用户会升级到新版本。Ru果新版本修改了数据库结构,直接升级会导致老用户的数据丢失或崩溃。这绝对是用户差评的导火索。所以我们需要一套机制,在App启动时检查数据库版本,并自动执行升级脚本。
2. 迁移实现我们创建一个 DatabaseMigration 类。它利用 UserDefaults 存储当前的数据库版本号。Ru果检测到版本号低于预期,就执行对应的升级逻辑。这里演示了如何添加新列,并处理了SQLite不支持 ADD COLUMN IF NOT EXISTS 的坑爹特性。
final class DatabaseMigration {
private let db: Connection
private let versionKey = "database_version"
private let currentVersion = 1
init {
self.db = db
}
func migrate throws {
let storedVersion = UserDefaults.standard.integer
guard storedVersion
3. 集成迁移
别忘了在 DatabaseManager.setup 中调用迁移逻辑,确保每次App启动时dou是Zui新的结构。
func setup throws {
let path = try FileManager.default
.url
.appendingPathComponent
.path
db = try Connection
try db.execute
try TaskTable.create
// 添加迁移逻辑,保证数据平滑升级
try DatabaseMigration.migrate
}
七、完整的使用示例:从入口到业务
1. App 入口集成
在App启动时我们必须确保数据库Yi经准备就绪。Ru果初始化失败,直接让App崩溃通常比带着一个损坏的数据库运行要好,至少Neng让你在开发阶段就发现问题。
@main
struct TodoAppApp: App {
init {
do {
try DatabaseManager.shared.setup
} catch {
// 这里应该上报错误日志,而不是直接崩溃
// 但对于数据层初始化失败,崩溃往往是geng安全的选择
fatalError")
}
}
var body: some Scene {
WindowGroup {
ContentView
}
}
}
2. ViewModel 调用
Zui后我们kankanViewModel是如何使用Repository的。ViewModel完全不需要知道SQL的存在它只需要处理业务逻辑,比如“加载任务”、“切换状态”、“删除任务”。这里使用了 async/await,让异步代码kan起来像同步代码一样清爽。
@MainActor
class TaskListViewModel: ObservableObject {
@Published var tasks: =
@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) {
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 async {
do {
try await repository.insertTask
await loadTasks
} catch {
self.error = error
}
}
func toggleTaskStatus 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
await loadTasks
} catch {
self.error = error
}
}
func deleteTask async {
do {
try await repository.deleteTask
await loadTasks
} catch {
self.error = error
}
}
func deleteAllCompleted async {
do {
let count = try await repository.deleteAllCompletedTasks
print completed tasks")
await loadTasks
} catch {
self.error = error
}
}
}
八、今天的代码架构
经过这一天的折腾,我们搭建了一个非常稳固的数据层。让我们回顾一下这个架构的精髓:
数据层架构
│
├── DatabaseManager
│ └── DatabaseMigration
│
├── TaskTable
│ ├── create: 建表 + 索引
│ └── rowToTask: Row → Task
│
└── TaskRepository
├── fetchAllTasks
├── fetchTasks
├── insertTask
├── updateTask
├── deleteTask
├── countTasks
└── fetchOverdueTasks
关键设计原则
单一职责Manager管连接,Table管结构,Repository管CRUD,各司其职。
依赖倒置ViewModel依赖Repository协议,而不是具体实现,方便测试和替换。
性Neng优先通过索引优化查询,通过异步操作避免阻塞主线程。
安全第一外键约束保证数据完整性,迁移策略保证版本平滑升级。
Ru果你完成了今天的代码编写,欢迎在评论区分享你遇到的问题或优化思路。30天我们一起坚持。
明天我们将完成这个待办清单 App 的 UI 层:使用 SwiftUI + MVVM + Combine 实现完整的响应式界面包括列表展示、滑动操作、筛选排序等交互。
我们将完成:
待办清单 App 的核心功Neng:
往期回顾无
专栏iOS功Neng实战30Days 编号B01 · 系列第 1 篇 字数约 2800 字 标签iOS / SwiftUI / SQLite / Repository 模式 / 数据持久化
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback