RxSwift是一个用于与 Swift 语言交互的框架,但它只是基础,并不能用来进行用户交互、网络请求等。
而 RxCocoa 是让 Cocoa APIs 更容易使用响应式编程的一个框架。RxCocoa 能够让我们方便地进行响应式网络请求、响应式的用户交互、绑定数据模型到 UI 控件等等。而且大多数的 UIKit 控件都有响应式扩展,它们都是通过 rx 属性进行使用。
一、UILabel
1.将数据绑定到 text 属性上(普通文本)
以实现一个计时显示的功能
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)
}
}
2.将数据绑定到 attributedText 属性上(富文本)
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
}
}
二、UITextField 与 UITextView
1.监听单个 textField 内容的变化
注意:.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)
//当然我们直接使用 change 事件效果也是一样的
//当文本框内容改变时,将内容输出到控制台上
textField.rx.text.orEmpty.changed
.subscribe(onNext: {
print("您输入的是:\($0)")
})
.disposed(by: disposeBag)
}
}
2.将内容绑定到其他控件上
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)
}
}
3.同时监听多个 textField 内容的变化(textView 同理)
● 界面上有两个输入框分别用于填写电话的区号和号码。
● 无论那一个输入框内容发生变化,都会将它们拼成完整的号码并显示在 label 中。
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)
}
}
4.事件监听
通过 rx.controlEvent 可以监听输入框的各种事件,且多个事件状态可以自由组合。除了各种 UI 控件都有的 touch 事件外,输入框还有如下几个独有的事件:
● editingDidBegin:开始编辑(开始输入内容)
● editingChanged:输入内容发生改变
● editingDidEnd:结束编辑
● editingDidEndOnExit:按下 return 键结束编辑
● allEditingEvents:包含前面的所有编辑相关事件
textField.rx.controlEvent([.editingDidBegin]) //状态可以组合
.asObservable()
.subscribe(onNext: { _ in
print("开始编辑内容!")
}).disposed(by: disposeBag)
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)
}
}
5.UITextView 独有的方法
UITextView 还封装了如下几个委托回调方法:
● 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)
}
}
三、UIButton与UIBarButtonItem
1.按钮点击响应
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)
按钮点击响应
button.rx.tap
.bind { [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)
}
}
2.按钮标题(title)的绑定
//创建一个计时器(每1秒发送一个索引数)
let timer = Observable<Int>.interval(1,
scheduler: MainScheduler.instance)
//根据索引数拼接最新的标题,并绑定到button上
timer.map{"计数\($0)"}
.bind(to: button.rx.title(for: .normal))
.disposed(by: disposeBag)
3.按钮富文本标题(attributedTitle)的绑定
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())
.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
}
}
4.按钮图标(image)的绑定
//创建一个计时器(每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())
.disposed(by: disposeBag)
5.按钮背景图片(backgroundImage)的绑定
//创建一个计时器(每1秒发送一个索引数)
let timer = Observable<Int>.interval(1,
scheduler: MainScheduler.instance)
//根据索引数选择对应的按钮背景图,并绑定到button上
timer.map{ UIImage(named: "\($0%2)")! }
.bind(to: button.rx.backgroundImage())
.disposed(by: disposeBag)
6.按钮是否可用(isEnabled)的绑定
switch1.rx.isOn
.bind(to: button1.rx.isEnabled)
.disposed(by: disposeBag)
7.按钮是否选中(isSelected)的绑定
三个按钮只有一个按钮处于选中状态。即点击选中任意一个按钮,另外两个按钮则变为未选中状态。
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)
}
}
}
四、UISwitch与UISegmentedControl
1.UISwitch开关状态改变
//当 switch 开关状态改变时,输出当前值
switch1.rx.isOn.asObservable()
.subscribe(onNext: {
print("当前开关状态:\($0)")
})
.disposed(by: disposeBag)
//当我们切换 switch 开关时,button 会在可用和不可用的状态间切换。
switch1.rx.isOn
.bind(to: button1.rx.isEnabled)
.disposed(by: disposeBag)
2.UISegmentedControl(分段选择控件)
当 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)
}
}
五、UIActivityIndicatorView
UIActivityIndicatorView 又叫状态指示器,它会通过一个旋转的“菊花”来表示当前的活动状态。
//通过开关我们可以控制活动指示器是否显示旋转
mySwitch.rx.value
.bind(to: activityIndicator.rx.isAnimating)
.disposed(by: disposeBag)
六、UIApplication
RxSwift 对 UIApplication 增加了一个名为 isNetworkActivityIndicatorVisible 绑定属性,我们通过它可以设置是否显示联网指示器(网络请求指示器)。
● 当开关打开时,顶部状态栏上会有个菊花状的联网指示器。
● 当开关关闭时,联网指示器消失。
mySwitch.rx.value
.bind(to: UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
.disposed(by: disposeBag)
七、UISlider与UIStepper
1.UISlider(滑块)
当我们拖动滑块时,在控制台中实时输出 slider 当前值
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)
}
}
2.UIStepper(步进器)
当 stepper 值改变时,在控制台中实时输出当前值
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)
八、UIGestureRecognizer
RxCocoa 同样对 UIGestureRecognizer 进行了扩展,并增加相关的响应方法。下面以滑动手势为例,其它手势用法也是一样的。
样例:当手指在界面上向上滑动时,弹出提示框,并显示出滑动起点的坐标。
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)
//方式1:手势响应
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)
//方式2: 手势响应
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)
}
}
九、UIDatePicker
1.日期选择响应
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)
}
}
2.倒计时功能
● 通过上方的 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{
_ = 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)
}
}
● <-> 是自定义的双向绑定符号
● 加 DispatchQueue.main.async 是为了解决第一次拨动表盘不触发值改变事件的问题(这个是 iOS 的 bug)