個人でつくっているiOSアプリ(swift)で、データバインディングのライブラリとしてBond
をつかってみました。
https://github.com/SwiftBond/Bond
データバインディングのみの軽量なライブラリだから特にハマることもないだろうと思ったけれどそうでもなかったので、そういったことも含め個人的な感想を書いてみようと思います。Bond
の基本的な使い方に関してはREADM.md
に書いてあるので割愛します。
注:あくまで現時点(2015/8)のBondについての話で、バージョンは3.10.0
ものになります。
細かなこと
->>
, <->>
といったオペレータの書き方は直感的でいいかも
group.name ->> nameTextField.dynText
こんな感じで、group
モデルのname
という文字列のプロパティを、nameTextField
のtext
プロパティにバインディングする。
group.name <->> nameTextField.dynText
<->>
だと双方向バインディングになる(UITextFieldで入力した値がgroup
モデルのname
プロパティにもバインディングされる)。
.value
で値にアクセスするのがめんどい
var name = Dynamic<String>("")
Bond
ではこんな感じで変数を定義して、例えばAPIにパラメータとして渡すために実際にその値を取得するときなんかは
name.value
と書く。これが個人的に少しめんどいのと、久しぶりに書くと一瞬忘れてることがある。
宣言的に書くということ
Bond
に限った話ではないと思いますが、こういった宣言的なプログラミングのスタイルってコードが1箇所に集中しやすいのかなと思いました。
今回も下のような(アプリからそのままもってきているので、わかりづらいのですが独自のヘルパークラスとかもまぎれてます。)、UIViewController
のviewDidLoad
にバインディングの記述をひたすら書いていくみたいな感じでした。
override func viewDidLoad() {
/** 省略... */
// アイテムが複数ないときはソートボタンを無効にする
map(self.list.dynCount) { count in
return self.list.count > 1
} ->> sortButton.dynEnabled
// 投稿可能文字数のときのみ投稿ボタンを有効にする
map(self.postTextView.dynText) { string in
return (count(string) > 0 && count(string) < 140)
} ->> postButton.dynEnabled
// アイデア一覧テーブル
self.list.map { [unowned self] (idea: Idea) -> IdeaTableViewCell in
let cell = IdeaTableViewCell.getView("IdeaTableViewCell") as! IdeaTableViewCell
cell.selectionStyle = UITableViewCellSelectionStyle.None
idea.identifier ->> cell.identifier
idea.postUser.profile.displayName ->> cell.posterLabel.dynText
idea.likeCount <->> cell.likeCount
idea.content ->> cell.contentLabel!.dynText
map(idea.createdAt) { dateString in
return DateHelper.getDateString(dateString)
} ->> cell.dateLabel.dynText
return cell
} ->> self.tableViewDataSourceBond
}
テーブルビューのセルも上のようには書けるのですが、(少なくとも自分は)これまではセルにデータを引数としたメソッドを生やして、セル側のほうで処理をするような書き方をしていたので(=セル側に記述を分離)ちょっとコードの見通しが悪くて気になりました。ベストプラクティスあれば知りたい。
UITableViewまわりが意外に煩雑
UITableView
まわりはもう1つ個人的にしっくりこないことがあって、ちょっとしたUITableView
であれば
var tableViewDataSourceBond: UITableViewDataSourceBond<UITableViewCell>!
override func viewDidLoad() {
/** 省略... */
self.tableViewDataSourceBond = UITableViewDataSourceBond(tableView: self.ideaTableView)
self.list.map { [unowned self] (idea: Idea) -> IdeaTableViewCell in
let cell = IdeaTableViewCell.getView("IdeaTableViewCell") as! IdeaTableViewCell
// バインディング処理
return cell
} ->> self.tableViewDataSourceBond
}
このような感じで書けて、UITableViewDataSourceBond
がUITableViewDataSource
を内包しているので、
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
などのUITableViewDataSource
のデリゲートメソッドは書かなくていい。
が、README.md
にあるように、
numberOfSectionsInTableView:
tableView:numberOfRowsInSection:
tableView:cellForRowAtIndexPath:
If you need to provide other information to the table view, you can have your class adhere to protocol UITableViewDataSource and implement methods you need. After that, set nextDataSource property of UITableViewDataSourceBond to your object.
上の3つのデリゲートメソッド以外が必要なときは、`UITableViewDataSource`を実装してとのこと。今回はセクションのヘッダのタイトルを指定して表示するという仕様があったので、結局`UITableViewDataSource`をプロトコルとしてもたなくちゃいけなかったりで、ちょっとゴチャゴチャな実装になってしまいました。
## UIKitまわりのプロパティ
```swift
group.name ->> nameTextField.dynText
このdynText
などの、dyn
というprefixのついた各々のUIKitのプロパティはBond
側で1つ1つ実装しているもので、当然すべてのプロパティを網羅しているわけではないので、場合によっては対応していないものに出くわすことがあるかもしれません。自分はUINavigationBar
のbarTintColor
を動的に変える必要があったので、仕方なくPR投げてみました。
後述するように、これを書いている時点ではとくにマージもコメントもされていません。
queueまわりの話
さきほどからテーブルビューのセルを例に出していますが、以下のようにURL文字列をセル上のUIImageView
にバインディングしたいということもあると思います。
map(some.image_url) { urlString in
var url = NSURL(string: urlString)
if let existUrl = url {
var data = NSData(contentsOfURL: existUrl)
if let existData = data {
return UIImage(data: existData)!
}
}
return UIImage()
} ->> cell.avatarImageView.dynImage
が、このままだと、map
内のクロージャの処理はメインスレッドで実行されてしまうため、よくある「テーブルビューのカタつく問題」になってしまいます。
deliver
という値を反映させるときにキュー(スレッド)を指定するメソッドも用意されているのですが、上記の例とは利用ケースが異なるものです。
こちらもPR投げてみました。
が、作者からは「今はswift 2
に向けて移行中なので現行の安定バージョンはいじりたくない」といった旨のコメントいただきました。さきほどのUINavigationBar
の件もこれかもしれません。
Bond 4.0.0
への移行
ということで、これもBondに限った話ではないと思いますが、現時点(2015/8)ではSwift 2
に対応したバージョンに移行中(Bond 4.0,0
)とのことです。言われてみれば至極当然の話ですが、今はライブラリの選定だったり悩ましい時期ですね。
ちなみにさきほどのmap
内のクロージャの処理の件については、
some.image_url
.deliverOn(Queue.Background)
.map { url in
// download and return UIImage
}
.deliverOn(Queue.Main)
.bindTo(cell.avatarImageView.bnd_image)
と書けるようになるとのことです。作者さんがPRに答えてくれています。
とりあえず自分はforkした自分のレポジトリのものを使っていって、また4.0.0
リリースされたらまたどうするか考えようと思っています。