Replace nil with Empty or Error in Combine - swift

I have a Combine publisher like this:
enum RemoteError: Error {
case networkError(Error)
case parseError(Error)
case emptyResponse
}
func getPublisher(url: URL) -> AnyPublisher<Entiy, RemoteError> {
return URLSession.shared
.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: RemoteResponse.self, decoder: decoder)
.mapError { error -> RemoteError in
switch error {
case is URLError:
return .networkError(error)
default:
return .parseError(error)
}
}
.map { response -> Entiy in
response.enitities.last
}
.eraseToAnyPublisher()
}
struct RemoteResponse: Codable {
let enitities: [Entity]
let numberOfEntries: Int
}
struct Entity {
}
By the above setting, the compiler complains because response.enitities.last can be nil. The question is can I replace nil with Empty publisher and if not can I replace it with error emptyResponse in Combine chain? The first option is preferable.

You have a couple of options here.
If you don't want the publisher to publish anything in case entities is empty, you can use coampactMap instead of map:
.compactMap { response in
response.entities.last
}
If you would rather publish an error in such a case you can use tryMap which allows you to throw an Error. You would need mapError to come after it:
.tryMap { response in
guard let entity = response.entities.last else {
throw RemoteError.emptyResponse
}
return entity
}
.mapError { error -> RemoteError in
switch error {
case is URLError:
return .networkError(error)
case is DecodingError:
return .parseError(error)
default:
return .emptyResponse
}
}

You need a flat map in order to map to another publisher:
.flatMap {
$0.enitities.last.publisher
}
Optional has a convenient publisher property that gives you a publisher that publishes only that value if the value is not nil, and an empty publisher if it is nil. This is only available in iOS 14+. If you are targeting a lower version, you need to do something like:
.flatMap { (response) -> AnyPublisher<Entity, Never> in
if let last = response.entities.last {
return Just(last).eraseToAnyPublisher()
} else {
return Empty(completeImmediately: true).eraseToAnyPublisher()
}
}

Following the answer from #Sweeper, Here edit with below iOS 14.0
.setFailureType(to: NSError.self)
.flatMap { (response) -> AnyPublisher<Entity, Never> in
if let last = response.entities.last {
return Just(last).eraseToAnyPublisher()
} else {
return Empty(completeImmediately: true).eraseToAnyPublisher()
}
NSError -> will be error type which you are returning
Happy coding 🚀

Related

Cannot map error after flatMap usage (Never result type)

I have RestManager class which is used for fetching data from Internet and is returning AnyPublisher
class RestManager {
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<T, ErrorType> {
URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap({ data, _ in
let value = try JSONDecoder().decode(T.self, from: data)
if let array = value as? Array<Any>, array.isEmpty {
throw ErrorType.empty
}
return value
})
.mapError { error -> ErrorType in
switch error {
case is ErrorType:
return ErrorType.empty
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet, .networkConnectionLost, .timedOut:
return .noInternetConnection
case .cannotDecodeRawData, .cannotDecodeContentData:
return .empty
default:
return .general
}
default:
return .general
}
}
.eraseToAnyPublisher()
}
}
Repository has two functions (getWorldwideData and getCountryData returning AnyPublisher<(WorldwideResponse item or CountryResponse item), ErrorType>)
In viewModel, I made these functions.
private func getData() {
$useCaseSelection
.flatMap { value -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> in
self.loader = true
self.error = nil
switch value {
case let .country(name):
return self.countryPipeline(name: name)
case .worldwide:
return self.worldwidePipeline()
}
}
.mapError { error in
self.error = error
}
.assign(to: &$homeScreenDomainItem)
}
private func worldwidePipeline() -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> {
repository
.getWorldwideData()
.map { response -> Covid19StatisticsDomainItem in
self.error = nil
self.loader = false
return Covid19StatisticsDomainItem(worldwideResponseItem: response)
}
.eraseToAnyPublisher()
}
private func countryPipeline(name: String) -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> {
repository
.getCountryData(for: name)
.map { response -> Covid19StatisticsDomainItem in
self.error = nil
self.loader = false
return Covid19StatisticsDomainItem(countryDayOneStatsResponse: response)
}
.eraseToAnyPublisher()
}
I wanted to make clean code, so I split code into two separate function based on useCaseSelection.
useCaseSelection is enum with two types.
error is ErrorType? value wrapped with #Published, in which I want to save error type if there is any error.
homeScreenDomainItem is Covid19StatisticsDomainItem instance wrapped with #Published.
Problem is in getData function where in MapError pipeline I am getting:
Cannot convert value of type () to closure result type Never
I tried to use setFailureType(to: ErrorType.self) but that is not helping.

