Where is the correct place I should put the code that would trigger a loading to display in my app.
It is correct to do is on view? since it is displaying something on screen, so it fits as a UI logic
class ViewController: UIViewController {
func fetchData() {
showLoading()
interactor?.fetchData()
}
}
or on interactor? since it's a business logic. something like, everytime a request is made, we should display a loading. View only knows how to construct a loading, not when to display it.
class Interactor {
func fetchData() {
presenter?.presentLoading(true)
worker?.fetchData() { (data) [weak self] in
presenter?.presentLoading(false)
self?.presenter?.presentData(data)
}
}
}
same question applies to MVVM and MVP.
it is totally up to you . i am showing loading using an Observable .
in my viewModel there is an enum called action :
enum action {
case success(count:Int)
case deleteSuccess
case loading
case error
}
and an Observable of action type :
var actionsObservable = PublishSubject<action>()
then , before fetching data i call onNext method of actionObservable(loading)
and subscribing to it in viewController :
vm.actionsObserver
.observeOn(MainScheduler.instance)
.subscribe(onNext: { (action) in
switch action {
case .success(let count):
if(count == 0){
self.noItemLabel.isHidden = false
}
else{
self.noItemLabel.isHidden = true
}
self.refreshControl.endRefreshing()
self.removeSpinner()
case .loading:
self.showSpinner(onView : self.view)
case .error:
self.removeSpinner()
}
}, onError: { (e) in
print(e)
}).disposed(by: disposeBag)
You can use the delegate or completion handler to the update the UI from view model.
class PaymentViewController: UIViewController {
// for UI update
func showLoading() {
self.showLoader()
}
func stopLoading() {
self.removeLoader()
}
}
protocol PaymentOptionsDelegate : AnyObject {
func showLoading()
func stopLoading()
}
class PaymentOptionsViewModel {
weak var delegate : PaymentOptionsDelegate?
func fetchData() {
delegate?.showLoading()
delegate?.stopLoading()
}
}
Related
I'm struggling with specific use-case incorporating RxSwift's PublishSubject.
For sake of simplicity unimportant details were omitted.
There is a MVVM setup. In VC I have a UIButton, on tap of which a network call should dispatch. In ViewModel I have a buttonDidTapSubject: PublishSubject<Void>.
class ViewModel {
let disposeBag = DisposeBag()
let buttonDidTapSubject = PublishSubject<Void>()
let service: Service
typealias Credentials = (String, String)
var credentials: Observable<Credentials> {
return Observable.just(("testEmail", "testPassword"))
}
init(_ service: Service) {
self.service = service
buttonDidTapSubject
.withLatestFrom(credentials)
.flatMap(service.login) // login method has signature func login(_ creds: Credentials) -> Observable<User>
.subscribe(onNext: { user in print("Logged in \(user)") },
onError: { error in print("Received error") })
.disposed(by: disposeBag)
}
}
class ViewController: UIViewController {
let viewModel: ViewModel
let button = UIButton()
init(_ viewModel: ViewModel) {
self.viewModel = viewModel
}
}
In controller's viewDidLoad I make a binding:
override func viewDidLoad() {
button.rx.tap.asObservable()
.subscribe(viewModel.buttonDidTapSubject)
.disposed(by: disposeBag)
}
The problem is, since network request can fail and Observable that is returned from login(_:) method will produce an error, the whole subscription to buttonDidTapSubject in ViewModel will be disposed. And all other taps on a button will not trigger sequence to login in ViewModel.
Is there any way to avoid this kind of behavior?
You can use retry to prevent finishing the subcription. If you only want to retry in specific cases or errors you can also use retryWhen operator
In the view model:
lazy var retrySubject: Observable<Void> = {
return viewModel.buttonDidTapSubject
.retryWhen { error in
if (error == .networkError){ //check here your error
return .just(Void())
} else {
return .never() // Do not retry
}
}
}()
In the view controller I would have done it in another way:
override func viewDidLoad() {
super.viewDidLoad()
button.rx.tap.asObservable()
.flatMap { [weak self] _ in
return self?.viewModel.retrySubject
}
.subscribe(onNext: {
//do whatever
})
.disposed(by: disposeBag)
}
Not sure if still relevant - Use PublishRelay ( although it is RxCocoa )
I write a subscribe in viewWillAppear.
But it also run once in first launch app.
When I push to another viewcontroller, I use dispose().
Then I back in first viewcontroller, my subscribe func in viewWillAppear don't run.
What's wrong with my rx subscribe?
var listSubscribe:Disposable?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
listSubscribe = chatrooms.notifySubject.subscribe({ json in
print("*1") //just print once in first launch
self.loadContents()
})
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let controllers = tabBarController?.navigationController?.viewControllers
if (controllers?.count)! > 1 {
listSubscribe?.dispose()
}
}
RxSwift documentation says "Note that you usually do not want to manually call dispose; this is only an educational example. Calling dispose manually is usually a bad code smell."
Normally, you should be doing something like this -
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
whatever.subscribe(onNext: { event in
// do stuff
}).disposed(by: self.disposeBag)
}
As for your question, I believe you don't need to re-subscribe because you subscription will be alive and 'notifySubject' will send you updates whenever there are any.
Maybe you can get some reactive implementation of viewWillAppear and similar functions? And forget about manual disposables handling... For example your UIViewController init will contain something like this:
rx.driverViewState()
.asObservable()
.filter({ $0 == .willAppear })
.take(1) // if you need only first viewWillAppear call
.flatMapLatest({ _ in
// Do what you need
})
And the implementation of driverViewState:
public extension UIViewController {
public enum ViewState {
case unknown, didAppear, didDisappear, willAppear, willDisappear
}
}
public extension Reactive where Base: UIViewController {
private typealias _StateSelector = (Selector, UIViewController.ViewState)
private typealias _State = UIViewController.ViewState
private func observableAppearance(_ selector: Selector, state: _State) -> Observable<UIViewController.ViewState> {
return (base as UIViewController).rx
.methodInvoked(selector)
.map { _ in state }
}
func driverViewState() -> Driver<UIViewController.ViewState> {
let statesAndSelectors: [_StateSelector] = [
(#selector(UIViewController.viewDidAppear(_:)), .didAppear),
(#selector(UIViewController.viewDidDisappear(_:)), .didDisappear),
(#selector(UIViewController.viewWillAppear(_:)), .willAppear),
(#selector(UIViewController.viewWillDisappear(_:)), .willDisappear)
]
let observables = statesAndSelectors
.map({ observableAppearance($0.0, state: $0.1) })
return Observable
.from(observables)
.merge()
.asDriver(onErrorJustReturn: UIViewController.ViewState.unknown)
.startWith(UIViewController.ViewState.unknown)
.distinctUntilChanged()
}
}
I'm beginning with MVVM in order to well separate logic code from the view. But I have some concern about where to put the progressHUD related code when tapping a button that makes a request.
Before, I used to do that:
//Before
#IBAction func startRequestTapped() {
SVProgressHUD.show()
self.apiClient.requestObservable().subscribe(onError: { (error) in
SVProgressHUD.hide()
}, onCompleted: {
SVProgressHUD.hide()
})
}
But when I use mvvm, I do like that:
//In the viewModel
public var validateButtonDidTap = PublishSubject<Void>()
init() {
validateButtonDidTap.flatMap { (_)
return self.apiClient.requestObservable()
}
}
// In the viewController
viewDidLoad() {
let tap = self.validateButton.rx.tap
tap.bindTo(self.viewModel.validateButtonDidTap)
}
And amongst that, I don't know where to put the the ProgressHUD hide or show.
Mark answer is right, but I am going to guide you step by step.
Let's supose you're going to try signing in.
Copy ActivityIndicator.swift file in your project.
In the viewModel:
//MARK: - Properties
/// The http client
private let apiClient: YourApiClient
/// Clousure when button is tapped
var didTappedButton: () -> Void = {}
/// The user
var user: Observable<User>
/// Is signing process in progress
let signingIn: Observable<Bool> = ActivityIndicator().asObservable()
//MARK: - Initialization
init(client: YourApiClient) {
self.client = client
self.didTappedButton = { [weak self] in
self.user = self.apiClient
.yourSignInRequest()
.trackActivity(self.signingIn)
.observeOn(MainScheduler.instance)
}
}
Create an extension of SVProgressHUD: (I don't know SVProgressHUD library, but it would be something like this. Please fix it if needed)
extension Reactive where Base: SVProgressHUD {
/// Bindable sink for `show()`, `hide()` methods.
public static var isAnimating: UIBindingObserver<Base, Bool> {
return UIBindingObserver(UIElement: self.base) { progressHUD, isVisible in
if isVisible {
progressHUD.show() // or other show methods
} else {
progressHUD.dismiss() // or other hide methods
}
}
}
}
In your viewController:
#IBAction func startRequestTapped() {
viewModel.didTappedButton()
}
override func viewDidLoad() {
// ...
viewModel.signingIn
.bindTo(SVProgressHUD.rx.isAnimating)
.addDisposableTo(disposeBag)
}
Accepted answer updated to Swift 4, RxSwift 4.0.0 and SVProgressHUD 2.2.2:
3- Extension:
extension Reactive where Base: SVProgressHUD {
public static var isAnimating: Binder<Bool> {
return Binder(UIApplication.shared) {progressHUD, isVisible in
if isVisible {
SVProgressHUD.show()
} else {
SVProgressHUD.dismiss()
}
}
}
}
4- Controller:
viewModel.signingIn.asObservable().bind(to: SVProgressHUD.rx.isAnimating).disposed(by: disposeBag)
You could try using an ActivityIndicator.
See the example here:
https://github.com/RxSwiftCommunity/RxSwiftUtilities
I have a view controller and a class for doing the bits to call the services and get the data from server.
The ViewController code is below,
class ViewController : UIViewController
{
override func viewDidLoad() {
let parser = Parser()
parser.connectServer("abc URL" , ..... <gotDataFromServer> ..... )
}
func gotDataFromServer(response:String)
{
...... Do our things here .......
}
}
and the parser code is below,
class Parser
{
func connectServer(apiURL:String,...<call back function name>...)
{
let manager = RequestOperationManager.sharedManager()
manager.GET(apiURL ,
parameters: nil,
success: { (operation,responseObject) ->Void in
.....<Call back the function which is passed in parameter> ....
},
failure: { (operation , error) in
print ("error occurred")
})
}
}
Now in the above sample code i want to pass call back function "gotDataFromServer" as a parameter and when the inner function get the response from the server then i want to call this function back.
Can anyone please help.
You can use delegates to achieve that. Try out following code
class ViewController : UIViewController, DataDelegate
{
override func viewDidLoad() {
let parser = Parser()
parser.delegate = self
parser.connectServer("abc URL" , ..... <gotDataFromServer> ..... )
}
func gotDataFromServer(response:String)
{
...... Do our things here .......
}
}
And add protocol in parser as follows
protocol DataDelegate {
func gotDataFromServer(response:String)
}
class Parser
{
var delegate : DataDelegate!
func connectServer(apiURL:String,...<call back function name>...)
{
let manager = RequestOperationManager.sharedManager()
manager.GET(apiURL ,
parameters: nil,
success: { (operation,responseObject) ->Void in
delegate.gotDataFromServer("") //parameter is your data
},
failure: { (operation , error) in
print ("error occurred")
})
}
}
Here's an example how you can do it using closure
class Parser {
func connectServer(apiURL: String, completion: String -> Void) {
// ... make call, get data
// share the results via completion closure
completion("data")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
let parser = Parser()
// Option #1
parser.connectServer("mybackend.com/connect") {
print("received data \($0)")
}
// Option #2 is the same as Option #1 but a bit longer
parser.connectServer("mybackend.com/connect") { (data) -> Void in
print("received data \(data)")
}
// Option #3 - Or if you have a separate funciton
// be careful with retain cycle
parser.connectServer("mybackend.com/connect", completion: gotDataFromServer)
}
func gotDataFromServer(response:String) { }
}
I have this custom implementation of Alamofire:
protocol HTTPProtocol: class {
typealias RequestType
typealias RespondType
func doRequest(requestData: RequestType) -> Self
func completionHandler(block:(Result<RespondType, NSError>) -> Void) -> Self
}
//example of a request:
locationInfo
//Make a request
.doRequest(HTTPLocationInfo.RequestType(coordinate: $0))
//Call back when request finished
.completionHandler { result in
switch result {
case .Success(let info): self.locationInfoRequestSuccess(info)
case .Failure(let error): self.locationInfoRequestFailed(error)
}
}
I want to apply MVVM and RxSwift into my project. However, I can't find a proper way to do this.
What I want to achieve is a ViewModel and a ViewController that can do these things:
class ViewController {
func googleMapDelegate(mapMoveToCoordinate: CLLocationCoordinate2D) {
// Step 1: set new value on `viewModel.newCoordinate` and make a request
}
func handleViewModelCallBack(resultParam: ...*something*) {
// Step 3: subscribeOn `viewModel.locationInfoResult` and do things.
}
}
class ViewModel {
//Result if a wrapper object of Alamofire.
typealias LocationInfoResult = (Result<LocationInfo.Respond, NSError>) -> Void
let newCoordinate = Variable<CLLocationCoordinate2D>(kInvalidCoordinate)
let locationInfoResult: Observable<LocationInfoResult>
init() {
// Step 2: on newCoordinate change, from step 1, request Location Info
// I could not find a solution at this step
// how to make a `completionHandler` set its result on `locationInfoResult`
}
}
Any help is deeply appreciated. Thank you.
You can use RxAlamofire as #Gus said in the comment. But if you are using any library that doesn't support Rx extensions by default you may need to do the conversion by hand.
So for the above code snippet, you can create an observable from the callback handler you had implemented
func getResultsObservable() -> Observable<Result> {
return Observable.create{ (observer) -> Disposable in
locationInfo
//Make a request
.doRequest( .... )
//Call back when request finished
.completionHandler { result in
switch result {
case .Success(let info): observer.on(Event.Next(info))
case .Failure(let error): observer.on(Event.Error(NetworkError()))
}
}
return Disposables.create {
// You can do some cleaning here
}
}
}
Callback handlers are implementation to observer pattern, so mapping it to a custom Observable is a straight forward operation.
A good practice is to cancel the network request in case of disposing, for example this is a complete disposable Post request:
return Observable<Result>.create { (observer) -> Disposable in
let requestReference = Alamofire.request("request url",
method: .post,
parameters: ["par1" : val1, "par2" : val2])
.validate()
.responseJSON { (response) in
switch response.result{
case .success:
observer.onNext(response.map{...})
observer.onCompleted()
case .failure:
observer.onError(NetworkError(message: response.error!.localizedDescription))
}
}
return Disposables.create(with: {
requestReference.cancel()
})
Note: before swift 3 Disposables.create() is replaced with NopDisposable.instance
It doesn't seem like you need to subscribe to newCoordinate so I would just make that a request func.
Then, using the info you get back from Alamofire, just set the value on the locationInfoResult and you will get the new result in the ViewController
class ViewController: UIViewController {
func viewDidLoad() {
super.viewDidLoad()
//subscribe to info changes
viewModel.locationInfoResult
.subscribeNext { info in
//do something with info...
}
}
func googleMapDelegate(mapMoveToCoordinate: CLLocationCoordinate2D) {
viewModel.requestLocationInfo(mapMoveToCoordinate)
}
}
class ViewModel {
let locationInfoResult: Variable<LocationInfoResult?>(nil)
init() {
}
func requestLocationInfo(location: CLLocationCoordinate2D) {
//do Alamofire stuff to get info
//update with the result
locationInfoResult.value = //value from Alamofire
}
}