Swift Combine - Async calls - swift

I have the following functions:
func getUserProfile() -> AnyPublisher<UserProfileDTO?, Error> {
return Future { [unowned self] promise in
do {
if let data = KeychainWrapper.standard.data(forKey: profileKey) {
let profileDTO = try PropertyListDecoder().decode(UserProfileDTO.self, from: data)
setCurrentSession(profileDTO)
promise(.success(profileDTO))
}
else {
promise(.success(nil))
}
}
catch {
// Delete current UserProfile if cannot decode
let _ = KeychainWrapper.standard.removeAllKeys()
promise(.failure(error))
}
}.eraseToAnyPublisher()
}
func connect(userProfile: UserProfileDTO) -> AnyPublisher<UserProfileDTO, Error> {
return Future { promise in
SBDMain.connect(withUserId: userProfile.email) { (user, error) in
if let error = error {
promise(.failure(error))
}
else {
promise(.success(userProfile))
}
}
}.eraseToAnyPublisher()
}
What I want to do is to first call the getUserProfile() method and if the return value in not nil then call the connect() method. However, if the getUserProfile() has nil response it does not need to call the connect() and it should just return the nil response. Both these methods needs to be called from the autoLoginUser() method.
The problem I'm having right now is figuring out how to do this in a clean swift way without writing too much nested statements.
I tried to use flatMaps but it didn't workout the way I expected. Any help is much appreciated.
A solution I've been working on at the moment is this. But it doesn't quite work.
func autoLoginUser2() -> AnyPublisher<UserProfile?,Error> {
getUserProfile()
.tryMap { [unowned self] in
if let currentProfile = $0 {
return connect(userProfile: currentProfile)
.tryMap {
//Map from UserProfileDTO --> UserProfile
return UserProfileDTOMapper.map($0)
}
}
return nil
}.eraseToAnyPublisher()
}

With some adjustment for used types and error types this should work. First you ask for the profile, then you force unwrap the profile if it is nil you throw an error that will be sent to the sink as a failure.
If the profile is present you call connect.
getUserProfile()
.tryMap { userDTO -> UserProfileDTO in
if let id = userDTO {
return id
}
throw MyError.noProfileDT
}
.flatMap { id in
connect(id)
}
.sink {
//.....
}

