Combine using optional struct variable - swift

I'm trying to load an array of SCEvents into an array of EventModels using Combine. The variable imagePath is optional, which I'd like to translate it to an empty Data() in its corresponding EventModel.imageData variable.
struct SCEvent {
let name: String
let imagePath: String?
}
struct EventModel {
let name: String
let imageData: Data
}
The following code seems to work, but I can't help but wonder if it is the most optimal way of doing it:
func loadEvents(_ events: [SCEvent]) -> AnyPublisher<[EventModel], Error> {
events.publisher
.flatMap(loadEvent(_:))
.collect()
.eraseToAnyPublisher()
}
func loadEvent(_ event: SCEvent) -> AnyPublisher<EventModel, Error> {
if let imagePath = event.imagePath {
return DataDownloader.downloadData(fromPath: imagePath)
.map { EventModel(name: event.name, imageData: $0) }
.eraseToAnyPublisher()
}
return Just(EventModel(name: event.name, imageData: Data())
.eraseToAnyPublisher()
}
Ideally, I'd like to use a single publisher in the loadEvent function. Maybe something like this (doesn't work, but serves as an example of what I expect):
func loadEvent(_ event: SCEvent) -> AnyPublisher<[EventModel], Error> {
event.imagePath
.flatMap(DataDownloader.downloadData(_:))
.replaceNil(with: Data())
.map {
EventModel(name: event.name, imageData: $0)
}
.eraseToAnyPublisher()
}
Which doesn't work because .replaceNil should be used after event.imagePath to replace a nil string. Another possible approach would be:
func loadEvent(_ event: SCEvent) -> AnyPublisher<[EventModel], Error> {
event.imagePath
.replaceNil(with: "")
.flatMap(
DataDownloader.downloadData(_:)
.replaceError(with: Data())
)
.map {
EventModel(name: event.name, imageData: $0)
}
.eraseToAnyPublisher()
}
But it seems forced. Is it even possible with Combine? Is my initial approach the only valid solution?

You can use the publisher of Optional, which gives you a publisher that publishes one element if the optional is not nil, and an empty publisher otherwise.
You can then replaceEmpty(with: Data()), and map to an EventModel.
func loadEvent(_ event: SCEvent) -> AnyPublisher<EventModel, Error> {
event.imagePath.publisher
.flatMap(DataDownloader.downloadData(fromPath:))
.replaceEmpty(with: Data())
.map {
EventModel(name: event.name, imageData: $0)
}
.eraseToAnyPublisher()
}
However, I don't think replacing with a Data() is a good idea. A better design would be to replace with nil, in which case you'll have to map to an optional first:
struct EventModel {
let name: String
let imageData: Data?
}
...
.flatMap(DataDownloader.downloadData(fromPath:))
.map { $0 as Data? } // here
.replaceEmpty(with: nil)

Related

Use of flatMap on a generic Publisher results in a compile error

I'm writing a transform function that would take network request results and try to parse them automatically using a dict to Model transformer(not Decodable due to several backend reasons).
So the chain should look like this:
func getModel -> Single<Model> {
return networkRequest(requestParameters).parse(modelTranslator)
}
The translator is a generic protocol:
public protocol Translator {
associatedtype Model
func translateFrom(dictionary json: [String: Any]) throws -> Model
}
Single is a wrapper around Deferred and Future:
public typealias Single<T> = Deferred<Future<T, Error>>
The problematic parse extension method here is:
public extension Publisher {
func parse<T: Translator, M>(translator: T) -> Single<M> where T.Model == M {
return self.flatMap { (data: Data) -> Single<M> in
return Deferred {
return Future<M, any Error> { promise in
guard
let json = try? JSONSerialization.jsonObject(with: data, options: []),
let dict = json as? [String : Any]
else {
let error: any Error = TranslatorError.invalidJSONObject
return promise(Result.failure(error))
}
do {
let translatedModel: M = translator.translateFrom(dictionary: dict)
return promise(Result.success(translatedModel))
} catch let error {
return promise(Result.failure(error))
}
}
}
}
}
}
It won't compile. It shows 2 errors on the .flatmap row:
No 'flatMap' candidates produce the expected contextual result type 'Single' (aka 'Deferred<Future<M, any Error>>')
No exact matches in call to instance method 'flatMap'
I believe that it has something to do with a type mismatch?
Could you please help me see the problem?
Thank you in advance!
You are trying too hard. A simple tryMap is all you need to parse your [String: Any] into the appropriate model type. Here is a complete example:
func getFoo(_ requestParameters: RequestParameters) -> AnyPublisher<Foo, Error> {
getModel(requestParameters, modelTranslator: FooTranslator())
}
func getModel<T>(_ requestParameters: RequestParameters, modelTranslator: T) -> AnyPublisher<T.Model, Error> where T: Translator {
networkRequest(requestParameters)
.tryMap { try modelTranslator.translateFrom(dictionary: $0) }
.eraseToAnyPublisher()
}
The above assumes the following declarations:
func networkRequest(_ params: RequestParameters) -> Single<[String: Any]> ...
struct FooTranslator: Translator {
func translateFrom(dictionary json: [String : Any]) throws -> Foo ...
}

