多次异步调用时,Swift 模型视图不会更新@Published var

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

尝试更新代码以刷新 OAuth 中的令牌后,我无法刷新视图。

我的屏幕:

struct MyScreen: View {
    @ObservedObject var viewModel = MyViewModel()

    var body: some View {
        NavigationStack {
            List {
                ForEach(viewModel.activeElements) { element in
                    NavigationLink(destination: ElementView(elementId: 1)) {
                        ElementRowView(element: element)
                    }
                }
            }
            .refreshable {
                await viewModel.fetchElements()
            }
            .onReceive(viewModel.$elements) { _ in
                print("Elements updated")
            }
        }
        .task {
            await viewModel.fetchElements()
        }
    }
}

我的视图模型:

class MyViewModel : ObservableObject {
    @Published var elements: [Element] = []

    @Published var error: APIError?
    
    var activeElements: [Element] {
        elements.filter { $0.name != "" }
    }
    
    func fetchElements() async {
        print("fetching")

        do {
            let response = try await ElementsAction().call()
print("assigning")
            self.elements = response
            self.error = nil
        } catch let apiError as APIError {
            self.error = apiError
        } catch {
            self.error = .unknown
        }
    }
}

元素动作:

struct ElementsAction {
    let path = "/api/v1/elements"
    let method: HTTPMethod = .get
    //    var parameters: LoginRequest

    func call() async throws -> [Element] {
        do {
            let data = try await APIRequest<EmptyRequest, Element>.call(
                path: path,
                method: .get,
                authorized: true
            )

            let response = try JSONDecoder().decode([Element].self, from: data)
            return response
        } catch {
            throw error
        }
    }
}

和 API 请求:

class APIRequest<Parameters: Encodable, Model: Decodable> {
    
    static func call(
        scheme: String = Config.shared.scheme,
        host: String = Config.shared.host,
        path: String,
        method: HTTPMethod,
        authorized: Bool = false,
        queryItems: [URLQueryItem]? = nil,
        parameters: Parameters? = nil,
        retryCount: Int = 0
    ) async throws -> Data {
        if !NetworkMonitor.shared.isReachable {
            throw APIError.noInternet
        }

        var components = URLComponents()
        components.scheme = scheme
        components.host = host
        components.path = path

        if let queryItems = queryItems {
            components.queryItems = queryItems
        }

        guard let url = components.url else {
            throw APIError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue

        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue("application/json", forHTTPHeaderField: "Accept")

        if let parameters = parameters {
            request.httpBody = try? JSONEncoder().encode(parameters)
        }

        if authorized, let token = Auth.shared.getAccessToken() {
            request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw APIError.response
        }

print("------------------")
print("on request: \(request) (\(httpResponse.statusCode))")
        switch httpResponse.statusCode {
        case 200...299:
print("on request: \(request) (\(httpResponse.statusCode)) - return data")
            return data
        case 401 where retryCount < 1 && authorized:
print("on request: \(request) (\(httpResponse.statusCode)) - 401")
            let success = await refreshToken()
            if success {
                let responseData = try await call(scheme: scheme, host: host, path: path, method: method, authorized: authorized, queryItems: queryItems, parameters: parameters, retryCount: retryCount + 1)
print("2nd returns data")
                return responseData
            } else {
                throw APIError.unauthorized
            }
        case 401 where retryCount >= 1 && authorized:
            throw APIError.unauthorized
        default:
            throw APIError.response
        }
    }
    
    private static func refreshToken() async -> Bool {
        do {
            let response = try await RefreshTokenAction(
                parameters: RefreshTokenRequest(
                    refresh_token: Auth.shared.getRefreshToken() ?? ""
                )
            ).call()
print("updating token")
            await MainActor.run {
                Auth.shared.setCredentials(
                    accessToken: response.accessToken,
                    refreshToken: response.refreshToken
                )
            }
            return true
        } catch {
            return false
        }
    }
}

当不需要调用refreshToken()时,此代码可以正常工作 - 只有一个请求。

但是,在更新刷新令牌时,它无法正常工作 - 在这种情况下,它会正确设置内容,因此下一个请求可以工作,但在该请求上,分配元素时不会更新元素。我使用当前的打印方法进行了调查,这是一个日志:

Elements updated
fetching
on request: https://myapp.com/api/v1/elements (401)
on request: https://myapp.com/api/v1/elements (401) - 401
on request: https://myapp.com/oauth/token (200)
on request: https://myapp.com/oauth/token (200) - return data
updating token
Elements updated <------ RECIEVE BEFORE FINISHING ALL REQUESTS
on request: https://myapp.com/api/v1/elements (200)
on request: https://myapp.com/api/v1/elements (200) - return data
2nd returns data
assigning <--------- IT DOESN'T UPDATE @Published elements 

我尝试了多种方法,我猜问题可能出在并发上,但我完全不知道如何解决这个问题。谁能告诉我这里有什么问题以及如何解决它?

swift swiftui
1个回答
0
投票

当您使用 async/await 时,您可以删除旧的合并

ObservableObject
@Published
代码,正如您所发现的,它们可能容易出错。
.task
自动为您提供类似引用的生命周期,因此不需要对象。您只需将异步函数更改为返回值的标准类型,例如

func fetchElements() async throws -> [Element] {

然后你可以为你的结果添加一个@State:

@State var elements: [Element] = []

并使用如下:

.task {
    elements = try? await fetchElements()
}

为了进一步改进架构,您可以将异步函数放入控制器结构中,并创建一个环境密钥来保存它。然后你可以在预览时用模拟替换控制器,例如

@Environment(\.myController) var myController
...
.task {
    elements = try? await myController.fetchElements()
}
© www.soinside.com 2019 - 2024. All rights reserved.