[Combine] @dynamicMemberLookup 어트리뷰트로 assign에 사용할 keyPath 자동완성하기
지난 글에서 RxSwift와 Combine의 Binding 방식에 대해 알아보며, Combine에서도 RxSwift의 Binder 및 Bind와 같은 방식을 사용할 수 있는 방법을 고민해 보았습니다.
https://jazz-the-it.tistory.com/70
근본적인 문제는 combine의 assign 메서드가 keyPath를 파라미터로 요구하기 때문에, 자동완성을 사용할 수 없다는 점이었습니다. 따라서 아래와 같은 방식으로 열거형을 정의해서 가능한 input에 대한 keyPath를 return하는 방식을 사용해 보았는데, 사실 코드가 꽤 길고 optional unwrapping의 위험도 있었습니다.
extension CustomTextFieldView {
enum BindableInput {
case alert
case currentStatus
var keyPath: AnyKeyPath {
switch self {
case .alert: return \CustomTextFieldView.alertText
case .currentStatus: return \CustomTextFieldView.viewStatus
}
}
}
var alertText: String {
get { return alertlabel.text ?? "" }
set { bindAlertText(newValue) }
}
var viewStatus: Int {
get { return self.currentStatus }
set { bindViewStatus(newValue) }
}
private func bindAlertText(_ alertText: String) {
self.changeAlertLabelText(alertText)
if !alertText.isEmpty {
self.setTextFieldViewState(.alert)
}
}
func bindableInput<T>(_ input: BindableInput) -> ReferenceWritableKeyPath<CustomTextFieldView, T> {
return input.keyPath as! ReferenceWritableKeyPath<CustomTextFieldView, T>
}
}
더 좋은 방법에 대해 고민하던 중, 링크에서 Builder Pattern 구현을 위해 @dynamicMemberLookup 어트리뷰트를 사용하는 예제를 보고, 동적으로 멤버 변수들에 대해 .(dot) syntax에 접근할 수 있다면 keyPath에도 마찬가지로 접근할 수 있을 거라는 생각이 들었고, 시도해 보았습니다!
@dynamicMemberLookup
dynamicMemberLookup의 개념은 여기에 잘 정의되어 있습니다. 어트리뷰트의 기능은 이름과 같이, 말 그대로 '동적 멤버 조회'이며 python과 같은 동적 언어와의 확장성을 위해 고안되었다고 합니다. apple의 SE0195에서 친절하게 소개하고 있습니다.
이 어트리뷰트의 사용하기 위해서는 dynamicMember key또는 keyPath를 인자로 하는 subscript를 구현해주면 됩니다.
key(String)을 인자로 할 경우 동일한 기능을 구현 가능하지만 type safety를 보장할 수 없고, 이러한 단점을 극복하기 위해 타입에 대한 정보를 알고 있는 keyPath를 사용할 수 있습니다.
@dynamicMemberLookup의 본래 목적은 keyPath를 통해 getter나 setter를 입력하는 것이지만, 그러지 않고 keyPath를 그대로 return할 수 있습니다. 아래는 member 변수들에 대한 keyPath를 가져오기 위한 KeyPathFindable 프로토콜입니다.
@dynamicMemberLookup
public struct KeyFinder<Base: AnyObject> {
private var base: Base
public init(_ base: Base) {
self.base = base
}
public subscript<Value>(dynamicMember keyPath: ReferenceWritableKeyPath<Base, Value>) -> ReferenceWritableKeyPath<Base, Value> {
return keyPath
}
}
public protocol KeyPathFindable {
associatedtype Base: AnyObject
var kf: KeyFinder<Base> { get }
}
public extension KeyPathFindable where Self: AnyObject {
var kf: KeyFinder<Self> {
get { KeyFinder(self) }
set { }
}
}
extension NSObject: KeyPathFindable { }
KeyFinder Struct에 어트리뷰트를 붙여주고, 제네릭을 이용해서 keyPath를 찾을 대상이 될 타입을 정의합니다. 그리고 프로토콜 초기구현을 통해 kf라는 프로퍼티를 추가해 줍니다. 최종적으로 NSObject에 채택해 주면, 'NSObject.kf.'을 통해 KeyPath를 자동완성 시킬 수 있습니다.
Combine의 assign 메서드에, IDE의 자동완성 기능을 바탕으로 KeyPath를 넣어줄 수 있다.
이와 같이 KeyPath를 자동완성 시킬 수 있게 되었으니, 최종 목적이었던 Combine의 assign 메서드를 아래와 같이 사용할 수 있습니다.
이전과 달리 클래스 마다 구현해줄 필요도 없고, '.kf' 신택스를 이용해 편리하게 접근할 수 있습니다. 이외에도 다양한 keyPath를 요구하는 경우 사용할 수 있습니다.
output.nicknameAlert
.assign(to: nickNameTextFieldView.kf.alertText,
on: nickNameTextFieldView)
.store(in: cancelBag)
참고
링크 : 비슷한 방식으로 RxSwift Observable의 멤버 변수를 편하게 캐스팅할 때도 사용할 수 있습니다.