SwiftUI 2.0と UIKit と ストーリーボード (パート 1)

# パート 1: SwiftUI 2.0 と ストーリーボード
記事は3つの異なる部分に分かれている：

- パート 1: SwiftUI 2.0 と ストーリーボード ([English](https://qiita.com/loic-meister-guild/items/61256e5edf06af48eb30))
- [パート 2: SwiftUI 2.0 と UIKit](https://qiita.com/loic-meister-guild/items/a45574e6059209b8c87a)
- [パート 3: ストーリーボード と SwiftUI 2.0](https://qiita.com/loic-meister-guild/items/609a0d91498776b2cc85)

パート 1は、SwiftUI2.0アプリケーションでのストーリーボードのビューの使用についてです。
## SwiftUIビューからストーリーのビューコントローラーの使用
SwiftUIでストーリーボードビューコントローラーを使用できるようにするには、[UIViewRepresentable](https://developer.apple.com/documentation/swiftui/uiviewrepresentable)および[UIViewControllerRepresentable](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable)プロトコルに準拠するタイプでラップする必要があります。レガシーアプリの一部を再利用したり、ストーリーボードとUIKitで可能であるが、SwiftUIではまだ可能ではないことを実行する場合に役立ちます。
### XCodeプロジェクト
新しい__SwiftUI__アプリプロジェクトを作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/2eb13d16-1b15-5b6c-0a80-963291f9665d.png)
プロジェクトに名前を付ける
![Screen Shot 2021-05-19 at 15.40.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/72e884d8-197f-7728-d20f-2f3b2f2c96b9.png)
新しいストーリーボードのファイルを作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/b1a32fd5-d04f-3ba2-cc04-f56cb9ddbcaf.png)
ストリーボードに名前を付ける
![Screen Shot 2021-05-19 at 15.45.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/d9b94e5d-fe97-59aa-6142-0c307a29bf4a.png)
次に、デフォルトの`UIViewController`にUI要素を挿入して、最初のビューコントローラーにする
![Screen Shot 2021-05-19 at 22.50.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/a741f2b2-e277-a350-6cd3-c2dd1e561632.png)
これでストーリーボードを含むSwiftUIプロジェクトができましたが、SwiftUIプロジェクトのデフォルトの `ContentView`はまだ表示されています。
それらを接続する必要があります...
### StoryboardViewController：カスタムの`UIViewController`のクラス
まず、カスタムの `UIViewController`クラスをストーリーボードビューコントローラーに追加します...

`UIViewController`のサブクラスである` StoryboardViewController`クラスのSwiftファイルを作成します：

```Swift:StoryboardViewController.swift
import SwiftUI

class StoryboardViewController: UIViewController {
}
```
次に、`IBOutlet`と`IBAction`を使用して、クラスをストーリーボードビューコントローラーのレイアウトのUI要素に接続します：
- `labelText`：2番目の`Label`要素に接続された`IBOutlet`。
- `inputText`：`TextField`要素に接続された`IBOutlet`。
- `changeText(_:)`：「ChangeText！」ボタンの`TouchUpInside`イベントに接続された`IBAction`。
- `goBack(_:)`：「GoBack！」ボタンの`TouchUpInside`イベントに接続された`IBAction`。

```Swift:StoryboardViewController.swift
class StoryboardViewController: UIViewController {
    @IBOutlet weak var labelText: UILabel!
    @IBOutlet weak var inputText: UITextField!
    @IBAction func changeText(_ sender: Any) {
        // ...
    }
    @IBAction func goBack(_ sender: Any) {
        // ...
    }
}
```
最後に、デリゲートプロトコル（次のセクションで定義されます）への参照を追加し、対応する `IBAction`でそのメソッドを呼び出します：

```Swift:StoryboardViewController.swift
import SwiftUI

class StoryboardViewController: UIViewController {
    var delegate: StoryboardViewControllerDelegate = DefaultStoryboardViewControllerDelegate()
    @IBOutlet weak var labelText: UILabel!
    @IBOutlet weak var inputText: UITextField!
    @IBAction func changeText(_ sender: Any) {
        delegate.didPressChangeText(self)
    }
    @IBAction func goBack(_ sender: Any) {
        delegate.didPressGoBack(self)
    }
}
```
### StoryboardViewControllerDelegate
それでは、ストーリーボードビューコントローラー内で発生する変更をSwiftUIインターフェイスの他の部分に伝達するために使用されるデリゲートプロトコルを作成しましょう...

Swiftファイルを作成して、下記の2つのタイプを中で宣言します：

- `StoryboardViewControllerDelegate`：`StoryboardViewController`に使用されるデリゲートプロトコル。
- `DefaultStoryboardViewControllerDelegate`：`StoryboardViewControllerDelegate`のデフォルトの実装。メソッドが呼び出されても何も実行されません。

```Swift:StoryboardViewControllerDelegate.swift
import Foundation

protocol StoryboardViewControllerDelegate: AnyObject {
    func didPressChangeText(_ storyboardVC: StoryboardViewController)
    func didPressGoBack(_ storyboardVC: StoryboardViewController)
}

class DefaultStoryboardViewControllerDelegate: StoryboardViewControllerDelegate {
    func didPressChangeText(_ storyboardVC: StoryboardViewController) {}
    func didPressGoBack(_ storyboardVC: StoryboardViewController) {}
}
```
`StoryboardViewControllerDelegate`プロトコルには2つのメソッドがあります：

- `didPressChangeText(_:)`：ユーザーが「ChangeText！」ボタンを押したときに呼び出されるメソッド。
- `didPressGoBack(_:)`：ユーザーが「GoBack！」ボタンを押したときに呼び出されるメソッド。

簡単にするために、両方のメソッドには1つのパラメーターしかありません。変更が監視される`StoryboardViewController`インスタンスです。
### StoryboardView：[`UIViewControllerRepresentable`](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable)に準拠するタイプ

次に、`StoryboardViewController`をラップする`StoryboardView`タイプを作成しましょう。

[`UIViewControllerRepresentable`](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable)プロトコルには、次の2つの要件があります：
- `UIViewController`を作成および設定する[`makeUIViewController(context:)`](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable/makeuiviewcontroller(context:))メソッド
- 指定されたビューコントローラーの状態をSwiftUIからの新しい情報で更新する[`updateUIViewController(_:context:)`](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable/updateuiviewcontroller(_:context:))メソッド。この例では使用しないため、**空白のままにします**。

```Swift:StoryboardView.swift
import SwiftUI

struct StoryboardView: UIViewControllerRepresentable {
    func makeUIViewController(context: UIViewControllerRepresentableContext<StoryboardView>) -> StoryboardViewController {
        // ...
    }
    func updateUIViewController(_ uiViewController: StoryboardViewController, context: UIViewControllerRepresentableContext<StoryboardView>) {
    }
}
```

次に、[`makeUIViewController(context:)`](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable/makeuiviewcontroller(context:))メソッドを実装して、ストーリーボードから`StoryboardViewController`のインスタンスを戻します。 SwiftUIによって呼び出されたとき。 SwiftUIは、ビューを表示する準備ができたときにこのメソッドを1度呼び出し、その後、ビューコントローラーのライフサイクルを管理します。

これを行うには、ストーリーボードから`StoryboardViewController`を取得する必要があります：

- ストーリーボードを取得するには、ストーリーボードファイルの名前で`UIStoryboard`オブジェクトをインスタンス化します（ここでは、ファイルの名前が「Main.storyboard」であるため、ストーリーボードの名前は「Main」です）。

```Swift:Swift
let storyboard = UIStoryboard(name: "Main", bundle: nil)
```

- `StoryboardViewController`を取得するには、[`instantiateInitialViewController()`](https://developer.apple.com/documentation/uikit/uistoryboard/1616213-instantiateinitialviewcontroller)メソッドを使用してストーリーボードの初期ビューコントローラーをインスタンス化するだけです。

```Swift:Swift
let storyboardVC = storyboard.instantiateInitialViewController()
```
※ もう1つの方法は、ストーリーボードのビューコントローラーに__storyboardID__ （ "storyboardView"など）を設定し、ビューコントローラーの__storyboardID__を引数として使用する[`instantiateViewController(withIdentifier:)`](https://developer.apple.com/documentation/uikit/uistoryboard/1616214-instantiateviewcontroller)メソッドを呼び出すこと：

```Swift:Swift
let storyboardVC = storyboard.instantiateViewController(withIdentifier: "storyboardView")
```

- `StoryboardView`の`StoryboardViewController`のイベント通知を処理するデリゲートオブジェクトの`Coordinator`を設定します：

```Swift:Swift
    storyboardVC.delegate = context.coordinator
```
※　次のセクションでデリゲートオブジェクトの`Coordinator`について説明します...

- そして最後に、`StoryboardViewController`を戻します：

```Swift:Swift
    return storyboardVC
```
### Coordinator: `StoryboardViewController`のデリゲート
ビューコントローラ内で発生する変更をSwiftUIインターフェイスの他の部分に伝達するには、カスタム**コーディネータ**オブジェクトをSwiftUIビューに提供する必要があります。

ストリーボードのコンポーネントの変更を聞くために、デリゲートを使用する。デリゲートは、イベント自体がシステムによって処理される前または後にイベント通知を処理するために使用されます。

また、`StoryboardViewController`の変更を聞くには、`Coordinator`が`StoryboardViewControllerDelegate`プロトコルに準拠する必要があります。また、`NSObject`から継承して、Swiftで宣言できない[`NSObjectProtocol`](https://developer.apple.com/documentation/objectivec/nsobjectprotocol)プロトコルに準拠する必要があります。

変更が観察される`StoryboardView`インスタンスへの参照を設定するコンストラクターを使用して`Coordinator`タイプを宣言しましょう：
 
```Swift:Swift
struct StoryboardView: UIViewControllerRepresentable {
    // ...
    final class Coordinator: NSObject, StoryboardViewControllerDelegate {
        var parent: StoryboardView
        init(_ parent: StoryboardView) {
            self.parent = parent
        }
        // ...
    }
    // ...
}
```
下記で、`StoryboardViewControllerDelegate`に必要な2つのメソッドを実装します：

##### 1.`didPressChangeText(_:)`
`labelText`の内容を` StoryboardViewController`の `inputText`の内容に変更します：

```Swift:Swift
    func didPressChangeText(_ storyboardVC: StoryboardViewController) {
        storyboardVC.labelText.text = storyboardVC.inputText.text
        storyboardVC.inputText.text = ""
    }
```

##### 2.`didPressGoBack(_:)`
`Coordinator`の親から`presentationMode`の環境プロパティを使用してビューを閉じるだけです：

```Swift:Swift
    func didPressGoBack(_ storyboardVC: StoryboardViewController) {
        parent.presentationMode.wrappedValue.dismiss()
    }
```
##### カスタムCoordinatorオブジェクト
提供されているデフォルトの実装の代わりに独自の**コーディネーター**オブジェクトを提供するには、`StoryboardView`で[`makeCoordinator()`](https://developer.apple.com/documentation/swiftui/nsviewcontrollerrepresentable/makecoordinator()-1enxe)メソッドを実装する必要があります：

```Swift:Swift
struct StoryboardView: UIViewControllerRepresentable {
    // ...
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    // ...
}
```
### StoryboardViewのソースコード

```Swift:StoryboardView.swift
import SwiftUI

struct StoryboardView: UIViewControllerRepresentable {
    @Environment(\.presentationMode) private var presentationMode
    final class Coordinator: NSObject, StoryboardViewControllerDelegate {
        var parent: StoryboardView
        init(_ parent: StoryboardView) {
            self.parent = parent
        }
        func didPressChangeText(_ storyboardVC: StoryboardViewController) {
            storyboardVC.labelText.text = storyboardVC.inputText.text
            storyboardVC.inputText.text = ""
        }
        func didPressGoBack(_ storyboardVC: StoryboardViewController) {
            parent.presentationMode.wrappedValue.dismiss()
        }
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    func makeUIViewController(context: UIViewControllerRepresentableContext<StoryboardView>) -> StoryboardViewController {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let storyboardVC = storyboard.instantiateInitialViewController() as! StoryboardViewController
        storyboardVC.delegate = context.coordinator
        return storyboardVC
    }
    func updateUIViewController(_ uiViewController: StoryboardViewController, context: UIViewControllerRepresentableContext<StoryboardView>) {
    }
}
```
### ContentViewのソースコード
それでは、 `StoryboardView`をアプリ`のContentView`に追加しましょう。

[`Button`](https://developer.apple.com/documentation/swiftui/button)を使用し、[`sheet(isPresented:onDismiss:content:)`](https://developer.apple.com/documentation/SwiftUI/AnyView/sheet(isPresented:onDismiss:content:))メソッドを使用してシートで`StoryboardView`を表示します：

```Swift:ContentView.swift
import Foundation
import UIKit
import SwiftUI

struct ContentView: View {
    @State private var isShowingStoryboardView = false

    var body: some View {
        VStack {
        Text("Launch Storyboard View!")
            .padding()
            Button(action: {
                self.isShowingStoryboardView = true
            }) {
                Image(systemName: "play.fill")
                    .font(.system(size: 40))
            }.sheet(isPresented: self.$isShowingStoryboardView) {
                StoryboardView()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```
## アプリ
アプリを起動し、`ContentView`で「▶」ボタンをクリックして、ストリーボードのビューコントローラーが表示される：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/bcd1aef8-45e0-ca41-e170-8f0d78cbc4d9.png)
ストリーボードのビューで、テキストフィールドに新しい文章を入力する：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/060bad35-165d-b314-9926-b0b34326bb50.png)
次に、「Change Text!」ボタンをクリックすると、2番目のText要素の「InitialText」が新しい文章に置き換えられる：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/c1e1fe3a-09d1-b800-05a0-dc97d522a4b6.png)
最後に、「Go Back!」ボタンをクリックすると、ストーリーボードビューを表示しているシートは却下されます：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/bcd1aef8-45e0-ca41-e170-8f0d78cbc4d9.png)
やった！