관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] KeyPath 이해하기 (map(\.), 디코딩 map atKeyPath) 본문

iOS 응용 (swift)

[iOS - swift] KeyPath 이해하기 (map(\.), 디코딩 map atKeyPath)

jake-kim 2022. 5. 25. 01:22

KeyPath 이해하기

  • Key의 개념은 Objective-C에서 프로퍼티에 접근할 때 사용하는 개념
    • 단순히 Key는 문자열 값이고, 이 문자열 값을 가지고 프로퍼티에 접근하는 방식
    • 구체적인 개념은 이전 포스팅 글 참고
  • KeyPath는 Root라는 타입으로부터 구체적인 Value Type으로의 key의 경로를 의미

https://developer.apple.com/documentation/swift/keypath

 

ex) KeyPath 개념 이해하기

  • 이전 포스팅 글에서 알아본 KVC (Key Value Coding)에서 key값으로 프로퍼티에 접근하지만 또다른 방법으로 KeyPath를 통해 접근이 가능
// KVC에서 Key값으로 접근하는 방법

class Person: NSObject { // NSObject 서브클래싱
  @objc var name: String? // @objc 어노테이션
}

// KVC - key로 프로퍼티 값 접근
let person = Person()
person.value(forKey: "name") // nil
person.setValue("jake", forKey: "name")
person.value(forKey: "name") // "jake"
  • KeyPath를 통해 접근하는 방법
    • KeyPath의 개념에 대입하여 이해: KeyPath는 Root라는 타입(=Person타입)으로부터 구체적인 Value Type(프로퍼티타입)으로의 key의 경로를 의미
    • keyPath로 인스턴스 안 프로퍼티에 접근이 가능
person[keyPath: \Person.name]
  • Root 타입은 생략 가능
person[keyPath: \.name]

KeyPath를 직접 정의하여 사용하는 방법

(KeyPath를 사용하지 않은 경우)

  • School의 him, han에 메소드를 통해서 갖고오고 싶은 경우 각각 getKim(), getHan()을 정의하여 사용
struct PersonInfo {
  var name: String
  var age: Int
}

struct School {
  var kim: PersonInfo
  var han: PersonInfo
  
  func getKim() -> PersonInfo {
    return self.kim
  }
  func getHan() -> PersonInfo {
    return self.han
  }
}

let kim = PersonInfo(name: "jake", age: 12)
let han = PersonInfo(name: "jack", age: 13)
let school = School(kim: kim, han: han)

(KeyPath를 사용한 경우)

  • KeyPath를 직접 정의하여 사용 KeyPath<School, PersonInfo>
    • Root타입이 School이고, Value타입이 프로퍼티의 타입인 PersonInfo
extension School {
  func getSchool(keyPath: KeyPath<Self, PersonInfo>) -> PersonInfo {
    self[keyPath: keyPath]
  }
}

print(school.getSchool(keyPath: \.kim))
print(school.getSchool(keyPath: \.han))

KeyPath를 사용하는 이유

  • KVC(Key Value Coding)에서는 프로퍼티에 접근할 때 문자열 key값을 이용했지만, KeyPath를 이용하면 문자열이 아닌 프로퍼티  이름으로 접근이 가능 -> runtime error 방지에 유리
// KeyPath를 사용하지 않은 경우 -> 문자열을 직접 입력하므로 오타날 가능성이 존재
person.value(forKey: "name")

// KeyPath를 사용한 경우
person[keyPath: \.name]
  • swift에서는 KeyPath를 통해 'syntactic sugar' (간결한 코드)를 사용할 수 있는 테크닉이 존재

ex1) keyPath 예제 - map

struct SomeModel {
  let name: String
  let age: Int
}
let someModel1 = SomeModel(name: "jake", age: 12)
let someModel2 = SomeModel(name: "lee", age: 32)

let res1 = [someModel1, someModel2].map(\.name)

ex2) keyPath 예제 - compactMap

Optional인 경우, \.?.으로 접근

let someModel3: SomeModel? = SomeModel(name: "han", age: 20)
let someModel4: SomeModel? = nil

let res1 = [someModel1, someModel2].map(\.name)
let res2 = [someModel3, someModel4].compactMap(\.?.name)

ex3) RxSwift를 사용하고, 서버로부터 받은 특정 모델을 디코딩하고 싶은 경우

// 예시로 사용할 json

{
  "items":[
     {
        "name":"jake",
        "age": 12
     },
     {
        "name":"jake",
        "age": 30
     },
  ]
}
  • 사용하는쪽에서 keyPath를 사용하지 않으면 아래처럼 모델을 생성
    • 사용하는쪽에서 map(MyResponseModel.self)과 같이 사용
    • 불필요한 users프로퍼티가 존재하여, 사용하는 쪽에서도 users.로 접근해야하는 경우가 발생
// 사용하는쪽에서 keyPath x
struct MyResponseModel: Codable {
  struct User: Codable {
    let name: String
    let age: Int
  }
  enum CodingKeys: String, CodingKey {
    case users = "items"
  }
  let users: [User]
}
  • 사용하는쪽에서 keyPath를 사용하면 모델을 더욱 단순화하고 불필요한 users 프로퍼티를 삭제
    • 사용하는쪽에서 map([MyResponseModel2].self, atKeyPath: "items")과 같이 사용
// 사용하는쪽에서 keyPath o
struct MyResponseModel2: Codable {
  let name: String
  let age: Int
}

사용하는쪽 예시) map(_:atKeyPath:)로 사용

// https://stackoverflow.com/questions/55847632/rxswift-api-response

func getMostPopularRepositories(byLanguage language: String) -> Observable<[Repository]> {
    let encodedLanguage = language.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)!

    let provider = MoyaProvider<Github>(plugins: [NetworkLoggerPlugin(verbose: true)])
    let parameters = [“q”: “language:\(encodedLanguage)“,“sort”: “stars”]

    let request = provider.rx.request(.repositories(parameters)).asObservable()
    let pRepos = request.map([Repository].self, atKeyPath: “items”)
    return pRepos
}

ex4) RxSwift를 사용하고, 서버로부터 받은 특정 모델을 디코딩하고 싶은 경우 2번째

{
   "profile":{
      "user_id":"abcd",
      "secret":{
         "age":12
      }
   }
}
  • keyPath 사용 x
    • 불필요한 profile 프로퍼티가 존재 (사용하는 쪽에서도 myResponseModel3.profile.으로 접근해야하는 경우가 발생)
struct MyResponseModel3: Codable {
  struct User: Codable {
    struct Secret: Codable {
      let age: Int
    }
    enum CodingKeys: String, CodingKey {
      case userID = "user_id"
      case age
    }
    let userID: String
    let age: Int
  }
  let profile: [User]
}

// map(MyResponseModel3.self)
  • KeyPath 사용 o
struct MyResponseModel4: Codable {
  struct Secret: Codable {
    let age: Int
  }
  enum CodingKeys: String, CodingKey {
    case userID = "user_id"
    case age
  }
  let userID: String
  let age: Int
}

// map([MyResponseModel4].self, atKeyPath: "profile")

cf) atKeyPath에는 키를 연달아서 접근 가능 (items.someSubName1.someSubName2)

RxSwift의 map 인자에 있는 atKeyPath 구현부

* 전체 코드: https://github.com/JK0369/ExKeyPath

* 참고

https://stackoverflow.com/questions/55847632/rxswift-api-response

https://developer.apple.com/documentation/swift/keypath

Comments