post Image
iOSフォトライブラリの写真を画面上でトリミングして保存するまで

はじめに

この記事は確かiOS10あたりでの記事になっていると思います。iOS12が発表された今現在(2018/9/20)では、このようなトリミングの自前実装は必要のないような素晴らしいiOS標準のAPIがある可能性があります。まずは公式のAPIがないか探すことをお勧めします。)

iOSアプリでフォトライブラリ(「写真」)を読み込んで使いたいときがあると思います。さらに、その写真・画像を微妙にトリミング等ができるようにしたいという場合もあるでしょう。
そこでそんな時のための一連の流れをご紹介します。
全体的に機能を説明するgif動画があるので、ざっとそれに目を通していただければどんな内容かわかると思います。
ここもっと良くなるよ!とか似たようなライブラリ既にあるよ!というコメントお待ちしております。

追記 2018/6/4

注意: iPhoneなどで撮影した写真は縦横が思っているものと逆に表示されることがあるようです。対応法調査中です。

参考: https://qiita.com/RichardImaokaJP/items/385beb77eb39243e50a6

この記事で説明すること

  1. 「フォトライブラリ(「写真」)からの写真・画像の取得
  2. 読み込んだ写真・画像のトリミング
  3. トリミングした画像の保存・読み取り
  4. 発展形のgithubレポジトリの紹介(自分のです)

コード全体

Githubにあります。 https://github.com/TetsuFe/PhotoLibraryImageTrimminger

1. 「フォトライブラリから写真の取得

PhotoLibraryTrimingプレイ動画・導入.gif

1-1. フォトライブラリの使用用途の記述

まず初めに、info.plistにフォトライブラリの使用用途を記述します。
iOS10以降の場合、これを書かないでフォトライブラリを使用しようとするとアプリが落ちてしまうので、必須の設定です。
info.plistの開き方は、info.plistを右クリック>Open As>Source Codeで開けます。

info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    **中略**(この行は削除してください)
    <key>NSPhotoLibraryUsageDescription</key>
    <string>例:use the photo library's photo as a background image in this app</string>
</dict>
</plist>

1-2. フォトライブラリを呼び出す

次に、フォトライブラリを使用するためのコードを書きます。これはPhoto FrameworkのAPIを呼び出すことで行います。

フォトライブラリを呼び出すためには、フォトライブラリへのアクセス権を取得する必要があります。初回にフォトライブラリを呼び出す際に、iOS10以前であれば、アクセス許可を求めるアラートが自動的に表示されます。iOS11以降では、アクセス許可は自動的にONになります。(従って、どのバージョンでも初回はアクセス許可を求めるコードを自分で書く必要はありません)しかし、ここで拒否をされてしまう場合に対応するために、requestAuthorizationOn()でユーザがアクセスを拒否していれば、許可するように促す関数を定義します。そしてこの関数を毎回フォトライブラリを呼び出す前に実行するようにします。

PhotoLibraryManager.swift
import Photos

struct PhotoLibraryManager{

    var parentViewController : UIViewController!

    init(parentViewController: UIViewController){
        self.parentViewController = parentViewController
    }

    // 写真へのアクセスがOFFのときに使うメソッド
    func requestAuthorizationOn(){
        // authorization
        let status = PHPhotoLibrary.authorizationStatus()
        if (status == PHAuthorizationStatus.denied) {
            //アクセス不能の場合。アクセス許可をしてもらう。snowなどはこれを利用して、写真へのアクセスを禁止している場合は先に進めないようにしている。
            //アラートビューで設定変更するかしないかを聞く
            let alert = UIAlertController(title: "写真へのアクセスを許可",
                                          message: "写真へのアクセスを許可する必要があります。設定を変更してください。",
                                          preferredStyle: .alert)
            let settingsAction = UIAlertAction(title: "設定変更", style: .default) { (_) -> Void in
                guard let _ = URL(string: UIApplicationOpenSettingsURLString ) else {
                    return
                }
            }
            alert.addAction(settingsAction)
            alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel) { _ in
                // ダイアログがキャンセルされた。つまりアクセス許可は得られない。
            })
            self.parentViewController.present(alert, animated: true)
        }
    }

    //フォトライブラリを呼び出すメソッド
    func callPhotoLibrary(){
        //権限の確認
        requestAuthorizationOn()

        if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.photoLibrary) {

            let picker = UIImagePickerController()
            picker.modalPresentationStyle = UIModalPresentationStyle.popover
            picker.delegate = self.parentViewController as? UIImagePickerControllerDelegate & UINavigationControllerDelegate
            picker.sourceType = UIImagePickerControllerSourceType.photoLibrary
            //以下を設定することで、写真選択後にiOSデフォルトのトリミングViewが開くようになる
            //picker.allowsEditing = true
            if let popover = picker.popoverPresentationController {
                popover.sourceView = self.parentViewController.view
                popover.sourceRect = self.parentViewController.view.frame // ポップオーバーの表示元となるエリア
                popover.permittedArrowDirections = UIPopoverArrowDirection.any
            }
            self.parentViewController.present(picker, animated: true, completion: nil)
        }
    }
}

補足:UIImagePickerControlleのデフォルト機能でのトリミングについて


ここで、UIImagePickerControlle.rallowsEditing = true(上のコードでは、picker.allowsEditing = true)とすることで、iOSデフォルトの写真トリミング機能が起動します。これでもトリミングはできるのですが、一回しか範囲の選択ができない上に、部分的に切り出すためには一度拡大させなくてはいけません。これでは、範囲の指定が難しく、また一回で成功させなくてはいけません。そして、失敗した時にはやり直すのが面倒です。

iOS標準のトリミング機能qiita版.gif

さて、ここまででフォトライブラリへのアクセスはできるようになりました。しかし、フォトライブラリから取得した写真はどこへいったのでしょう?次は取得した写真をトリミング用のビューへ受け渡す処理を説明します。

1-3. 取得した写真のトリミングビューへの受け渡し

先ほどは、UIImagePickerControllerを呼び出し、そこで写真の選択までを行いました。写真の選択が終わった後は、UIImagePickerControllerDelegate.imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any])が実行されますので、これをoverrideし、トリミングビューへ写真(UIImage)を渡せるようにします。
トリミングビューへの値の受け渡しには、AppDelegateクラスを用います。AppDelegate.photoLibraryImageとして、UIImage形式で取得した写真の保持を行います。

appDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var photoLibraryImage : UIImage! //これで取得した写真を保持
/*以下省略*/
}

このAppDelegate.photoLibraryImageをどこでsetするかというと、UIImagePickerControllerDelegate.imagePickerController()メソッド内で行います。このメソッドは、フォトライブラリでの写真選択完了後に呼ばれます。これは、写真選択画面に映るViewに紐づけられたViewController中で定義する必要があります。
info[UIImagePickerControllerOriginalImage] as? UIImageが写真のデータとなりますので、これをAppDelegate.photoLibraryImageに代入します。

StoredImageListVC.swift
class StoredImageListVC: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate{

    //写真選択完了後に呼ばれる標準メソッド
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
            let appDelegate = UIApplication.shared.delegate as! AppDelegate
            appDelegate.photoLibraryImage = pickedImage
        }

        //withIdentifier: "imageEditVC"は遷移先のViewControllerのIdentifier。as! ImageEditVCは遷移先ののViewControllerの型名
        let nextViewController = self.storyboard?.instantiateViewController(withIdentifier: "trimImageVC") as! TrimImageVC
        picker.present(nextViewController, animated: true)
    }
}

これで、他のビュー(トリミングを行うビュー)に選択した写真を渡す準備ができました。次は、取得した写真(UIImage)をもとにUIImageViewを作成し、トリミングする機能を作成していきます。

2. 取得した写真・画像のトリミング

注意:

  • トリミング処理はUIImageを操作します。
  • 拡大・縮小処理はUIImageViewを操作し、UIImageは変更しません。

これは、UIImageを拡大・縮小処理しようとすると、解像度が下がってしまうという問題があり、解決できなかったためです。結果的に、「トリミング範囲はUIImageViewのサイズで決定するが、トリミングで変更するのはUIImage」となるため、UIImageViewとUIImageとの倍率を保存しておく必要が生じています。

トリミング範囲を示すUIViewがあり、画像を動かしたり拡大・縮小することで希望の範囲でトリミングができるようにします。

画面のイメージは以下のようなものです。

トリミング画面紹介 2.png

(ここでは画面の実装については深くは触れません。Githubのコード(のMain.storyboard)を参照してください)

まずは、2章で定義するTrimImageVC(ViewController)メソッドで使われる主要なメンバ変数を挙げておきます。

TrimImageVC.swift
@IBOutlet weak var trimImageButton: UIButton! //トリミングの処理と紐づけられたUIButton
@IBOutlet weak var editPhotoView: UIView! //トリミングで残す範囲を可視化するためのUIView
var imageView: UIImageView! //トリミング対象画像を表示するためのUIImageView
var scaleZoomedInOut : CGFloat = 1.0 //拡大・縮小した時にはUIImageViewのサイズのみが変わるので、実際のUIImageのサイズとUIImageViewとの倍率の差を記録する

基本的に、TrimImageVC.swift(ViewController)にトリミングをするビューで行う処理を書いていきます。
このViewControllerのメンバ変数であるvar imageView: UIImageView!は、トリミング対象の画像から生成されるUIImageViewです。
また、実際にトリミング時に変更されるUIImageと、拡大・縮小などで変更されるUIImageViewとの倍率の差を記録するvar scaleZoomedInOut : CGFloat = 1.0も重要です。

2-1. 取得した写真からUIImageViewを作成

まずは、さきほどAppDelegateのメンバ変数AppDelegate.photoLibraryImageを元に、imageViewを初期化して画面に表示させてみます。初期化に際して、画像の全体が把握できるように、画像本体(UIImage)の幅がスクリーンの幅よりも大きい時は、画像を画面の幅に合わせたUIImageViewとして表示するという処理を施すために、createImageView()というメソッドを使います。

TrimImageVC.swift
class TrimImageVC: UIViewController {

    //中略

    override func viewDidAppear(_ animated: Bool) {
            super.viewWillAppear(true)  
            editPhotoView.layer.borderColor = UIColor.black.cgColor
            editPhotoView.layer.borderWidth = 1

            let appDelegate = UIApplication.shared.delegate as! AppDelegate
            image = appDelegate.photoLibraryImage
            createImageView(sourceImage: image, on: editPhotoView)
    }

    func createImageView(sourceImage:UIImage, on parentView: UIView){
        imageView = UIImageView(image: sourceImage)
        // 画像の幅・高さの取得
        let imageWidth = sourceImage.size.width
        let imageHeight = sourceImage.size.height
        let screenWidth = editPhotoView.frame.width
        let screenHeight = editPhotoView.frame.height

        if scaleZoomedInOut == 1.0{
            if imageWidth > screenWidth{
                scaleZoomedInOut = screenWidth/imageWidth
            }
        }

        let rect:CGRect = CGRect(x:0, y:0, width:scaleZoomedInOut*imageWidth, height:scaleZoomedInOut*imageHeight)
        // ImageView frame をCGRectで作った矩形に合わせる
        imageView.frame = rect

        // 画像の中心をスクリーンの中心位置に設定
        imageView.center = CGPoint(x:screenWidth/2, y:screenHeight/2)

        parentView.addSubview(imageView)
        parentView.sendSubview(toBack: imageView)
    }

    //後略
}

これでフォトライブラリから取得した画像(UIImage)をUIImageViewとして画面に表示させることができました。
これにより、画面操作で画像を操作するための第一ステップがクリアできました。

2-2. トリミング処理のextensionを書く

UIImageにトリミング用extensionを追加します。

UIImage+Trimming.swift
import UIKit

//トリミングを行うメソッド
//トリミング範囲の大きさcroppingRectと元画像の大きさとの拡大倍率をzoomedInOutScaleを用いる
extension UIImage {
    func trimming(to croppingRect : CGRect , zoomedInOutScale: CGFloat) -> UIImage? {
        var opaque = false
        if let cgImage = cgImage {
            switch cgImage.alphaInfo {
            case .noneSkipLast, .noneSkipFirst:
                opaque = true
            default:
                break
            }
        }
        UIGraphicsBeginImageContextWithOptions(CGSize(width: croppingRect.size.width/zoomedInOutScale, height: croppingRect.size.height/zoomedInOutScale), opaque, scale)
        draw(at: CGPoint(x: -croppingRect.origin.x/zoomedInOutScale, y: -croppingRect.origin.y/zoomedInOutScale))
        let result = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return result
    }
}

次に、トリミング用のUIを作ります。四角形のUIViewの中に収まっている部分の画像を切り出すという処理を行えるようにします。

この枠内でトリミングできるように、拡大・縮小・上下左右の移動をできるようにします。

2-2. 拡大・縮小(ダブルタップ・ピンチイン/アウト)

画像の拡大・縮小は、ダブルタップ・ピンチイン/アウトで行えるようにします。ただし、縮小は画面幅よりも小さくはできないこととしました。

2-2-1. ピンチイン/アウト・ダブルタップの検出

まずは、ピンチイン/アウト・ダブルタップの検出ができるようにします。

ピンチイン/アウト・ダブルタップの判定は写真が表示されている範囲editPhotoViewに限定します。
以下のコードでは、ダブルタップのためのUITapGestureRecognizerのセットアップも行なっています。このメソッドをviewDidLoadで呼ぶことで、ピンチイン/アウト・タップの検出を有効にします。

