Swift 实战项目

学了 16 篇基础,是时候把它们串起来做点真东西了。这一章给三个完整、可运行的项目——Todo App、天气查询、简易计算器,每个都给出完整代码、运行结果和要点讲解。三个项目分别覆盖 Swift 的核心能力:数据建模与持久化、异步网络、类型安全建模。建议你把代码复制到 Swift Playground 或 Xcode 项目里亲手跑一遍。

实战 1:Todo App(struct + Array + UserDefaults)

需求:做一个待办管理程序,支持增加、完成、删除,数据用 UserDefaults 持久化——重启后数据还在。

涉及知识:struct、Codable、Identifiable、Array 高阶函数(filter / sorted / firstIndex)、JSONEncoder/JSONDecoder、UserDefaults。

// Todo.swift — Todo App 数据层(struct + Array + UserDefaults 持久化)
// 可在 Swift Playground 或任何 Swift 项目里运行
// 知识点:struct、Identifiable、Array 过滤/排序、UserDefaults、Codable

import Foundation

// 1. 数据模型:struct + Codable(可序列化)+ Identifiable(列表能用 id 区分)
struct Todo: Codable, Identifiable {
    var id: UUID = UUID()           // 自动生成唯一 id
    var title: String
    var done: Bool = false
    var createdAt: Date = Date()
}

// 2. ViewModel:封装数据操作 + 持久化
class TodoStore {
    private let key = "todos.json"

    // @Published 让 SwiftUI 视图自动刷新(这里演示纯命令行也可用)
    private(set) var items: [Todo] = []

    init() { load() }

    func add(_ title: String) {
        items.append(Todo(title: title))
        save()
    }

    func toggle(_ todo: Todo) {
        // struct 是值类型,要找到下标重新赋值
        if let i = items.firstIndex(where: { $0.id == todo.id }) {
            items[i].done.toggle()
            save()
        }
    }

    func delete(_ todo: Todo) {
        items.removeAll { $0.id == todo.id }
        save()
    }

    // 函数式编程:filter + sorted,不修改原数组,返回新数组
    var pending: [Todo] {
        items.filter { !$0.done }.sorted { $0.createdAt < $1.createdAt }
    }

    var completed: [Todo] {
        items.filter { $0.done }
    }

    // 持久化:用 JSONEncoder 把 [Todo] 编码成 Data,存到 UserDefaults
    private func save() {
        if let data = try? JSONEncoder().encode(items) {
            UserDefaults.standard.set(data, forKey: key)
        }
    }

    private func load() {
        guard let data = UserDefaults.standard.data(forKey: key),
              let saved = try? JSONDecoder().decode([Todo].self, from: data)
        else { return }
        items = saved
    }
}

// 3. 演示用法
let store = TodoStore()
store.add("买牛奶")
store.add("写季度报告")
store.add("回邮件")

print("=== 全部 ===")
store.items.forEach { print($0.done ? "[x]" : "[ ]", $0.title) }

if let first = store.items.first {
    store.toggle(first)             // 标记完成
}
print("\n=== 未完成 ===")
store.pending.forEach { print("[ ] \($0.title)" )}

print("\n=== 已完成 ===")
store.completed.forEach { print("[x] \($0.title)" )}

// 输出:
// === 全部 ===
// [ ] 买牛奶
// [ ] 写季度报告
// [ ] 回邮件
//
// === 未完成 ===
// [ ] 写季度报告
// [ ] 回邮件
//
// === 已完成 ===
// [x] 买牛奶

要点讲解:

实战 2:天气查询(URLSession + async/await + Codable)

需求:调用 wttr.in 的简易天气 API,查询指定城市当前天气。先了解 async/await 是 Swift 5.5 引入的现代异步语法。

涉及知识:URLSession、async/await、Codable、JSONDecoder、CodingKeys、自定义 Error、HTTPURLResponse。

// Weather.swift — 天气查询:调用 wttr.in 简易天气 API
// 知识点:URLSession、async/await、Codable、JSONDecoder、错误处理
// 运行:swift Weather.swift(Swift 5.5+ 支持顶层 await)
import Foundation

// 1. 数据模型:只声明需要的字段,JSON 里多余字段会被忽略
struct WeatherResponse: Codable {
    let currentCondition: [Current]
    struct Current: Codable {
        let tempC: String
        let humidity: String
        let weatherDesc: [Desc]
        struct Desc: Codable {
            let value: String
        }
        // CodingKeys:把 JSON 字段映射成 Swift 友好的名字
        enum CodingKeys: String, CodingKey {
            case tempC = "temp_C"
            case humidity
            case weatherDesc
        }
    }
}

// 2. 错误类型:实现 Error 协议,可被 throw/catch
enum WeatherError: Error, LocalizedError {
    case badURL
    case httpError(Int)
    case decodeError

    var errorDescription: String? {
        switch self {
        case .badURL:        return "URL 格式错误"
        case .httpError(let code): return "HTTP 错误: \(code)"
        case .decodeError:   return "JSON 解析失败"
        }
    }
}

