動作原理から理解するSwiftUI

# 前置き

- まだ調べ中の部分があります。
- 主にSwift UI 1の仕様で確認しているのでSwift UI 2だと違う部分があるかもしれないです。

# SwiftUIの特徴

- iOS 13から利用可能
- UIKitの完全なリプレイス
    - Objective-Cとの決別
    - 今まで培ってきたUIKitの大部分の知見が失われたため、一から学ぶ必要があり学習コストが高い（UIViewControllerの階層構造の概念は変わっていないのでそういう部分はまだ活用できますが）。
- MVCの廃止
    - SwiftUIのデータバインディング機構のおかげでUIViewControllerが不要になった
- Storyboard, XibのXMLからプレビューのGUIを構築してUIの変更してXMLを書き換えていたのを、SwiftコードからプレビューのGUIを構築してUIを変更しSwiftコード自体を書き換えるように変わった
    - XMLのコンフリクト問題が解消！
- Swift 5.1以前の言語仕様から見ると、いくつか魔法のような挙動がある
    - State, ObservedObject, @&#xFFF0;propertyWrapper, @&#xFFF0;_functionBuilder

## 役に立つ資料

以下は一通り目を通すことをおすすめします。

- [Swift UI Tutorials](https://developer.apple.com/tutorials/swiftui/tutorials)
    - 特にSwiftUI Essentialsの3つは必ず見た方がいいです
- WWDC 2019
    - [Data Flow Through SwiftUI](https://developer.apple.com/videos/play/wwdc2019/226/)
    - [Integrating SwiftUI](https://developer.apple.com/videos/play/wwdc2019/231/)
    - [Building Custom Views with SwiftUI](https://developer.apple.com/videos/play/wwdc2019/237/)
    - [SwiftUI Essentials](https://developer.apple.com/videos/play/wwdc2019/216/)
    - [Introducing Combine](https://developer.apple.com/videos/play/wwdc2019/722/)
    - [Combine in Practice](https://developer.apple.com/videos/play/wwdc2019/721/)
- THE SWIFT PROGRAMMING LANGUAGE
    - [LANGUAGE GUIDE > Properties > Property Wrappers](https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617) 
    - → [非公式の日本語訳](https://rusutikaa.github.io/docs/docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617)
    - [LANGUAGE REFERENCE > Attributes > propertyWrapper](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID348)
    - → [非公式の日本語訳](https://rusutikaa.github.io/docs/docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID348)

**その他役立つ資料**

- [Fucking SwiftUI - UIKitとSwiftUIの対応表](https://fuckingswiftui.com)
- [RxSwiftとCombineのオペレータの対応表](https://github.com/CombineCommunity/rxswift-to-combine-cheatsheet)

# SwiftUIの動作原理

## 検証環境

- Xcode 11.5とXcode 12.0 beta3で検証

## SwiftUIはどうやってUIの再レンダリングをコントロールしているか

- `State` と `ObservedObject` のみが値を変更したときに `View` の再レンダリングが行われる
    - 自前で定義する方法については後述
- 値は `View.body` 内で変更しないと再レンダリングされない？
    - ドキュメント探し中

### State

- Stateをプロパティの属性として定義すると、そのプロパティが変更されたらViewのbodyが再計算される

```swift
struct ContentView: View {
    
  // @State属性のプロパティが変更されたらbodyを再生成している
  @State var flag = false
  
  var body: some View {
    print(#function)
    return List {
      Button(action: {
        self.flag.toggle()
      }) {
        Text("self.flagを変更: \(self.flag.description)")
      }
    }
  }
}
```

- この挙動は以下のようにドキュメントに記載されている

**[State](https://developer.apple.com/documentation/swiftui/state)**

> SwiftUI manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body. Use the state as the single source of truth for a given view.

- StateがどうやってViewを更新しているかは不明だった
- [Is it correct to expect internal updates of a SwiftUI DynamicProperty property wrapper to trigger a view update?](https://stackoverflow.com/questions/59580065/is-it-correct-to-expect-internal-updates-of-a-swiftui-dynamicproperty-property-w)

### ObservableObjectとObservedObject

- ObservableObjectプロトコルに準拠したクラス内の@&#xFFF0;Published属性があるプロパティが変更されると、そのクラスを@&#xFFF0;ObservedObject属性でプロパティを持つViewは変更のたびにbodyを再生成して画面を更新してくれるそのプロパティがバインディングされたSwiftUIのViewが更新される

```swift
final class Content: ObservableObject {
    
  // @Publshed属性はwillSetでobjectWillChangeのsend()メソッドを呼び出している
  @Published var flag = false
  
  // flagを@Published属性なしで実装すると以下
  var flag2 = false {
    willSet {
      print("manual objectWillChange.send()")
      // objectWillChangeはObservableObjectのメソッド
      // willSetのタイミングでsendしている
      objectWillChange.send() 
    }
  }
}
```

```swift
struct ContentView: View {
  
  // @ObservedObject属性はObservableObject.objectWillChangeを
  // sink (サブスクライブ)して、変更をreceiveしたタイミングでbodyを再生成している
  @ObservedObject var content = Content()
  private var cancellables = [AnyCancellable]()
  
  var body: some View {
    print(#function)
    return List {
      Button(action: {
        self.content.flag.toggle()
      }) {
        Text("@Publishedのflagを変更")
      }
      Button(action: {
        self.content.flag2.toggle()
      }) {
        Text("flag2を変更(@Publishedなし)")
      }
    }
  }
  
  init() {
    content
      .objectWillChange // objectWillChangeが呼び出されていることを確認
      .print("check")
      .sink(receiveValue: {})
      .store(in: &cancellables)
  }
}
```

- この挙動は以下のようにドキュメントに記載されている

**[ObservableObject](https://developer.apple.com/documentation/combine/observableobject)**

> By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its @Published properties changes.

**[ObservedObject](https://developer.apple.com/documentation/swiftui/observedobject)**

> A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.

- @&#xFFF0;ObservedObject属性をつけられるプロパティはObservableObjectを準拠している必要がある(`where ObjectType : ObservableObject`になっている)

### 自前実装でViewを再レンダリングをさせる方法

- 現状は完全に独自実装でViewを再レンダリングさせることは不可で、StateとObservedObjectのみが値を変更したときにViewの再レンダリングが行われるので、それらをラップする方法で実現できる
- Swift Forumsでも独自実装は不可と[回答](https://forums.swift.org/t/custom-property-wrapper-that-updates-view/31782/2)があった。

```swift
@propertyWrapper
struct Refreshing<Value> : DynamicProperty {
    let storage: State<Value>

    init(wrappedValue value: Value) {
        self.storage = State<Value>(initialValue: value)
    }

    public var wrappedValue: Value {
        get { storage.wrappedValue }

        nonmutating set { self.process(newValue) }
    }

    public var projectedValue: Binding<Value> {
        storage.projectedValue
    }

    private func process(_ value: Value) {
        // do some something here or in background queue
        DispatchQueue.main.async {
            self.storage.wrappedValue = value
        }
    }

}


struct TestPropertyWrapper: View {

    @Refreshing var counter: Int = 1
    var body: some View {
        VStack {
            Text("Value: \(counter)")
            Divider()
            Button("Increase") {
                self.counter += 1
            }
        }
    }
}
```

*※ [Is it correct to expect internal updates of a SwiftUI DynamicProperty property wrapper to trigger a view update?](https://stackoverflow.com/a/59622915/4356324)*

### bodyは変更のたびに再生成されるがパフォーマンス上の問題はないか

- SwiftUIのレンダリングシステムが賢く差分でレンダリングしてくれるので問題なし

## propertyWrapper宣言属性

- propertyWrapperはDeclaration Attributes（宣言属性）
- propertyWrapper宣言属性がある定義は `wrappedValue` というプロパティ名のgetterが必須になる（大抵はStored property）。
- propertyWrapper宣言属性がある定義はプロパティの属性に定義可能で、暗黙的に以下の3つのプロパティに変換される

```swift
@State private var flag = false
```

↓ 変換

```swift
private var _flag: State<Bool> = State(initialValue: false)
private var $flag: Binding<Bool> { return _flag.projectedValue }
private var flag: Bool {
    get { return _flag.wrappedValue }
    nonmutating set { _flag.wrappedValue = newValue }
}
```

*※ [Why I Can Mutate @State var? How Does @State Property Wrapper Work Inside?](https://forums.swift.org/t/why-i-can-mutate-state-var-how-does-state-property-wrapper-work-inside/27209/2)

```swift
@propertyWrapper
struct SomeWrapper {
  var wrappedValue: String
}

struct ContentView: View {
  @SomeWrapper var wrapper = ""

  init() {
    print(wrapper)
    print(_wrapper.wrappedValue) // wrapperと等価
  }
}
```

## DynamicPropertyプロトコル

- StateやObservedObjectはView.bodyが再生成されても前回のプロパティの値を保持している仕組みはDynamicPropertyプロトコルが処理している
- View.bodyの再生成される直前にDynamicProperty.update()が呼ばれるので、そこで

```swift
@propertyWrapper
struct Wrapper: DynamicProperty {
  var wrappedValue: Int
  
  mutating func update() {
    // StateはどうにかしてwrappedValueに前回の値を入れている
  }
}
```

## その他 Swift 5.1の機能

### 関数でのreturnを省略できるようになった

- [Implicit returns from single-expression functions](https://github.com/apple/swift-evolution/blob/master/proposals/0255-omit-return.md)

### some 構文

- Opaque Result Type (不透明な戻りの型、具体的な型はこうかいしないけど、プロトコルに準拠した何らかの型)
    - 内部実装を隠蔽しながらパフォーマンスにも影響しない手段
    - Protocolを返すと[オーバーヘッド](https://qiita.com/koher/items/338d2f2d0c4731e3508f#%E3%81%AA%E3%81%9C%E3%83%97%E3%83%AD%E3%83%88%E3%82%B3%E3%83%AB%E5%9E%8B%E3%81%AB%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9%E4%B8%8A%E3%81%AE%E3%83%AD%E3%82%B9%E3%81%8C%E3%81%82%E3%82%8B%E3%81%AE%E3%81%8B)がありパフォーマンスに影響するので、Swiftの標準ライブラリでもほとんど使われていない
- Opaque Result Type は[リバースジェネリクス](https://qiita.com/koher/items/338d2f2d0c4731e3508f#%E3%83%AA%E3%83%90%E3%83%BC%E3%82%B9%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%82%AF%E3%82%B9)のシンタックスシュガー
- 参考: [Swift 5.1 に導入される Opaque Result Type とは何か](https://qiita.com/koher/items/338d2f2d0c4731e3508f)

```swift
// リバースジェネリクス（実際はこういう構文はないけど概念）
func makeAnimal() -> <A: Animal> A {
    return Cat()
}

// Opaque Result Type
func makeAnimal() -> some Animal {
    return Cat()
}
```

### ViewBuilder

- 以下のTextは複数ありreturnしてるわけでもないのに、なぜか複数のTextをVStackに渡せている

```swift
VStack {
  Text("Turtle Rock")  
  Text("Joshua Tree National Park")
}
```

- VStackの定義は以下でクロージャーに@&#xFFF0;ViewBuilder属性がついている

```swift
@inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
```

- ViewBuilderの定義は以下で@&#xFFF0;_functionBuilder属性（宣言属性？）がついている

```swift
@_functionBuilder public struct ViewBuilder {

    /// Builds an empty view from an block containing no statements, `{ }`.
    public static func buildBlock() -> EmptyView

    /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through
    /// unmodified.
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}
```

- @&#xFFF0;_functionBuilderはドキュメントに記載がない
- @&#xFFF0;ViewBuilderは改行区切りの要素から、変数宣言と関数呼び出しを生成する
- ViewBuilderの定義は `@_functionBuilder public struct ViewBuilder`
    - @&#xFFF0;_functionBuilderはクロージャーの中で複数の子Viewを生成できるパラメータ属性
    - ViewBuilderのextensionに1から10個までViewが渡せる関数が定義されている。以下は引数10個ある定義

```swift
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
```

- クロージャに書くだけで、複数の子Viewを渡せる関数のパラメータに入れてくれるシンタックスシュガーと言える
- 10個を超えて渡したい場合はGroupを使用
- 参考: [@_functionBuilderを理解してSwiftUIイリュージョンのタネをあかす](https://qiita.com/shintykt/items/ebae99c894995311c2cd)


## State, ObservedObject, EnvironmentObject について

- 参考: [SwiftUIの機能 @State, @ObservedObject, @EnvironmentObjectの違いとは](https://www.isoroot.jp/blog/2381/)


- [How to use @EnvironmentObject to share data between views](https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views)

```swift
struct ContentView: View {
    @EnvironmentObject var settings: UserSettings

    var body: some View {
        NavigationView {
            VStack {
                // A button that writes to the environment settings
                Button(action: {
                    self.settings.score += 1
                }) {
                    Text("Increase Score")
                }

                NavigationLink(destination: DetailView()) {
                    Text("Show Detail View")
                }
            }
        }
    }
}

struct DetailView: View {
    @EnvironmentObject var settings: UserSettings

    var body: some View {
        // A text view that reads from the environment settings
        Text("Score: \(settings.score)")
    }
}
```

## EinvironmentObjectとEnvironmentについて

- スコープの違いがある
- Environmentはグローバル
- EinvironmentObjectはNavigationLinkでViewの子ならokだけど、sheetではアクセス不可なのでView階層内のみにスコープを限定
- Environmentはアプリ全体で使う環境変数として使ったり、共通で使うService層のDIとして使うのがいいと思った。

```swift
protocol APIClientType {
  
  func request()
}

final class APIClient {}

extension APIClient: APIClientType {
  
  func request() {
    print(#function)
  }
}

struct APIClientKey: EnvironmentKey {
  
  static let defaultValue: APIClientType = APIClient()
}

extension EnvironmentValues {
  
  var apiClient: APIClientType {
    get { self[APIClientKey.self] }
    set { self[APIClientKey.self] = newValue }
  }
}
```

```swift
@Environment(\.apiClient) private var apiClient
```

## `@_dynamicReplacement(for: )` について

- 調べ中

## Appleが推奨するアーキテクチャはMVCから変わった？

- 今まではMVCでしたが[Data Flow Through SwiftUI](https://developer.apple.com/videos/play/wwdc2019/226/)の資料にはFluxっぽい図が書かれていた

<img width="2129" alt="data-flow.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16700/09833d61-7309-7fa0-8659-edd7c9b6c68a.jpeg">


# 疑問

- @&#xFFF0;Stateなどの変更がどういう仕組みでViewに対しての更新処理を行わせているのかが不明
    - ObservableObjectはobjectWillChangeがあるのまだ理解可能なのですが…
    - setNeedsBodyUpdateみたいなメソッドがあるのかなと想像（[WKHostingController](https://developer.apple.com/documentation/swiftui/wkhostingcontroller/3308835-setneedsbodyupdate)にはあります）

# まとめ

- Storyboard, XibのXMLベースでGUIからUIを構築していたものから、SwiftコードベースのGUIでUIを構築するものに変わった
- CombineフレームワークのおかげでUIViewControllerが不要になった
- Swift UIはまだ発展と途上っぽいので本格的に採用は難しそう
- 現状はSwift UIよりCombineとApple純正Fluxに期待したい

# 参考

- [しくみから理解するSwiftUI](https://speakerdeck.com/kumamotone/sikumikarali-jie-suruswiftui)
- [SwiftUIで使用されているSwift5.1の新機能](https://techlife.cookpad.com/entry/2019/06/25/120000)
- [SwiftUIのProperty Wrappersとデータへのアクセス方法](https://qiita.com/shiz/items/6eaf87fa79499623306a)