Saving string in flatMap block to database in api call using Combine Swift

I am trying to fetch a value from local database and when not found wants to save it to local database and return it. All of these I am doing in Interactor file and actual saving or fetching is done in seperate file. Following is my code:
public func fetchCode(codeId: String) -> AnyPublisher<String?, Error> {
//Get code from localdb
codeStorageProvider.fetchCode(codeId).flatMap { (code) -> AnyPublisher<String?, Error> in
if let code = code {
return Just(code).mapError{ $0 as Error }.eraseToAnyPublisher()
}
//If not found in db, Get code from server
let code = self.voucherCodeProvider.fetchVoucherCode(codeId: codeId)
return code.flatMap { code in
//save the code to local db
self.codeStorageProvider.saveVoucherCode(code, codeId)
return code
}.eraseToAnyPublisher()
//return code to presenter
}.eraseToAnyPublisher()
}
I am getting following error in flatMap:
Type of expression is ambiguous without more context
Can someone please help me?
If your saveVoucher doesn't return a Publisher and you are not interested in knowing when the operation is completed, there's no need to use flatMap but you can use handleEvents and call the side effect to save the code from there. Something like this:
func fetchLocal(codeId: String) -> AnyPublisher<String?, Error> {
return Empty().eraseToAnyPublisher()
}
func fetchRemote(codeId: String) -> AnyPublisher<String, Error> {
return Empty().eraseToAnyPublisher()
}
func saveLocal(code: String, codeId: String) {
// Save to BD
}
func fetch(codeId: String) -> AnyPublisher<String?, Error> {
return fetchLocal(codeId: codeId)
.flatMap { code -> AnyPublisher<String, Error> in
if let code = code {
return Just(code)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
} else {
return fetchRemote(codeId: codeId)
.handleEvents(receiveOutput: {
saveLocal(code: $0, codeId: codeId)
})
.eraseToAnyPublisher()
}
}
.map(Optional.some)
.eraseToAnyPublisher()
}

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.

How do I map a codable JSON struct into Data for decoding a response?