If you change the signature of connect to return Optional profile:
func connect(userProfile: UserProfileDTO) -> AnyPublisher<UserProfileDTO?, Error>
You could do something like this:
getUserProfile()
.flatMap { userProfile -> AnyPublisher<UserProfileDTO?, Error> in
if let userProfile = userProfile {
return connect(userProfile: userProfile)
.eraseToAnyPublisher()
} else {
return Just<UserProfileDTO?>(nil)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
//.sink etc.
If you don't need the publisher to emit nil, you could leave the connect signature as is and use compactMap:
getUserProfile()
.compactMap { $0 }
.flatMap {
connect(userProfile: $0)
.eraseToAnyPublisher()
}

Related

Transform callback approach to reactive with Combine

This is what I am doing:
-> Login/Signup to Firebase using FirebaseAuthentification
-> Listining to AuthStateDidChangeListenerHandle
-> I store extra user information in Firestore, therefore I check if the user exists in Firestore
-> If the user does not exist I create an empty user
-> If everything was successful I return a Future Publisher via callback (I want to change that as well)
This is the checkLoginState function:
func checkLoginState(completion: #escaping (AnyPublisher<AccountDetails,Error>) -> Void) {
self.handler = Auth.auth().addStateDidChangeListener { [weak self] auth, user in
guard let safeSelf = self else { return }
completion(Future<AccountDetails,Error> { promise in
if let user = user {
print(user)
print(auth)
safeSelf.checkIfUserIsInDatabase(user: user.uid) { result in
switch result {
case .success(let isAvailable):
if isAvailable {
promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
} else {
safeSelf.createEmptyUser(user: user.uid,email: user.email) { result in
switch result {
case .success(_):
promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
case .failure(let error):
print(error)
}
}
}
case .failure(let error):
print(error)
}
}
} else {
promise(.success(AccountDetails(userUID: nil, loggedIn: false, premiumUser: false)))
}
}.eraseToAnyPublisher()
)
}
}
These are my current functions:
private func checkIfUserIsInDatabase(user id: String, completion: #escaping (Result<Bool,Error>) -> Void)
private func createEmptyUser(user id: String, email:String?, completion: #escaping (Result<Bool,Error>) -> Void)
Thats what I want to use:
private func checkIfUserIsInDatabase(user id: String) -> AnyPublisher<Bool,Error>
private func createEmptyUser(user id: String) -> AnyPublisher<Bool,Error>
func checkLoginState() -> AnyPublisher<AccountDetails,Error>
I had something like that, but it does not work, also looks confusing:
func checkLoginState(completion: #escaping (AnyPublisher<AccountDetails,Error>) -> Void) {
self.handler = Auth.auth().addStateDidChangeListener { [weak self] auth, user in
guard let safeSelf = self else { return }
completion(Future<AccountDetails,Error> { promise in
if let user = user {
print(user)
print(auth)
safeSelf.checkIfUserIsInDatabase(user: user.uid)
.sinkToResult { value in
switch value {
case .success(let isUserInDatabase):
if isUserInDatabase {
promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
} else {
safeSelf.createEmptyUser(user: user.uid)
.sinkToResult { value in
switch value {
case .success( _):
promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
case .failure(let error):
print(error)
}
}
}
case .failure(let error):
print(error)
}
}
} else {
promise(.success(AccountDetails(userUID: nil, loggedIn: false, premiumUser: false)))
}
}.eraseToAnyPublisher()
)
}
}
So you have some AccountDetails type:
import Combine
import FirebaseAuth
struct AccountDetails {
var userId: String
var name: String?
var isLoggedIn: Bool
var isPremiumUser: Bool
}
Let's extend it with an init that takes a User, because it will simplify things later:
extension AccountDetails {
init(user: User) {
self.userId = user.uid
self.name = user.displayName
self.isLoggedIn = true
self.isPremiumUser = false
}
}
I think your end goal is a Publisher that emits AccountDetails. But since there isn't always a logged-in user, it should really emit Optional<AccountDetails>, so that it can emit nil when the user logs out.
Let's start by wrapping the addStateDidChangeListener API in a Publisher. We can't use a Future for this, because a Future emits at most one output, but addStateDidChangeListener can emit multiple events. So we'll use a CurrentValueSubject instead. That means we need a place to store the subject and the AuthStateDidChangeListenerHandle. You could store them as globals, or in your AppDelegate, or wherever you feel is appropriate. For this answer, let's create a Demo class to hold them:
class Demo {
static let shared = Demo()
let userPublisher: AnyPublisher<User?, Error>
private let userSubject = CurrentValueSubject<User?, Error>(nil)
private var tickets: [AnyCancellable] = []
private init() {
userPublisher = userSubject.eraseToAnyPublisher()
let handle = Auth.auth().addStateDidChangeListener { [userSubject] (_, user) in
userSubject.send(user)
}
AnyCancellable { Auth.auth().removeStateDidChangeListener(handle) }
.store(in: &tickets)
}
}
So now you can get a Publisher of the logged-in user (or nil if no user is logged in) like this:
let loggedInUserPublisher: AnyPublisher<User?, Error> = Demo.shared.userPublisher
But you really want an AccountDetails? publisher, not a User? publisher, like this:
let accountDetailsPublisher: AnyPublisher<AccountDetails?, Error> = Demo.shared
.accountDetailsPublisher()
So we need to write an accountDetailsPublisher method that maps the User? to an AccountDetails?.
If the User? is nil, we just want to emit nil. But if the User? is .some(user), we need to do more asynchronous actions: we need to check whether the user is in the database, and add the user if not. The flatMap operator lets you chain asynchronous actions, but there's some complexity because we need to take different actions depending on the output of the upstream publisher.
We'd really like to hide the complexity away and just write this:
extension Demo {
func loggedInAccountDetailsPublisher() -> AnyPublisher<AccountDetails?, Error> {
return userPublisher
.flatMap(
ifSome: { $0.accountDetailsPublisher().map { Optional.some($0) } },
ifNone: { Just(nil).setFailureType(to: Error.self) })
.eraseToAnyPublisher()
}
}
But then we need to write flatMap(ifSome:ifNone:). Here it is:
extension Publisher {
func flatMap<Wrapped, Some: Publisher, None: Publisher>(
ifSome: #escaping (Wrapped) -> Some,
ifNone: #escaping () -> None
) -> AnyPublisher<Some.Output, Failure>
where Output == Optional<Wrapped>, Some.Output == None.Output, Some.Failure == Failure, None.Failure == Failure
{
return self
.flatMap { $0.map { ifSome($0).eraseToAnyPublisher() } ?? ifNone().eraseToAnyPublisher() }
.eraseToAnyPublisher()
}
}
Now we need to implement accountDetailsPublisher in a User extension. What does this method need to do? It needs to check whether the User is in the database (an asynchronous action) and, if not, add the User (another asynchronous action). Since we need to chain asynchronous actions, we again need flatMap. But we'd really like to just write this:
extension User {
func accountDetailsPublisher() -> AnyPublisher<AccountDetails, Error> {
return isInDatabasePublisher()
.flatMap(
ifTrue: { Just(AccountDetails(user: self)).setFailureType(to: Error.self) },
ifFalse: { self.addToDatabase().map { AccountDetails(user: self) } })
}
}
Here is flatMap(ifTrue:ifFalse:):
extension Publisher where Output == Bool {
func flatMap<True: Publisher, False: Publisher>(
ifTrue: #escaping () -> True,
ifFalse: #escaping () -> False
) -> AnyPublisher<True.Output, Failure>
where True.Output == False.Output, True.Failure == Failure, False.Failure == Failure
{
return self
.flatMap { return $0 ? ifTrue().eraseToAnyPublisher() : ifFalse().eraseToAnyPublisher() }
.eraseToAnyPublisher()
}
}
Now we need to write isInDatabasePublisher and addToDatabase methods on User. I don't have the source code to your checkIfUserIsInDatabase and createEmptyUser functions, so I can't convert them to publishers directly. But we can wrap them using Future:
extension User {
func isInDatabasePublisher() -> AnyPublisher<Bool, Error> {
return Future { promise in
checkIfUserIsInDatabase(user: self.uid, completion: promise)
}.eraseToAnyPublisher()
}
func addToDatabase() -> AnyPublisher<Void, Error> {
return Future { promise in
createEmptyUser(user: self.uid, email: self.email, completion: promise)
} //
.map { _ in } // convert Bool to Void
.eraseToAnyPublisher()
}
}
Note that, since your example code ignores the Bool output of createEmptyUser, I wrote addToDatabase to output Void instead.
Thats what I came up with:
Reference by matt:
http://www.apeth.com/UnderstandingCombine/operators/operatorsflatmap.html#SECSerializingAsynchronicity
https://stackoverflow.com/a/60418000/341994
var handler: AuthStateDidChangeListenerHandle?
var storage = Set<AnyCancellable>()
func checkLoginState(completion: #escaping (AnyPublisher<AccountDetails,Error>) -> Void) {
self.handler = Auth.auth().addStateDidChangeListener { [weak self] auth, user in
guard let safeSelf = self else { return }
completion(Future<AccountDetails,Error> { promise in
if let user = user {
safeSelf.handleUserInDatabase(user: user.uid)
.sink(receiveCompletion: { completion in
if let error = completion.error {
print(error.localizedDescription)
promise(.failure(error))
}
}, receiveValue: { result in
if result {
promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
}
}).store(in: &safeSelf.storage)
} else {
promise(.success(AccountDetails(userUID: nil, loggedIn: false, premiumUser: false)))
}
}.eraseToAnyPublisher())
}
}
/// Checks if User exists in Firestore, if not creates an empty User and returns true
private func handleUserInDatabase(user: String) -> AnyPublisher<Bool,Error> {
return Future<Bool,Error>( { [weak self] promise in
guard let safeSelf = self else { return }
safeSelf.checkIfUserIsInDatabase(user: user)
.flatMap { result -> AnyPublisher<Bool,Error> in
if result == false {
return safeSelf.createEmptyUser(user: user).eraseToAnyPublisher()
} else {
promise(.success(true))
return Empty<Bool,Error>(completeImmediately: true).eraseToAnyPublisher()
}}
.sink(receiveCompletion: { completion in
if let error = completion.error {
promise(.failure(error))
}}, receiveValue: {promise(.success($0))})
.store(in:&safeSelf.storage)
}
).eraseToAnyPublisher()
}

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

Translating async method into Combine

I'm trying to wrap my head around Combine.
Here's a method I want to translate into Combine, so that it would return AnyPublisher.
func getToken(completion: #escaping (Result<String, Error>) -> Void) {
dispatchQueue.async {
do {
if let localEncryptedToken = try self.readTokenFromKeychain() {
let decryptedToken = try self.tokenCryptoHelper.decrypt(encryptedToken: localEncryptedToken)
DispatchQueue.main.async {
completion(.success(decryptedToken))
}
} else {
self.fetchToken(completion: completion)
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
The whole thing executes on a separate dispatch queue because reading from Keychain and decryption can be slow.
My first attempt to embrace Combine
func getToken() -> AnyPublisher<String, Error> {
do {
if let localEncryptedToken = try readTokenFromKeychain() {
let decryptedToken = try tokenCryptoHelper.decrypt(encryptedToken: localEncryptedToken)
return Result.success(decryptedToken).publisher.eraseToAnyPublisher()
} else {
return fetchToken() // also rewritten to return AnyPublisher<String, Error>
}
} catch {
return Result.failure(error).publisher.eraseToAnyPublisher()
}
}
But how would I move reading from Keychain and decryption onto separate queue? It probably should look something like
func getToken() -> AnyPublisher<String, Error> {
return Future<String, Error> { promise in
self.dispatchQueue.async {
do {
if let localEncryptedToken = try self.readTokenFromKeychain() {
let decryptedToken = try self.tokenCryptoHelper.decrypt(encryptedToken: localEncryptedToken)
promise(.success(decryptedToken))
} else {
// should I fetchToken().sink here?
}
} catch {
promise(.failure(error))
}
}
}.eraseToAnyPublisher()
}
How would I return a publisher from my private method call? (see comment in code)
Are there any prettier solutions?
Assuming you’ve refactored readTokenFromKeyChain, decrypt, and fetchToken to return AnyPublisher<String, Error> themselves, you can then do:
func getToken() -> AnyPublisher<String, Error> {
readTokenFromKeyChain()
.flatMap { self.tokenCryptoHelper.decrypt(encryptedToken: $0) }
.catch { _ in self.fetchToken() }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
That will read the keychain, if it succeeded, decrypt it, and if it didn’t succeed, it will call fetchToken. And having done all of that, it will make sure the final result is delivered on the main queue.
I think that’s the right general pattern. Now, let's talk about that dispatchQueue: Frankly, I’m not sure I’m seeing anything here that warrants running on a background thread, but let’s imagine you wanted to kick this off in a background queue, then, you readTokenFromKeyChain might dispatch that to a background queue:
func readTokenFromKeyChain() -> AnyPublisher<String, Error> {
dispatchQueue.publisher { promise in
let query: [CFString: Any] = [
kSecReturnData: true,
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: "token",
kSecAttrService: Bundle.main.bundleIdentifier!]
var extractedData: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &extractedData)
if
status == errSecSuccess,
let retrievedData = extractedData as? Data,
let string = String(data: retrievedData, encoding: .utf8)
{
promise(.success(string))
} else {
promise(.failure(TokenError.failure))
}
}
}
By the way, that’s using a simple little method, publisher that I added to DispatchQueue:
extension DispatchQueue {
/// Dispatch block asynchronously
/// - Parameter block: Block
func publisher<Output, Failure: Error>(_ block: #escaping (Future<Output, Failure>.Promise) -> Void) -> AnyPublisher<Output, Failure> {
Future<Output, Failure> { promise in
self.async { block(promise) }
}.eraseToAnyPublisher()
}
}
For the sake of completeness, this is a sample fetchToken implementation:
func fetchToken() -> AnyPublisher<String, Error> {
let request = ...
return URLSession.shared
.dataTaskPublisher(for: request)
.map { $0.data }
.decode(type: ResponseObject.self, decoder: JSONDecoder())
.map { $0.payload.token }
.eraseToAnyPublisher()
}
I think I could find a solution
private func readTokenFromKeychain() -> AnyPublisher<String?, Error> {
...
}
func getToken() -> AnyPublisher<String, Error> {
return readTokenFromKeychain()
.flatMap { localEncryptedToken -> AnyPublisher<String, Error> in
if let localEncryptedToken = localEncryptedToken {
return Result.success(localEncryptedToken).publisher.eraseToAnyPublisher()
} else {
return self.fetchToken()
}
}
.flatMap {
return self.tokenCryptoHelper.decrypt(encryptedToken: $0)
}
.subscribe(on: dispatchQueue)
.eraseToAnyPublisher()
}
But I had to make functions I call within getToken() return publishers too to Combine them well.
There probably should be error handling somewhere but this is the next thing for me to learn.

"Non-void function should return a value" when capturing Self in Swift 4.2 guard statement in closure in function

How can we use the Swift 4.2 style of capturing self in closures in functions that returns a type?
For example:
func checkEmailExists(_ email: String) -> Observable<Bool> {
return Observable.create { [weak self] observer in
guard let self = self else { return }
self.callAPI()
}
}
This generates an error Non-void function should return a value but also it should not return false as this will influence the outcome of the function call.
EDIT: Another example, throws Non-void function should return a value
func loginWithCredentials() -> Observable<User> {
return Observable.create { [weak self] observer in
guard let self = self else { return }
let decoder = JSONDecoder()
let json = ["id": 1, "name": "John Doe", "email": "john#doe.com"] as [String: Any]
do {
let user = try decoder.decode(User.self, from: json.jsonData()!)
observer.onNext(user) // Simulation of successful user authentication.
} catch {
print("error")
}
return Disposables.create()
}
}
You have to return a disposable in any case.
func checkEmailExists(_ email: String) -> Observable<Bool> {
return Observable.create { [weak self] observer in
guard let self = self else { return Disposables.create() }
// and so on...
return Disposables.create { /* cancelation code */ }
}
}
This feels very wrong though. Your function callAPI() should itself be returning an Observable of some sort which would make this function pointless.
Try make return type as optional and return nil if there is no self
OR
You can avoid this guard statement by calling your method with ? sign like:
self?.callAPI()

Networking with RxSwift

I've been trying to understand rxSwift. I faced with request problem and want to implement this in a good way. Currently, I'm using this code:
enum RequestState<T> {
case loading
case loaded(T)
case error(Error)
}
struct Response<T: Decodable>: Decodable {
let data: T
let error: ResponseError?
}
searchBar.rx.text.asObservable()
.flatMap { self.provider.rx.request(Request(query: $0)) }
.map({ RequestState<Response<Bool>>.loaded($0) })
.asDriver(onErrorRecover: { error in
return Driver.just(.error(error))
})
.startWith(.loading)
.drive(onNext: { state in
switch state {
case .loading: ()
case .loaded(let response): ()
case .error(let error): ()
}
})
.disposed(by: disposeBag)
This works good but not too convenient to work with data and request state. I saw in rxSwift demo project following code.
struct RequestState<T: Decodable> {
let isLoading: Bool
let data: T
let error: ResponseError?
}
let state = viewModel.requestMethod()
state
.map { $0.isLoading }
.drive(self.loaderView.isOffline)
.disposed(by: disposeBag)
state
.map { $0.data }
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
state
.map { $0.error }
.drive(onNext: { error in
showAlert(error)
})
.disposed(by: disposeBag)
And my problem in the following method, I can't understand Rx magic here:
func requestMethod() -> Driver<RequestState> {
// supper code
}
Can someone advise me what I have to do here?
Here's what I ended up with while looking at both of your code samples:
First here is the point of use:
let request = searchBar.rx.text
.unwrap()
.map { URLRequest.search(forQuery: $0) }
let networkRequest = createRequest(forType: MyType.self)
let state = request
.flatMap(networkRequest)
state
.map { $0.isLoading }
.bind(to: loaderView.isOffline)
.disposed(by: bag)
state
.map { $0.data }
.unwrap()
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
state
.map { $0.error }
.unwrap()
.subscribe(onNext: showAlert)
.disposed(by: bag)
Here is the support code for the above:
enum RequestState<T> {
case loading
case loaded(T)
case error(Error)
var isLoading: Bool {
guard case .loading = self else { return false }
return true
}
var data: T? {
guard case let .loaded(t) = self else { return nil }
return t
}
var error: Error? {
guard case let .error(e) = self else { return nil }
return e
}
}
You will see that the above RequestState enum is a bit of an amalgamation of both RequestState types you showed in your example. The enum makes it easy to create the object while the computed properties make it easy to extract the information.
func createRequest<T>(forType type: T.Type, session: URLSession = URLSession.shared) -> (URLRequest) -> Observable<RequestState<T>> where T: Decodable {
return { request in
return Observable.create { observer in
observer.onNext(.loading)
let disposable = session.rx.data(request: request)
.subscribe { event in
switch event {
case let .error(error):
observer.onNext(.error(error))
case let .next(data):
do {
let item = try JSONDecoder().decode(type, from: data)
observer.onNext(.loaded(item))
}
catch {
observer.onNext(.error(error))
}
case .completed:
observer.onCompleted()
}
}
return Disposables.create([disposable])
}
}
}
The above is a factory function. You use it to create a function that knows how to make network requests for the appropriate type. Recall in the code where it's being used I had let networkRequest = createRequest(forType: MyType.self). This line produces a function networkRequest that takes a URLRequest and returns an Observable that has been specialized for the type in question.
When the Observable from networkRequest is subscribed to, it will immediately push out a .loading case, then make the request. Then it will use the response to either push out a .loaded(T) or an .error(Error) depending on the results.
Personally, I'm more inclined to use something like the ActivityIndicator system from the examples in the RxSwift repository instead.