在 SwiftUI 的后台线程上运行 API

问题描述 投票:0回答:1

因此,对于此场景,代码遵循模型、视图、视图模型架构,其中

@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)
        }
    }
}
ios swift background-thread
1个回答
0
投票

新的并发(异步/等待)取代了“旧的”

DispatchQueue
方法,它们不应该混合。 WWDC 的“Meet async/await”视频内容非常丰富。

理想情况下,您将有一个“服务”或“管理器”来处理自定义

globalActor
actor
中的后台工作。

View
ViewModel
应该留在
MainActor
中,因为它们处理 UI。

流程应该是这样的

  • MainActor 触发请求>
  • 请求在其他地方执行>
  • 请求返回到MainActor >
  • 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
方法,但我发现它难看且反模式。

© www.soinside.com 2019 - 2024. All rights reserved.