Why use RxSwift

RxSwift is a Programming idea, not a language, and the hardest part of learning it is thinking in Reactive Programming: thinking of all events as a stream. It is intended to facilitate serialization of data/event streams and asynchronous tasks

If each action in our program is an event: For example, a TextFiled text change, a button click, or the end of a network request, etc., each event source can be regarded as a pipeline, that is, a Sequence, such as a TextFiled. When we change the text inside, TextFiled will continuously send events. We only need to listen to this Sequence and process each event that flows out. Similarly UIButton is a Sequence that emits an event every time it is clicked


Next, we use thinking in Reactive Programming to realize an app that displays stock information in real time step by step.
App requirements: 1. Preparation: 2. Preparation for network request 3Copy the code

1. Preparation

We need to create a Stock project and use Podfile to manage third-party libraries. Basic instructions on how to use RxSwift can be found at RxSwift on Github.

The Podfile configuration is as follows:

platform :ios, '11.0'
use_frameworks!
target 'stock' do
    pod 'RxSwift'.'~ > 4.0'
    pod 'RxCocoa'.'~ > 4.0' # Combine UI library with RX
    pod 'FMDB'.'~ > 2.6.2'  # sqlite database
    pod 'SwiftyJSON'  # json processing
    pod 'Moya/RxSwift'  # network request
    pod 'ObjectMapper'.'~ > 3.1' Model # Json
    pod 'MJRefresh', :inhibit_warnings => true # dropdown refresh
    pod 'RxDataSources'.'~ > 3.0'  Help us use the data source method of tableView elegantly
    pod 'NSObject+Rx'     # Provide us with rx_disposeBag
    pod 'Then'            # Provide syntax sugar for quick initialization
    pod 'Reusable'        # Help us use custom cells and views elegantly, no more Optional
end
 
 # debug use of rx
 post_install do |installer|
     installer.pods_project.targets.each do |target|
         if target.name == 'RxSwift'
             target.build_configurations.each do |config|
                 if config.name == 'Debug'
                     config.build_settings['OTHER_SWIFT_FLAGS'] | | = ['-D'.'TRACE_RESOURCES']
                 end
             end
         end
     end
 end


Copy the code

2. Network request

Observable Create Subscribe disposed moyaCopy the code

thinking in Reactive Programming

