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

# パート 2: SwiftUI 2.0 と UIKit
記事は3つの異なる部分に分かれている：

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

パート 2は、SwiftUI2.0アプリケーションでのUIKit要素の使用についてです。
## SwiftUIビューからUIKitビューの使用
SwiftUIでUIKitビューとビューコントローラーを使用できるようにするには、[UIViewRepresentable](https://developer.apple.com/documentation/swiftui/uiviewrepresentable)および[UIViewControllerRepresentable](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable)プロトコルに準拠するタイプでラップする必要があります。
### XCodeプロジェクト
新しい__SwiftUI__アプリプロジェクトを作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/2eb13d16-1b15-5b6c-0a80-963291f9665d.png)
プロジェクトに名前を付ける
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/ddaa13b7-77fd-7404-249b-18395aa0d9da.png)
新しい__Swift__ファイルを作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/2f326aa3-6637-e3c4-3575-4d9d5696b6e1.png)
ファイルに名前を付ける
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/28fbef11-e2b6-3d4a-0a62-61689ff347a7.png)
次に、UIKitコンポーネントをラップするために[`UIViewControllerRepresentable`](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable)プロトコルに準拠するタイプを作成する必要があります...
### ImagePicker: [`UIViewControllerRepresentable`](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable)に準拠するタイプ

