Swift 将完成处理程序与 return 个值组合

Swift Combine Completion Handler with return of values

我实现了 API 服务处理程序,它在请求的 header 中使用身份验证令牌。当用户在应用程序启动时登录时获取此令牌。 30 分钟后,令牌过期。因此,当在此时间跨度之后发出请求时,API return 是一个 403 状态码。然后 API 应该再次登录并重新启动当前的 API 请求。

我遇到的问题是获取新令牌的登录函数使用完成处理程序让调用代码知道异步登录过程是否成功。当 API 获得 403 状态代码时,它会调用登录过程,完成后,它应该再次发出当前请求。但是这个重复的 API 请求应该再次 return 一些值。但是,returning 值在完成块中是不可能的。有谁知道整个问题的解决方案?

登录功能如下:

func login (completion: @escaping (Bool) -> Void) {
    
    self.loginState = .loading
    
    let preparedBody = APIPrepper.prepBody(parametersDict: ["username": self.credentials.username, "password": self.credentials.password])

    let cancellable = service.request(ofType: UserLogin.self, from: .login, body: preparedBody).sink { res in
        switch res {
        case .finished:
            if self.loginResult.token != nil {
                self.loginState = .success
                self.token.token = self.loginResult.token!

                _ = KeychainStorage.saveCredentials(self.credentials)
                _ = KeychainStorage.saveAPIToken(self.token)

                completion(true)
            }
            else {
                (self.banner.message, self.banner.stateIdentifier, self.banner.type, self.banner.show) = ("ERROR", "TOKEN", "error", true)
                self.loginState = .failed(stateIdentifier: "TOKEN", errorMessage: "ERROR")
                completion(false)
            }
        case .failure(let error):
            (self.banner.message, self.banner.stateIdentifier, self.banner.type, self.banner.show) = (error.errorMessage, error.statusCode, "error", true)
            self.loginState = .failed(stateIdentifier: error.statusCode, errorMessage: error.errorMessage)
            completion(false)
        }
    } receiveValue: { response in
        self.loginResult = response
    }
    
    self.cancellables.insert(cancellable)
}

API服务如下:

func request<T: Decodable>(ofType type: T.Type, from endpoint: APIRequest, body: String) -> AnyPublisher<T, Error> {
    
    var request = endpoint.urlRequest
    request.httpMethod = endpoint.method
    
    if endpoint.authenticated == true {
        request.setValue(KeychainStorage.getAPIToken()?.token, forHTTPHeaderField: "token")
    }
    
    if !body.isEmpty {
        let finalBody = body.data(using: .utf8)
        request.httpBody = finalBody
    }
    
    return URLSession
        .shared
        .dataTaskPublisher(for: request)
        .receive(on: DispatchQueue.main)
        .mapError { _ in Error.unknown}
        .flatMap { data, response -> AnyPublisher<T, Error> in
            
            guard let response = response as? HTTPURLResponse else {
                return Fail(error: Error.unknown).eraseToAnyPublisher()
            }
            
            let jsonDecoder = JSONDecoder()
            
            if response.statusCode == 200 {
                return Just(data)
                    .decode(type: T.self, decoder: jsonDecoder)
                    .mapError { _ in Error.decodingError }
                    .eraseToAnyPublisher()
            }
            else if response.statusCode == 403 {
                
                let credentials = KeychainStorage.getCredentials()
                let signinModel: SigninViewModel = SigninViewModel()
                signinModel.credentials = credentials!
        
                signinModel.login() { success in
                    
                    if success == true {
------------------->    // MAKE THE API CALL AGAIN AND THUS RETURN SOME VALUE
                    }
                    else {
------------------->    // RETURN AN ERROR
                    }
        
                }
    
            }
            else if response.statusCode == 429 {
                return Fail(error: Error.errorCode(statusCode: response.statusCode, errorMessage: "Oeps! Je hebt teveel verzoeken gedaan, wacht een minuutje")).eraseToAnyPublisher()
            }
            else {
                do {
                    let errorMessage = try jsonDecoder.decode(APIErrorMessage.self, from: data)
                    return Fail(error: Error.errorCode(statusCode: response.statusCode, errorMessage: errorMessage.error ?? "Er is iets foutgegaan")).eraseToAnyPublisher()
                }
                catch {
                    return Fail(error: Error.decodingError).eraseToAnyPublisher()
                }
            }
        }
        .eraseToAnyPublisher()
}

您正在尝试将 Combine 与旧的异步代码结合使用。您可以使用 Future 来完成,请在 this apple article:

中查看更多信息
Future { promise in
    signinModel.login { success in

        if success == true {
            promise(Result.success(()))
        }
        else {
            promise(Result.failure(Error.unknown))
        }

    }
}
    .flatMap { _ in
        // repeat request if login succeed
        request(ofType: type, from: endpoint, body: body)
    }.eraseToAnyPublisher()

但是当你不能修改异步方法或者你的大部分代码库都在使用它时,应该这样做。

在您的情况下,您似乎可以将 login 重写为 Combine。我无法构建你的代码,所以我的也可能有错误,但你应该明白:

func login() -> AnyPublisher<Void, Error> {

    self.loginState = .loading

    let preparedBody = APIPrepper.prepBody(parametersDict: ["username": self.credentials.username, "password": self.credentials.password])

    return service.request(ofType: UserLogin.self, from: .login, body: preparedBody)
        .handleEvents(receiveCompletion: { res in
            if case let .failure(error) = res {
                (self.banner.message,
                    self.banner.stateIdentifier,
                    self.banner.type,
                    self.banner.show) = (error.errorMessage, error.statusCode, "error", true)
                self.loginState = .failed(stateIdentifier: error.statusCode, errorMessage: error.errorMessage)
            }
        })
        .flatMap { loginResult in
            if loginResult.token != nil {
                self.loginState = .success
                self.token.token = loginResult.token!

                _ = KeychainStorage.saveCredentials(self.credentials)
                _ = KeychainStorage.saveAPIToken(self.token)

                return Just(Void()).eraseToAnyPublisher()
            } else {
                (self.banner.message, self.banner.stateIdentifier, self.banner.type, self.banner.show) = ("ERROR",
                    "TOKEN",
                    "error",
                    true)
                self.loginState = .failed(stateIdentifier: "TOKEN", errorMessage: "ERROR")
                return Fail(error: Error.unknown).eraseToAnyPublisher()
            }
        }
        .eraseToAnyPublisher()
}

然后这样称呼它:

signinModel.login()
    .flatMap { _ in
        request(ofType: type, from: endpoint, body: body)
    }.eraseToAnyPublisher()