I am having trouble compiling the following code — it fails with “'Cannot convert value of type 'CheckUserNameAvailability' to closure result type 'JSONDecoder.Input' (aka 'Data')”.
How do I map the CheckUserNameAvailability JSON returned by the server to Data?
// This is the JSON returned by the server
// {
// "Username" : "Foo1Bar2",
// "Available" : true
// }
struct CheckUserNameAvailability: Codable {
let Username: String
let Available: Bool
}
enum tvAPI {
static func checkUserName(username: String) -> AnyPublisher<CheckUserNameAvailability, Error> {
// ...
}
}
func testUserName() {
var cancellable: AnyCancellable?
let me = "Foo1Bar2"
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .useDefaultKeys
cancellable = tvAPI.checkUserName(username: me)
.map { $0 } // compilation fails with “Cannot convert value of type 'CheckUserNameAvailability' to closure result type 'JSONDecoder.Input' (aka 'Data')”
.decode(type: [CheckUserNameAvailability].self, decoder: JSONDecoder())
// .print()
.sink(receiveCompletion: {completion in
print(completion)
},
receiveValue: { availability in
print("\(availability)")
})
RunLoop.main.run(until: Date(timeIntervalSinceNow: 10))
withExtendedLifetime(cancellable, {})
}
tvAPI.checkUserName(username:) returns a publisher with output type CheckUserNameAvailability. The decode method on the publisher requires that the publisher's output matches the decoder's input:
func decode<Item, Coder>(type: Item.Type, decoder: Coder) ->
Publishers.Decode<Self, Item, Coder> where
Item : Decodable,
Coder : TopLevelDecoder,
Self.Output == Coder.Input
However, CheckUserNameAvailability is not compatible with JSONDecoder.Input (aka Data).
You don't need to decode the data:
cancellable = tvAPI.checkUserName(username: me)
// .print()
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { availability in
print("\(availability)")
})

Loop over Publisher Combine framework

I have the following function to perform an URL request:
final class ServiceManagerImpl: ServiceManager, ObservableObject {
private let session = URLSession.shared
func performRequest<T>(_ request: T) -> AnyPublisher<String?, APIError> where T : Request {
session.dataTaskPublisher(for: self.urlRequest(request))
.tryMap { data, response in
try self.validateResponse(response)
return String(data: data, encoding: .utf8)
}
.mapError { error in
return self.transformError(error)
}
.eraseToAnyPublisher()
}
}
Having these 2 following functions, I can now call the desired requests from corresponded ViewModel:
final class AuditServiceImpl: AuditService {
private let serviceManager: ServiceManager = ServiceManagerImpl()
func emptyAction() -> AnyPublisher<String?, APIError> {
let request = AuditRequest(act: "", nonce: String.randomNumberGenerator)
return serviceManager.performRequest(request)
}
func burbleAction(offset: Int) -> AnyPublisher<String?, APIError> {
let request = AuditRequest(act: "burble", nonce: String.randomNumberGenerator, offset: offset)
return serviceManager.performRequest(request)
}
}
final class AuditViewModel: ObservableObject {
#Published var auditLog: String = ""
private let auditService: AuditService = AuditServiceImpl()
init() {
let timer = Timer(timeInterval: 5, repeats: true) { _ in
self.getBurbles()
}
RunLoop.main.add(timer, forMode: .common)
}
func getBurbles() {
auditService.emptyAction()
.flatMap { [unowned self] offset -> AnyPublisher<String?, APIError> in
let currentOffset = Int(offset?.unwrapped ?? "") ?? 0
return self.auditService.burbleAction(offset: currentOffset)
}
.receive(on: RunLoop.main)
.sink(receiveCompletion: { [unowned self] completion in
print(completion)
}, receiveValue: { [weak self] burbles in
self?.auditLog = burbles!
})
.store(in: &cancellableSet)
}
}
Everything is fine when I use self.getBurbles() for the first time. However, for the next calls, print(completion) shows finished, and the code doesn't perform self?.auditLog = burbles!
I don't know how can I loop over the getBurbles() function and get the response at different intervals.
Edit
The whole process in a nutshell:
I call getBurbles() from class initializer
getBurbles() calls 2 nested functions: emptyAction() and burbleAction(offset: Int)
Those 2 functions generate different requests and call performRequest<T>(_ request: T)
Finally, I set the response into auditLog variable and show it on the SwiftUI layer
There are at least 2 issues here.
First when a Publisher errors it will never produce elements again. That's a problem here because you want to recycle the Publisher here and call it many times, even if the inner Publisher fails. You need to handle the error inside the flatMap and make sure it doesn't propagate to the enclosing Publisher. (ie you can return a Result or some other enum or tuple that indicates you should display an error state).
Second, flatMap is almost certainly not what you want here since it will merge all of the api calls and return them in arbitrary order. If you want to cancel any existing requests and only show the latest results then you should use .map followed by switchToLatest.