RxSwift URLSession request is disposed - swift

I'm learning to use RxSwift and I'm stuck using this simple code. My intention is to take an APIRequest type, as simple as this:
public protocol APIRequest: Encodable {
associatedtype Response: Decodable
var path: String { get }
}
Pass it to the API Client and finally return an Observable of type T.Response, however, I'm constantly seeing cancelled status in my console:
2019-07-01 10:46:04.847: test api request -> subscribed
2019-07-01 10:46:04.855: test api request -> isDisposed
This is my APIClient's code:
func send<T: APIRequest>(_ request: T) -> Observable<T.Response> {
guard let fullURL = endpoint(for: request) else {
return Observable.error(APIError.invalidBaseURL)
}
return Observable<T.Response>.create { observer in
let request = URLRequest(url: fullURL)
let response = URLSession.shared.rx.response(request: request)
.debug("test api request")
return response.subscribe(onNext: { response, data in
if 200..<300 ~= response.statusCode {
guard let responseItems = try? self.jsonDecoder.decode(T.Response.self, from: data) else {
return observer.onError(APIError.decodingFailed)
}
observer.onNext(responseItems)
observer.onCompleted()
}
}, onError: { error in
observer.onError(APIError.other(error))
}, onCompleted: nil,
onDisposed: nil)
}
}
I've been trying to get the results printed to the console with:
apiClient.send(countriesRequest)
.subscribe(onNext: {
print("Success", $0)
}, onError: {
print("Error: ", $0)
}, onCompleted: {
print("Completed!")
})
.disposed(by: disposeBag)
What am I doing wrong and why?

I've tried to recreate your code, and it seem working fine on my device:
func send<T>(_ request: T) -> Observable<Data> {
let request = URLRequest(url: URL(string: "sdf")!)
return Observable.create { obs in
URLSession.shared.rx.response(request: request).debug("r").subscribe(
onNext: { response in
return obs.onNext(response.data)
},
onError: {error in
obs.onError(error)
})
}
}
I'm subscribing to it, and it produces an error
send("qwe").subscribe(
onNext: { ev in
print(ev)
}, onError: { error in
print(error)
}).disposed(by: disposeBag)

Related

Swift Combine Return Int From URLSession.shared.dataTaskPublisher

I have the following code that makes an API call, receives data and assigns it to Core Data managed objects. This works well, and updates my data.
func importUsers(url: URL) {
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("DataImporter.runImport failed with error: \(error)")
}
}, receiveValue: { [weak self] data in
guard let self = self
else { return }
self.importContext.perform {
do {
// 2. Decode the response. This decodes directly to the Core Data Store
let users = try self.decoder.decode([GitUser].self, from: data)
try? self.importContext.save()
} catch {
print("DataImporter.runImport failed to decode json with error: \(error)")
}
}
})
.store(in: &self.cancellables) // store the returned cancellable in a property on `DataImporter`
}
However, I need to return the number of objects returned and decoded as a result of this call. If it fails, I return 0. Essentially, I want this:
func importUsers(url: URL) -> Int {
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("DataImporter.runImport failed with error: \(error)")
}
}, receiveValue: { [weak self] data in
guard let self = self
else { return 0 }
var users: [GitUser] = []
self.importContext.perform {
do {
// 2. Decode the response. This decodes directly to the Core Data Store
users = try self.decoder.decode([GitUser].self, from: data)
try? self.importContext.save()
} catch {
print("DataImporter.runImport failed to decode json with error: \(error)")
}
}
return users.count
}).store(in: &self.cancellables) // error: Cannot convert return expression of type '()' to return type 'Int'
}
How do I return the count of objects received as a result of the network call?
My suggestion is to decode the data and create the Core Data records within the pipeline and return a publisher. In another function subscribe to the publisher and sink the number of items and/or handle the error.
I don't have your custom items, you have to manage self properly
func importUsers(url: URL) -> AnyPublisher<Int,Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.tryMap{data -> Int in
var users: [GitUser] = []
var cdError : Error?
self.importContext.performAndWait {
do {
let users = try self.decoder.decode([GitUser].self, from: data)
try self.importContext.save()
} catch {
cdError = error
}
}
if let error = cdError { throw error }
return users.count
}
.eraseToAnyPublisher()
}
However you could also use async/await
func importUsers(url: URL) async throws -> Int {
let (data, _) = try await URLSession.shared.data(from: url)
let users = try await self.importContext.perform {
try self.decoder.decode([GitUser].self, from: data)
try self.importContext.save()
}
return users.count
}
Or an iOS 13 compatible async version, here perform can be asynchronous
func importUsers(url: URL) async throws -> Int {
try await withCheckedThrowingContinuation { continuation in
let task = URLSession.shared.dataTask(with: url) { [unowned self] (data, _ , error) in
if let error = error { continuation.resume(with: .failure(error)); return }
self.importContext.perform {
do {
let users = try self.decoder.decode([GitUser].self, from: data!)
try self.importContext.save()
continuation.resume(with: .success(users.count))
} catch {
continuation.resume(with: .failure(error))
}
}
}
task.resume()
}
}

Swift Combine Completion Handler with return of values

I have an API Service handler implemented which uses an authentication token in the header of the request. This token is fetched when the user logs in at the launch of the application. After 30 minutes, the token is expired. Thus, when a request is made after this timespan, the API returns an 403 statuscode. The API should then login again and restart the current API request.
The problem I am encountering is that the login function to fetch a new token, makes use of a completion handler to let the calling code know if the asynchronous login procedure has been successful or not. When the API gets a 403 statuscode, it calls the login procedure and and when that is complete, it should make the current request again. But this repeated API request should return some value again. However, returning a value is not possible in a completion block. Does anyone know a solution for the problem as a whole?
The login function is as follows:
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)
}
The API service is as follows:
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()
}
You're trying to combine Combine with old asynchronous code. You can do it with Future, check out more about it in 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()
But this should be done when you cannot modify the asynchronous method or most of your codebase uses it.
In your case it looks like you can rewrite login to Combine. I can't build your code, so there might be errors in mine too, but you should get the idea:
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()
}
And then call it like this:
signinModel.login()
.flatMap { _ in
request(ofType: type, from: endpoint, body: body)
}.eraseToAnyPublisher()

Parallel URLSession requests w/ DispatchGroup call completion handler twice on 1 request

Using DispatchGroup I am trying to run 2 network requests against my client, returning the results when both have completed.
I am having an issue in that sometimes the completion handler for one of DispatchGroup requests is called twice and the other is not called at all.
An example would be -
func fetchProfileWithRelatedArticle(onSuccess: #escaping (User, [RelatedArticle]) -> Void, onError: #escaping (Error) -> Void) {
let dispatchGroup = DispatchGroup()
var user: User?
var articles: [RelatedArticle] = []
var errors: [Error] = []
dispatchGroup.enter()
fetchProfileForUser(onSuccess: {
user = $0
print("fetchProfile:",$0)
print("123")
dispatchGroup.leave()
}, onError: { error in
errors.append(error)
dispatchGroup.leave()
})
dispatchGroup.enter()
getArticlesForUser(onSuccess: {
articles = $0
print("getArticlesForUser:",$0)
print("456")
dispatchGroup.leave()
}, onError: { error in
errors.append(error)
dispatchGroup.leave()
})
dispatchGroup.notify(queue: .main) {
guard let user = user, errors.isEmpty else { return }
onSuccess(user, articles)
}
}
Here I fetch a user profile and also fetch a list of articles they have written. These are returned via a completion handler and presented elsewhere.
Most of the time this works, however it appears on occasion either one of those requests will call its own completion handler twice and the other request wont.
I suspect this may be down to when my access token expires as it occurs if I leave the app for a short time. My access token has a life of 2 minutes.
Should a request receive a 401 response, I have the following method in my network client that requests a new token, then invokes the call again. I believe this may not be working as I'd like.
if response.statusIs401() {
self?.refreshHandler { success in
guard success else { completion(.failure(TokenError.refused)); return }
self?.request(resource, completion)
}
return
}
I suspect calling the method again after the update is doing something to the requests my dispatch group is returning.
Is it possible to chain requests in this fashion?
struct NoContent: Codable { }
typealias RefreshHandler = (#escaping (Bool) -> Void) -> ()
typealias TokenGetter = () -> [String: String]
protocol ClientType: class {
associatedtype Route: RouterType
func request<T: Codable>(_ resource: Route, _ completion: #escaping (Result<T>)-> Void)
}
class Client<Route: RouterType>: ClientType {
enum APIError: Error {
case unknown, badResponse, jsonDecoder, other
}
enum TokenError: String, Error {
case expired = "Access Token Expired"
case refused = "Refresh Token Failed"
}
private(set) var session: SessionType
private(set) var tokenGetter: TokenGetter
private(set) var refreshHandler: RefreshHandler
private lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
return decoder
}()
init(session: SessionType, tokenGetter: #escaping TokenGetter, refreshHandler: #escaping RefreshHandler) {
self.session = session
self.tokenGetter = tokenGetter
self.refreshHandler = refreshHandler
}
func request<T: Codable>(_ resource: Route, _ completion: #escaping (Result<T>)-> Void) {
let request = URLRequest(
resource: resource,
headers: tokenGetter()
)
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard error == nil else { completion(.failure(APIError.unknown)); return }
guard let response = response as? HTTPURLResponse else { completion(.failure(APIError.badResponse)); return }
if response.statusIs401() {
self?.refreshHandler { success in
guard success else { completion(.failure(TokenError.refused)); return }
self?.request(resource, completion)
}
return
}
if response.statusIsSuccess() {
guard let self = self, let data = self.deserializeNoContentResponse(data: data) else { completion(.failure(APIError.badResponse)); return }
do {
let value = try self.decoder.decode(T.self, from: data)
DispatchQueue.main.async {
completion(.success(value))
}
} catch let error {
print(error)
}
return
}
completion(.failure(APIError.other))
}.resume()
}
// some calls return a 200/201 with no data
private func deserializeNoContentResponse(data: Data?) -> Data? {
if data?.count == 0 {
return "{ }".data(using: .utf8)
}
return data
}
}
Sounds like you would need to something like the following in your networking client:
func makeTheRequest(_ completion: CompletionHandler) {
URLSession.shared.dataTask(with: someURL) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else {
return
}
if httpResponse.statusCode == 401 {
self.refreshToken { success in
if success { self.makeTheRequest(completion) }
}
}
// handle response
completion(whateverDataYouNeedToPass)
}
}
That would make the call, check the response code, refresh the token is needed and if that succeeds it calls the method that should make the response again with the completion handler that was passed to in the first place without calling it first. So the completion handler wouldn't be called until after the API call is made the second time.
Of course, adopt this for your own code, shouldn't be too hard to do

