Networking with RxSwift - swift

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.

Related

What is the best way to stop an image uploading and restart it using RxSwift?

I'm trying to create some functionality for uploading images to remote server using RxSwift.
My upload function is below:
func upload(image: UIImage) -> Single<UploadResponse> {
guard let data = image.jpegData(compressionQuality: 0.6) else { return .never()}
let target = UserEndpoint.addNewProfileImage(data: data, nama: "image", fileName: "image.jpeg", mimeType: "image/jpeg")
return networkProvider.request(target)}
}
And how I call it:
selectImageTrigger
.flatMapLatest { [weak self] image -> Observable<UploadResponse> in
guard let self = self else { return .never()}
return self.upload(image: image)
.trackError(self.errorTracker)
.trackActivity(self.activityIndicator)
.catchErrorJustComplete()
}
.subscribe()
.disposed(by: rx.disposeBag)
And I also need to make it possible to stop uploading images simply by clicking on the "stop" button.
Here's my approach, and it looks, in my opinion, ugly. But anyway it works :).
var token: Disposable?
selectedImage
.subscribe(onNext: { [weak self] image in
guard let self = self else { return }
token = self.upload(image: image)
.trackError(self.errorTracker)
.trackActivity(self.activityIndicator)
.catchErrorJustComplete()
.subscribe()
})
.disposed(by: rx.disposeBag)
stopTrigger
.subscribe(onNext: { _ in
token?.dispose()
})
.disposed(by: rx.disposeBag)
I use RxAlamofire which means that in order to cancel the request I have to dispose the subscription. But I want to stop and repeat (dispose and resubscribe?) at any time.
So what's the best way to do it?
Here's a cleaner way... The merge emits an Optional image, if it emit nil, that will stop the current upload (if there is one).
Whenever a new event comes in, the flatMapLatest operator will dispose the previous Observable and subscribe to the new one.
The function will emit a next event containing an UploadResponse if the upload completes, and will emit next(nil) if an upload is stopped.
func example(select: Observable<UIImage>, stop: Observable<Void>) -> Observable<UploadResponse?> {
Observable.merge(
select.map(Optional.some),
stop.map(to: Optional.none)
)
.flatMapLatest { [weak self] (image) -> Observable<UploadResponse?> in
guard let self = self else { return .empty() }
guard let image = image else { return .just(nil) }
return self.upload(image: image)
.map(Optional.some)
.trackError(self.errorTracker)
.trackActivity(self.activityIndicator)
.asObservable()
}
}
Also, that return .never() in the upload(image:) function is a mistake. It should either be .error or .empty and if the latter, then the function needs to return a Maybe instead of a Single.
I have created an extension based on #daniel-t answer
extension ObservableType {
public func flatMapLatestCancellable<Source: ObservableConvertibleType>(cancel: Observable<Void>, _ selector: #escaping (Element) throws -> Source)
-> Observable<Source.Element?> {
Observable<Element?>.merge(
self.map(Optional.some),
cancel.map { _ in Optional.none }
)
.flatMapLatest { element -> Observable<Source.Element?> in
guard let element = element else { return .just(nil)}
return try selector(element)
.asObservable()
.map(Optional.some)
}
}
}
Simple usage:
start
.flatMapLatestCancellable(cancel: cancel, { [unowned self] _ in
return anyTask()
.catch { error in
print(error.localizedDescription)
return .empty()
}
})
.observe(on: MainScheduler.instance)
.subscribe(onNext: { item in
print(item)
})
.disposed(by: disposeBag)

Chain requests and return both results with RxSwift

I have a ContentService that makes a request for an article. That article response contains an authorId property.
I have a ProfileService that allows me to request a user profile by an userId.
I am trying to request an article from the ContentService, chain on a request once that completes to the ProfileService using the authorId property, I would then like to return a ContentArticleViewModel that contains both the article and profile information.
My ArticleInteractor looks something like this -
final class ArticleInteractor: ArticleInteractorInputProtocol {
let fetchArticleTrigger = PublishSubject<String>()
private lazy var disposeBag = DisposeBag()
weak var output: ArticleInteractorOutputProtocol? {
didSet {
configureSubscriptions()
}
}
private func configureSubscriptions() {
guard let output = output else { return }
fetchArticleTrigger
.bind(to: dependencies.contentSvc.fetchContentByIdTrigger)
.disposed(by: disposeBag)
dependencies.contentSvc.fetchContentByIdResponse
.bind(to: output.fetchArticleResponse)
.disposed(by: disposeBag)
}
}
Quite simply fetchArticleTrigger starts a request, I then bind on dependencies.contentSvc.fetchContentByIdResponse and pick up the response.
The method on my ContentService is -
// MARK:- FetchContentById
// #params: id - String
// return: PublishSubject<ContentArticle>
fetchContentByIdTrigger
.flatMapLatest { [unowned self] in self.client.request(.getContentById(id: $0)) }
.map { (resp: Result<ContentArticle>) in
guard case .success(let props) = resp else { return ContentArticle() }
return props
}
.bind(to: fetchContentByIdResponse)
.disposed(by: disposeBag)
I have a very similair setup on my ProfileService -
// MARK:- FetchUserProfileById
// #params: id - String
// return: PublishSubject<User>
fetchUserProfileByIdTrigger
.flatMapLatest { [unowned self] in self.client.request(.getProfileByUserId(id: $0)) }
.map { (resp: Result<User>) in
guard case .success(let props) = resp else { return User() }
return props
}
.bind(to: fetchUserProfileByIdResponse)
.disposed(by: disposeBag)
I imagine I will create a model for the article, something like -
struct ContentArticleViewModel {
var post: ContentArticle
var user: User
}
I was imaging something like this pseudo code within my ArticleInteractor-
dependencies.contentSvc.fetchContentByIdResponse
.flatMapLatest { article in
/* fetch profile using `article.authorId */
}.map { article, profile in
return ContentArticleViewModel(post: article, user: profile)
}
.bind(to: output.fetchArticleResponse)
.disposed(by: disposeBag)
But I am completely lost how best to handle this. I have seen a number of articles on chaining requests but am struggling to apply anything successfully.
EDIT
I have something working currently -
private func configureSubscriptions() {
guard let output = output else { return }
fetchArticleTrigger
.bind(to: dependencies.contentSvc.fetchContentByIdTrigger)
.disposed(by: disposeBag)
dependencies.contentSvc.fetchContentByIdResponse
.do(onNext: { [unowned self] article in self.dependencies.profileSvc.fetchUserProfileByIdTrigger.onNext(article.creator.userId)})
.bind(to: fetchArticleResponse)
.disposed(by: disposeBag)
let resp = Observable.combineLatest(fetchArticleResponse, dependencies.profileSvc.fetchUserProfileByIdResponse)
resp
.map { [unowned self] in self.enrichArticleAuthorProps(article: $0, user: $1) }
.bind(to: output.fetchArticleResponse)
.disposed(by: disposeBag)
}
private func enrichArticleAuthorProps(article: ContentArticle, user: User) -> ContentArticle {
var updatedArticle = article
updatedArticle.creator = user
return updatedArticle
}
I am not sure this is correct however.
I'm not sure why you have so much code for such a small job. Below is an example that does what you describe (downloads the article, the downloads the author profile and emits both) with far less code and even it is more code than I would normally use.
protocol ContentService {
func requestArticle(id: String) -> Observable<Article>
func requestProfile(id: String) -> Observable<User>
}
class Example {
let service: ContentService
init(service: ContentService) {
self.service = service
}
func bind(trigger: Observable<String>) -> Observable<(Article, User)> {
let service = self.service
return trigger
.flatMapLatest { service.requestArticle(id: $0) }
.flatMapLatest {
Observable.combineLatest(Observable.just($0), service.requestProfile(id: $0.authorId))
}
}
}
Or maybe you want to display the article while waiting for the author profile to download. In that case something like this:
func bind(trigger: Observable<String>) -> (article: Observable<Article>, author: Observable<User>) {
let service = self.service
let article = trigger
.flatMapLatest { service.requestArticle(id: $0) }
.share(replay: 1)
let author = article
.flatMapLatest {
service.requestProfile(id: $0.authorId)
}
return (article, author)
}

How to handle error from api request properly with RxSwift in MVVM?

So, I have a button and will make an API request upon tapping it. When the API request returns an error, if my understanding is correct, the sequence will be terminated and no subsequent action will be recorded. How do I handle this properly so that I can still make another API request when tapping the button.
My thoughts are to have two observables that I can subscribe to in ViewController and on button pressed, one of it will print the success response and one of it will print the error. Just not quite sure how I can achieve that.
PS: In Post.swift, I have purposely set id as String type to fail the response. It should have be an Int type.
Post.swift
import Foundation
struct Post: Codable {
let id: String
let title: String
let body: String
let userId: Int
}
APIClient.swift
class APIClient {
static func request<T: Codable> (_ urlConvertible: URLRequestConvertible, decoder: JSONDecoder = JSONDecoder()) -> Observable<T> {
return Observable<T>.create { observer in
URLCache.shared.removeAllCachedResponses()
let request = AF.request(urlConvertible)
.responseDecodable (decoder: decoder) { (response: DataResponse<T>) in
switch response.result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
switch response.response?.statusCode {
default:
observer.onError(error)
}
}
}
return Disposables.create {
request.cancel()
}
}
}
}
PostService.swift
class PostService {
static func getPosts(userId: Int) -> Observable<[Post]> {
return APIClient.request(PostRouter.getPosts(userId: userId))
}
}
ViewModel.swift
class LoginLandingViewModel {
struct Input {
let username: AnyObserver<String>
let nextButtonDidTap: AnyObserver<Void>
}
struct Output {
let apiOutput: Observable<Post>
let invalidUsername: Observable<String>
}
// MARK: - Public properties
let input: Input
let output: Output
// Inputs
private let usernameSubject = BehaviorSubject(value: "")
private let nextButtonDidTapSubject = PublishSubject<Void>()
// MARK: - Init
init() {
let minUsernameLength = 4
let usernameEntered = nextButtonDidTapSubject
.withLatestFrom(usernameSubject.asObservable())
let apiOutput = usernameEntered
.filter { text in
text.count >= minUsernameLength
}
.flatMapLatest { _ -> Observable<Post> in
PostService.getPosts(userId: 1)
.map({ posts -> Post in
return posts[0]
})
}
let invalidUsername = usernameEntered
.filter { text in
text.count < minUsernameLength
}
.map { _ in "Please enter a valid username" }
input = Input(username: usernameSubject.asObserver(),
nextButtonDidTap: nextButtonDidTapSubject.asObserver())
output = Output(apiOutput: apiOutput,
invalidUsername: invalidUsername)
}
deinit {
print("\(self) dellocated")
}
}
ViewController
private func configureBinding() {
loginLandingView.usernameTextField.rx.text.orEmpty
.bind(to: viewModel.input.username)
.disposed(by: disposeBag)
loginLandingView.nextButton.rx.tap
.debounce(0.3, scheduler: MainScheduler.instance)
.bind(to: viewModel.input.nextButtonDidTap)
.disposed(by: disposeBag)
viewModel.output.apiOutput
.subscribe(onNext: { [unowned self] post in
print("Valid username - Navigate with post: \(post)")
})
.disposed(by: disposeBag)
viewModel.output.invalidUsername
.subscribe(onNext: { [unowned self] message in
self.showAlert(with: message)
})
.disposed(by: disposeBag)
}
You can do that by materializing the even sequence:
First step: Make use of .rx extension on URLSession.shared in your network call
func networkCall(...) -> Observable<[Post]> {
var request: URLRequest = URLRequest(url: ...)
request.httpMethod = "..."
request.httpBody = ...
URLSession.shared.rx.response(request)
.map { (response, data) -> [Post] in
guard let json = try? JSONSerialization.jsonObject(with: data, options: []),
let jsonDictionary = json as? [[String: Any]]
else { throw ... } // Throw some error here
// Decode this dictionary and initialize your array of posts here
...
return posts
}
}
Second step, materializing your observable sequence
viewModel.networkCall(...)
.materialize()
.subscribe(onNext: { event in
switch event {
case .error(let error):
// Do something with error
break
case .next(let posts):
// Do something with posts
break
default: break
}
})
.disposed(by: disposeBag)
This way, your observable sequence will never be terminated even when you throw an error inside your network call, because .error events get transformed into .next events but with a state of .error.
So I have also found the way to achieve what I wanted, which is assigning the success output and error output into two different observable respectively. By using RxSwiftExt, there are two additional operators, elements() and errors() which can be used on an observable that is materialized to get the element.
Here is how I did it,
ViewModel.swift
let apiOutput = usernameEntered
.filter { text in
text.count >= minUsernameLength
}
.flatMapLatest { _ in
PostService.getPosts(userId: 1)
.materialize()
}
.share()
let apiSuccess = apiOutput
.elements()
let apiError = apiOutput
.errors()
.map { "\($0)" }
Then, just subscribe to each of these observables in the ViewController.
As reference: http://adamborek.com/how-to-handle-errors-in-rxswift/

