model을 만들면서 key path
에 대해서 알아봤다.
keyPath를 하나의 변수로 사용할 수 없을까 고민하던 중 이 글을 읽고 한번 정리해보게됐다.
내가 하고싶은 하나의 변수라 함은.
struct Zoo {
let animal: String
let age: Int
}
가 있을때 Zoo
의 age
라는 property를 변수로 사용하고 싶은것이다.
만약
let agePathOfZooStruct = Zoo.age
이게 된다면 ... key 맵핑이랄까.. 이전에 했던 규칙없고 두서없던 model 조합을 일일이 하드코딩하지 않고 변수로 넣어서 할 수 있지 않을까... 라는 생각에 Keypath에 대해서 알아보게 됐다.
아직 솔루션은 찾지 못했지만 아래 글은 꽤 재밌다.
Key paths essentially let us reference any instance property as a separate value.
As such, they can be passed around, used in expressions, and enable a piece of code to get or set a property without actually knowing which exact property its working with.
Key Paths를 사용하면 instance property를 별도의 값으로 참조할 수 있습니다.
따라서, 실제로 어떤 property가 동작하는지 알 수 없어도 코드를 전달하여 표현식에 사용하고 property를 가져오거나 설정할 수 있습니다.
Key paths는 주요 3가지 variants가 있다.
Keypath : property에 read-only(읽기전용) 접근을 제공한다.
WritableKeyPath : mutable property와 read-write접근을 제공한다.
ReferenceWritableKeyPath : class instance와 같은 reference type들과 만 쓰일 수 있다. 그리고 mutable property에 readwrite access를 제공한다.
There are some additional key path types( + AnyKeyPath, APartialKeyPath) as well, that are there to reduce internal code duplication and to help with type erasure, but we’ll focus on the main types in this article.
(내부 코드 중복을 줄이고 유형을 삭제하는 데 도움이 되는 몇 가지 추가 키 경로 유형도 있지만, 여기서는 주요 유형을 중점적으로 살펴보겠습니다.)
Article
이라는 model이 있다.
struct Article {
let id: UUID
let source: URL
let title: Stirng
let body: String
}
Article
model에서 id나 source같이 하나의 값들만 추출해서 array로 만들고 싶다면 map
을 이용해서 하는 방법이 있다.
// articles = array of Article model
let articleIDs = articles.map { $0.id }
let articleSources = articles.map { $0.source }
Swift 5.2 이전에는 key path를 사용하기위해서 Sequence를 다음과 같이 extend해서 사용했었는데, 이후 버전에는 기능이 추가되었는지 확장없이 사용해도된다.
// Earlier Swift 5.2
extension Sequence {
func map<T>(_ keyPath: KeyPath<Element, T>) -> [T] {
return map { $0[keyPath: keyPath] }
}
}
let articleIDs = articles.map(\.id)
let articleSources = articles.map(\.source)
extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted { a, b in
return a[keyPath: keyPath] < b[keyPath: keyPath]
}
}
}
이렇게 하면
articles.sorted(by: \.title)
이렇게 하면 오름차순으로 정렬되겠다.
The true power of key paths comes from the fact that they let us reference a property without having to associate it with any specific instance.
(key paths의 진정한 힘은 우리가 특정한 instance와 연관시키지 않고 property를 참조할 수 있게 해준다는 사실에서 나옵니다.)
struct SongCellConfigurator {
func configure(_ cell: UITableViewCell, for song: Song) {
cell.textLabel?.text = song.name
cell.detailTextLabel?.text = song.artistName
cell.imageView?.image = song.albumArtwork
}
}
위의 코드에는 아무런 문제가 없지만, 다른 모델도 비슷한 방식으로 렌더링할 가능성이 매우 높습니다(많은 테이블 뷰 셀이 나타내는 모델에 관계없이 제목, 부제 및 이미지를 렌더링하는 경향이 있음). 따라서 주요 경로의 기능을 사용하여 공유 구성자 구현을 만들 수 있는지 살펴보겠습니다.
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) {
cell.textLabel?.text = model[keyPath: titleKeyPath]
cell.detailTextLabel?.text = model[keyPath: subtitleKeyPath]
cell.imageView?.image = model[keyPath: imageKeyPath]
}
}
-> Usage
let songCellConfigurator = CellConfigurator<Song>(
titleKeyPath: \.name,
subtitleKeyPath: \.artistName,
imageKeyPath: \.albumArtwork
)
let playlistCellConfigurator = CellConfigurator<PlayList>(
titleKeyPath: \.title,
subtitleKeyPath: \.authorName,
imageKeyPathL \.artwork
)
class ListViewController {
private var items = [Items]() { didSet { render() } }
func loadItems() {
loader.load { [weak self] items in
self?.items = items
}
}
func setter<Object: AnyObject, Value>(
for object: Object,
keyPath: ReferenceWritableKeyPath<Object, Value>
) -> (Value) -> Void {
return { [weak object] value in
object?[keyPath: keyPath] = value
}
}
Let't see if key paths again can help us make the above syntax a bit simpler, and if we also can get rid of that weak self
dance we so often have to do(and with it - the risk of accidentally introducing retain cycle if we forget to capture a weak reference to self
).
class ListViewController {
private var items = [Item]() { didSet { render() } }
func loadItems() {
loader.load(then: setter(for: self, keyPath: \.items))
}
}
이 글은 아래 포스팅을 읽고 기록한 글입니다.
https://www.swiftbysundell.com/articles/the-power-of-key-paths-in-swift/