📜 ⬆️ ⬇️

RxSwift in action - we write reactive application

According to the latest trends, the FRP is gaining momentum and is not going to stop. Not so long ago, I was faced with a project dedicated to FRP - ReactiveX , and its implementation for Swift - RxSwift . On Habré already there was a small article which will be useful for initial understanding of RxSwift. I would like to develop this topic, so if you are interested - welcome under the cat!

Down and Out trouble started


And indeed it is. The most difficult thing I had to deal with was a completely different construction of the program code. With my experience of imperative programming it was hard to rebuild in a new way. But her instinct told me that it was worth finding out; It took me 2 weeks of panic to get to the core of ReactiveX and I do not regret the time spent. Therefore, I would immediately like to warn you - the article requires an understanding of the terms ReactiveX, such as Observable, Subscriber, etc.

So, let's begin. We will write a simple reader of our wall with Facebook. For this we need RxSwift , ObjectMapper for data mapping, Facebook iOS SDK and MBProgressHUD for load indication. Create a project in XCode, connect the above libraries to it (I use CocoaPods ), set up a bunch with Facebook according to the instructions and go to coding.
')

Login screen


We will not invent a bicycle, we will simply place a ready-made button from Facebook - FBSDKLoginButton in the center of the screen:

let loginButton = FBSDKLoginButton() loginButton.center = self.view.center loginButton.readPermissions = ["user_posts"] loginButton.delegate = self 

Do not forget to add the FBSDKLoginButtonDelegate delegate for the login button, and also implement the delegate methods:

 // MARK: Facebook Delegate Methods func loginButton(loginButton: FBSDKLoginButton!, didCompleteWithResult result: FBSDKLoginManagerLoginResult!, error: NSError!) { if ((error) != nil) { // Process error let alert = UIAlertController(title: "", message: error.localizedDescription, preferredStyle: .Alert) alert.addAction(UIAlertAction(title: "OK", style: .Cancel, handler: nil)) self.presentViewController(alert, animated: true, completion: nil) } else if result.isCancelled { let alert = UIAlertController(title: "", message: "Result is cancelled", preferredStyle: .Alert) alert.addAction(UIAlertAction(title: "OK", style: .Cancel, handler: nil)) self.presentViewController(alert, animated: true, completion: nil) } else { let storyboard = UIStoryboard(name: "Main", bundle: nil) let vc = storyboard.instantiateViewControllerWithIdentifier("navController") as! UINavigationController self.presentViewController(vc, animated: true, completion: nil) } } func loginButtonDidLogOut(loginButton: FBSDKLoginButton!) { print("User Logged Out") } 

Everything is simple - if the login error or the user presses the “Cancel” button on the Facebook authorization screen, we display a message about this in the form of an alert, and if everything is OK, send it to the next screen with a list of news. I did not touch the logout function. As we see, so far everything is rather trivial and there is no question of any reactivity. Here, too, is a rather subtle point - do not stick reactivity into all the gaps to remember about the KISS principle.

News screen


Let's write the function of getting the list of news from the Facebook wall, the return type of which will be Observable:

 func getFeeds() -> Observable<GetFeedsResponse> { return Observable.create { observer in let parameters = ["fields": ""] let friendsRequest = FBSDKGraphRequest.init(graphPath: "me/feed", parameters: parameters, HTTPMethod: "GET") friendsRequest.startWithCompletionHandler { (connection, result, error) -> Void in if error != nil { observer.on(.Error(error!)) } else { let getFeedsResponse = Mapper<GetFeedsResponse>().map(result)! observer.on(.Next(getFeedsResponse)) observer.on(.Completed) } } return AnonymousDisposable { } } } 

What happens in this code? A FBSDKGraphRequest network request for receiving “me / feed” news is formed, after which we give the command to execute the request and monitor the status in the completition block; in case of an error, we transfer it to Observable; in case of success, we transmit it to Observable.

Note: I am passing a variable to FBSDKGraphRequest
 let parameters = ["fields": ""] 
with an empty parameter set. This is necessary so that Facebook does not cry, displaying warnings in the logs that the fields in the parameters is mandatory. In principle, everything works without this parameter, but I sleep so calmly.