Using flatMap in RestManager with generic function not working

I am new to Combine, so I wanted to create class RestManager for networking with generic
fetchData function. Function is returning AnyPublisher<Result<T, ErrorType>, Never> where ErrorType is enum with .noInternetConnection, .empty and .general cases.
I tried to use URLSession with dataTaskPublisher and flatMap
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<Result<T, ErrorType>, Never> {
URLSession
.shared
.dataTaskPublisher(for: url)
.flatMap { (data, response) -> AnyPublisher<Result<T, ErrorType>, Never> in
switch response.result {
case .success(let data):
if let data = try? JSONDecoder().decode(T.self, from: data){
return Just(data).eraseToAnyPublisher()
}
case .failure(let error):
if let error = error as? URLError {
switch error.code {
case .notConnectedToInternet, .networkConnectionLost, .timedOut:
return Fail(ErrorType.noInternetConnection).eraseToAnyPublisher()
case .cannotDecodeRawData, .cannotDecodeContentData:
return Fail(ErrorType.empty).eraseToAnyPublisher()
default:
return Fail(ErrorType.general).eraseToAnyPublisher()
}
}
}
}
.eraseToAnyPublisher()
}
But I am getting
Cannot convert return expression of type 'AnyPublisher<AnyPublisher<Result<T, ErrorType>, Never>.Output, URLSession.DataTaskPublisher.Failure>' (aka 'AnyPublisher<AnyPublisher<Result<T, ErrorType>, Never>.Output, URLError>') to return type 'AnyPublisher<Result<T, ErrorType>, Never>' error.
There are several major flows in your implementation.
Firstly, you shouldn't be using Result as the Output type of the Publisher and Never as its Failure type. You should be using T as the Output and ErrorType as Failure.
Second, you need tryMap and mapError, not flatMap.
Lastly, you are handling the result of dataTaskPublisher completely wrong. When dataTaskPublisher fails, it emits an error, so you need to handle that in mapError. When it succeeds, it emits its result as data, so you need to be decoding that, not response.
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<T, ErrorType> {
URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap { data, _ in
return try JSONDecoder().decode(T.self, from: data)
}
.mapError { error -> ErrorType in
switch error {
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet, .networkConnectionLost, .timedOut:
return .noInternetConnection
case .cannotDecodeRawData, .cannotDecodeContentData:
return .empty
default:
return .general
}
default:
return .general
}
}
.eraseToAnyPublisher()
}

HTTP POST request using Swift Combine

