Swift 5, RxSwift: Network request with RxSwift

Viewed 480

I am starting to use RxSwift to make the service call. This was my old code:

class Service: GraphQLService {

func graphQL(body: [String: Any?], onSuccess: @escaping (Foundation.Data) throws -> (), onFailure: @escaping (Error) -> ()) {
    
    guard let urlValue = Bundle.main.urlValue else { return }
    guard let url = URL(string: urlValue) else { return
        print("Error with info.plist")
    }
    var request = URLRequest(url: url)
    
    let userKey = Bundle.main.userKeyValue
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue(userKey, forHTTPHeaderField: "userid")

    request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
    
    URLSession.shared.dataTask(with: request) { (data, response, error) in
        if let error = error {
            onFailure(error)
        }

        if let data = data {
            do{
                try onSuccess(data)
            }
            catch{
                onFailure(error)
            }
        }
    }.resume()
}

And here I do the function to get time deposits:

final class TimeDepositManager: Service, TimeDepositManagerProtocol {
let timeDepositQuery = Bundle.main.queryValue

func getTimeDeposits(onSuccess: @escaping ([TimeDeposits]) -> (), onFailure: @escaping (Error) -> ()) {
    let body = ["query": timeDepositQuery]
    Service().graphQL(body: body, onSuccess: { data in
        let json = try? JSONDecoder().decode(GraphQLResponse.self, from: data)
        onSuccess(json?.data?.account?.timeDeposits ?? [])
    }, onFailure: onFailure)
}

And so far this is my code with RxSwift:

class Service: GraphQLService {

func graphQL(body: [String : Any?]) -> Observable<Foundation.Data> {
    
    return Observable.create { observer in
        
        let urlValue = Bundle.main.urlValue
        let url = URL(string: urlValue ?? "")
        
        let session = URLSession.shared
        var request = URLRequest(url: url!)
        let userKey = Bundle.main.userKeyValue
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(userKey, forHTTPHeaderField: "userid")
        request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
        
        session.dataTask(with: request) { (data, response, error) in
            if let error = error {
                observer.onError(error)
            }
            
            if let data = data {
                do{
                    try onSuccess(data)
                    observer.onNext(data)
                }
                catch{
                    //onFailure(error)
                    observer.onError(error)
                    print("Error: \(error.localizedDescription)")
                }
            }
        }.resume()
        return Disposables.create {
            session.finishTasksAndInvalidate()
        }
    }
}

This is where I don't understand how in my getTimeDeposits () I can do the deserialization with try? JSONDecoder () ... with RxSwift without using onSuccess?

final class TimeDepositManager: Service, TimeDepositManagerProtocol {
let timeDepositQuery = Bundle.main.queryValue

func getTimeDeposits() -> Observable<[TimeDeposits]> {
    let body = ["query": timeDepositQuery]
    Service().graphQL(body: body)
}
1 Answers

You can have getTimeDeposits() return an Observable as well and handle the deserialization in a map closure. A couple of other things.

  • RxCocoa already has a method on URLSession so you don't need to write your own.
  • I suggest reducing the amount of code you have in a function that makes the network request. You want to be able to test your logic for making the request without actually making it.

Something like this:

final class TimeDepositManager: Service, TimeDepositManagerProtocol {
    let timeDepositQuery = Bundle.main.queryValue

    func getTimeDeposits() -> Observable<[TimeDeposits]> {
        let body = ["query": timeDepositQuery]
        return Service().graphQL(body: body)
            .map { try JSONDecoder().decode(GraphQLResponse.self, from: $0).data?.account?.timeDeposits ?? [] }
    }
}

class Service: GraphQLService {

    func graphQL(body: [String: Any?]) -> Observable<Data> {
        guard let urlValue = Bundle.main.urlValue else { fatalError("Error with info.plist") }
        let request = urlRequest(urlValue: urlValue, body: body)
        return URLSession.shared.rx.data(request: request) // this is in RxCocoa
    }

    func urlRequest(urlValue: String, body: [String: Any?]) -> URLRequest {
        guard let url = URL(string: urlValue) else { fatalError("Error with urlValue") }
        var request = URLRequest(url: url)
        let userKey = Bundle.main.userKeyValue
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(userKey, forHTTPHeaderField: "userid")
        request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
        return request
    }
}

If you don't want to use RxCocoa for some reason, here is the correct way to wrap the URLSession.dataTask method:

extension URLSession {
    func data(request: URLRequest) -> Observable<Data> {
        Observable.create { observer in
            let task = self.dataTask(with: request, completionHandler: { data, response, error in
                guard let response = response as? HTTPURLResponse else {
                    observer.onError(URLError.notHTTPResponse(data: data, response: response))
                    return
                }
                guard 200 <= response.statusCode && response.statusCode < 300 else {
                    observer.onError(URLError.failedResponse(data: data, response: response))
                    return
                }
                guard let data = data else {
                    observer.onError(error ?? RxError.unknown)
                    return
                }
                observer.onNext(data)
                observer.onCompleted() // be sure to call `onCompleted()` when you are done emitting values.
                // make sure every possible path through the code calls some method on `observer`.
            })
            return Disposables.create { task.cancel() } // don't forget to handle cancelation properly. You don't want to kill *all* tasks, just this one.
        }
    }
}

enum URLError: Error {
    case notHTTPResponse(data: Data?, response: URLResponse?)
    case failedResponse(data: Data?, response: HTTPURLResponse)
}
Related