Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift에서 키 경로(Key Path)란 무엇이며, 어떻게 사용하나요? #26

Open
Phangg opened this issue Apr 26, 2024 · 2 comments
Assignees

Comments

@Phangg
Copy link
Member

Phangg commented Apr 26, 2024

Swift에서 키 경로(Key Path)란 무엇이며, 어떻게 사용하나요?

  • 키 경로 표현식(Key Path Expression)의 문법과 사용 예시를 설명해주세요.
  • 런타임에 키 경로를 사용하여 속성에 접근하는 방법은 무엇인가요?
  • 키 경로와 KVO(Key-Value Observing)의 관계를 설명해주세요.
@Phangg Phangg self-assigned this Apr 26, 2024
@Phangg
Copy link
Member Author

Phangg commented Apr 28, 2024

KeyPath

[ KeyPath 공식문서 ]

  • 평소에 우리는 SwiftUI 의 ForEach 에서 \.self 를 많이 써왔을 텐데, 이게 가장 많이 봐왔던 KeyPath 이지 않을까
  • 아, 그렇다면 정확하게 어떤 일을 하는 친구일까?
  • 어떤 타입이 가진 프로퍼티에 접근할 때, 경로를 통해 접근하는 것
  • 값에 대한 참조가 아닌, 프로퍼티 ( 이름 ) 을 참조하는 것
  • \타입이름.프로퍼티이름 의 형태를 가지고 있으며, 컴파일을 거쳐 KeyPath 클래스의 인스턴스로 생성이 됨
  • 객체의 속성에 대한 Type-Safty 참조를 제공
  • class, struct, enum 에서 모두 사용 가능
  • 컴파일 시점에 타입 검사를 제공하여 잘못된 속성 참조는 컴파일 오류가 발생
  • Swift 4 부터 사용하게 되었음

가장 기본적인 KeyPath 의 모양

KeyPath 형태

\.id 를 사용하듯.. 타입추론이 가능한 경우, BaseType 의 생략도 가능 & 프로퍼티 뒤에 추가적인 연결 가능

KeyPath 타입추론 & 연속된 프로퍼티

Optional 과 Subscript 의 사용도 가능

KeyPath 옵셔널 & 서브스크립트

KeyPath 서브스크립트 + 타입추론

KeyPath Type

KeyPath Type

코드 예시

struct Person {
    var name: String
    var age: Int
}

struct Study {
    var leader: Person
    var subject: String
    var attending: [Person]
}

//
let manchae = Person(name: "manchae", age: 26)
let phang = Person(name: "phang", age: 30)
let hamfan = Person(name: "hamfan", age: 25)
let kmh = Person(name: "kmh", age: 28)
let xohxe = Person(name: "xohxe", age: 29)

//
var study02 = Study(leader: manchae, subject: "iOS", attending: [manchae, phang, hamfan, kmh, xohxe])

// Person 의 이름에 대한 KeyPath<Person, String>
let personNameKeyPath = \Person.name

// \Study.leader 의 타입추론 형식
let leader = study02[keyPath: \.leader]
print(leader)           // Person(name: "manchae", age: 26)

// \Study.leader 의 타입추론 형식
study02[keyPath: \.leader] = xohxe
print(study02.leader)   // Person(name: "xohxe", age: 29)

// Study 의 Leader 의 age 에 대한 KeyPath<Study, Int>
// study02 의 leader 의 age 에 만들어 둔 ketpath 로 접근
let studyLeaderAgeKeyPath = \Study.leader.age
let studyLeaderAge = study02[keyPath: studyLeaderAgeKeyPath]
print(studyLeaderAge)   // 29

// KeyPath 를 타입으로 받는 메서드 사용 예시
func getStudyMemberAge(study: Study, path: KeyPath<Study, Person?>) -> Int {
    let personAgePath = path.appending(path: \.?.age)
    guard let age = study[keyPath: personAgePath] else { fatalError("Error!!") }
    return age
}
print(getStudyMemberAge(study: study02, path: \Study.attending.first))      // 26
  • .appending 으로 path 를 추가하던데.. 이건 뭐지?
  • KeyPath 를 추가해주는 것인데 아래의 순서대로 동작이 된다고 한다

KeyPath appending - 1

KeyPath appending - 1

KeyPath 의 Type

KeyPath 의 Type
Type Description
AnyKeyPath 타입이 지워진 KeyPath
PartialKeyPath 부분적으로 타입이 지워진 KeyPath
KeyPath Read-only
get하는 용도로만 사용가능
WriteableKeyPath Value type instance에 사용가능
변경 가능한 모든 Property (var) 에 대해 read & write access 제공
ReferenceWriteableKeyPath Reference type instance에 사용가능
변경 가능한 모든 Property (var) 에 대해 read & write access 제공

KeyPath 를 함수로 사용

  • Swift 5.2 에서 추가된 기능
  • map, filter 등.. 후행 클로저를 사용하던 코드에서 KeyPath 로 함수를 실행 할 수 있음