I'm fairly new to Combine declarative API. I'm trying to implement a generic network layer for a SwiftUI application. For all requests that receive data I understand how to structure the data flow.
My problem is that I have some HTTP POST requests that returns no data. Only a HTTP 200 on success. I can't figure out how to create a publisher that will handle a decoding that can fail since there could be not data in the body of the response. Here's what I tried:
func postResource<Resource: Codable>(_ resource: Resource, to endpoint: Endpoint) -> AnyPublisher<Resource?, NetworkError> {
return Just(resource)
.subscribe(on: queue)
.encode(encoder: JSONEncoder())
.mapError { error -> NetworkError in
return NetworkError.encoding(error)
}
.map { data -> URLRequest in
return endpoint.makeRequest(with: data)
}
.tryMap { request -> Resource? in
self.session.dataTaskPublisher(for: request)
.tryMap { data, response -> Data in
guard let httpUrlResponse = response as? HTTPURLResponse else { throw NetworkError.unknown }
guard (200 ... 299).contains(httpUrlResponse.statusCode) else { throw NetworkError.error(for: httpUrlResponse.statusCode) }
return data
}
.tryMap { data -> Resource? in
return try? JSONDecoder().decode(Resource.self, from: data)
}
}
.mapError({ error -> NetworkError in
switch error {
case is Swift.DecodingError:
return NetworkError.decoding(error)
case let urlError as URLError:
return .urlError(urlError)
case let error as NetworkError:
return error
default:
return .unknown
}
})
.eraseToAnyPublisher()
}
The compiler is complaining with the following error on tryMap row:
Declared closure result 'Publishers.TryMap<URLSession.DataTaskPublisher, Resource?>' is incompatible with contextual type 'Resource?'
Anyone has an idea?
Thanks!
enum NetworkError: Error {
case encoding(Error)
case error(for: Int)
case decoding(Error)
case urlError(URLError)
case unknown
}
func postResource<Resource: Codable>(_ resource: Resource, to endpoint: Endpoint) -> AnyPublisher<Resource?, NetworkError> {
Just(resource)
.subscribe(on: queue)
.encode(encoder: JSONEncoder())
.mapError { error -> NetworkError in
NetworkError.encoding(error)
}
.map { data -> URLRequest in
endpoint.makeRequest(with: data)
}
.flatMap { request in // the key thing is here you should you use flatMap instead of map
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { data, response -> Data in
guard let httpUrlResponse = response as? HTTPURLResponse else { throw NetworkError.unknown }
guard 200 ... 299 ~= httpUrlResponse.statusCode else { throw NetworkError.error(for: httpUrlResponse.statusCode) }
return data
}
.tryMap { data -> Resource? in
try? JSONDecoder().decode(Resource.self, from: data)
}
}
.mapError({ error -> NetworkError in
switch error {
case is Swift.DecodingError:
return NetworkError.decoding(error)
case let urlError as URLError:
return .urlError(urlError)
case let error as NetworkError:
return error
default:
return .unknown
}
})
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}

Swift: Extending Combine operators

I'm trying to wrap a tryMap operator along the lines of this article.
extension Publisher where Output == Data {
func decode<T: Decodable>(as type: T.Type = T.self, using decoder: JSONDecoder = .init()) -> Publishers.Decode<Self, T, JSONDecoder> {
decode(type: type, decoder: decoder)
}
}
extension Publisher where Output == URLSession.DataTaskPublisher.Output {
func processData(_: #escaping (Self.Output) throws -> Data) -> Publishers.TryMap<Self, Data> {
tryMap { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200
else {
throw URLError(.badServerResponse)
}
return element.data
}
}
}
While using it I'm getting a compiler error which I'm struggling with:
return urlSession
.dataTaskPublisher(for: request)
.processData // <- Value of type '(#escaping (URLSession.DataTaskPublisher.Output) throws -> Data) -> Publishers.TryMap<URLSession.DataTaskPublisher, Data>' (aka '(#escaping ((data: Data, response: URLResponse)) throws -> Data) -> Publishers.TryMap<URLSession.DataTaskPublisher, Data>') has no member 'decode'
.decode(as: InstantResponse.self)
.eraseToAnyPublisher()
What would be the correct way of doing it?
Thanks!
First of all, you aren't calling processData - you are missing the parentheses, which would actually execute the function. Second, your processData declaration is incorrect, it should not take a closure as its input argument, since you aren't using that closure anyways.
extension Publisher where Output == URLSession.DataTaskPublisher.Output {
func processData() -> Publishers.TryMap<Self, Data> {
tryMap { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200
else {
throw URLError(.badServerResponse)
}
return element.data
}
}
}
return urlSession
.dataTaskPublisher(for: request)
.processData() // parentheses necessary here
.decode(as: InstantResponse.self)
.eraseToAnyPublisher()

Swift Combine Nested Publishers

