The Power of Key paths in Swift

Zion·2021년 12월 11일
1

계기

model을 만들면서 key path 에 대해서 알아봤다.
keyPath를 하나의 변수로 사용할 수 없을까 고민하던 중 이 글을 읽고 한번 정리해보게됐다.
내가 하고싶은 하나의 변수라 함은.

struct Zoo {
    let animal: String
    let age: Int
}

가 있을때 Zooage라는 property를 변수로 사용하고 싶은것이다.
만약

let agePathOfZooStruct = Zoo.age

이게 된다면 ... key 맵핑이랄까.. 이전에 했던 규칙없고 두서없던 model 조합을 일일이 하드코딩하지 않고 변수로 넣어서 할 수 있지 않을까... 라는 생각에 Keypath에 대해서 알아보게 됐다.

아직 솔루션은 찾지 못했지만 아래 글은 꽤 재밌다.

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.
(내부 코드 중복을 줄이고 유형을 삭제하는 데 도움이 되는 몇 가지 추가 키 경로 유형도 있지만, 여기서는 주요 유형을 중점적으로 살펴보겠습니다.)

Functional shorthands

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)

Sorting

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)

이렇게 하면 오름차순으로 정렬되겠다.

No instance required

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
  )

Converting to functions

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/

profile
어제보다만 나아지는

0개의 댓글