【SwiftUI】カメラorアルバムから取得した画像をトリミングして表示するアプリ

# はじめに

初めまして、たなぼうと申します。
記事を書いている現在は学生であり、独学でiosアプリを開発をしています。

今回のテーマは
__・カメラorアルバムから取得した画像をトリミングして表示したい！__

私は「画像のトリミング」ってどうやるの？って悩んだので、共有する記事になります。

全コードは以下に載せてます。
https://github.com/RyotaLab/trimmingImage.git

# 完成イメージ

![cropgif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692824/09257442-285a-e45b-eb07-3d3ca356d687.gif)

カメラorアルバムから画像を取得し、トリミングするアプリです。

今回のアプリでは[CropViewController](https://github.com/TimOliver/TOCropViewController)というライブラリを使用しています。

# 1.下準備

コードを書く前に、以下の準備が必要です。

* ライブラリ：CropViewControllerをインストール
* Xcode上でinfoにPrivacy - Camera Usage Descriptionを追加

![スクリーンショット 2024-09-09 12.31.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692824/98fcaa83-27c1-5c0b-0d6b-e6c4df434897.png)

# 2.実装

ここからは実際のコードをもとに、意味やポイントについて解説していきます。
以下の3ファイルを順番に作成or更新します。
* CameraController
* imageCropper
* ContentView

## 2-1.カメラの準備

写真を撮影するviewを用意します。
image：UIImage?を引数に持ち、写真を撮影後、imageの内容を変更するというものです。

こちらに関してはテンプレが以下の記事に存在します。ありがたいですね。
[【SwiftUI】カメラを使いたい](https://qiita.com/SNQ-2001/items/2cc6e7e35ab98ba02397)

コードは以下の通りです。

<details><summary><b>━━━━━━━━━━━━━━━━━━┓<br>┃&nbsp;&nbsp;&nbsp;&nbsp;ソースコードを表示（折りたたみ）&nbsp;&nbsp;&nbsp;┃<br>┗━━━━━━━━━━━━━━━━━━┛</b></summary><div>

~~~swift:CameraController
import Foundation
import SwiftUI


import SwiftUI

public struct CameraView: UIViewControllerRepresentable {
    @Binding private var image: UIImage?

    @Environment(\.dismiss) private var dismiss

    public init(image: Binding<UIImage?>) {
        self._image = image
    }

    public func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    public func makeUIViewController(context: Context) -> UIImagePickerController {
        let viewController = UIImagePickerController()
        viewController.delegate = context.coordinator
        if UIImagePickerController.isSourceTypeAvailable(.camera) {
            viewController.sourceType = .camera
        }

        return viewController
    }

    public func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
}

extension CameraView {
    public class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        let parent: CameraView

        init(_ parent: CameraView) {
            self.parent = parent
        }

        public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
            if let uiImage = info[.originalImage] as? UIImage {
                self.parent.image = uiImage
            }
            self.parent.dismiss()
        }

        public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            self.parent.dismiss()
        }
    }
}
~~~

</div></details>

```swift:使用例
CameraView(image: $UIImage?)
```

## 2-2.画像をトリミングする準備


画像をトリミングするviewを作成します。ここが肝ですね。
以下の引数を持ちます。
* image：UIimage?
* visible：Bool
* done：Void

imageはトリミングする画像です。
visibleはトリミングが終わった時、トリミングの画面を非表示する時に使用。
doneはトリミングが完了した時に呼ばれる自分で設定する関数です（後で解説）。

コードは以下の通り。

<details><summary><b>━━━━━━━━━━━━━━━━━━┓<br>┃&nbsp;&nbsp;&nbsp;&nbsp;ソースコードを表示（折りたたみ）&nbsp;&nbsp;&nbsp;┃<br>┗━━━━━━━━━━━━━━━━━━┛</b></summary><div>

~~~swift:imageCropper
import Foundation
import SwiftUI
import UIKit
import CropViewController

struct ImageCropper: UIViewControllerRepresentable{
    
    @Binding var image: UIImage?
    @Binding var visible: Bool
    
    var done: (UIImage) -> Void
    
    class Coordinator: NSObject, CropViewControllerDelegate{
        let parent : ImageCropper
        
        
        
        init(_ parent: ImageCropper){
            self.parent = parent
        }
        
        
        //編集完了時(done）
        func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
            
            print("didcroped")
            parent.done(image)
            self.parent.visible = false
        }
        