Network request is a classic asynchronous request. Moya is a network request implemented based on Alamofire. Its network request is also a subscriptible stream

    
    / * * here we use moya to do network requests, the specific way of using the moya see [moya] (https://github.com/Moya/Moya), we put the moya API the results returned from the network as a stream, Use rxSwift's Create to turn the returned result into a subscriptable stream */
    // The method of create can be seen in the manual:
    create(_ subscribe: @escaping (RxSwift.AnyObserver<Self.E- > >)Disposable)
    // The main steps are:
    Observable.create { observer in
        observer.onNext(10)
        observer.onCompleted()
        return  Disposables.create()
    }
        
    // Next we implement the method of getting stock information from the API, creating a subscriptable data model
    let netToolProvider = MoyaProvider<StockApi> ()func searchFromApi(repositoryName: String) -> Observable"[SearchModel] > {return Observable.create { observer in
            netToolProvider.rx.request(.SearchStocks(code:repositoryName))
                .mapJSON() //Moya RxSwift extends the method to parse the returned data into JSON format
                .subscribe( // Subscribe to the result returned
                    onSuccess:{json in
                        let info = self.parseSearchResponse(response: json as AnyObject) // Parse the return into a listModel object
                        observer.on(.next(info)) // Send next content is listModel
                        observer.on(.completed)  // The message was sent successfully
                },
                    onError:{error in
                        observer.on(.error(error)) // Error sending
                }).disposed(by: self.bag)// Automatic memory processing mechanism
            
            return  Disposables.create()
        }
    }
    
    private func parseSearchResponse(response: AnyObject)- > [SearchModel] {
        var info: [SearchModel] = []
        let items = response["stocks"] as! [[String:AnyObject]]
        let _ = items.filter{
                return $0["stock_id"] as! Int! =0 // Remove the delisted ones
            }.map{
                info.append(SearchModel(JSON: $0)! }return info;
    }
    
Copy the code

So let’s leave that method for now, and then we’ll subscribe to this model and display it in uitableview

3. The response input box displays the results to the list

The user enters the stock number and calls the interface to display the search results: RxCocoa, bind, driveCopy the code

Thinking in Reactive Programming:

Think of the input box event as a stream, and the user’s input will continuously emit a stream of events. We only need to listen for the stream of events to respond to each user’s input.

searchFiled.rx.text
    .filter{($0? .lengthOfBytes(using: .utf8))! >0 // The input length is greater than 1
    }
    .throttle(0.5, scheduler: MainScheduler.instance) // Delay execution for 0.5 seconds
    .subscribe{ // Subscribe to this event
        print($0) // Prints user input
}.disposed(by: rx.disposeBag)
Copy the code

After getting user input, we also need to get the search results from the API based on user input in real time, which we have just done. All you need to do now is tie the user input to the events searched by the API. Here RxSwift provides the bind method.

Bind: The intent of this method is to bind an observer to a specified observer. Any event elements emitted in the stream of the observed events will be received by the observer. Example: textField.rx_text.bindto (label.rx_text).addDisposableto (disposeBag)Copy the code

SearchFiled and resultTableView are bound together. When the user enters the query string, the query result is obtained by the interface and displayed in the UITableView:

searchFiled.rx.text
    .filter{($0? .lengthOfBytes(using: .utf8))! >0 // The length is greater than 1
    }
    .throttle(0.5, scheduler: MainScheduler.instance) // Delay execution for 0.5 seconds
    .flatMap{
        ListViewModel().searchFromApi(repositoryName:String(describing: $0!). )// Return the SearchModel that can be subscribed to
    }
    // searchFiled and resultTableView are bound together
    .bind(to: self.resultTableView.rx.items(cellIdentifier: "SearchTableViewCell", cellType: SearchTableViewCell.self)) {(_, model:SearchModel, cell:SearchTableViewCell) in
        cell.nameLabel.text = model.name
        cell.codeLabel.text = model.code
    }
.disposed(by: rx.disposeBag)

Copy the code

If you use bind, however, there are several problems: 1. If the searchFromApi fails (connection failure, argument error, etc.), the error will be lost and the UI will no longer process and respond to any results

2. If the result returned by the searchFromApi is not in the main thread, then the UI is not bound in the child thread and an unknown error occurs

3. If the result is bound to two UIs, which means that each UI makes one network request, that is, two network requests, then we can change it a little

We can modify it like this:

    searchFiled.rx.text
        .filter{($0? .lengthOfBytes(using: .utf8))! >0 // The length is greater than 1
        }
        .throttle(0.5, scheduler: MainScheduler.instance) // Delay execution for 0.5 seconds
        .flatMap{
            ListViewModel().searchFromApi(repositoryName:String(describing: $0!). )// Return the SearchModel that can be subscribed to
                .observeOn(MainScheduler.instance) // Switch the return result to the main thread
                .catchErrorJustReturn([])  // If there is a problem, the error result will be handled
        }
        .share(replay: 1)
        // searchFiled and resultTableView are bound together
        .bind(to: self.resultTableView.rx.items(cellIdentifier: "SearchTableViewCell", cellType: SearchTableViewCell.self)) {(_, model:SearchModel, cell:SearchTableViewCell) in
            cell.nameLabel.text = model.name
            cell.codeLabel.text = model.code
        }
    .disposed(by: rx.disposeBag)

Copy the code

The difference between subscribeOn and observeOn: which thread is the starting point for subscribeOn(), and which thread is the subsequent work that is set.

Our design here is not perfect, rXSwift provides drive methods, but it is specifically designed for UI binding

The drive method can only be used in the Driver sequence, which has the following features: 1. The Driver sequence does not allow errors, and 2.Copy the code

Let’s take a look at the implementation of Drive:

searchFiled.rx.text
    .filter{($0? .lengthOfBytes(using: .utf8))! >0 // The length is greater than 1
    }
    .throttle(0.5, scheduler: MainScheduler.instance) // Delay execution for 0.5 seconds
    .flatMap{
        ListViewModel().searchFromApi(repositoryName:String(describing: $0!). ) } .asDriver(onErrorJustReturn: []) .drive(resultTableView.rx.items(cellIdentifier:"SearchTableViewCell", cellType: SearchTableViewCell.self)) {
        (tableView, element, cell) in
        cell.nameLabel.text = element.name
        cell.codeLabel.text = element.code
    }
    .disposed(by: rx.disposeBag)

Copy the code
So you can replace BindTo with Driver if: 1. 2. Listen on the main thread; 3. Shared event flow;Copy the code

This section stops here, but the code can be found here: rxStock sample download reference

The next section covers usage scenarios for RxCocoa and more rxSwift methods used to implement the stock app.

【RxSwift practice Series 3/3】 Thinking in RX-UitableView

Previous section review RxSwift Practices Series 1/3 why use RxSwift