TrimImageVC.swift
func setUpPinchInOutAndDoubleTap(){
    // ピンチイン・アウトの準備
    let pinchGetsture = UIPinchGestureRecognizer(target: self, action: #selector(pinchAction(gesture:)))
    pinchGetsture.delegate = self as? UIGestureRecognizerDelegate

    self.editPhotoView.isUserInteractionEnabled = true
    self.editPhotoView.isMultipleTouchEnabled = true

    self.editPhotoView.addGestureRecognizer(pinchGetsture)
    // ダブルタップの準備
    let doubleTapGesture = UITapGestureRecognizer(target: self, action:#selector(doubleTapAction(gesture:)))
    doubleTapGesture.numberOfTapsRequired = 2
    self.editPhotoView.addGestureRecognizer(doubleTapGesture)
}

2-2-2. ピンチイン/アウトで縮小/拡大の処理

PhotoLibraryTrimingプレイ動画・拡大縮小ピンチ.gif

ピンチインで縮小・ピンチアウトで拡大という処理を作ります。これはUIPinchGestureRecognizerを使います。

ピンチイン/アウトが検出された時にはpinchAction(gesture: UIPinchGestureRecognizer)メソッドが呼ばれるので、これをoverrideして画像を拡大する操作を実装します。

TrimImageVC.swift
var pichCenter : CGPoint!
var touchPoint1 : CGPoint!
var touchPoint2 : CGPoint!
let maxScale : CGFloat = 1
var pinchStartImageCenter : CGPoint!

@objc func pinchAction(gesture: UIPinchGestureRecognizer) {

    if gesture.state == UIGestureRecognizerState.began {
        // ピンチを開始したときの画像の中心点を保存しておく
        pinchStartImageCenter = imageView.center
        touchPoint1 = gesture.location(ofTouch: 0, in: self.view)
        touchPoint2 = gesture.location(ofTouch: 1, in: self.view)

        // 指の中間点を求めて保存しておく
        // UIGestureRecognizerState.Changedで毎回求めた場合、ピンチ状態で片方の指だけ動かしたときに中心点がずれておかしな位置でズームされるため
        pichCenter = CGPoint(x: (touchPoint1.x + touchPoint2.x) / 2, y: (touchPoint1.y + touchPoint2.y) / 2)

    } else if gesture.state == UIGestureRecognizerState.changed {
        // ピンチジェスチャー・変更中
        var pinchScale :  CGFloat// ピンチを開始してからの拡大率。差分ではない
        if gesture.scale > 1 {
            pinchScale = 1 + gesture.scale/100
        }else{
            pinchScale = gesture.scale
        }
        if pinchScale*self.imageView.frame.width < editPhotoView.frame.width {
            pinchScale = editPhotoView.frame.width/self.imageView.frame.width
        }
        scaleZoomedInOut *= pinchScale

        // ピンチした位置を中心としてズーム(イン/アウト)するように、画像の中心位置をずらす
        let newCenter = CGPoint(x: pinchStartImageCenter.x - ((pichCenter.x - pinchStartImageCenter.x) * pinchScale - (pichCenter.x - pinchStartImageCenter.x)),y: pinchStartImageCenter.y - ((pichCenter.y - pinchStartImageCenter.y) * pinchScale - (pichCenter.y - pinchStartImageCenter.y)))
        self.imageView.frame.size = CGSize(width: pinchScale*self.imageView.frame.width, height: pinchScale*self.imageView.frame.height)
        imageView.center = newCenter
    }
}

2-2-3. ダブルタップで拡大の処理

PhotoLibraryTrimingプレイ動画・ダブルタップ.gif

(わかりにくいですが、ダブルタップしています!)

ダブルタップは、ダブルタップを行うとタップをしたところを中心にUIImageViewが2倍の大きさになるという単純なものです。
ダブルタップ時にはdoubleTapAction(gesture: UITapGestureRecognizer)が実行されるので、これをoverrideして拡大する処理を実装します。

TrimImageVC.swift
@objc func doubleTapAction(gesture: UITapGestureRecognizer) {

    if gesture.state == UIGestureRecognizerState.ended {

        let doubleTapStartCenter = imageView.center
        let doubleTapScale: CGFloat = 2.0 // ダブルタップでは現在のスケールの2倍にする
        scaleZoomedInOut *= doubleTapScale
        let tapPoint = gesture.location(in: self.view)
        let newCenter = CGPoint(x:
            doubleTapStartCenter.x - ((tapPoint.x - doubleTapStartCenter.x) * doubleTapScale - (tapPoint.x - doubleTapStartCenter.x)),
                                y: doubleTapStartCenter.y - ((tapPoint.y - doubleTapStartCenter.y) * doubleTapScale - (tapPoint.y - doubleTapStartCenter.y)))

        // 拡大・縮小とタップ位置に合わせた中心点の移動
        UIView.animate(withDuration: 0.3, delay: 0.0, options: UIViewAnimationOptions.curveEaseOut, animations: {() -> Void in
            self.imageView.frame.size = CGSize(width: doubleTapScale*self.imageView.frame.width, height: doubleTapScale*self.imageView.frame.height)
            self.imageView.center = newCenter
        }, completion: nil)
    }
}

2-3. 上下左右の移動

PhotoLibraryTrimingプレイ動画・平行移動.gif
上下左右の移動は、ドラッグ操作で行えるようにします。touchesMovedメソッドをoverrideしてドラッグ中の座標の変化をUIImageViewの位置に対応させるようにします。

TrimImageVC.swift
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    // タッチイベントを取得.
    let aTouch: UITouch = touches.first!

    // 移動した先の座標を取得.
    let location = aTouch.location(in: editPhotoView)

    // 移動する前の座標を取得.
    let prevLocation = aTouch.previousLocation(in: editPhotoView)

    let deltaX = location.x - prevLocation.x
    let deltaY = location.y - prevLocation.y

    // 移動した分の距離をmyFrameの座標にプラスする.
    imageView.frame.origin.x += deltaX
    imageView.frame.origin.y += deltaY
}

2-4. トリミングする

PhotoLibraryTrimingプレイ動画・トリミング.gif

トリミング範囲を返す関数を作成します。makeTrimmingRect(targetImageView: UIImageView, trimmingAreaView: UIView) -> CGRect? というように定義します。そして、トリミングUIViewの中に収まっている画像の範囲をCGRectとして返す関数を作成します。trimmingAreaViewはトリミング範囲を示すビューです。ここをimageViewがはみ出していれば、その部分を削除して、はみ出ていないところだけが残ります。トリミング範囲をこの関数で指定し、この範囲でUIImage.trimmingを実行し、トリミングを行うという流れです。

TrimImageVC.swift
func makeTrimmingRect(targetImageView: UIImageView, trimmingAreaView: UIView) -> CGRect?{
    var width = CGFloat()
    var height = CGFloat()
    var trimmingX = CGFloat()
    var trimmingY = CGFloat()

    let deltaX = targetImageView.frame.origin.x
    let deltaY = targetImageView.frame.origin.y

    //空の部分ができてしまった場合、その部分を切り取る

    var xNotTrimFlag = false
    //x方向
    if targetImageView.frame.width > trimmingAreaView.frame.width {

        //3. 最初の時点で画面のy方向に画像がはみ出している時
        if targetImageView.frame.origin.x > 0{
            //origin.y > 0の場合、確実にy方向に画面から外れている
            //上方向にはみ出し
            width = trimmingAreaView.frame.size.width - deltaX
        }else{
            //origin.y < 0の場合、上方向には確実にはみ出している
            //下方向がはみ出していない
            if trimmingAreaView.frame.size.width > (targetImageView.frame.size.width + targetImageView.frame.origin.x) {
                width = targetImageView.frame.size.width + targetImageView.frame.origin.x
            }else{
                //下方向もはみ出し
                width = trimmingAreaView.frame.size.width
            }
        }
    }else{
        //4. 最初の時点で画面のy方向に画像がすっぽり全て収まっている時
        if targetImageView.frame.origin.x > 0{
            if trimmingAreaView.frame.size.width < (targetImageView.frame.size.width + targetImageView.frame.origin.x) {
                //下方向にはみ出し
                width = trimmingAreaView.frame.size.width - deltaX
            }else{
                xNotTrimFlag = true
                width = targetImageView.frame.size.width
            }
        }else{
            //上方向にはみ出し
            width = targetImageView.frame.size.width + targetImageView.frame.origin.x
        }
    }
    //y方向
    if targetImageView.frame.height > trimmingAreaView.frame.height {

        //3. 最初の時点で画面のy方向に画像がはみ出している時
        if targetImageView.frame.origin.y > 0{
            //origin.y > 0の場合、確実にy方向に画面から外れている
            //下方向にはみ出し
            height = trimmingAreaView.frame.size.height - deltaY
        }else{
            //origin.y < 0の場合、上方向には確実にはみ出している
            //下方向がはみ出していない
            if trimmingAreaView.frame.size.height > (targetImageView.frame.size.height + targetImageView.frame.origin.y) {
                height = targetImageView.frame.size.height + targetImageView.frame.origin.y
            }else{
                //下方向もはみ出し
                height = trimmingAreaView.frame.size.height
            }
        }
    }else{
        //4. 最初の時点で画面のy方向に画像がすっぽり全て収まっている時
        if targetImageView.frame.origin.y > 0{
            if trimmingAreaView.frame.size.height < (targetImageView.frame.size.height + targetImageView.frame.origin.y) {
                //下方向にはみ出し
                height = trimmingAreaView.frame.size.height - deltaY
            }else{
                if xNotTrimFlag {
                    return nil
                }else{
                    height = targetImageView.frame.size.height
                }
            }
        }else{
            //上方向にはみ出し
            height = targetImageView.frame.size.height + targetImageView.frame.origin.y
        }
    }

    //trimmingRectの座標を決定
    if targetImageView.frame.origin.x > trimmingAreaView.frame.origin.x {
        trimmingX = 0
    }else{
        trimmingX = -deltaX
    }

    if targetImageView.frame.origin.y > 0 {
        trimmingY = 0
    }else{
        trimmingY = -deltaY
    }

    return CGRect(x: trimmingX, y: trimmingY, width: width, height: height)
}

これでトリミングができるようになりました。トリミング範囲の決定は、UIButtonにメソッドを紐づけるなどしてボタン操作で行えるようにします。

TrimImageVC.swift
//viewDidLoadなどで
trimImageButton.addTarget(self, action: #selector(trimImage), for: .touchUpInside)

@objc func trimImage(){
    if let trimmingRect = makeTrimmingRect(targetImageView: imageView, trimmingAreaView: editPhotoView){
        //githubのコードにはもう少し書いてあるがここには関係ないので省略
        image = image.trimming(to: trimmingRect, zoomedInOutScale: scaleZoomedInOut)
        updateImageView() //imageViewの再描画などを行うメソッド
    }
}

次は、トリミングした画像をフォルダに保存する処理を実装していきます。

3-1. トリミングした画像の保存

トリミングした画像を保存します。ここで、UIImageはData型に変換して保存します。
data = UIImagePNGRepresentation(image)のように、UIImagePNGRepresentationメソッドを用いて、PNGのUIImageをData型に変換し、ファイル保存しやすい形式に変換します。(jpegなどでも問題なくData型に変換できます。)

TrimImageVC.swift
func saveImage(){
    let imageListFileManager = ImageListFileManager()
    let imageFileManager = ImageFileManager()
    //if let data = UIImageJPEGRepresentation(image, 1.0) {
    if let data = UIImagePNGRepresentation(image) {
        let autoIncrementedFileName = imageListFileManager.autoIncrementedFileName()
        imageFileManager.writeNewImageFile(data: data, fileName: autoIncrementedFileName)
        imageListFileManager.addNewImageFileName(fileName: autoIncrementedFileName)
    }
}

ここで、自作の構造体ImageListFileManagerやImageFileManagerが色々と処理を行なっていますが、これは、保存された画像のファイルパスも同時に保存する必要があるからです。しかし、ここに関しては省略します。
画像の保存に関する部分のコードを紹介します。
画像の保存に関するメソッドであるImageFileManager.writeNewImageFile(data: Data, fileName: String)では、createImageFile(data: Data, fileName: String)を呼び出すことで画像の保存を行なっており、以下のように実装しています。

ImageFileManager.swift
let defaultImageFileDirectoryPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] + "/image"

func createImageFile(data: Data, fileName: String){    
    let path = defaultImageFileDirectoryPath

    let fileManager = FileManager.default
    var isDir : ObjCBool = false
    //ファイルを作成したいディレクトリが存在するかを確認。存在するかどうかが引数のisDirにBoolで書き込まれる
    fileManager.fileExists(atPath: path, isDirectory: &isDir)

    //ファイルを作成したいディレクトリが存在しなければ、ディレクトリを作成する
    if !isDir.boolValue{
        try! fileManager.createDirectory(atPath: path ,withIntermediateDirectories: false, attributes: nil)
    }

    //書き方は違えど、defaultImageFileDirectoryPath + fileNameと同じ場所に書き込んでいる
    try! data.write(to: FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("/image/\(fileName)"))
}

これで、ファイルの保存処理も行うことができるようになりました。

3-2. 保存した画像ファイルの読み込み

保存したら、読み込みももちろん行うと思います。読み込みは書き込みと少し異なります。

ImageFileManager.swift
func readImageFile(fileName: String) -> UIImage?{
    let dataUrl = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("/image/\(fileName)")
    let data = try! Data(contentsOf: dataUrl)
    return UIImage(data: data)
}

4 発展形

Githubに、他に中央揃え、元に戻す/やり直すなどの機能もつけたコード全体をあげておいたので、もしよろしければ参考にしてください。
https://github.com/TetsuFe/PhotoLibraryImageTrimminger


『 Swift 』Article List