KeyPath
とは?
KeyPath
とは?
Swift4で追加された、プロパティに動的にアクセスするための記法
Use key-path expressions to access properties dynamically.1
Swift3.xまでの #keyPath
class Hoge: NSObject { // 🤔
@objc var foo: String // 🤔
let bar: String
init(foo: String, bar: String) {
self.foo = foo
self.bar = bar
}
}
let foo = hoge.value(forKeyPath: #keyPath(Hoge.foo))
print(type(of: foo)) // Optional<Any>
print(#keyPath(hoge.bar)) // error: argument of '#keyPath' refers to non-'@objc' property 'bar'
Obj-C時代からあるKVOでのtypoを防ぐ程度の役割しかない
-
NSObject
にしか使えない -
@objc
宣言されたプロパティのみ - 返却値は
Any!
(型情報が消える😫) - 解析が遅い、Darwinでしか使えない2といった問題点も
Swift4からの KeyPath
- Swiftの強力な型システムに基づいた機構
- structにもenumにも使える
- 型情報が維持される
- Swiftのstdlibに含まれているのでプラットフォームに依存しない
NSObject
も@objc
宣言もいらない
class Hoge {
var foo: String
let bar: String
}
let hogeFoo: ReferenceWritableKeyPath<Hoge, String> = \.foo
let hogeBar = \Hoge.bar // KeyPath<Hoge, String>
let hoge = Hoge(foo: "foo", bar: "bar")
print(hoge.foo) // foo
hoge[keyPath: hogeFoo] = "hoge-foo"
print(hoge.foo) // hoge-foo
hoge[keyPath: hogeBar] = "bar" // error: cannot assign through subscript: 'hogeBar' is a read-only key path
structにも使える
struct Fuga {
var foo: String
let bar: String
}
let fugaFoo: WritableKeyPath<Fuga, String> = \.foo
let fugaBar = \Fuga.bar // KeyPath<Fuga, String>
var fuga = Fuga(foo: "foo", bar: "bar")
print(fuga.foo) // foo
fuga[keyPath: fugaFoo] = "fuga-foo"
print(fuga.foo) // fuga-foo
fuga[keyPath: fugaBar] = "bar" // error: cannot assign through subscript: 'fugaBar' is a read-only key path
KeyPath
関連クラス
_AppendKeyPath
└ AnyKeyPath
└ PartialKeyPath<Root>
└ KeyPath<Root, Value>
└ WritableKeyPath<Root, Value> // Value is variable
└ ReferenceWritableKeyPath<Root, Value> // Root is class
小ネタ
TupleもRoot
になれる
let hogeFuga = (hoge, fuga)
let hogeKeyPath = \(Hoge, Fuga).0 // no label
let fugaKeyPath = \(hoge: Hoge, fuga: Fuga).fuga // with labels
hogeFuga[keyPath: hogeKeyPath] // hoge
hogeFuga[keyPath: fugaKeyPath] // fuga
プロパティのネストも可能
struct FooBar {
var hoge: Hoge
var fuga: Fuga
}
let fooBarHogeFoo = (\FooBar.hoge).appending(path: \Hoge.foo)
\FooBar.hoge.foo == fooBarHogeFoo // true
print(fooBar.hoge.foo) // hoge-foo
fooBar[keyPath: fooBarHogeFoo] = "foo-bar-hoge-foo"
print(fooBar.hoge.foo) // foo-bar-hoge-foo
fooBar[keyPath: \.hoge.foo] = "foo"
print(fooBar.hoge.foo) // foo
let fooBarFugaFoo = (\FooBar.fuga).appending(path: \Fuga.foo)
fooBar[keyPath: fooBarFugaFoo] = "foo-bar-fuga-foo" // error: cannot assign through subscript: 'fooBar' is a 'let' constant
KeyPath
の実用例
プロパティでソートする3
public extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
}
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T?>) -> [Element] {
sorted {
guard let l = $0[keyPath: keyPath],
let r = $1[keyPath: keyPath] else { return false }
return l < r
}
}
func min<T: Comparable>(by keyPath: KeyPath<Element, T>) -> Element? {
self.min { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
}
}
struct Person {
var id: Int
var name: String
var age: Int
}
extension Person: CustomStringConvertible {
var description: String { name }
}
let people: [Person] = [
.init(id: 3, name: "Bob", age: 28),
.init(id: 1, name: "Emma", age: 40),
.init(id: 4, name: "Amelia", age: 18),
.init(id: 2, name: "George", age: 22),
]
// Before
people.sorted(by: { $0.id < $1.id }) // ["Emma", "George", "Bob", "Amelia"]
people.sorted(by: { $0.name < $1.name }) // ["Amelia", "Bob", "Emma", "George"]
// After
people.sorted(by: \.id) // ["Emma", "George", "Bob", "Amelia"]
people.sorted(by: \.name) // ["Amelia", "Bob", "Emma", "George"]
同じAnchor同士へのConstraintを貼る
public extension UIView {
func equal<Axis, Anchor: NSLayoutAnchor<Axis>>(_ anchor: KeyPath<UIView, Anchor>,
to target: UIView) -> NSLayoutConstraint {
self[keyPath: anchor].constraint(equalTo: target[keyPath: anchor])
}
func equal<Axis, Anchor: NSLayoutAnchor<Axis>>(_ anchor: KeyPath<UIView, Anchor>,
to target: UIView,
constant: CGFloat) -> NSLayoutConstraint {
self[keyPath: anchor].constraint(equalTo: target[keyPath: anchor], constant: constant)
}
}
// Before
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: target.topAnchor),
view.leadingAnchor.constraint(equalTo: target.leadingAnchor),
view.trailingAnchor.constraint(equalTo: target.trailingAnchor),
view.bottomAnchor.constraint(equalTo: target.bottomAnchor)
])
// After
NSLayoutConstraint.activate([
view.equal(\.topAnchor, to: target),
view.equal(\.bottomAnchor, to: target),
view.equal(\.leadingAnchor, to: target),
view.equal(\.trailingAnchor, to: target)
])
更に関数チックに書いた例
https://www.objc.io/blog/2018/10/30/auto-layout-with-key-paths/
Key Path Expressions as Functions
Key Path Expressions as Functions
- Swift5.2で追加(SE-0249)
-
KeyPath
表現をクロージャを引数とする関数に渡すことができる- コレクション操作などが簡潔に書けるように
- Xcode12.xならコード補完が効く
例
struct Department {
var name: String
var member: [Person]
}
extension Department {
var boss: Person {
member.min(by: \.id)!
}
}
let departments: [Department] = [
.init(name: "General Affairs Department",
member: [
.init(id: 3, name: "Bob", age: 28),
.init(id: 1, name: "Emma", age: 40)
]),
.init(name: "Development Department",
member: [
.init(id: 4, name: "Amelia", age: 18),
.init(id: 2, name: "George", age: 22)
])
]
Before
departments.flatMap { $0.member }
.map { $0.name } // ["Bob", "Emma", "Amelia", "George"]
departments.map { $0.boss }
.map { $0.name } // ["Emma", "George"]
departments.map { $0.boss.name }
After
departments.flatMap(\.member)
.map(\.name) // ["Bob", "Emma", "Amelia", "George"]
departments.flatMap(\.member.name) // Value of type '[Person]' has no member 'name'
departments.map(\.boss)
.map(\.name) // ["Emma", "George"]
departments.map(\.boss.name) // ["Emma", "George"]
Performance4
extension Person {
var canDrink: Bool { age >= 20 }
}
注意
変数/定数化したKeyPathを渡すことはできない(2020/12/9現在)
let bossKeyPath = \Department.boss
let nameKeyPath = \Person.name
let bossNameKeyPath = bossKeyPath.appending(path: nameKeyPath)
departments.map(bossKeyPath) // Cannot convert value of type 'KeyPath<Department, Person>' to expected argument type '(Department) throws -> T'
.map(nameKeyPath) // Cannot convert value of type 'WritableKeyPath<Person, String>' to expected argument type '(T) throws -> T'
departments.map(bossNameKeyPath) // Cannot convert value of type 'KeyPath<Department, String>' to expected argument type '(Department) throws -> T'
最後に
-
KeyPath
によってSwiftコードがより簡潔に書きやすくなった - RxSwift / Combine のストリーム変換にも使えるのでより宣言的に書ける
- Genericsと併用することで処理の汎用化も大幅に進みそう
- Sample in Xcode Playground