The Pragmatic Ball boy

iOSを中心にやってる万年球拾いの老害エンジニアメモ

Swift Bond v4

Swift BondがSwift2.0に対応し、更にインターフェースが大きくかわりv4としてmasterに統合されました。

主な変更点としては、

  • クラス名を刷新

    • これまではBond、Dynamicといった意味不明なクラス名がObservable, EventProducerに変わりわかりやすくなりました。
  • オペレーターの非推奨・廃止

    • ->>や<<-といったオペレータの利用が非推奨となり、bind onlyの->|が廃止されました

と言った風に、いままで個人的に微妙だなと思っていた独自オペレーターや、謎のクラス名が改善されてよくなったなと思ってます。

基本的な使い方

値の変化の監視

以下のようなusernameの値の変化を検知したい場合、

var username: String = "Steve"

以下のようにObservableを使った定義に置き換えます。

class LoginViewModel {
  let username = Observable<String?>("Steve")
}

そして値の変化のイベントを受け取る場合は、上記で定義したObservableのobserve()メソッドを使って値が変化した際に呼ばれるコールバックを登録します。

class LoginViewController: UIViewController
{
  let viewModel = LoginViewModel()

  override func viewDidLoad() {
    viewModel.username.observe { event in
      print(event.value)
    }
  }

observe()は呼ぶとすぐに、引数で渡したコールバックが呼ばれます。 これを避けたい場合はobserveNew()を使えば登録時はコールバックが呼ばれません。

値の変化の監視とバインディング

監視対象とViewの要素をバインディングして、監視対象が変更されたらViewの要素を更新するといった場合、 bindTo()メソッドを使います。

class LoginViewController: UIViewController
{
  @IBOutlet weak var usernameTextField: UITextField!
  
  let viewModel = LoginViewModel()

  override func viewDidLoad() {
    viewModel.username.bindTo(usernameTextField.bnd_text)
  }

UITextFieldのtextとバインディングする場合はbnd_textというObservableがBond側でUITextFieldのextensionとして用意されています。 この他にも基本的なViewは用意されているのでExtensions以下を見るか、bnd_で予測変換をだせば確認できます。

また、bindTo()もobserve()と同様に、登録時に即時バインディングが行われます。 これを避けたい場合はskip()を使います。

viewModel.username.skip(1).bindTo(usernameTextField.bnd_text)

skip(1)とやると最初はusernameの値がTextFieldには反映されず、次にusernameが変化したときから反映されるようになります。

双方向バインディング

bindTo()は監視対象→監視側への一方通行のバインディングでしたが、双方向にすることも可能です。

viewModel.username.bidirectionalBindTo(usernameTextField.bnd_text)

イベントのフィルタリング

特定の条件の変化があった際だけ、コールバックを受け取りたい場合は、fileterを使います。

filterはEventProducer型(Observableのsuper class)を返しますので、filterの戻り値をそのままチェーンしてやることでObservableと同じように扱えます。

  @IBOutlet weak var loginButton: UIButton!
  
  let viewModel = LoginViewModel()

  override func viewDidLoad() {
    loginButton.bnd_controlEvent
        .filter { $0 == UIControlEvents.TouchUpInside }
        .observeNew { [unowned self] event in
            self.viewModel.login()
    }
  }

バインディングの解除

Observableが破棄されると自動的にそれに関わるバインディングは解消されるようになっていますので、普通に使っているぶんには解除を気にする必要はあまりありません。

解除方法

ObservableやbindToは戻り値としてDisposableTypeを返します。 DisposableTypeのdispose()メソッドを呼べば解除できます。

let disposer = viewModel.username.bindTo(usernameTextField.bnd_text)
disposer.dispose()

Cellについて

普通はあまりバインディングの解除を気にする必要はありませんが、UITableViewCellなど再利用されるものについて使う場合は手動で解除してやる必要があります。(Cellが再利用されると解除しないと、他のバインディングが残ってしまうため)

やり方としては、cellにbindする際に、cellのbnd_bagにdisposeInしておき

      let cell = (self.tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as? ListCellView)!
      let viewModel = dataSource[indexPath.section][indexPath.row]
      viewModel.name.bindTo(cell.nameLabel.bnd_text).disposeIn(cell.bnd_bag)
      return cell

prepareForReuse()でbnd_bagをdispose()します

  override func prepareForReuse() {
    super.prepareForReuse()

    bnd_bag.dispose()
  }