QuestionTop

精选面试高频问题

专题复习/SwiftUI 实战专题课
SwiftUI 专题

SwiftUI 实战专题课

围绕网络、表单、列表、导航、搜索与工程分层,用真实业务问题串起可迁移的解决思路。

6 章系统内容约 1.5 小时当前仅开放前两页试看
SwiftUI状态管理网络列表导航工程化

专题亮点

先看这份专题能帮你补齐什么,再从侧边栏目录快速跳到想复习的章节。

01按业务痛点组织:加载态、表单、分页、路由、搜索与分层
02每节配有可迁移示例代码,便于对照项目改造
03帮助建立「问题 → 边界 → 状态 → 交互 → 组织」的实战思路
试看说明未登录用户当前仅可查看前两页内容,登录后可继续阅读全文;当前专题暂不支持打印和 PDF 导出。
SwiftUI 专题学习课程

SwiftUI 实战专题课:不是把页面写出来,而是把问题解决掉

这是一篇适合放在技术站点首页、专题页或学习中心的 SwiftUI 长文课程。它不从零碎 API 出发,而是围绕真实业务里最常见的坑来讲:为什么列表会卡、为什么表单总是体验不好、为什么一接搜索和分页就开始混乱、为什么项目一大导航就难维护。

作者:阿克苏定位:实战导向模块数:6 节核心课程适合:SwiftUI 初中级开发者

不按组件 API 罗列知识点,而是按真实业务问题组织内容。

每节都给你一个高频场景、一套落地思路、一段可迁移代码。

学完后,你能更稳地处理状态、网络、表单、列表、路由和异步任务。

课程主线

从"会写界面"到"能扛业务"

这套内容的设计方式很简单:每节先给你一个真实项目里会遇到的问题,再给你一套能在实际开发中复用的解决办法。你会看到的不只是控件怎么用,而是状态该怎么建、边界该怎么分、交互细节该怎么处理。

01

网络请求不是 fetch 一下就结束

高频痛点

页面常见的问题不是"拿不到数据",而是 loading、error、empty、retry、pull to refresh 混在一起后,状态很快失控。

你会得到什么

这节会带你做一个可复用的加载状态模型,把"请求成功"之外的所有分支都设计清楚。

用统一的状态枚举管理 idle、loading、success、failure。
把网络层的错误信息转换成用户能理解的提示。
让列表页和详情页都能复用同一套加载与重试思路。
可直接迁移的示例代码
enum LoadState<Value> {
    case idle
    case loading
    case empty
    case success(Value)
    case failure(String)
}

@MainActor
final class ArticleListViewModel: ObservableObject {
    @Published private(set) var state: LoadState<[Article]> = .idle

    func fetchArticles() async {
        state = .loading

        do {
            let articles = try await ArticleService.shared.fetchArticles()
            state = articles.isEmpty ? .empty : .success(articles)
        } catch {
            state = .failure(error.localizedDescription)
        }
    }
}

struct ArticleListView: View {
    @StateObject private var viewModel = ArticleListViewModel()

    var body: some View {
        Group {
            switch viewModel.state {
            case .idle, .loading:
                ProgressView("Loading")
            case .empty:
                ContentUnavailableView(
                    "No Content",
                    systemImage: "tray",
                    description: Text("You can refresh later or check other categories")
                )
            case .success(let articles):
                List(articles) { article in
                    Text(article.title)
                }
            case .failure(let message):
                ContentUnavailableView(
                    "Load Failed",
                    systemImage: "wifi.exclamationmark",
                    description: Text(message)
                )
            }
        }
        .task {
            await viewModel.fetchArticles()
        }
        .refreshable {
            await viewModel.fetchArticles()
        }
    }
}
02

登录和注册页,最容易写出"能点但不好用"的表单

高频痛点

用户输入体验差通常不是 UI 不够漂亮,而是焦点跳转、即时校验、禁用按钮、重复提交这些细节没有处理好。

你会得到什么