I'm trying to compose a nested publisher chain in combine with Swift and I'm stumped. My current code starts throwing errors at the .flatMap line, and I don't know why. I've been trying to get it functional but am having no luck.
What I'm trying to accomplish is to download a TrailerVideoResult and decode it, grab the array of TrailerVideo objects, transform that into an array of YouTube urls, and then for each YouTube URL get the LPLinkMetadata. The final publisher should return an array of LPLinkMetadata objects. Everything works correctly up until the LPLinkMetadata part.
EDIT: I have updated the loadTrailerLinks function. I originally forgot to remove some apart of it that was not relevant to this example.
You will need to import "LinkPresentation". This is an Apple framework for to fetch, provide, and present rich links in your app.
The error "Type of expression is ambiguous without more context" occurs at the very last line (eraseToAnyPublisher).
func loadTrailerLinks() -> AnyPublisher<[LPLinkMetadata], Error>{
return URLSession.shared.dataTaskPublisher(for: URL(string: "Doesn't matter")!)
.tryMap() { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return element.data
}
.decode(type: TrailerVideoResult.self, decoder: JSONDecoder(.convertFromSnakeCase))
.compactMap{ $0.results }
.map{ trailerVideoArray -> [TrailerVideo] in
let youTubeTrailer = trailerVideoArray.filter({$0.site == "YouTube"})
return youTubeTrailer
}
.map({ youTubeTrailer -> [URL] in
return youTubeTrailer.compactMap{
let urlString = "https://www.youtube.com/watch?v=\($0.key)"
let url = URL(string: urlString)!
return url
}
})
.flatMap{ urls -> [AnyPublisher<LPLinkMetadata, Never>] in
return urls.map{ url -> AnyPublisher <LPLinkMetadata, Never> in
return self.getMetaData(url: url)
.map{ metadata -> LPLinkMetadata in
return metadata
}
.eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
func fetchMetaData(url: URL) -> AnyPublisher <LPLinkMetadata, Never> {
return Deferred {
Future { promise in
LPMetadataProvider().startFetchingMetadata(for: url) { (metadata, error) in
promise(Result.success(metadata!))
}
}
}.eraseToAnyPublisher()
}
struct TrailerVideoResult: Codable {
let results : [TrailerVideo]
}
struct TrailerVideo: Codable {
let key: String
let site: String
}
You can use Publishers.MergeMany and collect() for this:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
func loadTrailerLinks() -> AnyPublisher<[LPLinkMetadata], Error> {
// Download data
URLSession.shared.dataTaskPublisher(for: URL(string: "Doesn't matter")!)
.tryMap() { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return element.data
}
.decode(type: TrailerVideoResult.self, decoder: decoder)
// Convert the TrailerVideoResult to a MergeMany publisher, which merges the
// [AnyPublisher<LPLinkMetadata, Never>] into a single publisher with output
// type LPLinkMetadata
.flatMap {
Publishers.MergeMany(
$0.results
.filter { $0.site == "YouTube" }
.compactMap { URL(string: "https://www.youtube.com/watch?v=\($0.key)") }
.map(fetchMetaData)
)
// Change the error type from Never to Error
.setFailureType(to: Error.self)
}
// Collect all the LPLinkMetadata and then publish a single result of
// [LPLinkMetadata]
.collect()
.eraseToAnyPublisher()
}
It's a bit tricky to convert an input array of values to array of results, each obtained through a publisher.
If the order isn't important, you can flatMap the input into a Publishers.Sequence publisher, then deal with each value, then .collect them:
.flatMap { urls in
urls.publisher // returns a Publishers.Sequence<URL, Never> publisher
}
.flatMap { url in
self.getMetaData(url: url) // gets metadata publisher per for each url
}
.collect()
(I'm making an assumption that getMetaData returns AnyPublisher<LPLinkMetadata, Never>)
.collect will collect all the emitted values until the upstream completes (but each value might arrive not in the original order)
If you need to keep the order, there's more work. You'd probably need to send the original index, then sort it later.