RxSwift equivalent for switchmap

In RxJS you can use the value from a observable for a new observable. For example:
this.authService.login(username, password).pipe(
switchMap((success: boolean) => {
if(success) {
return this.contactService.getLoggedInContact()
} else {
return of(null)
}
})
).subscribe(contact => {
this.contact = contact
})
But now I have to do a project in Swift and I want to achieve the same thing. I can get the two methods working, but using the result of the first observable for the second observable is something i can't get working. The switchMap pipe is something that does not exist in RxSwift and I cannot find the equivalent.
I've tried mapping the result of the login function to the observable and then flatmapping it, but unfortunately that didn't work.
What is the best way to do this in Swift without using a subscribe in a subscribe?
EDIT I've tried flat map:
APIService.login(email: "username", password: "password")
.flatMapLatest { result -> Observable<Contact> in
if result {
return APIService.getLoggedInContact()
} else {
return .of()
}
}.subscribe(onNext: {result in
print("Logged in contact: \(result)")
}, onError: {Error in
print(Error)
}).disposed(by: disposeBag)
But unfortunately that didn't work, I get an error Thread 1: EXC_BAD_ACCESS (code=1, address=0x13eff328c)
EDIT2:
This is the login function
static func login(email: String, password: String) -> Observable<Bool> {
return Observable<String>.create { (observer) -> Disposable in
Alamofire.request(self.APIBASEURL + "/contact/login", method: .post, parameters: [
"email": email,
"password": password
], encoding: JSONEncoding.default).validate().responseJSON(completionHandler: {response in
if (response.result.isSuccess) {
guard let jsonData = response.data else {
return observer.onError(CustomError.api)
}
let decoder = JSONDecoder()
let apiResult = try? decoder.decode(ApiLogin.self, from: jsonData)
return observer.onNext(apiResult!.jwt)
} else {
return self.returnError(response: response, observer: observer)
}
})
return Disposables.create()
}.map{token in
return KeychainWrapper.standard.set(token, forKey: "authToken")
}
}
This is the getLoggedInContact function
static func getLoggedInContact() -> Observable<Contact> {
return Observable.create { observer -> Disposable in
Alamofire.request(self.APIBASEURL + "/contact/me", method: .get, headers: self.getAuthHeader())
.validate().responseJSON(completionHandler: {response in
if (response.result.isSuccess) {
guard let jsonData = response.data else {
return observer.onError(CustomError.api)
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(.apiNewsDateResult)
let apiResult = try? decoder.decode(Contact.self, from: jsonData)
return observer.onNext(apiResult!)
} else {
return self.returnError(response: response, observer: observer)
}
})
return Disposables.create()
}
}
There is operator flatMapLatest which does exactly the same as switchMap in RxJS.
You can find usage example here

Alamofire number of requests one after another

I have a number of requests witch I would like to call one after another without having nested spaghetti code.
I tried it already with a serial dispatch queue
let queue = dispatch_queue_create("label", DISPATCH_QUEUE_SERIAL)
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(1)
}
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(2)
}
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(3)
}
But unfortunately that does not work. The output of this can be 1,3,2 or 3,1,2 or any other combination.
What would be the best approach to get the output 1,2,3 so one after the other.
Ok I ended up writing my own implementation.
I created a class RequestChain wich takes Alamofire.Request as parameter
class RequestChain {
typealias CompletionHandler = (success:Bool, errorResult:ErrorResult?) -> Void
struct ErrorResult {
let request:Request?
let error:ErrorType?
}
private var requests:[Request] = []
init(requests:[Request]) {
self.requests = requests
}
func start(completionHandler:CompletionHandler) {
if let request = requests.first {
request.response(completionHandler: { (_, _, _, error) in
if error != nil {
completionHandler(success: false, errorResult: ErrorResult(request: request, error: error))
return
}
self.requests.removeFirst()
self.start(completionHandler)
})
request.resume()
}else {
completionHandler(success: true, errorResult: nil)
return
}
}
}
And I use it like this
let r1 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
print("1")
}
let r2 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
print("2")
}
let r3 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
print("3")
}
let chain = RequestChain(requests: [r1,r2,r3])
chain.start { (success, errorResult) in
if success {
print("all have been success")
}else {
print("failed with error \(errorResult?.error) for request \(errorResult?.request)")
}
}
Importent is that you are telling the Manager to not execute the request immediately
let manager = Manager.sharedInstance
manager.startRequestsImmediately = false
Hope it will help someone else
I'm using Artman's Signals to notify my app once a result is returned, after which a queue elsewhere can call it's next request:
Alamofire.request( httpMethod, url, parameters: params ).responseJSON
{
( response: Response< AnyObject, NSError > ) in
self._signals.dispatchSignalFor( Key: url, data: response.result )
}
More details here.
One solution is to call your second request in the first one's callback :
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(1)
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(2)
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(3)
}
}
}