        //cancel時
        func cropViewController(_ cropViewController: CropViewController, didFinishCancelled cancelled: Bool) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                withAnimation{
                    self.parent.visible = false
                }
            }
        }
    }//class
    
    func makeCoordinator() -> Coordinator {
         return Coordinator(self)
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
    
    
    func makeUIViewController(context: Context) -> some UIViewController {
        let img = self.image ?? UIImage()
        let cropViewController = CropViewController(image:img)
        cropViewController.delegate = context.coordinator
        cropViewController.aspectRatioPreset = .presetSquare
        cropViewController.aspectRatioPickerButtonHidden = true
        cropViewController.aspectRatioLockEnabled = true
        return cropViewController
    }
    
}
~~~

</div></details>

余談ですが、トリミング方法やアスペクト比などを自分で設定できる場所があります。
```swift:ここは自分で設定しよう
cropViewController.aspectRatioPreset = .presetSquare
cropViewController.aspectRatioPickerButtonHidden = true
cropViewController.aspectRatioLockEnabled = true
```
設定できる点は以下の記事で網羅されているので、詳しく知りたい方は以下をご覧ください。
設定の参考：[【Swift】画像をトリミングできるライブラリを使ってみた](https://dev.classmethod.jp/articles/torimin-good/)
SwiftUI実装の参考：https://github.com/TimOliver/TOCropViewController/issues/421

## 2-3.ContentViewで実装

<details><summary><b>━━━━━━━━━━━━━━━━━━┓<br>┃&nbsp;&nbsp;&nbsp;&nbsp;ソースコードを表示（折りたたみ）&nbsp;&nbsp;&nbsp;┃<br>┗━━━━━━━━━━━━━━━━━━┛</b></summary><div>

~~~swift:ContentView
import SwiftUI
import PhotosUI

struct ContentView: View {
    
    //アルバムから選択された画像
    @State var selectedPhotos: [PhotosPickerItem] = []
    //UIImageに変換したアイテムを格納する
    @State var getUImage: UIImage?
    //トリミングされた画像があれば表示
    @State var CreppedUImage: UIImage?
    //アルバムorカメラから画像が取得されれば、トリミングへ
    @State private var showImageCropper = false
    //カメラ表示
    @State var showCameraSheet: Bool = false
    
    
    var body: some View {
        VStack {
            //トリミング後に表示
            if (CreppedUImage != nil) {
                Image(uiImage: CreppedUImage!)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width:300, height: 300)
            }
            
            //アルバムから選択
            PhotosPicker(selection: $selectedPhotos, maxSelectionCount: 1, matching: .images){
                Text("getPhoto")
            }
            //カメラ表示
            Button{
                showCameraSheet = true
            }label:{
                Text("getCamera")
            }
            
            
        }
        .padding()
        
        //アルバムから取得した画像はuiImage型に変更する
        .onChange(of: selectedPhotos){
            Task{
                guard let data = try? await selectedPhotos[0].loadTransferable(type: Data.self) else {return}
                
                guard let uiImage = UIImage(data:data) else {return}
                
                getUImage = uiImage
                
            }
        }
        //写真を撮る時に表示
        .fullScreenCover(isPresented:$showCameraSheet){
            CameraView(image: $getUImage).ignoresSafeArea()
        }
        //画像を得た時
        .onChange(of:getUImage){
            showImageCropper = true
        }
        //得た画像を編集する時に表示
        .sheet(isPresented: $showImageCropper) {
            ImageCropper(image: self.$getUImage, visible: self.$showImageCropper, done: imageCropped)
        }
    }
    
    //トリミングが終わった後に呼ばれる
    func imageCropped(image: UIImage){
        CreppedUImage = image
    }
}
~~~

</div></details>

最後は今までに準備した、カメラ、画像トリミングに「アルバムから取得」を追加し、組み合わせるだけです。

型変換が少し面倒ですが、コードに記載したコメントを落ち着いて読めば、大体流れが掴めると思います。

```swift:一部解説
.sheet(isPresented: $showImageCropper) {
        ImageCropper(image: self.$getUImage, visible: self.$showImageCropper, done: imageCropped)
}

...

//トリミングが終わった後に呼ばれる
func imageCropped(image: UIImage){
    CreppedUImage = image
}
```
画像をトリミングするImageCropperはトリミングが完了時、doneの引数に設定している関数を呼び出します。
今回はimageCropped(image: UIImage)が呼ばれますね。

# 終わりに
お疲れ様です。カメラorアルバムから画像を取得し、トリミングできるようになったと思います！
是非ご自身のアプリに取り込んでみてください！

:::note warn
カメラで撮った写真は保存されないので、保存機能を作る方はご自身で追加してください。
:::

#### 最後に宣伝です。
このような誕生日を通知するシンプルなデザインのiosアプリをリリースしています。
[誕生日book-通知とメモをリストで管理](https://apps.apple.com/jp/app/誕生日book-通知とメモをリストで管理/id6496679663)
![スクリーンショット 2024-05-31 17.00.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692824/440d9c08-0882-3678-f122-9a9ca6276fe9.png)
是非使ってみてください！