【问题标题】:Swift Combine Completion Handler with return of valuesSwift将完成处理程序与返回值结合起来
【发布时间】:2021-11-02 13:03:13
【问题描述】:

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

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

登录功能如下:

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()
}

【问题讨论】:

    标签: swift asynchronous combine completionhandler


    【解决方案1】:

    您正在尝试将 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()
    

    【讨论】:

    • 谢谢 :) 我明天会看看它并分享我的发现。再次感谢您的回答!
    • 我好像收到了一个错误,我不太明白...你能看看吗? imgur.com/a/DEAtUwJ
    • @Björn 看起来我在.flatMap 之后忘记了.eraseToAnyPublisher(),将其添加到我的答案中
    • imgur.com/a/m6Og1QZ 是否不需要在这一行添加 AnyPublisher.flatMap { loginResult -&gt; AnyPublisher&lt;Void, Error&gt; in ?如果我只添加 .eraseToAnyPublisher() 它会给我:'表达式类型不明确,没有更多上下文'
    • @Björn 确定没问题。与局部变量冲突,在request前加self.,还需要在signinModel.login()前return
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-06-02
    • 1970-01-01
    • 2021-12-20
    • 1970-01-01
    • 1970-01-01
    • 2021-10-14
    • 1970-01-01
    相关资源
    最近更新 更多