第14章:SwiftUI 进阶
14.1 动画
SwiftUI 动画非常简单,只需添加 .animation:
struct AnimationView: View {
@State private var isExpanded = false
var body: some View {
VStack {
RoundedRectangle(cornerRadius: isExpanded ? 50 : 10)
.fill(isExpanded ? Color.blue : Color.red)
.frame(
width: isExpanded ? 300 : 100,
height: isExpanded ? 300 : 100
)
.animation(.spring(response: 0.3, dampingFraction: 0.5), value: isExpanded)
Button("切换") {
isExpanded.toggle()
}
}
}
}
**动画类型: **
-
.default- 默认动画 -
.linear- 线性 -
.easeIn/.easeOut/.easeInOut- 缓动 -
.spring()- 弹性动画 -
.interactiveSpring()- 交互式弹性
14.2 手势
struct GestureView: View {
@State private var offset: CGSize = .zero
@State private var scale: CGFloat = 1.0
@State private var rotation: Angle = .zero
var body: some View {
Image(systemName: "star.fill")
.font(.system(size: 100))
.foregroundColor(.yellow)
.offset(offset)
.scaleEffect(scale)
.rotationEffect(rotation)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded { _ in
withAnimation {
offset = .zero
}
}
)
.gesture(
MagnificationGesture()
.onChanged { scale = $0 }
)
.gesture(
RotationGesture()
.onChanged { rotation = $0 }
)
}
}
14.3 数据持久化
import SwiftUI
struct TodoItem: Identifiable, Codable {
let id: UUID
var title: String
var isCompleted: Bool
var createdAt: Date
}
class TodoStore: ObservableObject {
@Published var todos: [TodoItem] = []
private let saveKey = "todos"
init() {
load()
}
func add(_ title: String) {
let todo = TodoItem(
id: UUID(),
title: title,
isCompleted: false,
createdAt: Date()
)
todos.append(todo)
save()
}
func toggle(_ todo: TodoItem) {
if let index = todos.firstIndex(where: { $0.id == todo.id }) {
todos[index].isCompleted.toggle()
save()
}
}
func delete(at offsets: IndexSet) {
todos.remove(atOffsets: offsets)
save()
}
// UserDefaults 存储
private func save() {
if let encoded = try? JSONEncoder().encode(todos) {
UserDefaults.standard.set(encoded, forKey: saveKey)
}
}
private func load() {
if let data = UserDefaults.standard.data(forKey: saveKey),
let decoded = try? JSONDecoder().decode([TodoItem].self, from: data) {
todos = decoded
}
}
}
第15章:并发编程 - async/await
15.1 为什么需要并发?
想象一个场景:
-
用户点击按钮
-
App 需要从网络加载图片
-
如果用同步方式,界面会卡住,直到图片加载完成
-
用户看到"假死",体验极差
**并发让 App 能同时做多件事: **
-
主线程:响应用户操作、更新界面
-
后台线程:网络请求、文件读写、复杂计算
15.2 旧的方式 - GCD
// 旧的写法(仍然有效,但不推荐新代码使用)
DispatchQueue.global().async {
// 后台线程执行耗时操作
let data = try! Data(contentsOf: url)
DispatchQueue.main.async {
// 主线程更新 UI
imageView.image = UIImage(data: data)
}
}
**问题: **
-
嵌套层级深(回调地狱)
-
错误处理麻烦
-
代码难以阅读和维护
15.3 async/await - 新的方式
// 定义异步函数
func fetchImage(from url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
// 调用异步函数
func loadImage() async {
do {
let image = try await fetchImage(from: imageURL)
imageView.image = image
} catch {
print("加载失败: \(error)")
}
}
**代码看起来像同步的,实际上是异步执行的! **
15.4 在 SwiftUI 中使用
struct AsyncImageView: View {
@State private var image: UIImage?
@State private var isLoading = false
let url: URL
var body: some View {
Group {
if let image = image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
} else if isLoading {
ProgressView()
} else {
Image(systemName: "photo")
.font(.largeTitle)
.foregroundColor(.gray)
}
}
.onAppear {
loadImage()
}
}
private func loadImage() {
isLoading = true
Task {
do {
let (data, _) = try await URLSession.shared.data(from: url)
await MainActor.run {
self.image = UIImage(data: data)
self.isLoading = false
}
} catch {
await MainActor.run {
self.isLoading = false
}
}
}
}
}
**Task: **创建异步任务的环境
**MainActor: **确保代码在主线程执行(用于 UI 更新)
15.5 结构化并发
// 顺序执行(慢)
func fetchSequentially() async throws -> [User] {
let user1 = try await fetchUser(id: 1)
let user2 = try await fetchUser(id: 2)
let user3 = try await fetchUser(id: 3)
return [user1, user2, user3]
}
// 并行执行(快!)
func fetchConcurrently() async throws -> [User] {
async let user1 = fetchUser(id: 1)
async let user2 = fetchUser(id: 2)
async let user3 = fetchUser(id: 3)
return try await [user1, user2, user3]
}
// 使用 TaskGroup 处理动态数量的任务
func fetchUsers(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask {
try await fetchUser(id: id)
}
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
**async let: **启动并行任务
**withTaskGroup: **管理一组动态任务
15.6 异步序列
// 异步序列
let stream = AsyncStream<Int> { continuation in
Task {
for i in 1...5 {
try? await Task.sleep(for: .seconds(1))
continuation.yield(i)
}
continuation.finish()
}
}
// 遍历
for await number in stream {
print("收到: \(number)")
}
第16章:Actor 与数据竞争防护
16.1 数据竞争问题
class UnsafeCounter {
var count = 0
func increment() {
count += 1 // 非线程安全!
}
}
let counter = UnsafeCounter()
// 从多个线程同时增加
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
counter.increment()
}
// 结果可能小于 1000!
print(counter.count)
原因: count += 1 实际上分三步:
-
读取 count 的值
-
加 1
-
写回 count
如果两个线程同时执行,可能都读到 5,都变成 6,结果只增加了 1。
16.2 Actor - 隔离状态
actor SafeCounter {
private var count = 0
func increment() {
count += 1 // 线程安全!
}
func getCount() -> Int {
return count
}
}
let counter = SafeCounter()
await withTaskGroup(of: Void.self) { group in
for _ in 0..<1000 {
group.addTask {
await counter.increment()
}
}
}
let finalCount = await counter.getCount()
print(finalCount) // 1000
**Actor 的特点: **
-
同一时间只有一个线程能访问 actor 内部
-
自动防止数据竞争
-
访问 actor 的属性和方法需要
await
16.3 MainActor - 主线程 Actor
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
func load() async {
items = await fetchItems() // await 切到后台
// 自动回到主线程
}
}
// 或者只标记某个方法
class ViewModel2: ObservableObject {
@Published var items: [Item] = []
@MainActor
func updateUI() {
// 确保在主线程执行
items.append(newItem)
}
}
16.4 Sendable 协议
// 安全的值类型,可以跨 actor 传递
struct UserData: Sendable {
let id: UUID
let name: String
}
// 类也可以遵循 Sendable,但需要是 final 且属性都是 Sendable
final class SafeClass: Sendable {
let value: Int
init(value: Int) {
self.value = value
}
}
**Swift 6 会强制检查: **如果类型不是 Sendable,不能跨 actor 传递。
第一次做分享,期待更优秀的你~