这一节会把"能提交"升级成"提交体验顺手",特别适合登录、注册、修改资料和发布内容场景。

使用 FocusState 处理输入焦点,减少用户来回点输入框。
在 ViewModel 外层先做轻量校验,避免请求之前就明显出错。
用 isSubmitting 防止重复点击提交,解决线上重复下单/重复发布问题。
可直接迁移的示例代码
struct LoginView: View {
    enum Field {
        case email
        case password
    }

    @State private var email = ""
    @State private var password = ""
    @State private var isSubmitting = false
    @State private var errorMessage = ""
    @FocusState private var focusedField: Field?

    private var canSubmit: Bool {
        email.contains("@") && password.count >= 6 && !isSubmitting
    }

    var body: some View {
        Form {
            TextField("Email", text: $email)
                .keyboardType(.emailAddress)
                .textInputAutocapitalization(.never)
                .autocorrectionDisabled()
                .focused($focusedField, equals: .email)
                .submitLabel(.next)
                .onSubmit {
                    focusedField = .password
                }

            SecureField("Password", text: $password)
                .focused($focusedField, equals: .password)
                .submitLabel(.go)
                .onSubmit {
                    if canSubmit {
                        Task { await submit() }
                    }
                }

            if !errorMessage.isEmpty {
                Text(errorMessage)
                    .font(.footnote)
                    .foregroundStyle(.red)
            }

            Button(isSubmitting ? "Signing in..." : "Sign In") {
                Task { await submit() }
            }
            .disabled(!canSubmit)
        }
    }

    private func submit() async {
        guard canSubmit else { return }
        isSubmitting = true
        errorMessage = ""
        defer { isSubmitting = false }

        do {
            try await AuthService.shared.login(email: email, password: password)
        } catch {
            errorMessage = "Login failed. Please check your credentials or network."
        }
    }
}
03

列表卡顿和分页混乱,往往不是 SwiftUI 慢,而是边界没设计好

高频痛点

很多人写列表时把"首次加载"和"加载更多"混在一起,再叠加图片加载和刷新,结果出现闪烁、重复请求、滚动掉帧。

你会得到什么

你会学会给分页一个干净的边界,让列表页从"能跑"变成"顺"。

首次加载、下拉刷新、滚动触底是三种不同动作,状态要分开。
把"是否还有下一页"和"当前是否正在分页"放进 ViewModel。
让行视图轻量化,避免在 row 里堆太多计算逻辑。
可直接迁移的示例代码
@MainActor
final class FeedViewModel: ObservableObject {
    @Published private(set) var items: [FeedItem] = []
    @Published private(set) var isLoadingMore = false
    @Published private(set) var hasMore = true

    private var page = 1

    func loadInitial() async {
        page = 1
        hasMore = true
        items = []
        await loadMoreIfNeeded(currentItem: nil)
    }

    func loadMoreIfNeeded(currentItem: FeedItem?) async {
        guard !isLoadingMore, hasMore else { return }

        if let currentItem,
           let index = items.firstIndex(where: { $0.id == currentItem.id }) {
            let triggerIndex = max(items.count - 3, 0)
            guard index >= triggerIndex else { return }
        }

        isLoadingMore = true
        defer { isLoadingMore = false }

        let response = try? await FeedService.shared.fetchPage(page: page)
        let newItems = response?.items ?? []
        hasMore = !(response?.isLastPage ?? true)
        items.append(contentsOf: newItems)
        page += 1
    }
}

struct FeedView: View {
    @StateObject private var viewModel = FeedViewModel()

    var body: some View {
        List(viewModel.items) { item in
            FeedRow(item: item)
                .onAppear {
                    Task {
                        await viewModel.loadMoreIfNeeded(currentItem: item)
                    }
                }
        }
        .overlay(alignment: .bottom) {
            if viewModel.isLoadingMore {
                ProgressView().padding(.bottom, 12)
            }
        }
        .task {
            await viewModel.loadInitial()
        }
    }
}
04

NavigationStack 一复杂,页面跳转马上失控

