0

I have a flow where I should get posts from web, and save them on user defaults as test, if there is some issue, load the list from users default instead from web. now I have two emitters, I think I should perform a check in my repository, something like if gateposts emits error, put to the error decide to show local cached posts. How to catch the error? they told me I could use combine latest or mergeMany but Im totally new and have no idea how to perform this check

I have this hint, but is not working

dataSource.getPosts().catch {sharedPreferenceDataSource.getLocalPosts()}

this seems to work, but still not getting with error or using combine latest

return dataSource.getPosts().catch { error in
    return self.sharedPreferenceDataSource.getLocalPosts()
}.eraseToAnyPublisher()

my func

 func getPosts() -> AnyPublisher<[Post], Error> {
        
        //posts from web
        dataSource.getPosts() //emits AnyPublisher<[Post], Error>
        //posts on usersDeafault
        sharedPreferenceDataSource.getLocalPosts() //emits AnyPublisher<[Post], Error>
        
        //here someone told me I should perform a catch,I get something likeMyEnumeError.networkError, then I should emit local posts
        
        //my latest attempt
        return Publishers.CombineLatest(dataSource.getPosts(), sharedPreferenceDataSource.getLocalPosts())

//        but I think I could do something like
//        perform a catch on the error from network, in that case perform a catch and send local data
//        Publishers.MergeMany([dataSource.getPosts(),dataSource.getLocalPosts(defaults: defaults, key: UserDefaultKeys.allPost.rawValue)])
}

my call

enum NetworkError: Error {
    case genericError(code: Int)
    case invalidResponseCode
    case decodingFailure(reason: String)
}

func getPosts() -> AnyPublisher<[Post], Error> {


let session = URLSession.shared
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!


return Future<[Post], Error>() { promise in
    
    //***********************************************
    let task = session.dataTask(with: url, completionHandler: { data, response, error in
        
        if error != nil {
            print(error ?? "N/D")
            promise(Result.failure(NetworkError.invalidResponseCode))
            return
        }
        
        
        guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
            promise(Result.failure(NetworkError.invalidResponseCode))
            return
        }
        
        do {
            let posts = try JSONDecoder().decode([Post].self, from: data! )
            promise(Result.success(posts))
            //saving on local disk
            self.saveLocalPosts(defaults: self.defaults, data: AllPosts(Posts: posts))
        } catch {
            print("Error during JSON serialization: \(error.localizedDescription)")
            promise(Result.failure(NetworkError.decodingFailure(reason: error.localizedDescription)))
                
        }
        
    })
    task.resume()
    //***********************************************

}
//            .receive(on: DispatchQueue.main)
//            .subscribe(on: DispatchQueue.init(label: "test", qos: .default))
//            .map { value -> [Post] in
//                let c = value
//                return []
//            }
.eraseToAnyPublisher()


 
}
biggreentree
  • 1,633
  • 3
  • 20
  • 35
  • The `CombineLatest` operator combines two independent sources. But in your example the local task depends on the output of the remote task. So `map` or `flatMap` might be more suitable. – vadian Nov 22 '21 at 08:51
  • Hi Vadian! it was required to me at first to use the combineLatest and/or use the "hint" I added now to the question, matter is, how to perform error checking using that hint – biggreentree Nov 22 '21 at 08:57
  • To be more precise use the `tryMap` operator to be able to catch the error. – vadian Nov 22 '21 at 09:02
  • if I try to use catch or tryMap I cannot get the error type anyway, using a switch for example. – biggreentree Nov 22 '21 at 09:27

1 Answers1

1

My suggestion is to create a function which handles all network errors and returns the received Data or the network error.

enum NetworkError: Error {
    case urlError(Error)
    case invalidResponseCode
    case decodingFailure(reason: String)
}

func dataTask(url: URL) -> AnyPublisher<Data, NetworkError> {
    let request = URLRequest(url: url)
    
    return URLSession.DataTaskPublisher(request: request, session: .shared)
        .tryMap { data, response in
            guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else {
                throw NetworkError.invalidResponseCode
            }
            return data
        }
        .mapError { error in
            if let error = error as? NetworkError {
                return error
            } else {
                return NetworkError.urlError(error)
            }
        }
        .eraseToAnyPublisher()
} 

Then call the replaceError operator to load the data from UserDefaults (assuming it has the same JSON structure as the remote data).

var subscriptions = Set<AnyCancellable>()

dataTask(url: url)
    .replaceError(with: UserDefaults.standard.data(forKey: "posts") ?? Data() )
    .decode(type: [Post].self, decoder: JSONDecoder())
    .sink(receiveCompletion: { completion in
        print(completion)
    }, receiveValue: { value in
        print(value)
    })
    .store(in: &subscriptions)
vadian
  • 274,689
  • 30
  • 353
  • 361