RefreshNextPage with RxSwift

I am using RxSwift to make a pull to refresh and refreshNextPage.
Currently, here is the viewModel I work so far:
public final class MomentViewModel {
// Property
let refreshTrigger = PublishSubject<Void>()
let loadNextPageTrigger = PublishSubject<Void>()
let loading = Variable<Bool>(false)
let posts = Variable<[Post]>([])
var pageIndex: Int = 0
let error = PublishSubject<Swift.Error>()
private let disposeBag = DisposeBag()
public init() {
let refreshRequest = loading.asObservable()
.sample(refreshTrigger)
.flatMap { loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { observer in
self.pageIndex = 0
print("reset page index to 0")
observer.onNext(0)
observer.onCompleted()
return Disposables.create()
}
}
}
.debug("refreshRequest", trimOutput: false)
let nextPageRequest = loading.asObservable()
.sample(loadNextPageTrigger)
.flatMap { loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { [unowned self] observer in
self.pageIndex += 1
print(self.pageIndex)
observer.onNext(self.pageIndex)
observer.onCompleted()
return Disposables.create()
}
}
}
.debug("nextPageRequest", trimOutput: false)
let request = Observable.merge(refreshRequest, nextPageRequest)
.debug("Request", trimOutput: false)
let response = request.flatMapLatest { page in
RxAPIProvider.shared.getPostList(page: page).materialize()
}
.share(replay: 1)
.elements()
.debug("Response", trimOutput: false)
Observable
.combineLatest(request, response, posts.asObservable()) { request, response, posts in
return self.pageIndex == 0 ? response : posts + response
}
.sample(response)
.bind(to: posts)
.disposed(by: disposeBag)
Observable
.merge(request.map{ _ in true },
response.map { _ in false },
error.map { _ in false })
.bind(to: loading)
.disposed(by: disposeBag)
}
}
The refreshTrigger and loadNextPageTrigger is bind to difference target likes:
self.tableView.rx_reachedBottom
.map { _ in () }
.bind(to: self.viewModel.loadNextPageTrigger)
.disposed(by: disposeBag)
self.refreshControl.rx.controlEvent(.valueChanged)
.bind(to: self.viewModel.refreshTrigger)
.disposed(by: disposeBag)
self.rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
.map { _ in () }
.bind(to: viewModel.refreshTrigger)
.disposed(by: disposeBag)
Question:
When I scroll the tableView to bottom and trigger the loadNextPageTrigger, everything works fine.
However, if there is no more data in the next coming request, the loadnextPageTrigger will be triggered infinitely.
Any help would be appreciated.
You can download the Demo here.
Main idea is to check if all elements is loaded. I found in source code that you load parts by 20, so this condition should works fine if loadedPosts < 20 then stop loading new part. Here i share with you basic solution that you can refactor as you want (because i'm not good at RxSwift):
In MomentViewModel you should declare property
private var isAllLoaded = false
that set to true if you loaded all values. Then you should check every [Post] that came in response to set correct isAllLoaded:
Observable
.combineLatest(request, response, posts.asObservable()) { request, response, posts in
self.isAllLoaded = response.count < 20 // here the check
return self.pageIndex == 0 ? response : posts + response
}
.sample(response)
.bind(to: posts)
.disposed(by: disposeBag)
And then in nextPageRequest in you should return .empty() observer if all parts have been loaded:
if loading {
return Observable.empty()
} else {
guard !self.isAllLoaded else { return Observable.empty() }
return Observable<Int>.create { [unowned self] observer in
self.pageIndex += 1
print(self.pageIndex)
observer.onNext(self.pageIndex)
observer.onCompleted()
return Disposables.create()
}
}
P.S. The MomentViewModel.swift file to copy/paste.

Cannot convert value of type 'Observable<Data>' to expected argument type 'Data'

How do I cast Observable<Data> to Data? I.e getting the value of the observable.
Data.rx_subscribeData()
.map({ data -> [Model] in
return data.enumerated().map({ (index, item) in
return Model(data: item)
})
})
.bindTo(tableView.rx.items(dataSource: dataSource))
.addDisposableTo(disposeBag)
You do not cast an observable to a type, you subscribe to that observables events.
func observeSomeData() -> Observable<Data> {
// some rx magic
return Observable.create { observer in
let string = "Hello World"
let data = string.data(using: String.Encoding.utf8)
observer.on(.next(data))
observer.on(.completed)
return Disposables.create()
}
}
You now subscribe to this observable and respond to its events. You can do this in a number of ways, I prefer the more verbose way.
_ = observeSomeData.subscribe() { event in
switch event {
case .next(let data):
// here you have data.
case .error(let error):
// an error occurred
case .completed:
// the observable has finished sending events.
}
}
There is more information on the Getting Started documentation on the RxSwift GitHub page.
UPDATE:
I'm no expert in RxSwift but I believe I've managed to get it working.
here is the first version of the code I had in a test project.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
_ = getData().subscribe() { event in
switch event {
case .next(let data):
let str = String(data: data, encoding: String.Encoding.utf8)
print("Received string data: \(String(describing: str))")
case .completed:
print("completed")
case .error(let error):
print("error occurred: \(error)")
}
}
}
func getData() -> Observable<Data> {
return Observable.create { observer in
let data = "Hello World".data(using: String.Encoding.utf8)!
observer.on(.next(data))
observer.on(.completed)
return Disposables.create()
}
}
}
So this code just creates an observable of type Observable, which just emits a single event with some data in it. This works but does not transform the data as you wish.
The second version of the code I have..
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
_ = getData().flatMap({ (data: Data) -> Observable<String> in
return Observable.just(String(data: data, encoding: String.Encoding.utf8)!)
}).subscribe() { event in
switch event {
case .next(let str):
print("Received string data: \(String(describing: str))")
case .completed:
print("completed")
case .error(let error):
print("error occurred: \(error)")
}
}
}
func getData() -> Observable<Data> {
return Observable.create { observer in
let data = "Hello World".data(using: String.Encoding.utf8)!
observer.on(.next(data))
observer.on(.completed)
return Disposables.create()
}
}
}
So here, note the observable itself is unchanged. Now when I subscribe to the observable, I then use flatMap to transform the incoming data into an Observable of type Observable.
As I had this explained to me like this: in Rx world, .map expects you to return the same type that is passed into it, but flatMap can be used to transform the element into something else. can someone clarify this or explain in more detail?
So, to hopefully answer your question.. You can map on the observable to do transformations using flatMap and then subscribe to the new observable which will give you your transformed value.
Hope this helps