因此,对于此场景,代码遵循模型、视图、视图模型架构,其中
@Observable class DiscoverAPIViewModel
是一个 View-Model,它通过 API 调用来搜索某些数据。
@Observable class DiscoverAPIViewModel {
private (set) var contentRecsVideosByKeyword: [VideoData] = [] // note: A
func contentRecsVideosByKeywordApi(accessToken: String, username: String, keyword: String, count: Int = 5)
{
// ....
// fetch data via an api call
// on success
DispatchQueue.main.async { // note: B
strongSelf.contentRecsVideosByKeyword = [] // try force-refresh (by design ids are unique, so re-assigning may not always reset the list)
strongSelf.contentRecsVideosByKeyword = result.data!
}
请注意以上事项:
A.
contentRecsVideosByKeyword
是有状态的,因为该类使用 @Observable
宏
B.当数据检索成功时,
DispatchQueue.main.async
用于设置contentRecsVideosByKeyword
,因为它在视图部分中使用(见下文)
Model 只是一个简单的结构体,具有
id
、name
等原始值。 View 有警告
无法从 Sendable 闭包中引用主参与者隔离属性“discoverApiViewModel”;这是 Swift 6 中的错误
因为下面的代码
struct ApiView: View {
@State private var discoverApiViewModel = DiscoverAPIViewModel()
private let keyword: String
private let username: String
private let accessToken: String
private let videoCountPerPage = 7
// ...
// other properties like durationMilliseconds, and methods
init(username: String, accessToken: String, keyword: String) {
// ... init values for one time use
}
var body: some View {
List(discoverApiViewModel.contentRecsVideosByKeyword) { item in
Text(item.name) // displaying data in the view
}
.task {
do {
try? await Task.sleep(for: durationMilliseconds)
DispatchQueue.global(qos: .background).async {
discoverApiViewModel // Main actor-isolated property 'discoverApiViewModel'...
.contentRecsVideosByKeywordApi(accessToken: accessToken, username: username, keyword: keyword, count: videoCountPerPage)
}
}
}
}
}
请问,有人可以建议如何在后台正确调用 API,然后更新视图(使用一些 stateful 数据),使用
SwiftUI
表示 View,使用 Observable
表示 View-Model?目前,大多数代码都是基于 MVVM 架构,修改/重构它会导致运行时相关的main actor异常(在某些情况下,例如当应用程序从后台状态变为活动状态时,但它似乎高度相关)。
loadingView
.onAppear {
apiTask = createApiTask(viewModel: discoverApiViewModel)
}
哪里
createApiTask
是
func createApiTask(viewModel: DiscoverAPIViewModel) -> Task<Void,Error> {
Task(priority: .background) {
try await Task.sleep(for: durationMilliseconds)
DispatchQueue.global(qos: .background).async {
viewModel.contentRecsVideosByKeywordApi(accessToken: accessToken, username: username, keyword: keyword, count: videoCountPerPage)
}
}
}
新的并发(异步/等待)取代了“旧的”
DispatchQueue
方法,它们不应该混合。 WWDC 的“Meet async/await”视频内容非常丰富。
理想情况下,您将有一个“服务”或“管理器”来处理自定义
globalActor
或 actor
中的后台工作。
View
和 ViewModel
应该留在 MainActor
中,因为它们处理 UI。
流程应该是这样的
您的代码混合了请求和 UI。
这是一种方法...
import SwiftUI
// Isolated to Main Actor by default in iOS 18+
struct IsolatedSampleView: View {
@State private var model: IsolatedSampleViewModel = .init()
@State private var isProcessing: Bool = false
var body: some View {
VStack {
if isProcessing {
ProgressView()
} else {
Text(model.contentRecsVideosByKeyword.description)
}
}
.task { //trigger
isProcessing = true
await model.contentRecsVideosByKeywordApi(accessToken: "", username: "", keyword: "", count: 7)
isProcessing = false
}
}
}
#Preview {
IsolatedSampleView()
}
@MainActor //Update UI on MainActor
@Observable
class IsolatedSampleViewModel {
var contentRecsVideosByKeyword: [String] = []
func contentRecsVideosByKeywordApi(accessToken: String, username: String, keyword: String, count: Int) async {
//MainActor update //Detach from MainActor
contentRecsVideosByKeyword = await Task.detached(operation: {
// MainActor.assertIsolated("Not isolated!!") // Will cause a crash because we are no longer isolated to Main
try? await Task.sleep(for: .seconds(3))
//Return to the MainActor
return [accessToken, username, keyword, count.description]
}).value
}
}
https://developer.apple.com/documentation/swiftui/state
请注意,正如我上面所说,
Task.detached
可以轻松地替换为actor
或globalActor
隔离上下文或其他自定义Sendable
类型。
有很多方法可以离开
MainActor
,但方法应该与我上面列出的相同,在其他地方完成工作,然后return
将数据发送到主程序。
有些人会建议这样的事情
@MainActor
@Observable
class IsolatedSampleViewModel {
var contentRecsVideosByKeyword: [String] = []
func contentRecsVideosByKeywordApi(accessToken: String, username: String, keyword: String, count: Int) async {
//Detach from MainActor
await Task.detached(operation: { [weak self] in
// MainActor.assertIsolated("Not isolated!!") // Will cause a crash because we are no londer isolated to Main
try? await Task.sleep(for: .seconds(3))
guard let self else {return}
//Update on the MainActor
await MainActor.run {
self.contentRecsVideosByKeyword = [accessToken, username, keyword, count.description]
}
}).value
}
}
这模仿了
DispatchQueue
方法,但我发现它难看且反模式。