// 3. async 函数:用 await 拿结果,像同步代码一样清晰
func fetchWeather(city: String) async throws -> String {
    let encoded = city.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? city
    guard let url = URL(string: "https://wttr.in/\(encoded)?format=j1") else {
        throw WeatherError.badURL
    }
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
        throw WeatherError.httpError((response as? HTTPURLResponse)?.statusCode ?? -1)
    }
    guard let parsed = try? JSONDecoder().decode(WeatherResponse.self, from: data) else {
        throw WeatherError.decodeError
    }
    let cur = parsed.currentCondition[0]
    let desc = cur.weatherDesc.first?.value ?? "未知"
    return "🏙  \(city)\n🌡  温度 \(cur.tempC)°C\n💧  湿度 \(cur.humidity)%\n☁  天气 \(desc)"
}

// 4. 入口:用 Task 包一层才能 await(顶层 await 在脚本/playground 才有)
Task {
    do {
        let result = try await fetchWeather(city: "Beijing")
        print(result)
    } catch {
        print("查询失败:\(error.localizedDescription)")
    }
}

// 输出:
// 🏙  Beijing
// 🌡  温度 22°C
// 💧  湿度 45%
// ☁  天气 Partly cloudy

要点讲解:

实战 3:简易计算器(enum + protocol + switch)

需求:做一个支持加减乘除的计算器,用 enum 表示运算、用 protocol 抽象"可执行"、用 switch 穷尽所有情况。

涉及知识:enum 关联值、protocol、extension、switch 穷尽、throw/catch、where 守卫。

// Calculator.swift — 简易计算器:用 enum 表示操作,protocol 抽象运算
// 知识点:enum 关联值、protocol、switch 穷尽、扩展 extension、错误处理
import Foundation

// 1. 错误类型
enum CalcError: Error {
    case divideByZero
    case unknownOperator
}

// 2. 操作用 enum 关联值表示——比字符串更安全,编译器帮你查错
enum Operation {
    case add(Double, Double)
    case subtract(Double, Double)
    case multiply(Double, Double)
    case divide(Double, Double)
}

// 3. 协议:定义"可执行运算"的契约
protocol Executable {
    func execute() throws -> Double
}

// 4. 让 Operation 遵循协议:switch 必须穷尽所有 case(编译期保证)
extension Operation: Executable {
    func execute() throws -> Double {
        switch self {
        case .add(let a, let b):      return a + b
        case .subtract(let a, let b): return a - b
        case .multiply(let a, let b): return a * b
        case .divide(_, let b) where b == 0:
            throw CalcError.divideByZero
        case .divide(let a, let b):   return a / b
        }
    }

    // 符号描述:switch 再次穷尽
    var symbol: String {
        switch self {
        case .add:      return "+"
        case .subtract: return "-"
        case .multiply: return "×"
        case .divide:   return "÷"
        }
    }
}

// 5. 计算器:用字符串解析成 Operation,再执行
struct Calculator {
    static func parse(_ op: String, _ a: Double, _ b: Double) throws -> Operation {
        switch op {
        case "+":  return .add(a, b)
        case "-":  return .subtract(a, b)
        case "*", "×": return .multiply(a, b)
        case "/", "÷": return .divide(a, b)
        default:   throw CalcError.unknownOperator
        }
    }

    static func run(_ op: String, _ a: Double, _ b: Double) -> String {
        do {
            let operation = try parse(op, a, b)
            let result = try operation.execute()
            return String(format: "%.2f %@ %.2f = %.2f", a, operation.symbol, b, result)
        } catch CalcError.divideByZero {
            return "错误:除数不能为 0"
        } catch CalcError.unknownOperator {
            return "错误:未知运算符 \(op)"
        } catch {
            return "未知错误:\(error)"
        }
    }
}

// 6. 演示
print(Calculator.run("+", 12, 8))     // 12.00 + 8.00 = 20.00
print(Calculator.run("-", 12, 8))     // 12.00 - 8.00 = 4.00
print(Calculator.run("×", 12, 8))     // 12.00 × 8.00 = 96.00
print(Calculator.run("÷", 12, 0))     // 错误:除数不能为 0
print(Calculator.run("÷", 12, 4))     // 12.00 ÷ 4.00 = 3.00
print(Calculator.run("%", 12, 4))     // 错误:未知运算符 %

要点讲解:

小结

三个项目分别对应 Swift 的三大主力场景:数据建模(struct + Codable + 持久化)、网络异步(URLSession + async/await)、类型安全建模(enum + protocol + switch)。它们都用到了前面学的基础——struct、enum、protocol、错误处理——只是组合起来解决真实问题。建议你至少把其中一个改造成自己的版本(给 Todo 加 SwiftUI 视图、给天气查询加多城市、给计算器加历史记录),做过一遍比读十遍更扎实。

恭喜!到这里 Swift 系列就真正完结了。接下来可以挑一个方向深入——SwiftUI(苹果全平台 UI)、iOS App 开发、服务端 Vapor、Swift Data/Core Data、开源贡献(苹果官方 Swift 编译器是 Swift 写的)——Swift 的世界以"安全 + 现代"为核心,边做边玩进步最快。

← 上一篇 Swift 进阶与下一步

返回 Swift 教程目录

✈️💬