//
let manchae = Person(name: "manchae", age: 26)
let phang = Person(name: "phang", age: 30)
let hamfan = Person(name: "hamfan", age: 25)
let kmh = Person(name: "kmh", age: 28)
let xohxe = Person(name: "xohxe", age: 29)

let studyMembers = [manchae, phang, hamfan, kmh, xohxe]

// 기존 후행 클로저 사용
let memberNames = studyMembers.map { $0.name }
print(memberNames)  // ["manchae", "phang", "hamfan", "kmh", "xohxe"]

// KeyPath 함수로 사용
let membersName = studyMembers.map(\.name)
print(membersName)   // ["manchae", "phang", "hamfan", "kmh", "xohxe"]

@Phangg
Copy link
Member Author

Phangg commented Apr 30, 2024

런타임에 키 경로를 사용하여 속성에 접근하는 방법과 KVO ( Key-Value Observing )

  • 우선, KeyPath 에 대해서 다시 얘기해보면 컴파일 시점에 결정이 되기 때문에 실행 시점에 속성 이름을 변경할 수 없다는 단점이 있다.
  • 그렇기에 우리는 런타임에 동적으로 키 경로를 사용하는 방법을 궁금해하는 것..

  • KeyPath 의 처음 설명에서 Swift 4 부터 사용이 되었다고 했는데 그렇다면 그 전에는..?
  • 이전에는 KVC ( Key-Value-Coding ) 가 사용이 되었다.

KVC ( Key-Value-Coding )

  • KVC 와 KeyPath 는 모두 경로를 통해 간접적으로 객체의 속성에 접근하기 위해 사용
  • 문자열 키를 사용해서 동적으로 접근하고, 수정 가능
  • objective-C 에서 나온 개념.. ( 그래서 KVC 를 통해 접근할 프로퍼티는 @objc, dynamic키워드를 추가해야 함 )
  • 컴파일 시, 타입 안정성을 제공하지 않음 ( 런타임에서 오류가 생길 수 있음 )
  • Swfit 의 struct 와 enum 에서 사용 불가
  • setValue(_:forKey:)value(forKey:) 메서드를 사용하여 값을 설정하거나 가져올 수 있음
  • 추가적인 자세한 내용과 사용법

KVC 는 안정성이 떨어지고, 런타임에 오류를 가져올수도 있고, struct 와 enum 에서 사용이 어렵지만...
KeyPath 대신, 런타임에 키 경로를 사용하여 속성에 접근할 수 있다는 것을 알 수 있음

KVO ( Key-Value Observing )

  • Observing 이라는 말을 보면 알 수 있음..
  • 아, 뭔가 변화를 감지하겠구나 ( 그렇다 )
  • 특정한 키 값의 변화를 감지할 수 있는 기능
  • Model과 View 같이 논리적으로 분리된 파트간의 변경사항을 전달하는데 유용
  • KVO를 사용하려면, NSObject 를 상속해야 함.. ( 찐 Swift 사용만으로는 어렵다 + Class 에서만 사용할 수 있다 )
  • observe 하려는 프로퍼티에 @objcdynamic 을 추가해야 함 ( KVC..? )

사용예시

import Foundation

// class 사용, NSObject 상속
class Person: NSObject {
    // @objc Attribute 사용 + dynamic Modifire 사용
    @objc dynamic var name: String
    
    init(name: String) {
        self.name = name
    }
}

var person = Person(name: "Phang")

// Observer

// old, new - 변경 전, 변경 후
var person1 = Person(name: "Phang")
person1.observe(\.name, options: [.old, .new]) { (object, change) in
    print("Name changed from \(change.oldValue) to \(change.newValue)")
}
person1.name = "" // Name changed from Optional("Phang") to Optional("팽")

// initial - Observer 등록 전
var person2 = Person(name: "Kim")
person2.observe(\.name, options: [.old, .new, .initial]) { (object, change) in
    print("Name changed from \(change.oldValue) to \(change.newValue)")
} 
// Name changed from nil to Optional("Kim")

// prior - 변경 전, 후
var person3 = Person(name: "Hwang")
person3.observe(\.name, options: [.old, .new, .prior]) { (object, change) in
    print("Name changed from \(change.oldValue) to \(change.newValue)")
}
person3.name = "Choi"
// Name changed from Optional("Hwang") to nil
// Name changed from Optional("Hwang") to Optional("Choi")

위 코드의 options 에 대한 설명

old - 변경 전 값
new - 변경 후 값
initial - Observer 등록 전 handler 호출 시 (newValue로 들어감)
prior - 변경 전, 후 상태 모두 파악시

KVO 는 분리된 파트간의 변경사항을 전달할 수 있고, 내부 소스 변경 없이 상태변화에 대응하고, 변경 전후의 값을 알 수 있는 장점이 있지만, NSObject 를 상속받아야하고, dealloc 시 옵저버를 지워줘야 하는 불편함이 있다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant