SwiftのKeyPathの使いどころ
概要
Swift4.2からKeyPathという、動的にプロパティにアクセスできる表現が追加されました。
KeyPathの使いどころが分からなかったので、具体例を備忘録として残しておきます。
KeyPath
基本的な使い方
前述のとおりKeyPathはプロパティにアクセスできる表現です。
\(バックスラッシュ)
でアクセスしたいプロパティ名を宣言し、サブスクリプトでプロパティにアクセスします。
struct Song {
var title: String
var artist: Artist
var albumArtwork: UIImage
}
struct Artist {
var name: String
}
let song = Song(title: "Let it Be", artist: Artist(name: "Beatles"), albumArtwork: UIImage(systemName: "square.and.arrow.up")!)
let keyPath: KeyPath<Song, String> = \.title
print(song[keyPath: keyPath]) // Let it Be
ネストしたプロパティにアクセスしたい場合は以下のように、appending
メソッドを使用してKeyPathを作成します。
let song = Song(title: "Let it Be", artist: Artist(name: "Beatles"), albumArtwork: UIImage(systemName: "square.and.arrow.up")!)
let artistKeyPath: KeyPath<Song, Artist> = \.artist
let nameKeyPath: KeyPath<Artist, String> = \.name
let artistNameKeyPath = artistKeyPath.appending(path: nameKeyPath)
print(song[keyPath: artistNameKeyPath]) // Beatles
もしくは以下のようにもアクセスできます。
let song = Song(title: "Let it Be", artist: Artist(name: "Beatles"), albumArtwork: UIImage(systemName: "square.and.arrow.up")!)
let artistNameKeyPath: KeyPath<Song, String> = \Song.artist.name
print(song[keyPath: artistNameKeyPath]) // Beatles
KeyPathを使って実装してみる
ここまでKeyPathを用いたプロパティへのアクセス方法について見てきました。
ではどういうときにKeyPathを使うと便利なのでしょうか?
題材として以下のようなシンプルなテーブルビューを実装してみます。
テーブルセルには、曲情報とプレイリスト情報を表示します。
使用するモデルクラスは以下のとおりです。
struct Song {
var title: String
var artistName: String
var albumArtwork: UIImage
}
struct Playlist {
var name: String
var authorName: String
var artwork: String
}
通常cellForRowAtのデリゲートメソッドでセルを生成します。
セクションごとに表示する内容を変更したい場合セクションに応じて、代入するデータを以下のように変更します。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
if indexPath.section == 0 {
let song = songs[indexPath.row]
cell.textLabel?.text = song.title
cell.detailTextLabel?.text = song.artistName
cell.imageView?.image = song.albumArtwork
} else {
let playlist = playlists[indexPath.row]
cell.textLabel?.text = playlist.name
cell.detailTextLabel?.text = playlist.authorName
cell.imageView?.image = playlist.artwork
}
return cell
}
ただセクションに応じて扱うデータを変更したいがために、同じような処理を重複して書かなければなりません。更にセクションが増え、扱うデータも増えていけば煩雑なコードになってしまいます。
これを回避するためにKeyPathを使います。
セルのデータを設定する責務を持つ構造体を作成します。
struct CellConfigurator<Model> {
let titleKeyPath: KeyPath<Model, String>
let subtitleKeyPath: KeyPath<Model, String>
let imageKeyPath: KeyPath<Model, UIImage>
func configure(_ cell: UITableViewCell, for model: Model) {
// Generics型のModelに動的に値にアクセスしている!
cell.textLabel?.text = model[keyPath: titleKeyPath]
cell.detailTextLabel?.text = model[keyPath: subtitleKeyPath]
cell.imageView?.image = model[keyPath: imageKeyPath]
}
}
そして上記コードを以下のように使用すれば、コード行数はあまり変わりませんがインスタンスメソッドの引数にインスタンスを渡すだけになるので、煩雑さは消えスッキリしたコードになるように思います。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
if indexPath.section == 0 {
let songCellConfigurator = CellConfigurator<Song>(
titleKeyPath: \.title,
subtitleKeyPath: \.artistName,
imageKeyPath: \.albumArtwork
)
songCellConfigurator.configure(cell, for: songs[indexPath.row])
} else {
let playlistConfigurator = CellConfigurator<Playlist>(
titleKeyPath: \.name,
subtitleKeyPath: \.authorName,
imageKeyPath: \.artwork
)
playlistConfigurator.configure(cell, for: playlists[indexPath.row])
}
return cell
}
まとめ
このようにしてKeyPathを使うことで、実行時に動的にプロパティの値にアクセスできるようになりました。
またKeyPathを通してアクセスできるようになることで、煩雑なコードや重複コードを回避できる可能性があります。
Discussion