🔑

SwiftのKeyPathの使いどころ

2021/03/26に公開

概要

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を通してアクセスできるようになることで、煩雑なコードや重複コードを回避できる可能性があります。

参考URL

GitHubで編集を提案

Discussion