RxSwift学习之十二 (基础使用篇 3- UI控件扩展)

@[TOC](RxSwift学习之十二 (基础使用篇 3- UI控件扩展))

2. Rxswift基本使用之 UI控件扩展

RxSwift 是一个用于与 Swift 语言交互的框架,但它只是基础,并不能用来进行用户交互、网络请求等。
RxCocoa 是让 Cocoa APIs 更容易使用响应式编程的一个框架。RxCocoa 能够让我们方便地进行响应式网络请求、响应式的用户交互、绑定数据模型到 UI 控件等等。而且大多数的 UIKit 控件都有响应式扩展,它们都是通过 rx 属性进行使用。

2.17 UILabel rx 扩展

2.17.1 将数据绑定到 text 属性上(普通文本)

  • 实例2.17.1
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.17.2 将数据绑定到 attributedText 属性上(富文本)

  • 实例2.17.2
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
    }
}

2.18 UITextField,UITextView Rx扩展

2.18.1 监听单个 textField 内容的变化(textView 同理)

  • 实例2.18.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.18.2 将内容绑定到其他控件上

  • 实例2.18.2

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)
    }
}

2.18.3 同时监听多个 textField 内容的变化(textView 同理)

  • 实例2.18.3
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)
    }
}

2.18.4 事件监听

  • (1)通过 rx.controlEvent 可以监听输入框的各种事件,且多个事件状态可以自由组合。除了各种 UI 控件都有的 touch 事件外,输入框还有如下几个独有的事件:

editingDidBegin:开始编辑(开始输入内容)
editingChanged:输入内容发生改变
editingDidEnd:结束编辑
editingDidEndOnExit:按下 return 键结束编辑
allEditingEvents:包含前面的所有编辑相关事件

  • (2)下面代码监听输入框开始编辑事件(获取到焦点)并做相应的响应。

实例2.18.4.1

textField.rx.controlEvent([.editingDidBegin]) //状态可以组合
    .asObservable()
    .subscribe(onNext: { _ in
        print("开始编辑内容!")
    }).disposed(by: disposeBag)
  • (3)下面代码我们在界面上添加两个输入框分别用于输入用户名和密码:
  1. 如果当前焦点在用户名输入框时,按下 return 键时焦点自动转移到密码输入框上。
  2. 如果当前焦点在密码输入框时,按下 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)
    }
}

2.18.5 UITextView 独有的方法

  • UITextView 还封装了如下几个委托回调方法:

didBeginEditing:开始编辑
didEndEditing:结束编辑
didChange:编辑内容发生改变
didChangeSelection:选中部分发生变化

  • 实例2.18.5
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)
    }
}

2.19 UIButton 与 UIBarButtonItem 的Rx扩展

2.19.1 按钮点击响应

  • 实例2.19.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)
    }
     
    //显示消息提示框
    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)

2.19.2 按钮标题(title)的绑定

  • 实例2.19.2
//创建一个计时器(每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)

2.19.3 按钮富文本标题(attributedTitle)的绑定

  • 实例2.19.3
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
    }
}

2.19.4 按钮图标(image)的绑定

  • 实例2.19.4
//创建一个计时器(每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)

2.19.5 按钮背景图片(backgroundImage)的绑定

  • 实例2.19.5
//创建一个计时器(每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)

2.19.6 按钮是否可用(isEnabled)的绑定

  • 实例2.19.6
switch1.rx.isOn
    .bind(to: button1.rx.isEnabled)
    .disposed(by: disposeBag)

2.19.7 按钮是否选中(isSelected)的绑定

  • 实例2.19.7
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)
        }
    }
}

2.20 UISwitch、UISegmentedControl 的Rx扩展

2.20.1 UISwitch

  • 实例 2.20.1

实现当 switch 开关状态改变时,输出当前值。

switch1.rx.isOn.asObservable()
    .subscribe(onNext: {
        print("当前开关状态:\($0)")
    })
    .disposed(by: disposeBag)
switch1.rx.isOn
    .bind(to: button1.rx.isEnabled)
    .disposed(by: disposeBag)

2.20.2 UISegmentedControl

  • 实例 2.20.2

实现当 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)
    }
}

2.21 UIActivityIndicatorView、UIApplication 的Rx扩展

2.21.1 UIActivityIndicatorView

  • 实例2.21.1

通过开关我们可以控制活动指示器是否显示旋转

mySwitch.rx.value
    .bind(to: activityIndicator.rx.isAnimating)
    .disposed(by: disposeBag)

2.21.2 UIApplication

  • 实例2.21.2
    当开关打开时,顶部状态栏上会有个菊花状的联网指示器。
    当开关关闭时,联网指示器消失。
mySwitch.rx.value
    .bind(to: UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
    .disposed(by: disposeBag)

2.22 UISlider、UIStepper 的Rx扩展

2.22.1 UISlider

  • 实例2.22.1
    拖动滑块时,在控制台中实时输出 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.22.2 UIStepper

  • 实例2.22.2
    当 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)

2.23 双向绑定:<->

  • 前面讲到的UI使用,基本所有的绑定都是单向的。但有时候我们需要实现双向绑定。比如将控件的某个属性值与 ViewModel 里的某个 Subject 属性进行双向绑定:
  1. 这样当 ViewModel 里的值发生改变时,可以同步反映到控件上。
  2. 而如果对控件值做修改,ViewModel 那边值同时也会发生变化。

2.23.1 简单的双向绑定

  • 实例2.23.1


    image

(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)
    }
}

2.23.2 自定义双向绑定操作符(operator)

  • RxSwift 自带的双向绑定操作符

(1)如果经常进行双向绑定的话,最好还是自定义一个 operator 方便使用。
(2)好在 RxSwift 项目文件夹中已经有个现成的(Operators.swift),我们将它复制到我们项目中即可使用。当然如我们想自己写一些其它的双向绑定 operator 也可以参考它。

image
  • 实例2.23.2
    双向绑定操作符是:<->。我们修改上面实例2.23.1,可以发现代码精简了许多
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)
    }
}

2.24 UIGestureRecognizer

  • 实例2.24.1
    当手指在界面上向上滑动时,弹出提示框,并显示出滑动起点的坐标。

(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)
    }
}
  • 实例2.24.2
    实现点击页面任意位置,输入框便失去焦点
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)
    }
} 

2.25 UIDatePicker

2.25.1 日期选择响应

  • 实例2.25.1
    当日期选择器里面的时间改变后,将时间格式化显示到 label 中


    image
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.25.2 倒计时功能

  • 实例2.25.2


    image

通过上方的 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)
    }
}

2.26

2.27

2.28

2.29

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342