まず、[`UIImagePickerController`](https://developer.apple.com/documentation/uikit/uiimagepickercontroller)をラップする`ImagePicker`タイプを作成しましょう。

[`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:ImagePicker.swift
import Foundation
import SwiftUI
import AVKit

struct ImagePicker: UIViewControllerRepresentable {
    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        // ...
    }
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
    }
}
```
次に、[`makeUIViewController(context:)`](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable/makeuiviewcontroller(context:))メソッドで[`UIImagePickerController`](https://developer.apple.com/documentation/uikit/uiimagepickercontroller) を作成しましょう：

```Swift:Swift
    let imagePicker = UIImagePickerController()
```
次に、それを設定しましょう：

- 保存した写真や動画から選択するためのユーザーインターフェイスを設定します_（新しい写真や動画を撮影するためのユーザーインターフェイスを設定するには、代わりに`.camera`を使用します）_：

```Swift:Swift
    imagePicker.sourceType = .photoLibrary
```
- メディアピッカーコントローラーで、アクセスするメディアタイプを写真と動画に指定します：

```Swift:Swift
    imagePicker.mediaTypes = UIImagePickerController.availableMediaTypes(for: .photoLibrary) ?? ["public.image", "public.movie"]
```
- 選択した静止画像と動画の編集を無効にします：

```Swift:Swift
    imagePicker.allowsEditing = false
```
- ビデオ録画とトランスコーディングの品質を設定します：

```Swift:Swift
    imagePicker.videoQuality = .typeHigh
```
- イメージピッカーのイベント通知を処理するイメージピッカーのデリゲートオブジェクトを設定します：

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

- そして最後に、[`UIImagePickerController`](https://developer.apple.com/documentation/uikit/uiimagepickercontroller)を戻します：

```Swift:Swift
    return imagePicker
```
最後に、`ImagePicker`の作成時にソースタイプを変更できるようにするには、[`makeUIViewController(context:)`](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable/makeuiviewcontroller(context:))メソッドでの設定中に使用されるデフォルトの`.photoLibrary`ソースタイプを保持する変数を定義します：

```Swift:Swift
struct ImagePicker: UIViewControllerRepresentable {
    var sourceType: UIImagePickerController.SourceType = .photoLibrary
    // ...
    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        // ...
        imagePicker.sourceType = sourceType
        imagePicker.mediaTypes = UIImagePickerController.availableMediaTypes(for: sourceType) ?? ["public.image", "public.movie"]

        // ...
    }
    // ...
}
```
[`UIImagePickerController`](https://developer.apple.com/documentation/uikit/uiimagepickercontroller)のソースタイプは、作成時に次のように変更できます：

```Swift:SwiftUI
ImagePicker(sourceType: .camera, /* ... */)
```

### Coordinator：[`UIImagePickerController`](https://developer.apple.com/documentation/uikit/uiimagepickercontroller)のデリゲート
ビューコントローラ内で発生する変更をSwiftUIインターフェイスの他の部分に伝達するには、カスタム**コーディネータ**オブジェクトをSwiftUIビューに提供する必要があります。

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

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

変更が観察される`ImagePicker`インスタンスへの参照を設定するコンストラクターを使用して`Coordinator`タイプを宣言しましょう：
 
```Swift:Swift
struct ImagePicker: UIViewControllerRepresentable {
    // ...
    final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        var parent: ImagePicker
        init(_ parent: ImagePicker) {
            self.parent = parent
        }
        // ...
    }
    // ...
}
```
##### [`imagePickerController(_:didFinishPickingMediaWithInfo:)`](https://developer.apple.com/documentation/uikit/uiimagepickercontrollerdelegate/1619126-imagepickercontroller)
「ユーザーが画像または動画を選択しました」というイベント通知をキャッチするには、[`imagePickerController(_:didFinishPickingMediaWithInfo:)`](https://developer.apple.com/documentation/uikit/uiimagepickercontrollerdelegate/1619126-imagepickercontroller)メソッドを実装して、ユーザーが静止画像または動画を選択したことをデリゲートに通知する必要があります。

そのメソッドの中で、ユーザーが画像または動画を選んだかどうかを確認します：

- ユーザーが画像を選択した場合、`info`パラメーターの`UIImagePickerController.InfoKey.originalImage`には`UIImage`オブジェクトが含まれます。
- ユーザーが動画を選んだ場合、`info`パラメーターの`.mediaURL`には`URL`オブジェクトが含まれます。

```Swift:Swift
struct ImagePicker: UIViewControllerRepresentable {
    // ...
    final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        // ...
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
                // ...
            }
            if let videoURL = info[.mediaURL] as? URL {
                // ...
            }
            // ...
        }
    }
    // ...
}
```
ユーザーが選択したファイルをSwiftUIインターフェースに伝達するために、作成時に引数として`ImagePicker`に渡される`URL`の外部`Array`へのバインディングを使用することを選択しました。

```Swift:Swift
struct ImagePicker: UIViewControllerRepresentable {
    // ...
    @Binding var selectedFiles: Array<URL>
    final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        // ...
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
                // ...
                parent.selectedFiles.append(imagePath.absoluteURL)
            }
            if let videoURL = info[.mediaURL] as? URL {
                parent.selectedFiles.append(videoURL)
            }
            // ...
        }
    }
    // ...
}
```
ユーザーが動画を選ぶと、`URL`が表示されます。これは、`URL`の配列に直接追加できます。
ただし、ユーザーが画像を選択するときは、パスの `URL`表現を`URL`の配列に追加する前に、まず一時ファイルをディスクに書き込む必要があります：

```Swift:Swift
    // ...
    if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
        let imageName = UUID().uuidString
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let imagePath = paths[0].appendingPathComponent(imageName)

        if let jpegData = image.jpegData(compressionQuality: 0.8) {
            try? jpegData.write(to: imagePath)
        }

        parent.selectedFiles.append(imagePath.absoluteURL)
    }
    // ...
}
```
[`imagePickerController(_:didFinishPickingMediaWithInfo:)`](https://developer.apple.com/documentation/uikit/uiimagepickercontrollerdelegate/1619126-imagepickercontroller)メソッドの最後のステップは、環境プロパティ`presentationMode`を使用してピッカーを閉じることです：

```Swift:Swift
struct ImagePicker: UIViewControllerRepresentable {
    // ...
    @Environment(\.presentationMode) private var presentationMode
    final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        // ...
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            // ...
            parent.presentationMode.wrappedValue.dismiss()
        }
    }
    // ...
}
```
##### カスタムCoordinatorオブジェクト
提供されているデフォルトの実装の代わりに独自の**コーディネーター**オブジェクトを提供するには、`ImagePicker`で[`makeCoordinator()`](https://developer.apple.com/documentation/swiftui/nsviewcontrollerrepresentable/makecoordinator()-1enxe)メソッドを実装する必要があります：

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

```Swift:ImagePicker.swift
import Foundation
import SwiftUI
import AVKit

struct ImagePicker: UIViewControllerRepresentable {
    var sourceType: UIImagePickerController.SourceType = .photoLibrary
    @Binding var selectedFiles: Array<URL>
    @Environment(\.presentationMode) private var presentationMode
    final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        var parent: ImagePicker
        init(_ parent: ImagePicker) {
            self.parent = parent
        }
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
                let imageName = UUID().uuidString
                let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
                let imagePath = paths[0].appendingPathComponent(imageName)
                if let jpegData = image.jpegData(compressionQuality: 0.8) {
                    try? jpegData.write(to: imagePath)
                }
                parent.selectedFiles.append(imagePath.absoluteURL)
            }
            if let videoURL = info[.mediaURL] as? URL {
                parent.selectedFiles.append(videoURL)
            }
            parent.presentationMode.wrappedValue.dismiss()
        }
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        let imagePicker = UIImagePickerController()
        imagePicker.sourceType = sourceType
        imagePicker.mediaTypes = UIImagePickerController.availableMediaTypes(for: sourceType) ?? ["public.image", "public.movie"]
        imagePicker.allowsEditing = false
        imagePicker.videoQuality = .typeHigh
        imagePicker.delegate = context.coordinator
        return imagePicker
    }
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
    }
}
```
### ContentViewのソースコード
それでは、 `ImagePicker`をアプリ` ContentView`に追加しましょう。

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

```Swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @State var imageFiles = Array<URL>()
    @State private var isShowingPhotoLibrary = false

    var body: some View {
        VStack {
            Text("Pick an Image!")
                .padding()
            Button(action: {
                self.isShowingPhotoLibrary = true
            }) {
                Image(systemName: "photo.fill")
                    .font(.system(size: 40))
            }.sheet(isPresented: self.$isShowingPhotoLibrary) {
                ImagePicker(sourceType: .photoLibrary, selectedFiles: $imageFiles)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```
## アプリ
アプリを起動し、コンテンツビューで、ボタンをクリックして画像ピッカーを開きます。
![test-uikit-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/90cd7e31-20d9-70c9-901e-13b72c0581d5.png)
ピッカーで、画像を選択します：
![test-uikit-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/a03c5147-9d6a-f4ab-5a92-0ce33f30948a.png)
[`imagePickerController(_:didFinishPickingMediaWithInfo:)`](https://developer.apple.com/documentation/uikit/uiimagepickercontrollerdelegate/1619126-imagepickercontroller)の最後に`print`ステートメントを追加して、`URL`の配列の内容を表示しました。

最初の画像を選択した後：

```
selectedFiles= [
    file:///Users/loic/Library/Developer/CoreSimulator/Devices/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/data/Containers/Data/Application/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/Documents/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
]
```
2番目の画像を選択した後：

```
selectedFiles= [
    file:///Users/loic/Library/Developer/CoreSimulator/Devices/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/data/Containers/Data/Application/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/Documents/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX,
    file:///Users/loic/Library/Developer/CoreSimulator/Devices/YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY/data/Containers/Data/Application/YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY/Documents/YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY
]
```
やった！

これで、SwiftUIでのUIKit要素の使用に関するこの記事は終わりです。
## 参照
- [Interfacing with UIKit](https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit)