RxSwift (二)序列核心邏輯分析react
RxSwift (三)Observable的建立,訂閱,銷燬編程
RxSwift(四)高階函數swift
RxSwift(五)(Rxswift對比swift,oc用法)markdown
RxSwift (十) 基礎使用篇 1- 序列,訂閱,銷燬async
RxSwift學習之十二 (基礎使用篇 3- UI控件擴展)ide
RxSwift
是一個用於與Swift
語言交互的框架,但它只是基礎,並不能用來進行用戶交互、網絡請求等。 而RxCocoa
是讓Cocoa APIs
更容易使用響應式編程的一個框架。RxCocoa
可以讓咱們方便地進行響應式網絡請求、響應式的用戶交互、綁定數據模型到 UI 控件等等。並且大多數的 UIKit 控件都有響應式擴展,它們都是經過 rx 屬性進行使用。函數
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() override func viewDidLoad() { //建立文本標籤 let label = UILabel(frame:CGRect(x:20, y:40, width:300, height:100)) self.view.addSubview(label) //建立一個計時器(每0.1秒發送一個索引數) let timer = Observable<Int>.interval(0.1, scheduler: MainScheduler.instance) //將已過去的時間格式化成想要的字符串,並綁定到label上 timer.map{ String(format: "%0.2d:%0.2d.%0.1d", arguments: [($0 / 600) % 600, ($0 % 600 ) / 10, $0 % 10]) } .bind(to: label.rx.text) .disposed(by: disposeBag) } } 複製代碼
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() override func viewDidLoad() { //建立文本標籤 let label = UILabel(frame:CGRect(x:20, y:40, width:300, height:100)) self.view.addSubview(label) //建立一個計時器(每0.1秒發送一個索引數) let timer = Observable<Int>.interval(0.1, scheduler: MainScheduler.instance) //將已過去的時間格式化成想要的字符串,並綁定到label上 timer.map(formatTimeInterval) .bind(to: label.rx.attributedText) .disposed(by: disposeBag) } //將數字轉成對應的富文本 func formatTimeInterval(ms: NSInteger) -> NSMutableAttributedString { let string = String(format: "%0.2d:%0.2d.%0.1d", arguments: [(ms / 600) % 600, (ms % 600 ) / 10, ms % 10]) //富文本設置 let attributeString = NSMutableAttributedString(string: string) //從文本0開始6個字符字體HelveticaNeue-Bold,16號 attributeString.addAttribute(NSAttributedStringKey.font, value: UIFont(name: "HelveticaNeue-Bold", size: 16)!, range: NSMakeRange(0, 5)) //設置字體顏色 attributeString.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.white, range: NSMakeRange(0, 5)) //設置文字背景顏色 attributeString.addAttribute(NSAttributedStringKey.backgroundColor, value: UIColor.orange, range: NSMakeRange(0, 5)) return attributeString } } 複製代碼
注意:.orEmpty 能夠將 String? 類型的 ControlProperty 轉成 String,免得咱們再去解包。
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() override func viewDidLoad() { //建立文本輸入框 let textField = UITextField(frame: CGRect(x:10, y:80, width:200, height:30)) textField.borderStyle = UITextBorderStyle.roundedRect self.view.addSubview(textField) //當文本框內容改變時,將內容輸出到控制檯上 textField.rx.text.orEmpty.asObservable() .subscribe(onNext: { print("您輸入的是:\($0)") }) .disposed(by: disposeBag) } } 複製代碼
//當文本框內容改變時,將內容輸出到控制檯上 textField.rx.text.orEmpty.changed .subscribe(onNext: { print("您輸入的是:\($0)") }) .disposed(by: disposeBag) 複製代碼
Throttling 的做用: Throttling 是 RxSwift 的一個特性。由於有時當一些東西改變時,一般會作大量的邏輯操做。而使用 Throttling 特性,不會產生大量的邏輯操做,而是以一個小的合理的幅度去執行。好比作一些實時搜索功能時,這個特性頗有用。
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() override func viewDidLoad() { //建立文本輸入框 let inputField = UITextField(frame: CGRect(x:10, y:80, width:200, height:30)) inputField.borderStyle = UITextBorderStyle.roundedRect self.view.addSubview(inputField) //建立文本輸出框 let outputField = UITextField(frame: CGRect(x:10, y:150, width:200, height:30)) outputField.borderStyle = UITextBorderStyle.roundedRect self.view.addSubview(outputField) //建立文本標籤 let label = UILabel(frame:CGRect(x:20, y:190, width:300, height:30)) self.view.addSubview(label) //建立按鈕 let button:UIButton = UIButton(type:.system) button.frame = CGRect(x:20, y:230, width:40, height:30) button.setTitle("提交", for:.normal) self.view.addSubview(button) //當文本框內容改變 let input = inputField.rx.text.orEmpty.asDriver() // 將普通序列轉換爲 Driver .throttle(0.3) //在主線程中操做,0.3秒內值若屢次改變,取最後一次 //內容綁定到另外一個輸入框中 input.drive(outputField.rx.text) .disposed(by: disposeBag) //內容綁定到文本標籤中 input.map{ "當前字數:\($0.count)" } .drive(label.rx.text) .disposed(by: disposeBag) //根據內容字數決定按鈕是否可用 input.map{ $0.count > 5 } .drive(button.rx.isEnabled) .disposed(by: disposeBag) } } 複製代碼
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() @IBOutlet weak var textField1: UITextField! @IBOutlet weak var textField2: UITextField! @IBOutlet weak var label: UILabel! override func viewDidLoad() { Observable.combineLatest(textField1.rx.text.orEmpty, textField2.rx.text.orEmpty) { textValue1, textValue2 -> String in return "你輸入的號碼是:\(textValue1)-\(textValue2)" } .map { $0 } .bind(to: label.rx.text) .disposed(by: disposeBag) } } 複製代碼
editingDidBegin
:開始編輯(開始輸入內容)editingChanged
:輸入內容發生改變editingDidEnd
:結束編輯editingDidEndOnExit
:按下 return 鍵結束編輯allEditingEvents
:包含前面的全部編輯相關事件
實例2.18.4.1
textField.rx.controlEvent([.editingDidBegin]) //狀態能夠組合 .asObservable() .subscribe(onNext: { _ in print("開始編輯內容!") }).disposed(by: disposeBag) 複製代碼
- 若是當前焦點在用戶名輸入框時,按下 return 鍵時焦點自動轉移到密碼輸入框上。
- 若是當前焦點在密碼輸入框時,按下 return 鍵時自動移除焦點。
實例2.18.4.2
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { //用戶名輸入框 @IBOutlet weak var username: UITextField! //密碼輸入框 @IBOutlet weak var password: UITextField! let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() //在用戶名輸入框中按下 return 鍵 username.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: { [weak self] (_) in self?.password.becomeFirstResponder() }).disposed(by: disposeBag) //在密碼輸入框中按下 return 鍵 password.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: { [weak self] (_) in self?.password.resignFirstResponder() }).disposed(by: disposeBag) } } 複製代碼
didBeginEditing
:開始編輯didEndEditing
:結束編輯didChange
:編輯內容發生改變didChangeSelection
:選中部分發生變化
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() @IBOutlet weak var textView: UITextView! override func viewDidLoad() { //開始編輯響應 textView.rx.didBeginEditing .subscribe(onNext: { print("開始編輯") }) .disposed(by: disposeBag) //結束編輯響應 textView.rx.didEndEditing .subscribe(onNext: { print("結束編輯") }) .disposed(by: disposeBag) //內容發生變化響應 textView.rx.didChange .subscribe(onNext: { print("內容發生改變") }) .disposed(by: disposeBag) //選中部分變化響應 textView.rx.didChangeSelection .subscribe(onNext: { print("選中部分發生變化") }) .disposed(by: disposeBag) } } 複製代碼
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() @IBOutlet weak var button: UIButton! override func viewDidLoad() { //按鈕點擊響應 button.rx.tap .subscribe(onNext: { [weak self] in self?.showMessage("按鈕被點擊") }) .disposed(by: disposeBag) } //顯示消息提示框 func showMessage(_ text: String) { let alertController = UIAlertController(title: text, message: nil, preferredStyle: .alert) let cancelAction = UIAlertAction(title: "肯定", style: .cancel, handler: nil) alertController.addAction(cancelAction) self.present(alertController, animated: true, completion: nil) } } 複製代碼
或者這樣實現點擊事件訂閱
//按鈕點擊響應 button.rx.tap .bind { [weak self] in self?.showMessage("按鈕被點擊") } .disposed(by: disposeBag) 複製代碼
//建立一個計時器(每1秒發送一個索引數) let timer = Observable<Int>.interval(1, scheduler: MainScheduler.instance) //根據索引數拼接最新的標題,並綁定到button上 timer.map{"計數\($0)"} .bind(to: button.rx.title(for: .normal))// rx.title 爲 setTitle(_:for:) 的封裝。 .disposed(by: disposeBag) 複製代碼
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() @IBOutlet weak var button: UIButton! override func viewDidLoad() { //建立一個計時器(每1秒發送一個索引數) let timer = Observable<Int>.interval(1, scheduler: MainScheduler.instance) //將已過去的時間格式化成想要的字符串,並綁定到button上 timer.map(formatTimeInterval) .bind(to: button.rx.attributedTitle()) //rx.attributedTitle 爲 setAttributedTitle(_:controlState:) 的封裝。 .disposed(by: disposeBag) } //將數字轉成對應的富文本 func formatTimeInterval(ms: NSInteger) -> NSMutableAttributedString { let string = String(format: "%0.2d:%0.2d.%0.1d", arguments: [(ms / 600) % 600, (ms % 600 ) / 10, ms % 10]) //富文本設置 let attributeString = NSMutableAttributedString(string: string) //從文本0開始6個字符字體HelveticaNeue-Bold,16號 attributeString.addAttribute(NSAttributedStringKey.font, value: UIFont(name: "HelveticaNeue-Bold", size: 16)!, range: NSMakeRange(0, 5)) //設置字體顏色 attributeString.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.white, range: NSMakeRange(0, 5)) //設置文字背景顏色 attributeString.addAttribute(NSAttributedStringKey.backgroundColor, value: UIColor.orange, range: NSMakeRange(0, 5)) return attributeString } } 複製代碼
//建立一個計時器(每1秒發送一個索引數) let timer = Observable<Int>.interval(1, scheduler: MainScheduler.instance) //根據索引數選擇對應的按鈕圖標,並綁定到button上 timer.map({ let name = $0%2 == 0 ? "back" : "forward" return UIImage(named: name)! }) .bind(to: button.rx.image())// rx.image 爲 setImage(_:for:) 的封裝。 .disposed(by: disposeBag) 複製代碼
//建立一個計時器(每1秒發送一個索引數) let timer = Observable<Int>.interval(1, scheduler: MainScheduler.instance) //根據索引數選擇對應的按鈕背景圖,並綁定到button上 timer.map{ UIImage(named: "\($0%2)")! } .bind(to: button.rx.backgroundImage()) //rx.backgroundImage 爲 setBackgroundImage(_:for:) 的封裝。 .disposed(by: disposeBag) 複製代碼
switch1.rx.isOn
.bind(to: button1.rx.isEnabled)
.disposed(by: disposeBag)
複製代碼
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() @IBOutlet weak var button1: UIButton! @IBOutlet weak var button2: UIButton! @IBOutlet weak var button3: UIButton! override func viewDidLoad() { //默認選中第一個按鈕 button1.isSelected = true //強制解包,避免後面還須要處理可選類型 let buttons = [button1, button2, button3].map { $0! } //建立一個可觀察序列,它能夠發送最後一次點擊的按鈕(也就是咱們須要選中的按鈕) let selectedButton = Observable.from( buttons.map { button in button.rx.tap.map { button } } ).merge() //對於每個按鈕都對selectedButton進行訂閱,根據它是不是當前選中的按鈕綁定isSelected屬性 for button in buttons { selectedButton.map { $0 == button } .bind(to: button.rx.isSelected) .disposed(by: disposeBag) } } } 複製代碼
實現當 switch 開關狀態改變時,輸出當前值。
switch1.rx.isOn.asObservable() .subscribe(onNext: { print("當前開關狀態:\($0)") }) .disposed(by: disposeBag) 複製代碼
switch1.rx.isOn
.bind(to: button1.rx.isEnabled)
.disposed(by: disposeBag)
複製代碼
實現當 UISegmentedControl 選中項改變時,輸出當前選中項索引值。
segmented.rx.selectedSegmentIndex.asObservable() .subscribe(onNext: { print("當前項:\($0)") }) .disposed(by: disposeBag) 複製代碼
當 segmentedControl 選項改變時,imageView 會自動顯示相應的圖片。
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { //分段選擇控件 @IBOutlet weak var segmented: UISegmentedControl! //圖片顯示控件 @IBOutlet weak var imageView: UIImageView! let disposeBag = DisposeBag() override func viewDidLoad() { //建立一個當前須要顯示的圖片的可觀察序列 let showImageObservable: Observable<UIImage> = segmented.rx.selectedSegmentIndex.asObservable().map { let images = ["js.png", "php.png", "react.png"] return UIImage(named: images[$0])! } //把須要顯示的圖片綁定到 imageView 上 showImageObservable.bind(to: imageView.rx.image) .disposed(by: disposeBag) } } 複製代碼
經過開關咱們能夠控制活動指示器是否顯示旋轉
mySwitch.rx.value
.bind(to: activityIndicator.rx.isAnimating)
.disposed(by: disposeBag)
複製代碼
mySwitch.rx.value .bind(to: UIApplication.shared.rx.isNetworkActivityIndicatorVisible) .disposed(by: disposeBag) 複製代碼
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { @IBOutlet weak var slider: UISlider! let disposeBag = DisposeBag() override func viewDidLoad() { slider.rx.value.asObservable() .subscribe(onNext: { print("當前值爲:\($0)") }) .disposed(by: disposeBag) } } 複製代碼
stepper.rx.value.asObservable() .subscribe(onNext: { print("當前值爲:\($0)") }) .disposed(by: disposeBag) 複製代碼
使用滑塊(slider)來控制 stepper 的步長。
slider.rx.value .map{ Double($0) } //因爲slider值爲Float類型,而stepper的stepValue爲Double類型,所以須要轉換 .bind(to: stepper.rx.stepValue) .disposed(by: disposeBag) 複製代碼
ViewModel
裏的某個 Subject
屬性進行雙向綁定:
- 這樣當 ViewModel 裏的值發生改變時,能夠同步反映到控件上。
- 而若是對控件值作修改,ViewModel 那邊值同時也會發生變化。
(1)頁面上方是一個文本輸入框,用於填寫用戶名。它與 VM 裏的 username 屬性作雙向綁定。 (2)下方的文本標籤會根據用戶名顯示對應的用戶信息。(只有 hangge 顯示管理員,其它都是訪客)
(1)首先定義一個 VM,代碼以下:
import RxSwift struct UserViewModel { //用戶名 let username = Variable("guest") //用戶信息 lazy var userinfo = { return self.username.asObservable() .map{ $0 == "hangge" ? "您是管理員" : "您是普通訪客" } .share(replay: 1) }() } 複製代碼
(2)頁面代碼以下(高亮部分爲 textfield 與 VM 的雙向綁定):
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { @IBOutlet weak var textField: UITextField! @IBOutlet weak var label: UILabel! var userVM = UserViewModel() let disposeBag = DisposeBag() override func viewDidLoad() { //將用戶名與textField作雙向綁定 userVM.username.asObservable().bind(to: textField.rx.text).disposed(by: disposeBag) textField.rx.text.orEmpty.bind(to: userVM.username).disposed(by: disposeBag) //將用戶信息綁定到label上 userVM.userinfo.bind(to: label.rx.text).disposed(by: disposeBag) } } 複製代碼
(1)若是常常進行雙向綁定的話,最好仍是自定義一個
operator
方便使用。 (2)好在RxSwift
項目文件夾中已經有個現成的(Operators.swift
),咱們將它複製到咱們項目中便可使用。固然如咱們想本身寫一些其它的雙向綁定 operator 也能夠參考它。
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { @IBOutlet weak var textField: UITextField! @IBOutlet weak var label: UILabel! var userVM = UserViewModel() let disposeBag = DisposeBag() override func viewDidLoad() { //將用戶名與textField作雙向綁定 _ = self.textField.rx.textInput <-> self.userVM.username //將用戶信息綁定到label上 userVM.userinfo.bind(to: label.rx.text).disposed(by: disposeBag) } } 複製代碼
(1)第一種響應回調的寫法
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() //添加一個上滑手勢 let swipe = UISwipeGestureRecognizer() swipe.direction = .up self.view.addGestureRecognizer(swipe) //手勢響應 swipe.rx.event .subscribe(onNext: { [weak self] recognizer in //這個點是滑動的起點 let point = recognizer.location(in: recognizer.view) self?.showAlert(title: "向上划動", message: "\(point.x) \(point.y)") }) .disposed(by: disposeBag) } //顯示消息提示框 func showAlert(title: String, message: String) { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "肯定", style: .cancel)) self.present(alert, animated: true) } } 複製代碼
(2)第二種響應回調的寫法
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() //添加一個上滑手勢 let swipe = UISwipeGestureRecognizer() swipe.direction = .up self.view.addGestureRecognizer(swipe) //手勢響應 swipe.rx.event .bind { [weak self] recognizer in //這個點是滑動的起點 let point = recognizer.location(in: recognizer.view) self?.showAlert(title: "向上划動", message: "\(point.x) \(point.y)") } .disposed(by: disposeBag) } //顯示消息提示框 func showAlert(title: String, message: String) { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "肯定", style: .cancel)) self.present(alert, animated: true) } } 複製代碼
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() //添加一個點擊手勢 let tapBackground = UITapGestureRecognizer() view.addGestureRecognizer(tapBackground) //頁面上任意處點擊,輸入框便失去焦點 tapBackground.rx.event .subscribe(onNext: { [weak self] _ in self?.view.endEditing(true) }) .disposed(by: disposeBag) } } 複製代碼
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { @IBOutlet weak var datePicker: UIDatePicker! @IBOutlet weak var label: UILabel! //日期格式化器 lazy var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy年MM月dd日 HH:mm" return formatter }() let disposeBag = DisposeBag() override func viewDidLoad() { datePicker.rx.date .map { [weak self] in "當前選擇時間: " + self!.dateFormatter.string(from: $0) } .bind(to: label.rx.text) .disposed(by: disposeBag) } } 複製代碼
經過上方的 datepicker 選擇須要倒計時的時間後,點擊「開始」按鈕便可開始倒計時。 倒計時過程當中,datepicker 和按鈕都不可用。且按鈕標題變成顯示倒計時剩餘時間。
import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { //倒計時時間選擇控件 var ctimer:UIDatePicker! //開始按鈕 var btnstart:UIButton! //剩餘時間(必須爲 60 的整數倍,好比設置爲100,值自動變爲 60) let leftTime = Variable(TimeInterval(180)) //當前倒計時是否結束 let countDownStopped = Variable(true) let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() //初始化datepicker ctimer = UIDatePicker(frame:CGRect(x:0, y:80, width:320, height:200)) ctimer.datePickerMode = UIDatePickerMode.countDownTimer self.view.addSubview(ctimer) //初始化button btnstart = UIButton(type: .system) btnstart.frame = CGRect(x:0, y:300, width:320, height:30); btnstart.setTitleColor(UIColor.red, for: .normal) btnstart.setTitleColor(UIColor.darkGray, for:.disabled) self.view.addSubview(btnstart) //剩餘時間與datepicker作雙向綁定 DispatchQueue.main.async{//加 DispatchQueue.main.async 是爲了解決第一次撥動錶盤不觸發值改變事件的問題 _ = self.ctimer.rx.countDownDuration <-> self.leftTime //<-> 是自定義的雙向綁定符號 } //綁定button標題 Observable.combineLatest(leftTime.asObservable(), countDownStopped.asObservable()) { leftTimeValue, countDownStoppedValue in //根據當前的狀態設置按鈕的標題 if countDownStoppedValue { return "開始" }else{ return "倒計時開始,還有 \(Int(leftTimeValue)) 秒..." } }.bind(to: btnstart.rx.title()) .disposed(by: disposeBag) //綁定button和datepicker狀態(在倒計過程當中,按鈕和時間選擇組件不可用) countDownStopped.asDriver().drive(ctimer.rx.isEnabled).disposed(by: disposeBag) countDownStopped.asDriver().drive(btnstart.rx.isEnabled).disposed(by: disposeBag) //按鈕點擊響應 btnstart.rx.tap .bind { [weak self] in self?.startClicked() } .disposed(by: disposeBag) } //開始倒計時 func startClicked() { //開始倒計時 self.countDownStopped.value = false //建立一個計時器 Observable<Int>.interval(1, scheduler: MainScheduler.instance) .takeUntil(countDownStopped.asObservable().filter{ $0 }) //倒計時結束時中止計時器 .subscribe { event in //每次剩餘時間減1 self.leftTime.value -= 1 // 若是剩餘時間小於等於0 if(self.leftTime.value == 0) { print("倒計時結束!") //結束倒計時 self.countDownStopped.value = true //重製時間 self.leftTime.value = 180 } }.disposed(by: disposeBag) } } 複製代碼