1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Swift】@State はどう展開されるのか?コンパイラ出力(SIL)から内部挙動を理解する

Posted at

SwiftUI で @State を使っていると、

@State var name = ""

と書いただけなのに、name / $name / _name が自然に使えることに少し不思議さを感じる。

この記事では、@State の挙動をSwift コンパイラの出力(SIL)から確認していく。

この記事でわかること

  • @State$_ の役割
  • @State で参照できるプロパティがコンパイラによってどのように生成されているか

基本のおさらい: @Stateとは

SwiftUI における @State は、View が内部で保持する状態を表すための Property Wrapper である。

@State var name = ""

上記の name はただの変数ではなく、値が変わるとSwiftUI が自動的にView を再描画するという特別な性質を持つ。

また、@State を使うと、1つの状態に対して次の3つの形が使えるようになる。

  • name
  • $name
  • _name
import SwiftUI

struct ExampleView: View {
    @State var name: String

    init(name: String) {
        _name = State(initialValue: name)
    }

    var body: some View {
        VStack() {
            Text("Hello, \(name)")
            TextField("Name", text: $name)
        }
    }
}

このコードでは、1つの @State に対して

  • name は値として使われる
  • $name は Binding として渡されている
  • _name は init で初期化に使われる

この章では、@Stateの基本的な動作を確認してきた。次の章では、この $_ が何を意味しているのかを、コードを見ながら順番に深掘りしていく。

@State の挙動をコンパイラ出力から確認する

前の章で、@State と定義することで

  • name
  • $name
  • _name

という3つの形に実際にアクセスできることを確認した。
この章では、Swift コンパイラの出力を直接確認することで、これらについて理解を深めていく。

コンパイル結果を確認する

Swift では、コンパイル過程で生成される中間表現(SIL: Swift Intermediate Language)を出力することができる。
SILでは、コンパイラが実際に生成したコードの構造や呼び出し関係を確認できる。
ここでは、@State を含む View がコンパイラによってどのような形に展開されているかを見ていく。

まず、次のコマンドを実行する。

swiftc -emit-sil ExampleView.swift

name, $name, _nameが生成されていることの確認

出力結果の中から、ExampleView に関する部分を抜粋すると、次のような定義が確認できる(一部簡略)。

@MainActor @preconcurrency struct ExampleView : View {
  @State @_projectedValueProperty($name)
  var name: String {
    get
    nonmutating set
    nonmutating _modify
  }

  var $name: Binding<String> { get }

  @_hasStorage @_hasInitialValue
  var _name: State<String> { get set }

  var body: some View { get }
}

この出力は、Swift ソースコードをそのまま写したものではなく、コンパイラが型チェック後に認識している構造を表している。

ここから分かる点

この構造を見ると、@State var name に対して、次の3つが明確に別のプロパティとして生成されていることが分かる。

name

@State @_projectedValueProperty($name) var name: String

@State が付いたStringプロパティとして存在している

_projectedValueProperty($name) という属性が付与されており、name$name がコンパイラレベルで関連付けられていることが分かる

$name

var $name: Binding<String> { get }

$name は単なる記法ではなく、Binding<String> を返す独立したcomputed property として扱われ、name とは別のプロパティとして生成されている

_name

var _name: State<String> { get set }

_name は State<String> 型で、@_hasStorage が付いており、実際のストレージを持つプロパティとなっている。@_hasInitialValueとあることから、init 内で初期化できる理由がここに表れている。

この時点で言えること

このコンパイル結果から、name$name_name はコンパイラによって生成された実在するプロパティを参照しているということがわかる。

name_name の関係

出力結果の中には、以下のような箇所も確認できる。

// ExampleView.name.getter
// Isolation: global_actor. type: MainActor
sil hidden @$s10SampleView07ExampleB0V4nameSSvg : $@convention(method) (@guaranteed ExampleView) -> @owned String {                                   
bb0(%0 : $ExampleView):
  debug_value %0, let, name "self", argno 1
  %2 = struct_extract %0, #ExampleView._name      
  retain_value %2                                 
  %4 = alloc_stack $State<String>                 
  store %2 to %4                                  
  %6 = alloc_stack $String                        
  // function_ref State.wrappedValue.getter
  %7 = function_ref @$s7SwiftUI5StateV12wrappedValuexvg : $@convention(method) <τ_0_0> (@in_guaranteed State<τ_0_0>) -> @out τ_0_0 
  %8 = apply %7<String>(%6, %4) : $@convention(method) <τ_0_0> (@in_guaranteed State<τ_0_0>) -> @out τ_0_0
  %9 = load %6             
  ...
  return %9                
} 

最初に_name を取り出してる。その後 _name.wrappedValue を参照し、それが namegetterの戻り値となっている。

つまり name_name.wrappedValue の値の String を参照していることがSIL上で確認できる。

$name_name の関係

// ExampleView.$name.getter
// Isolation: global_actor. type: MainActor
sil hidden @$s10SampleView07ExampleB0V5$name7SwiftUI7BindingVySSGvg : $@convention(method) (@guaranteed ExampleView) -> @owned Binding<String> {
                  
bb0(%0 : $ExampleView):
  debug_value %0, let, name "self", argno 1 
  %2 = struct_extract %0, #ExampleView._name
  retain_value %2                           
  %4 = alloc_stack $State<String>           
  store %2 to %4                            
  %6 = alloc_stack $Binding<String>         
  // function_ref State.projectedValue.getter
  %7 = function_ref @$s7SwiftUI5StateV14projectedValueAA7BindingVyxGvg : $@convention(method) <τ_0_0> (@in_guaranteed State<τ_0_0>) -> @out Binding<τ_0_0>
  
  %8 = apply %7<String>(%6, %4) : $@convention(method) <τ_0_0> (@in_guaranteed State<τ_0_0>) -> @out Binding<τ_0_0>
  %9 = load %6 
  ...
  return %9
} 

ここでも最初に _name を取り出している。参照しているのは State.projectedValue
戻り値は Binding<String> となっている。

これより $name_name.projectedValue の値の Binding<String> を参照していることがわかる。

コンパイル結果からわかること

上記の結果より以下のことがわかる

記法 内容
_name 値を保持 State<String>
name _name.wrappedValue String
$name _name.projectedValue Binding<String>

まとめ:@State_$ をコンパイラから理解する

この記事では、@State に対して使える

  • name
  • _name
  • $name

という3つの形について、Swift コンパイラの生成物として追ってきた。

コンパイラ視点で分かったこと

コンパイルの生成物を追うことで、@State が Property Wrapper として実際にどのようなコードに展開されているかを確認できた。

具体的には、次の点が明らかになった。

  • _nameState<String> 型のストレージ
  • name_name を経由してState.wrappedValue を参照する
  • $name_name を経由してState.projectedValueを参照する

これらはコンパイラが生成したプロパティと関数呼び出しとして存在している。

最後に

@State が Property Wrapper であること自体は、公式ドキュメントを見れば分かる。
しかし本記事では、「仕様としてそう定義されている」という説明ではなく、その仕様が、実際にどのようなコードとして生成されているかを、SIL を通して確認してきた。

上記を通じて、なぜ _name が init で必要になるのか、なぜ $nameBinding<String> になるのかといった疑問も、仕様ではなく構造として理解できたと思う。

参考

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?