A little away from the process of writing an application and talk about data mapping. I solve this problem with the help of ObjectMapper, it allows you to do it rather quickly and simply:

 class GetFeedsResponse: Mappable { var data = [Feed]() var paging: Paging! required init?(_ map: Map){ } // Mappable func mapping(map: Map) { data <- map["data"] paging <- map["paging"] } } class Feed: Mappable { var createdTime: String! var id: String! var story: String? var message: String? required init?(_ map: Map){ } // Mappable func mapping(map: Map) { createdTime <- map["created_time"] id <- map["id"] story <- map["story"] message <- map["message"] } } class Paging: Mappable { var next: String! var previous: String! required init?(_ map: Map){ } // Mappable func mapping(map: Map) { next <- map["next"] previous <- map["previous"] } } 

I suggest to immediately write a network request for detailed information about the news:

 func getFeedInfo(feedId: String) -> Observable<GetFeedInfoResponse> { return Observable.create { observer in let parameters = ["fields" : "id,admin_creator,application,call_to_action,caption,created_time,description,feed_targeting,from,icon,is_hidden,is_published,link,message,message_tags,name,object_id,picture,place,privacy,properties,shares,source,status_type,story,story_tags,targeting,to,type,updated_time,with_tags"] let friendsRequest = FBSDKGraphRequest.init(graphPath: "" + feedId, parameters: parameters, HTTPMethod: "GET") friendsRequest.startWithCompletionHandler { (connection, result, error) -> Void in if error != nil { observer.on(.Error(error!)) } else { print(result) let getFeedInfoResponse = Mapper<GetFeedInfoResponse>().map(result)! observer.on(.Next(getFeedInfoResponse)) observer.on(.Completed) } } return AnonymousDisposable { } } } 

As we see, I passed a bunch of fields in the parameters variable — these are all fields that were in the documentation. I will not disassemble them all, only a part. Here is the data mapping:

 class GetFeedInfoResponse: Mappable { var createdTime: String! var from: IdName! var id: String! var isHidden: Bool! var isPublished: Bool! var message: String? var name: String? var statusType: String? var story: String? var to = [IdName]() var type: String! var updatedTime: String! required init?(_ map: Map){ } // Mappable func mapping(map: Map) { createdTime <- map["created_time"] from <- map["from"] id <- map["from"] isHidden <- map["is_hidden"] isPublished <- map["is_published"] message <- map["message"] name <- map["name"] statusType <- map["status_type"] story <- map["story"] // It necessary that Facebook API have a bad structure // buffer%varname% is a temporary variable var bufferTo = NSDictionary() bufferTo <- map["to"] if let bufferData = bufferTo["data"] as? NSArray { for bufferDataElement in bufferData { let bufferToElement = Mapper<IdName>().map(bufferDataElement) to.append(bufferToElement!) } } type <- map["type"] updatedTime <- map["updated_time"] } } class IdName: Mappable { var id: String! var name: String! required init?(_ map: Map){ } // Mappable func mapping(map: Map) { id <- map["id"] name <- map["name"] } } 

As you can see, there was also some tar in the ointment. For example, when parsing the “to” json object, which contains one “data” field, which in turn is a json array, I had to dodge as follows:

 var bufferTo = NSDictionary() bufferTo <- map["to"] if let bufferData = bufferTo["data"] as? NSArray { for bufferDataElement in bufferData { let bufferToElement = Mapper<IdName>().map(bufferDataElement) to.append(bufferToElement!) } } 

In principle, I could create a file with a single “data” field and quietly un-mars the object there, but the idea to create a new file for mapping one field seemed silly to me. If any of you have a more elegant solution to this problem - I will drink for joy I will be glad to learn about it.

Let's return to our sheep. We contacted Facebook, got a list of news in the form of Observable, now we need to display this data. For this purpose I will use the MVVM template. My free interpretation of this template with regard to the use of ReactiveX sounds like this: in the ViewModel there is an Observable that generates events, and the View subscribes to these events and processes them. That is, the ViewModel does not depend on who subscribes to it - if there are subscribers, the Observable generates data, if there are no subscribers, the Observable does not generate anything (this statement is true for “cold” Observable, because “hot” Observable always generate data). Let's write the ViewModel for the news screen:

 class FeedsViewModel { let feedsObservable: Observable<[Feed]> let clickObservable: Observable<GetFeedInfoResponse> // If some process in progress let indicator: Observable<Bool> init(input: ( UITableView ), dependency: ( API: APIManager, wireframe: Wireframe ) ) { let API = dependency.API let wireframe = dependency.wireframe let indicator = ViewIndicator() self.indicator = indicator.asObservable() feedsObservable = API.getFeeds() .trackView(indicator) .map { getFeedResponse in return getFeedResponse.data } .catchError { error in return wireframe.promptFor(String(error), cancelAction: "OK", actions: []) .map { _ in return error } .flatMap { error in return Observable.error(error) } } .shareReplay(1) clickObservable = input .rx_modelSelected(Feed) .flatMap { feed in return API.getFeedInfo(feed.id).trackView(indicator) } .catchError { error in return wireframe.promptFor(String(error), cancelAction: "OK", actions: []) .map { _ in return error } .flatMap { error in return Observable.error(error) } } // If error when click uitableview - set retry() if you want to click cell again .retry() .shareReplay(1) } } 

Let's break the code down. In the input field we transfer the table from View, in the dependency field - the API class and Wireframe. There are 3 variables in the class: feedsObservable returns Observable with a news list, clickObservable is a handler for clicking on a table cell, and indicator is a boolean variable that determines whether the load indicator should be displayed on the screen. There are 2 interesting classes in the code at once - Wireframe and ViewIndicator, let's dwell on them in more detail. Wireframe is nothing more than a “reactive” implementation of alert. I took this implementation from the examples in the RxSwift repository. ViewIndicator is a tracking, and the trackView function from this class is executed until at least one sequence is executed in the chain, so it is convenient to use trackView to show the loading indicator. I will not give the code in this article - you can find it in the project repository, the link is at the bottom of the article.

Let's touch the logic of our Observable. The first one - feedsObservable - gets a response from Facebook, then in the map block the list of news is retrieved and returned, then there is an error handling, well, and a trackView to display the download. The second, clickObservable, monitors clicking on a cell in a table, after which it triggers a network request to receive detailed information about the news. Super, with the model finished, go directly to the View.

I will immediately provide the View code, after which we will analyze it:

 class FeedsViewController: UIViewController, UITableViewDelegate { @IBOutlet weak var feedsTableView: UITableView! var disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() let viewModel = FeedsViewModel( input:feedsTableView, dependency: ( API: APIManager.sharedAPI, wireframe: DefaultWireframe.sharedInstance ) ) let progress = MBProgressHUD() progress.mode = MBProgressHUDMode.Indeterminate progress.labelText = " ..." progress.dimBackground = true viewModel.indicator .bindTo(progress.rx_mbprogresshud_animating) .addDisposableTo(self.disposeBag) feedsTableView.rx_setDelegate(self) viewModel.feedsObservable .bindTo(feedsTableView.rx_itemsWithCellFactory) { tableView, row, feed in let cell = tableView.dequeueReusableCellWithIdentifier("feedTableViewCell") as! FeedTableViewCell cell.feedCreatedTime.text = NSDate().convertFacebookTime(feed.createdTime) if let story = feed.story { cell.feedInfo.text = story } else if let message = feed.message { cell.feedInfo.text = message } cell.layoutMargins = UIEdgeInsetsZero return cell } .addDisposableTo(disposeBag) viewModel.clickObservable .subscribeNext { feed in let storyboard = UIStoryboard(name: "Main", bundle: nil) let feedInfoViewController = storyboard.instantiateViewControllerWithIdentifier("feedInfoViewController") as! FeedInfoViewController feedInfoViewController.feedInfo = feed self.navigationController?.pushViewController(feedInfoViewController, animated: true) } .addDisposableTo(disposeBag) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } // Deselect tableView row after click func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { tableView.deselectRowAtIndexPath(indexPath, animated: true) } } 

First of all, create a viewModel. Next you need to create a download indicator and somehow link it with the ViewIndicator. To do this, we need to write an Extension for MBProgressHUD:

 extension MBProgressHUD { /** Bindable sink for MBProgressHUD show/hide methods. */ public var rx_mbprogresshud_animating: AnyObserver<Bool> { return AnyObserver {event in MainScheduler.ensureExecutingOnScheduler() switch (event) { case .Next(let value): if value { let loadingNotification = MBProgressHUD.showHUDAddedTo(UIApplication.sharedApplication().keyWindow?.subviews.last, animated: true) loadingNotification.mode = self.mode loadingNotification.labelText = self.labelText loadingNotification.dimBackground = self.dimBackground } else { MBProgressHUD.hideHUDForView(UIApplication.sharedApplication().keyWindow?.subviews.last, animated: true) } case .Error(let error): let error = "Binding error to UI: \(error)" #if DEBUG rxFatalError(error) #else print(error) #endif case .Completed: break } } } } 

If any value is supplied to MBProgressHUD, then we display the indicator. If no value is given - hide it. Now we need to set up a binding between our MBProgressHUD and ViewIndicator. This is done like this:

 viewModel.indicator .bindTo(progress.rx_mbprogresshud_animating) .addDisposableTo(self.disposeBag) 

Then we set the binding between UITableView and data acquisition, as well as between clicking on the UITableView element and switching to a new screen. I also made a detailed information screen about the post without “reactivity”:

 override func viewDidLoad() { super.viewDidLoad() var feedDetail = "From: " + feedInfo.from.name if feedInfo.to.count > 0 { feedDetail += "\nTo: " for to in feedInfo.to { feedDetail += to.name + "\n" } } if let date = feedInfo.createdTime { feedDetail += "\nDate: " + NSDate().convertFacebookTime(date) } if let story = feedInfo.story { feedDetail += "\nStory: " + story } if let message = feedInfo.message { feedDetail += "\nMessage: " + message } if let name = feedInfo.name { feedDetail += "\nName: " + name } if let type = feedInfo.type { feedDetail += "\nType: " + type } if let updatedTime = feedInfo.updatedTime { feedDetail += "\nupdatedTime: " + NSDate().convertFacebookTime(updatedTime) } feedTextView.text = feedDetail self.navigationController?.navigationBar.tintColor = UIColor.whiteColor() } 

I have it all. The source code of the project on Github can be downloaded from this link . If you liked my article, then I’ll burst of happiness with pleasure and continue to write about RxSwift and try to uncover its potential in more trivial tasks.

Source: https://habr.com/ru/post/276971/


All Articles