不按组件 API 罗列知识点,而是按真实业务问题组织内容。
SwiftUI 实战专题课
围绕网络、表单、列表、导航、搜索与工程分层,用真实业务问题串起可迁移的解决思路。
专题亮点
先看这份专题能帮你补齐什么,再从侧边栏目录快速跳到想复习的章节。
SwiftUI 实战专题课:不是把页面写出来,而是把问题解决掉
这是一篇适合放在技术站点首页、专题页或学习中心的 SwiftUI 长文课程。它不从零碎 API 出发,而是围绕真实业务里最常见的坑来讲:为什么列表会卡、为什么表单总是体验不好、为什么一接搜索和分页就开始混乱、为什么项目一大导航就难维护。
每节都给你一个高频场景、一套落地思路、一段可迁移代码。
学完后,你能更稳地处理状态、网络、表单、列表、路由和异步任务。
从"会写界面"到"能扛业务"
这套内容的设计方式很简单:每节先给你一个真实项目里会遇到的问题,再给你一套能在实际开发中复用的解决办法。你会看到的不只是控件怎么用,而是状态该怎么建、边界该怎么分、交互细节该怎么处理。
网络请求不是 fetch 一下就结束
高频痛点
页面常见的问题不是"拿不到数据",而是 loading、error、empty、retry、pull to refresh 混在一起后,状态很快失控。
你会得到什么
这节会带你做一个可复用的加载状态模型,把"请求成功"之外的所有分支都设计清楚。
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()
}
}
}登录和注册页,最容易写出"能点但不好用"的表单
高频痛点
用户输入体验差通常不是 UI 不够漂亮,而是焦点跳转、即时校验、禁用按钮、重复提交这些细节没有处理好。
你会得到什么
这一节会把"能提交"升级成"提交体验顺手",特别适合登录、注册、修改资料和发布内容场景。
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."
}
}
}列表卡顿和分页混乱,往往不是 SwiftUI 慢,而是边界没设计好
高频痛点
很多人写列表时把"首次加载"和"加载更多"混在一起,再叠加图片加载和刷新,结果出现闪烁、重复请求、滚动掉帧。
你会得到什么
你会学会给分页一个干净的边界,让列表页从"能跑"变成"顺"。
@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()
}
}
}NavigationStack 一复杂,页面跳转马上失控
高频痛点
项目一旦进入真实业务,push、sheet、fullScreenCover、deep link 会同时出现。只靠局部 Bool 控制,很快会变成维护噩梦。
你会得到什么
这一节带你把路由抽象成明确的数据结构,让导航逻辑可读、可调试、可扩展。
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()
}
}
}
}
}搜索页最常见的问题,不是搜不到,而是"搜得太勤"
高频痛点
用户每输入一个字都发请求,旧请求还没回来,新请求又发出去了。最终结果是接口压力大,页面结果跳来跳去。
你会得到什么
我们会做一个可取消、可延迟的搜索流,让实时搜索真正可上线。
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 = []
}
}
}
}做完页面不代表能上线,最后还差一层工程化思维
高频痛点
很多 SwiftUI 项目在 demo 阶段很顺,一接业务就难维护,根本原因是视图、状态、服务和副作用没有分层。
你会得到什么
最后一节会帮你建立一条更稳的工程路径,让代码不是只适合演示,而是适合迭代。
你最终会建立起一套稳定的 SwiftUI 思维方式
遇到页面不刷新、状态打架、重复请求时,你知道先查哪里。
你可以把示例代码直接迁移到登录页、列表页、搜索页和个人中心页。
你会形成"问题 -> 边界 -> 状态 -> 交互 -> 代码组织"的实战思路。
建议你边学边做这 4 件事
做一个带登录、列表、搜索、详情的题库类 App,把课程里的 6 个模块串成一条主线。
把所有页面都补上 loading、empty、error 三态,不再只处理"请求成功"的 happy path。
把导航状态统一收口,至少让 push 和 sheet 不再分别散落在 5 个页面里。
给表单页补上焦点移动、即时校验和防重复提交,形成你自己的项目模板。
前两页试看结束
登录后继续阅读完整专题内容。当前专题暂不支持打印和 PDF 导出。