[SwiftUI] $hoge.fuga는 $(hoge.fuga)가 아니다

8977 단어 SwiftSwiftUI
세세한 것입니다만, SwiftUI등에서 가끔 쓴다 $hoge.fuga 라고 하는 식은 $(hoge.fuga) 는 아니다, 라고 하는 이야기를 합니다.



우선은 기술 예를 든다.
struct Hoge {
    var fuga: String = ""
}

struct MyContentView: View {
    @State private var hoge = Hoge()

    var body: some View {
        TextField("入力してね", text: $hoge.fuga)
            .textFieldStyle(RoundedBorderTextFieldStyle())
    }
}

매우 간단합니다.

그럼, 다음과 같이 써 봅시다. 오류가 발생합니다.
//Error: '$' is not an identifier; use backticks to escape it
TextField("入力してね", text: $(hoge.fuga)) 

그럼 이것도 안돼.
//Error: Referencing subscript 'subscript(dynamicMember:)' requires wrapper 'Binding<Hoge>'
//Error: Value of type 'Hoge' has no dynamic member '$fuga' using key path from root type 'Hoge'
TextField("入力してね", text: hoge.$fuga)

이것으로 일단 $hoge.fuga$(hoge.fuga) 가 아닌 것은 알 수 있었던 것 같습니다. 그러나 그럼 $hoge.fuga 란 무엇입니까?

정체



원래 $ 를 이용한 액세스는 propertyWrapper 의 기능에 의한 것입니다. State<T>projectedValueBinding<T> 입니다. 그러므로 $hogeBinding<Hoge> 의 값입니다.

그러나 그렇게 하면 Binding<Hoge> 에 속성 fuga 가 존재하는 것이 이상하게 보일 것입니다. 이것을 실현하고 있는 것이 dynamicMemberLookup 입니다. Swift5.1 이후에 KeyPath 를 사용한 Lookup이 가능하게 되었기 때문에, 아마 Binding 가 다음과 같이 정의되고 있습니다.
@dynamicMemberLookup
struct Binding<T> {
    subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> Binding<U> {
        //処理
    }
}

이것만으로는 조금 이해하기 어렵기 때문에, 더 간단한 샘플을 보자.
@dynamicMemberLookup
private struct Wrapper<T> {
    init(wrappedValue: T) {
        self.wrappedValue = wrappedValue
    }

    private var wrappedValue: T

    subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> Wrapper<U> {
        get {
            return Wrapper<U>(wrappedValue: wrappedValue[keyPath: keyPath])
        }
    }
}

이렇게 하면 valueWrapper 형식의 값임에도 불구하고 String 가 가지고 있어야 하는 속성에 액세스할 수 있습니다. 물론 정의한 대로 Wrapped<Int> 의 값입니다.


이상으로 $hoge.fuga 의 정체는 설명이 붙습니다. $hogeState 라고 하는 propertyWrapperprojectedValue 이며, 그 값은 Binding<Hoge> 형입니다. Binding<Hoge> 형은 dynamicMemberLookup 를 서포트하고 있어 이것을 통해서 hoge 의 프로퍼티인 fugaBinding<String> 로 변환해 돌려주는 것입니다.

보충



이상뿐이라고 혹시라면 $hoge[0] 와 같은 기법이 가능한 것이 이상하게 보일지도 모릅니다. 실제로 다음은 완전히 올바르게 움직입니다.
struct MyContentView: View {
    @State private var hoge = ["A", "B", "C"]

    var body: some View {
        TextField("入力してね", text: $hoge[0])
            .textFieldStyle(RoundedBorderTextFieldStyle())
    }
}

실은 이것도 dynamicMemberLookup 로 실현되고 있습니다. 놀랄 정도로 기분 나쁘지만, subscript 에의 KeyPath 라는 것이 존재하는 덕분입니다.
let keyPath: WritableKeyPath<[Int], Int> = \.[0]

이상으로 대체로 망라 할 수 있었던 것이 아닐까 생각합니다.

참고

좋은 웹페이지 즐겨찾기