高频痛点

项目一旦进入真实业务,push、sheet、fullScreenCover、deep link 会同时出现。只靠局部 Bool 控制,很快会变成维护噩梦。

你会得到什么

这一节带你把路由抽象成明确的数据结构,让导航逻辑可读、可调试、可扩展。

用 enum 描述路由,而不是到处散落的 Bool 标记。
把 path 放到容器层管理,子页面只关心"我要去哪里"。
为 deep link 和外部跳转预留统一入口,后期接功能更稳。
可直接迁移的示例代码
enum AppRoute: Hashable {
    case profile(userID: String)
    case article(id: Int)
    case settings
}

struct RootView: View {
    @State private var path: [AppRoute] = []

    var body: some View {
        NavigationStack(path: $path) {
            HomeView(
                openProfile: { userID in
                    path.append(.profile(userID: userID))
                },
                openArticle: { id in
                    path.append(.article(id: id))
                }
            )
            .navigationDestination(for: AppRoute.self) { route in
                switch route {
                case .profile(let userID):
                    ProfileView(userID: userID)
                case .article(let id):
                    ArticleDetailView(articleID: id)
                case .settings:
                    SettingsView()
                }
            }
        }
    }
}
05

搜索页最常见的问题,不是搜不到,而是"搜得太勤"

高频痛点

用户每输入一个字都发请求,旧请求还没回来,新请求又发出去了。最终结果是接口压力大,页面结果跳来跳去。

你会得到什么

我们会做一个可取消、可延迟的搜索流,让实时搜索真正可上线。

使用 task(id:) 配合取消机制,避免旧结果覆盖新结果。
通过短暂延迟模拟 debounce,减少无意义请求。
把空关键字、加载中、无结果、失败统一处理清楚。
可直接迁移的示例代码
struct SearchView: View {
    @State private var keyword = ""
    @State private var results: [Question] = []
    @State private var isSearching = false

    var body: some View {
        List(results) { item in
            Text(item.title)
        }
        .searchable(text: $keyword, prompt: "Search questions")
        .overlay {
            if isSearching {
                ProgressView("Searching")
            }
        }
        .task(id: keyword) {
            guard !keyword.isEmpty else {
                results = []
                return
            }

            isSearching = true
            defer { isSearching = false }

            do {
                try await Task.sleep(for: .milliseconds(300))
                results = try await SearchService.shared.search(keyword: keyword)
            } catch is CancellationError {
                // Old task cancelled when new search starts; no error needed
            } catch {
                results = []
            }
        }
    }
}
06

做完页面不代表能上线,最后还差一层工程化思维

高频痛点

很多 SwiftUI 项目在 demo 阶段很顺,一接业务就难维护,根本原因是视图、状态、服务和副作用没有分层。

你会得到什么

最后一节会帮你建立一条更稳的工程路径,让代码不是只适合演示,而是适合迭代。

View 负责展示和用户动作,ViewModel 负责状态与流程。
Service 只处理外部依赖,如网络、缓存、鉴权和上传。
先把边界拆清楚,再考虑抽通用组件,复用会自然发生。
学习结果

你最终会建立起一套稳定的 SwiftUI 思维方式

遇到页面不刷新、状态打架、重复请求时,你知道先查哪里。

你可以把示例代码直接迁移到登录页、列表页、搜索页和个人中心页。

你会形成"问题 -> 边界 -> 状态 -> 交互 -> 代码组织"的实战思路。

实战练习

建议你边学边做这 4 件事

01

做一个带登录、列表、搜索、详情的题库类 App,把课程里的 6 个模块串成一条主线。

02

把所有页面都补上 loading、empty、error 三态,不再只处理"请求成功"的 happy path。

03

把导航状态统一收口,至少让 push 和 sheet 不再分别散落在 5 个页面里。

04

给表单页补上焦点移动、即时校验和防重复提交,形成你自己的项目模板。

前两页试看结束

登录后继续阅读完整专题内容。当前专题暂不支持打印和 PDF 导出。