SwiftUIを支えている(かもしれない?) EnclosingSelfの話

4 min読了の目安(約4100字TECH技術記事

ある日、不思議な記述を見つけた

apple の swift のリポジトリを眺めていたら、見たことない記述を見つけました。
それがこちら ↓
propertyWrapper なのですが、wrappedValue にアクセスすると fatalError が発行され、subscript~~ の謎の記述があります。
この subscript~~ 節(以下、EnclosingSelf(便宜的に))が中々凄かったという話です。
おそらく EnclosingSelf を利用することで、SwiftUI の Published の値の更新による View の再構成が実行される仕組みが実装されてるのではと想像しています。
今回は、この EnclosingSelf の挙動を確認して、SwiftUI で使用する Published を自作してみました。

@propertyWrapper
struct Observable<Value> {
  private var stored: Value
  ~~ 中略 ~~
  var wrappedValue: Value {
    get { fatalError("called wrappedValue getter") }
    set { fatalError("called wrappedValue setter") }
  }
  static subscript<EnclosingSelf>(
      _enclosingInstance observed: EnclosingSelf,
      wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
      storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
    ) -> Value {
    get {
      observed[keyPath: storageKeyPath].stored
    }
    set {
      observed[keyPath: storageKeyPath].stored = newValue
    }
  }
}

https://github.com/apple/swift/blob/main/test/decl/var/property_wrappers_synthesis.swift

EnclosingSelf のココがすごいかも?

EnclosingSelf を使うと出来ること

次のコードの出力結果はどうなると思いますか?
hoge に 1 足されるだけなので、何も起出力されないはずです。

class Usage {
    @Wrapper var hoge: Int = 0
    func addOne() {
        hoge = 1
    }
    func fire() {
        print("fire!!")
    }
}
let usage = Usage()
usage.addOne()

実行結果がこちらです↓
呼び出していない Usage.fire() が呼び出されています。

fire!!

Mirror を使うことでも実現出来そうですが、EnclosingSelf を使うとこの挙動を簡単に実装出来ます。

EnclosingSelf の実装方法

上の不思議な挙動を実現した property wrapper です。
上記の場合、EnclosingSelf には Usage が入っています。

  • wrappedValue の get / set 節は呼ばれないです。
    ただし、wrappedValue に set 節が存在しない場合、EnclosingSelf の方に
    set 節があっても、set 出来ずにコンパイルエラーになります。
  • @Wrapper がついたプロパティに代入されると、subscript 節が実行されます。
    enclosingSelf に自身を保持しているインスタンスが入っています。
@propertyWrapper
struct Wrapper<Value> {
    private var value: Value
    init(wrappedValue: Value) {
        value = wrappedValue
    }
    var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }
    static subscript<EnclosingSelf>(
        _enclosingInstance enclosingSelf: EnclosingSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
    ) -> Value {
        get {
            return enclosingSelf[keyPath: storageKeyPath].value
        }
        set {
            enclosingSelf[keyPath: storageKeyPath].value = newValue
            (enclosingSelf as? Usage)?.fire()   // Usage の fire を呼べる!!
        }
    }
}

Published を作ってみた

SwiftUI は ObservableObject.objectWillChange にイベントが流れると View が更新されます。
この↓プログラムを実行すると、ボタンを押すと View が更新されることを確認出来ます。
ContentViewModel.action で title に新しい値が代入されると、MyPublished の EnclosingSelf の subscript 節が呼び出され、ContentViewModel.objectWillChange にイベントが流れて、ContentView が再構築されます。

struct ContentView: View {
    @ObservedObject var viewModel = ContentViewModel()
    var body: some View {
        Button("\(viewModel.title)", action: viewModel.action)
    }
}

class ContentViewModel: ObservableObject {
    @MyPublished var title: String = "prepared"
    func action() {
        title = "start!!"
    }
}

@propertyWrapper
public struct MyPublished<Value> {
    private var value: Value
    public init(wrappedValue: Value) {
        value = wrappedValue
    }
    public var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }
    public static subscript<EnclosingSelf: ObservableObject>(
        _enclosingInstance object: EnclosingSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
    ) -> Value {
        get {
            return object[keyPath: storageKeyPath].value
        }
        set {
            object[keyPath: storageKeyPath].value = newValue
            // 値が更新されたことを通知する
            (object.objectWillChange as? ObservableObjectPublisher)?.send()
        }
    }
}

さいごに

たまに swift のリポジトリを眺めると知らない機能がいっぱいで楽しいです。