皆さんSwiftでiOSアプリを書いていますか。
Objective-Cであれば素直に書けていたコードが、何度も同じようなキャストをしたりとめんどくささを感じることがあると思います。
この記事ではswiftの型推論とGenericsを使って、アプリケーションコードをシンプルにしていく方法を示します。
Swiftの開発に慣れている方には当たり前な内容かと思いますが、最近使い出したよという方の参考になれば幸いです。
問題
今回はUITableViewを使ったアプリのコードを想定しています。
UITableViewを使ったアプリの場合、Swiftでを書き出すとこのようなコードがよく登場してくると思います。
class ViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MyTableViewCell.self), for: indexPath) as! MyTableViewCell
cell.update(...)
return cell
}
}
tableViewからcellを取り出して、それを自前で定義したカスタムセルのクラスにキャスト、定義した関数を呼び出す。というような処理です。
dequeueReusableCell
がUITableViewCell
を返してくるので、自前で定義したクラスとして使いたい場合はキャストをしてあげる必要があります。
1,2箇所な良いのだけど、何度も出てくるとコードも長くなるしキャストもダルいですね。スッキリかけないものかと悩んでおりました。
Generics
SwiftにはGenericsという汎用的に様々な型で利用できる仕組みがあります。
Array
やDictionary
がそれですね。また、型だけでなく、関数にも使えます。
// Genericな関数
func max<T:Comparable>(l:T, r:T) -> T {
return l > r ? l : r
}
これを利用して、上記のコードのキャスト等を隠蔽するような関数を書いてあげれば、スッキリとしそうです。
T型を返す関数を定義してみる
単に戻り値をTとしたdequeueReusableCell
を定義してみます。
extension UITableView {
func genericDequeueReusableCell<T>(withClass cellClass: T, for indexPath: IndexPath) -> T {
return dequeueReusableCell(withIdentifier: String(describing: T.self), for: indexPath) as! T
}
}
元の処理を関数にしただけですが、使ってみるとうまく動いてくれています。
class ViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.genericDequeueReusableCell(withClass: MyTableViewCell.self, for: indexPath)
cell.update(...)
return cell
}
}
ただこの関数は1つ問題がありますね。identifierの文字列を型から作るような関数になっているので、自由な文字列を渡そうとすると困ってしまいます。
文字列を追加で渡すような関数にすると複雑になってしまってイマイチです。
型推論でGenericsの呼び出しを解決してもらう
ここで登場するのが型推論で、以下のように変数の型を明確にした状態で関数を呼び出すコードを書くと、Tがその変数の型であると推論してくれて、うまく呼び出してくれるのです。
extension UITableView {
func genericDequeueReusableCell<T>(withIdentifier identifier: String, for indexPath: IndexPath) -> T {
return dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! T
}
}
class ViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: MyTableViewCell = tableView.genericDequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.update(...)
return cell
}
}
うまく呼び出せていそうです。
さらにこれを利用すると、最初に定義した関数の第一引数も省略できてよりスッキリしそうです。
extension UITableView {
func genericDequeueReusableCell<T>(withIdentifier identifier: String, for indexPath: IndexPath) -> T {
return dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! T
}
func genericDequeueReusableCell<T>(for indexPath: IndexPath) -> T {
return genericDequeueReusableCell(withIdentifier: String(describing: T.self), for: indexPath)
}
}
class ViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: MyTableViewCell = tableView.genericDequeueReusableCell(for: indexPath)
cell.update(...)
return cell
}
}
シュッとしました!最初のコードと比べるとだいぶ共通の処理を週略できて、コードがスリムになりましたね。
(念のため補足ですが、キャストを隠蔽するような共通処理は、濫用するとswiftの型安全性を壊すことになります。今回の例のようにキャストが失敗しても問題のないところに留めるべきです)
おまけ. TがUITableViewCellのサブクラスである制約をつける
スッキリとはしましたが、現状だとTにどんな型でも取ることが出来てしまいます。例えば間違えてUICollectionView
を継承したセルを指定して実行時にミスに気づくというのはありそうな展開です。
SwiftではTが取りうる型に制約をつけてあげることができるので、上記のような問題にはコンパイル時に発覚させることができます。
extension UITableView {
func genericDequeueReusableCell<T:UITableViewCell>(for indexPath: IndexPath) -> T {
return genericDequeueReusableCell(withIdentifier: String(describing: T.self), for: indexPath)
}
}
let tableViewCell: UITableViewCell = tableView.genericDequeueReusableCell(for: indexPath) // ok
let collectionViewCell: UICollectionViewCell = tableView.genericDequeueReusableCell(for: indexPath) // compile error
期待通り、コンパイルエラーになってくれました。
まとめ
SwiftのGenericsを使ってあげるとUIKitとの連携部分に限らず、コードを汎用的でシンプルに書くことができます。
開発時に考えることを減らしてくれることでしょう。