CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
SwiftUI 开发入门/课时/综合练习
RawBlame
课时目录12 节

SwiftUI 开发入门

  1. 01Swift 与 SwiftUI 概览
  2. 02基础语法与类型
  3. 03控制流
  4. 04函数与闭包
  5. 05结构体、类与协议
  6. 06SwiftUI 视图
  7. 07状态管理
  8. 08导航与生命周期
  9. 09并发与 async/await
  10. 10数据持久化
  11. 11MVVM 与架构
  12. 12综合练习

综合练习

天气 App(SwiftUI + async API)。

40 分钟

综合练习:天气 App

用本 track 所学完成 天气 App:SwiftUI 界面 + MVVM + async API + 本地缓存。对照 /roadmaps/swift-ui 全路径验收。

学习目标

  • 完成可运行的天气应用(搜索城市、展示温度与状况)
  • 实践 MVVM 分层与 Repository 抽象
  • 用 async/await 加载数据,@AppStorage 记住最近城市
  • 规划下一步:接入真实 API 或上架 TestFlight

核心概念

项目结构

WeatherApp/
├── App/
│   └── WeatherAppApp.swift
├── Models/
│   └── Weather.swift
├── ViewModels/
│   └── WeatherViewModel.swift
├── Views/
│   ├── WeatherHomeView.swift
│   └── WeatherDetailCard.swift
├── Services/
│   ├── WeatherRepository.swift
│   └── MockWeatherAPI.swift
└── Tests/
    └── WeatherViewModelTests.swift

Model 与 ViewModel

struct Weather: Identifiable, Codable, Equatable {
    let id = UUID()
    let city: String
    let temp: Double
    let condition: String
    let humidity: Int
}

protocol WeatherRepositoryProtocol {
    func fetch(city: String) async throws -> Weather
}

final class MockWeatherRepository: WeatherRepositoryProtocol {
    func fetch(city: String) async throws -> Weather {
        try await Task.sleep(nanoseconds: 400_000_000)
        return Weather(city: city, temp: 21.0, condition: "多云", humidity: 65)
    }
}

@MainActor
final class WeatherViewModel: ObservableObject {
    @Published var weather: Weather?
    @Published var cityInput = ""
    @Published var isLoading = false
    @Published var errorMessage: String?

    private let repository: WeatherRepositoryProtocol

    init(repository: WeatherRepositoryProtocol = MockWeatherRepository()) {
        self.repository = repository
    }

    func search() async {
        let city = cityInput.trimmingCharacters(in: .whitespaces)
        guard !city.isEmpty else { return }
        isLoading = true
        errorMessage = nil
        do {
            weather = try await repository.fetch(city: city)
        } catch {
            errorMessage = "无法获取 \(city) 的天气"
        }
        isLoading = false
    }
}

SwiftUI 主界面

struct WeatherHomeView: View {
    @StateObject private var viewModel = WeatherViewModel()
    @AppStorage("lastCity") private var lastCity = "北京"

    var body: some View {
        NavigationStack {
            VStack(spacing: 24) {
                HStack {
                    TextField("输入城市", text: $viewModel.cityInput)
                        .textFieldStyle(.roundedBorder)
                    Button("查询") {
                        Task { await viewModel.search() }
                    }
                    .disabled(viewModel.isLoading)
                }

                if viewModel.isLoading {
                    ProgressView("查询中…")
                } else if let w = viewModel.weather {
                    WeatherDetailCard(weather: w)
                } else {
                    ContentUnavailableView(
                        "暂无数据",
                        systemImage: "cloud.sun",
                        description: Text("输入城市名查询天气")
                    )
                }
            }
            .padding()
            .navigationTitle("天气")
            .onAppear { viewModel.cityInput = lastCity }
            .onChange(of: viewModel.weather?.city) { _, newCity in
                if let newCity { lastCity = newCity }
            }
            .alert("错误", isPresented: .constant(viewModel.errorMessage != nil)) {
                Button("确定") { viewModel.errorMessage = nil }
            } message: {
                Text(viewModel.errorMessage ?? "")
            }
        }
    }
}

struct WeatherDetailCard: View {
    let weather: Weather

    var body: some View {
        VStack(spacing: 12) {
            Text(weather.city).font(.title.bold())
            Text("\(weather.temp, specifier: "%.1f")°C")
                .font(.system(size: 48, weight: .thin))
            Text(weather.condition).foregroundStyle(.secondary)
            Label("湿度 \(weather.humidity)%", systemImage: "humidity")
        }
        .frame(maxWidth: .infinity)
        .padding()
        .background(.ultraThinMaterial)
        .clipShape(RoundedRectangle(cornerRadius: 16))
    }
}

功能清单

功能要求
城市搜索TextField + 查询按钮
天气展示温度、状况、湿度卡片
加载态ProgressView
错误处理alert 提示
记住城市@AppStorage lastCity
架构MVVM + Repository Protocol
测试ViewModel 至少 2 个单元测试

与 React/SwiftUI 对照

本练习React 等价SwiftUI 实现
搜索框controlled inputTextField + @Published
加载态{loading && <Spinner />}if isLoading { ProgressView }
数据卡片条件组件WeatherDetailCard
持久化城市localStorage@AppStorage
异步查询fetch + useEffectasync ViewModel + .task
Mock APIMSW / mock fnMockWeatherRepository

选做:接入 OpenWeatherMap 等真实 API,Repository 内用 URLSession + Codable 替换 Mock。

练习清单

  • 完成上述天气 App,模拟器可运行
  • 编写 ViewModel 成功/失败单元测试
  • 在 /roadmaps/swift-ui 标记已学节点
  • 可选:替换 Mock 为真实 REST API

小结

SwiftUI track 闭环:语法 → 视图 → 状态 → 导航 → 并发 → 持久化 → MVVM → 天气 App。 下一步可深入 Combine、Widget 或 Apple 官方 Sample Code。见 swift-ui-essentials。

加载本节练习…
← 上一课MVVM 与架构