Appearance
Swift Concurrency
军规
- 只能在主线程中更新 UI
前置知识
hashable
- 实现 hashable 协议 和 hash 函数
- String 本身就实现 hashable 协议,因此才能
swift
let values: [String] = ["1", "2", "3"]
...
Foreach(values, \.self) { value in
Text(value)
}Background Thread
- DispatchQueue.global.async()
- DispatchQueue.main.async()
- DispatchQueue.main.asyncAfter()
weak self/@escaping
- Swift 的内存管理:ARC(Automatic Reference Counting)
weak self防止循环引用导致内存泄漏@escaping用于函数的闭包参数,函数会异步回调闭包,由于闭包会在函数结束后才执行,这时候需要使用它来延长闭包的生命周期,告诉系统先不要释放闭包
Details
swift
import SwiftUI
import Combine
typealias getAppetizersCallback = (_ data :[String]) -> Void
class Manager {
func getAppetizers(competed: @escaping getAppetizersCallback) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let result = ["New Title", "Cool sala"]
competed(result)
}
}
}
class ViewModel: ObservableObject {
let manager = Manager()
@Published var appetizers: [String] = []
func getAppetizers() {
manager.getAppetizers { [weak self] result in
self?.appetizers = result
}
}
}
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(viewModel.appetizers, id: \.self) { appetizer in
Text("\(appetizer)")
.fontWeight(.bold)
.font(.title)
.padding()
}
if viewModel.appetizers.count == 0 {
Text("Empty appetizer")
}
}
.padding()
.onAppear {
viewModel.getAppetizers()
}
}
}请求 API 数据 - @escaping
Details
swift
import SwiftUI
import Combine
class Manager {
let getAppetizersURL: String = "https://api.pingcx.cn/v3/appetizers"
func getAppetizers(completed: @escaping (Data?) -> Void) {
URLSession.shared.dataTask(with: URL(string: getAppetizersURL)!) { data, resp, err in
guard err == nil ,
let resp = resp as? HTTPURLResponse, resp.statusCode == 200,
let data
else {
completed(nil)
return
}
completed(data)
}.resume()
}
}
class ViewModel: ObservableObject {
let manager = Manager()
@Published var appetizers: [Appetizer] = []
func getAppetizers() {
manager.getAppetizers { data in
guard let data else {
print("fetch data failed")
return
}
guard let res = try? JSONDecoder().decode(GetAppetizerResponse.self, from: data) else {
print("parse data failed")
return
}
DispatchQueue.main.async {
self.appetizers = res.appetizers
}
}
}
}
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
ZStack {
List {
ForEach(viewModel.appetizers) { appetizer in
VStack {
Text(appetizer.name)
Text(appetizer.description)
}
}
}
.onAppear {
viewModel.getAppetizers()
}
if viewModel.appetizers.count == 0 {
Text("Empty appetizer")
}
}
.navigationTitle("Appetizers")
}
}
}请求 API 数据 - Combine
将 请求 API 数据 - @escaping 的 getAppetizers() 替换为如下代码
Details
swift
...
@Published var appetizers: [Appetizer] = []
var cancellables = Set<AnyCancellable>()
...
func getAppetizers() {
guard let url = URL(string: getAppetizersURL) else {
return
}
URLSession.shared.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.main)
.tryMap { (data, response) -> Data in
guard
let response = response as? HTTPURLResponse,
response.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
.decode(type: GetAppetizerResponse.self, decoder: JSONDecoder())
.replaceError(with: GetAppetizerResponse(appetizers: []))
.sink(receiveValue: { [weak self] result in
self?.appetizers = result.appetizers
})
.store(in: &cancellables)
}Combine
- Combine 是什么
苹果官方的响应式编程框架,专门解决“数据变化如何驱动 UI”
- 核心概念:
- Publisher(发布者):数据源
- Subscriber(订阅者):监听数据变化的人
- Operators(操作符):过滤器、转换器,比如 map、filter、debounce
- SwiftUI + Combine 的日常玩法
swift
class CounterViewModel: ObservableObject {
@Published var value = 0
}- value 就是 Publisher
- SwiftUI 的 View 订阅它
- value 变了,View 自动重绘
- Combine 核心操作符:map、filter、debounce
swift
// 举例:搜索防抖
@Published var query = ""
var cancellable = $query
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.sink { text in
print("发送网络请求: \(text)")
}- 总结
- Combine = 数据流 + 响应式处理
- SwiftUI + Combine = 数据变化自动刷新 UI
- 常用套路:@Published → ObservableObject → SwiftUI View 订阅 → Operator 加工
Combine: Timer 和 onReceive
- current time
- countdown
countdown to date
swift
import SwiftUI
import Combine
struct ContentView: View {
@State var timeRemaining = "--"
let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
let futureDate: Date = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) ?? Date()
func updateTimeRemaining() {
let remaining = Calendar.current.dateComponents([.hour, .minute, .second], from: Date(), to: futureDate)
let min = remaining.minute ?? 0
let sec = remaining.second ?? 0
timeRemaining = "\(min):\(sec)"
}
var body: some View {
Text(timeRemaining)
.font(.system(size: 100, weight: .semibold, design: .rounded))
.lineLimit(1)
.minimumScaleFactor(0.1)
.onReceive(timer) { _ in
updateTimeRemaining()
}
}
}动画:加载中
swift
import SwiftUI
import Combine
struct ContentView: View {
let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
@State var count = 0
var body: some View {
HStack(spacing: 15) {
Circle()
.offset(y: count == 1 ? -20: 0)
Circle()
.offset(y: count == 2 ? -20: 0)
Circle()
.offset(y: count == 3 ? -20: 0)
}
.frame(width: 150)
.onReceive(timer) { _ in
withAnimation(.easeInOut(duration: 0.45)) {
count = count == 3 ? 0 : count + 1
}
}
}
}动画:轮播图
swift
struct ContentView: View {
let timer = Timer.publish(every: 2.0, on: .main, in: .common).autoconnect()
@State var count = 0
var body: some View {
TabView(selection: $count, content: {
Rectangle()
.foregroundColor(.red)
.tag(1)
Rectangle()
.foregroundColor(.blue)
.tag(2)
Rectangle()
.foregroundColor(.green)
.tag(3)
Rectangle()
.foregroundColor(.orange)
.tag(4)
Rectangle()
.foregroundColor(.pink)
.tag(5)
})
.frame(height: 200)
.tabViewStyle(PageTabViewStyle())
.onReceive(timer) { _ in
if count == 5 {
withAnimation(nil) {
count = 1
}
} else {
withAnimation(.default) {
count += 1
}
}
}
}
}自定义 Publishers and Subscribers in Combine
定义在 ViewModel 中的计时器(之前是定义在 View 里的)
1. 自定义 Publisher
计数器案例
swift
import SwiftUI
import Combine
class ViewModel: ObservableObject {
@Published var count = 0
var timer: AnyCancellable?
init() {
setupTimer()
}
func setupTimer() {
timer = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
guard let self = self else {
return
}
self.count += 1
if self.count >= 10 {
self.timer?.cancel()
}
}
}
}
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
Text("\(viewModel.count)")
}
}2. 自定义 Subscriber
输入框 debounce 案例
swift
import SwiftUI
import Combine
class ViewModel: ObservableObject {
var cancellables = Set<AnyCancellable>()
@Published var textFieldText: String = ""
@Published var textIsValid: Bool = false
init() {
addTextFieldSubscriber()
}
func addTextFieldSubscriber() {
$textFieldText
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.map { (text) -> Bool in
text.count > 3
}
// .assign(to: \.textIsValid, on: self) // 推荐使用 sink, 而不是 assign 因为无法使用 weak self
.sink(receiveValue: { [weak self] (isValid) in
self?.textIsValid = isValid
})
.store(in: &cancellables)
}
}
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
TextField("Type something here...", text: $viewModel.textFieldText)
.padding(.leading)
.frame(height: 55)
.font(.headline)
.background(Color(.gray))
.cornerRadius(10)
.overlay(
ZStack {
Image(systemName: "xmark")
.foregroundColor(.red)
.opacity(viewModel.textFieldText.count < 1 ? 0.0 : viewModel.textIsValid ? 0.0 : 1.0)
Image(systemName: "checkmark")
.foregroundColor(.green)
.opacity(viewModel.textIsValid ? 1.0 : 0.0)
}
.font(.title)
.padding(.trailing),
alignment: .trailing
)
Button {
print("Pressed")
} label: {
Text("Submit".uppercased())
.font(.headline)
.foregroundColor(.white)
.frame(height: 55)
.frame(maxWidth: .infinity)
.background(Color(.blue))
.cornerRadius(10)
.opacity(viewModel.textIsValid ? 1.0 : 0.5)
}
.disabled(!viewModel.textIsValid)
}
}
}do/try/catch/throws
Details
swift
import SwiftUI
import Combine
class Manager {
var isActivate: Bool = true
func getTitle() throws -> String {
if !isActivate {
throw URLError(.badURL)
}
return "New Title"
}
}
class ViewModel: ObservableObject {
let manager = Manager()
@Published var title: String = ""
func getTitle() {
title = "Hello"
do {
let newTitle = try manager.getTitle()
title = newTitle
} catch {
title = error.localizedDescription
}
}
}
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("\(viewModel.title)")
}
.padding()
.onAppear {
viewModel.getTitle()
}
}
}getTitle()的写法有很多种,尝试都实现一下try?忽略抛出的错误
Download images
三种方式:
1. @escaping
swift
import SwiftUI
import Combine
typealias completedFn = (_ image: UIImage?, _ error: Error?) -> ()
class ImageManager {
let url = URL(string: "https://picsum.photos/200")!
func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
guard
let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
return nil
}
guard
let data = data,
let image = UIImage(data: data) else {
return nil
}
return image
}
func download(completed: @escaping completedFn) {
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
let image = self?.handleResponse(data: data, response: response)
completed(image, error)
}
.resume()
}
}
class ImageViewModel: ObservableObject {
let imageManager = ImageManager()
@Published var image: UIImage? = nil
func fetchImage() {
imageManager.download { [weak self] image, _ in
DispatchQueue.main.async {
self?.image = image
}
}
}
}
struct ContentView: View {
@StateObject private var viewModel = ImageViewModel()
var body: some View {
ZStack {
if let image = viewModel.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 250, height: 250)
}
}
.onAppear {
viewModel.fetchImage()
}
}
}2. Combine
swift
import SwiftUI
import Combine
typealias completedFn = (_ image: UIImage?, _ error: Error?) -> ()
class ImageManager {
let url = URL(string: "https://picsum.photos/200")!
func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
guard
let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
return nil
}
guard
let data = data,
let image = UIImage(data: data) else {
return nil
}
return image
}
func download() -> AnyPublisher<UIImage?, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map(handleResponse)
// 把 publisher 的错误类型统一成 Error
.mapError({ $0 })
// 类型擦除
.eraseToAnyPublisher()
}
}
class ImageViewModel: ObservableObject {
let imageManager = ImageManager()
var cancellables = Set<AnyCancellable>()
@Published var image: UIImage? = nil
func fetchImage() {
imageManager.download()
.receive(on: DispatchQueue.main)
.sink { _ in
} receiveValue: { [weak self] image in
self?.image = image
}
.store(in: &cancellables)
}
}
struct ContentView: View {
@StateObject private var viewModel = ImageViewModel()
var body: some View {
ZStack {
if let image = viewModel.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 250, height: 250)
}
}
.onAppear {
viewModel.fetchImage()
}
}
}3. Async/Await
swift
import SwiftUI
import Combine
typealias completedFn = (_ image: UIImage?, _ error: Error?) -> ()
class ImageManager {
let url = URL(string: "https://picsum.photos/200")!
func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
guard
let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
return nil
}
guard
let data = data,
let image = UIImage(data: data) else {
return nil
}
return image
}
func download() async throws -> UIImage? {
do {
let (data, response) = try await URLSession.shared.data(from: url, delegate: nil)
return handleResponse(data: data, response: response)
} catch {
throw error
}
}
}
class ImageViewModel: ObservableObject {
let imageManager = ImageManager()
var cancellables = Set<AnyCancellable>()
@Published var image: UIImage? = nil
func fetchImage() async {
let image = try? await imageManager.download()
await MainActor.run {
self.image = image
}
}
}
struct ContentView: View {
@StateObject private var viewModel = ImageViewModel()
var body: some View {
ZStack {
if let image = viewModel.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 250, height: 250)
}
}
.onAppear {
Task {
await viewModel.fetchImage()
}
}
}
}async & await
如何实现延迟 2 秒后执行?
- 旧方式:
DispatchQueue.global().asyncAfter(deadline: .now() + 2) { - 新方式:
try? await Task.sleep(nanoseconds: 2_000_000_000)
主线程延迟执行,直接修改 UI 的状态
swift
func addTitle1() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.dataArray.append("Title1 : \(Thread.current)")
}
}后台线程延迟执行,必须通过 DispatchQueue.main.async 修改 UI 的状态
swift
func addTitle2() {
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
let title = "Title2 : \(Thread.current)"
DispatchQueue.main.async {
self.dataArray.append(title)
let title3 = "Title3 : \(Thread.current)"
self.dataArray.append(title3)
}
}
}新方式也是一样,await 之后 可能 会从主线程切换到后台线程,因此在需要修改 UI 的状态时,必须通过 await MainActor.run(body: { 来修改
swift
func addAuthor1() async {
let author1 = "Author1 : \(Thread.current)"
self.dataArray.append(author1)
try? await Task.sleep(nanoseconds: 2_000_000_000)
let author2 = "Author2 : \(Thread.current)"
await MainActor.run(body: {
self.dataArray.append(author2)
let author3 = "Author3 : \(Thread.current)"
self.dataArray.append(author3)
})
}Task 和 .task()
- Task,
onAppear和onDisappear task()自动取消任务- 优先级
async let 和 TaskGroup
Details
swift
func fetchImagesWithTaskGroup() async throws -> [UIImage] {
let urlStrings = [
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
"https://picsum.photos/300",
]
return try await withThrowingTaskGroup(of: UIImage?.self) { group in
var images: [UIImage] = []
images.reserveCapacity(urlStrings.count)
for urlString in urlStrings {
group.addTask {
try? await self.fetchImage(urlString: urlString)
}
}
for try await image in group {
if let image = image {
images.append(image)
}
}
return images
}
}Continuations 转换头
withCheckedThrowingContinuation 的作用是:把传统的“回调闭包写法”包装成 async/await 可用的异步函数
- 把回调式网络请求转成 async/await
swift
func downloadImage(from url: URL) async throws -> UIImage {
try await withCheckedThrowingContinuation { continuation in
URLSession.shared.dataTask(with: url) { data, _, error in
if let data = data, let image = UIImage(data: data) {
continuation.resume(returning: image)
} else {
continuation.resume(throwing: error ?? URLError(.badServerResponse))
}
}.resume()
}
}把 Firebase、CoreLocation 这种闭包 API 转换
把 GCD 异步任务转换成 async
swift
func doHeavyWork() async throws -> Int {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global().async {
// 模拟耗时任务
sleep(2)
if Bool.random() {
continuation.resume(returning: 42)
} else {
continuation.resume(throwing: NSError(domain: "Failed", code: -1))
}
}
}
}actor
- actor 和 class 的区别:线程安全。actor 是带自动加锁的 class,专为并发安全设计
struct、class 和 actor 的区别
Links:
https://blog.onewayfirst.com/ios/posts/2019-03-19-class-vs-struct/
https://stackoverflow.com/questions/24217586/structure-vs-class-in-swift-language
https://medium.com/@vinayakkini/swift-basics-struct-vs-class-31b44ade28ae
https://stackoverflow.com/questions/24217586/structure-vs-class-in-swift-language/59219141#59219141
https://stackoverflow.com/questions/27441456/swift-stack-and-heap-understanding
https://stackoverflow.com/questions/24232799/why-choose-struct-over-class/24232845
https://www.backblaze.com/blog/whats-the-diff-programs-processes-and-threads/
https://medium.com/doyeona/automatic-reference-counting-in-swift-arc-weak-strong-unowned-925f802c1b99
VALUE TYPES:
- Struct, Enum, String, Int, etc.
- Stored in the Stack
- Faster
- Thread safe!
- When you assign or pass value type a new copy of data is created
REFERENCE TYPES:
- Class, Function, Actor
- Stored in the Heap
- Slower, but synchronized
- NOT Thread safe (by default)
- When you assign or pass reference type a new reference to original instance will be created (pointer)
- - - - - - - - - - - - - -
STACK:
- Stores Value types
- Variables allocated on the stack are stored directly to the memory, and access to this memory is very fast
- Each thread has it's own stack!
HEAP:
- Stores Reference types
- Shared across threads!
- - - - - - - - - - - - - -
STRUCT:
- Based on VALUES
- Can be mutated
- Stored in the Stack!
CLASS:
- Based on REFERENCES (INSTANCES)
- Stored in the Heap!
- Inherit from other classes
ACTOR:
- Same as Class, but thread safe!
- - - - - - - - - - - - - -
Structs: Data Models, Views
Classes: ViewModels
Actors: Shared 'Manager' and 'Data Stores'class 使用 DispatchQueue 来实现互斥
swift
class MyDataManager {
static let instance = MyDataManager()
private init() { }
var data: [String] = []
private let lock = DispatchQueue(label: "com.MyApp.MyDataManager")
func getRandomData(completionHandler: @escaping (_ title: String?) -> ()) {
lock.async {
self.data.append(UUID().uuidString)
print(Thread.current)
completionHandler(self.data.randomElement())
}
}
}actor 自带互斥锁
swift
actor MyActorDataManager {
static let instance = MyActorDataManager()
private init() { }
var data: [String] = []
nonisolated let myRandomText = "asdfasdfadfsfdsdfs"
func getRandomData() -> String? {
self.data.append(UUID().uuidString)
print(Thread.current)
return self.data.randomElement()
}
nonisolated func getSavedData() -> String {
return "NEW DATA"
}
}nonisolated修饰的函数或变量,调用方不需要使用await
@globalActor 和 @MainActor
| 特性 | @MainActor | @globalActor |
|---|---|---|
| 定义者 | Swift 内置 | 你自定义 |
| 并发域 | 主线程 | 任意 actor |
| 作用 | UI 安全 | 全局逻辑隔离 |
| 典型用途 | SwiftUI ViewModel | 日志、数据库、配置中心 |
- UI 层:用 @MainActor
- 后台任务(网络、数据库、缓存):定义各自的 @globalActor
Sendable
没搞懂...
Sendable 用来确保你把某个对象从一个并发域(actor、task)传到另一个时,不会造成数据竞争(data race)或内存不安全
AsyncPublisher
没搞懂...
将 @Published 转为 async 的方式...
manage strong & weak references with Async Await
- 弄明白什么时候用 strong 和 weak
onAppear()、onDisappear()、task()
MVVM with Async Await
Details
swift
import SwiftUI
final class MyManagerClass {
func getData() async throws -> String {
"Some Data!"
}
}
actor MyManagerActor {
func getData() async throws -> String {
"Some Data!"
}
}
@MainActor
final class MVVMBootcampViewModel: ObservableObject {
let managerClass = MyManagerClass()
let managerActor = MyManagerActor()
@Published private(set) var myData: String = "Starting text"
private var tasks: [Task<Void, Never>] = []
func cancelTasks() {
tasks.forEach({ $0.cancel() })
tasks = []
}
func onCallToActionButtonPressed() {
let task = Task {
do {
// myData = try await managerClass.getData()
myData = try await managerActor.getData()
} catch {
print(error)
}
}
tasks.append(task)
}
}
struct MVVMBootcamp: View {
@StateObject private var viewModel = MVVMBootcampViewModel()
var body: some View {
VStack {
Button(viewModel.myData) {
viewModel.onCallToActionButtonPressed()
}
}
.onDisappear {
}
}
}