Blog スタッフブログ

iOS Swift システム開発

[iOS]UIImagePickerControllerのカメラを独自UIで使う

Swift

こんにちは、株式会社MIXシステム開発担当のBloomです。

早速本題のUIImagePickerControllerのカメラで独自UIを利用する手順について、

お仕事の中で得た知見を共有させていただきたいと思います。

showsCameraControlsプロパティ

UIImagePickerControllerにはshowsCameraControlsプロパティがあります。ここにfalseを指定することで標準UIを瞬く間に消すことができます。これで全て解決しました。良かったですね。

実行結果

なんということでしょう、本当にありとあらゆるUIが消えカメラ画像がノッチ部分までめり込み下部に大きな空白が生まれた上にピンチイン・アウトによるズームすらできなくなりました。これでは使えないので今回は標準UIにある機能をできるだけ独自UIから再現してみましょう。

独自UIの構築

独自UIの設置にはcameraOverlayViewを利用します。これはカメラのレイヤーの上に載せる独自UIViewを指定するプロパティであり、UIImagePickerControllerのビュー構造で唯一操作していい項目になります。

今回はこのcameraOverlayViewに指定するCustomViewへUIを追加しつつ、カメラ機能を設置していきましょう。

let imagePickerController = UIImagePickerController()
imagePickerController.showsCameraControls = false
imagePickerController.cameraOverlayView = CustomView()

カメラ映像位置変更

cameraViewTransformプロパティを操作し、アフィン変換を利用してカメラ映像の位置を変更します。

このプロパティの初期値は後で利用するため保持しておきましょう。

var initialTransform = imagePickerController.cameraViewTransform
initialTransform.ty = 80 // 80px下にずらす
imagePickerController.cameraViewTransform = initialTransform

撮影機能

takePictureメソッドが利用できます。撮影後の確認画面は存在せず、そのままDelegateのdidFinishPickingMediaWithInfoがコールされます。

imagePickerController.takePicture()

カメラの前面/背面切り替え

cameraDeviceプロパティを指定するだけでアニメーションとともにカメラが切り替わります。

switch imagePickerController.cameraDevice {
case .front:
  imagePickerController.cameraDevice = .rear
case .rear:
  imagePickerController.cameraDevice = .front
@unknown default:
  return
}

フラッシュ切り替え

同様にcameraFlashModeプロパティを指定するのみで良いのですが、その前にisFlashAvailableでフラッシュが利用できるかチェックしておきましょう。

if UIImagePickerController.isFlashAvailable(for: imagePickerController.cameraDevice) {
  // UIの状態に合わせて変えましょう
  imagePickerController.cameraFlashMode = .on
  imagePickerController.cameraFlashMode = .off
  imagePickerController.cameraFlashMode = .auto
} else {
    // フラッシュが利用できないカメラです
}

ズーム機能

ズーム機能はUIImagePickerControllerのメソッドには存在せず、プロパティで操作もできません。

この機能の実現には二つの手順を踏みます。

1.ズーム操作が行われたとき、アフィン変換でカメラ画像を拡大する

2.撮影後、ズームしたスケールに応じて撮影画像から切り抜き処理を行い、画像を拡大する

まずはズーム操作のハンドリングとアフィン変換の処理からです。


class CustomView: UIView
{
  private var initialTransform: CGAffineTransform!
  private let minScale = 1.0
  private let maxScale = 10.0
  private var lastScale: CGFloat = 1.0
  var currentScale: CGFloat = 1.0
  var imagePickerController: UIImagePickerController!
  
  func setup(imagePickerController: UIImagePickerController) {
    self.imagePickerController = imagePickerController
    self.initialTransform = self.imagePickerController.cameraViewTransform
    self.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(pinchGestureRecognized(_:))))
  }

  @objc func pinchGestureRecognized(_ sender: UIPinchGestureRecognizer) {
    switch sender.state {
    case .began: fallthrough
    case .changed:
      currentScale = lastScale * sender.scale
      currentScale = min(max(currentScale, minScale), maxScale)
    case .cancelled: fallthrough
    case .ended:
      lastScale = currentScale
    default:
      return
    }
    // ここで拡大処理を行っています。
    self.cameraViewTransform = CGAffineTransformScale(self.initialTransform, currentScale, currentScale)
  }
}

このコードはピンチ操作でズームを行っています。iPadでは標準UIにUISliderが利用されているため適宜切り替えましょう。

次に切り抜き処理を行います。

extension ViewController: UIImagePickerControllerDelegate {
  func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    let image: UIImage?
    if picker.sourceType == .camera,
       picker.showsCameraControls == false, // sourceTypeがcameraでないときにshowsCameraControlsプロパティへアクセスするとクラッシュするので気をつけましょう
       let orgImg = (info[.originalImage] as? UIImage)?.getCorrectOrientationUIImage() {
      // 独自ズームの拡大率を取得
      let scale = customView.currentScale
      // まずsize / scaleの部分を真ん中中心で切り抜く
      let clipSize = CGSize(width: orgImg.size.width / scale,
                            height: orgImg.size.height / scale)
      let clipOrigin = CGPoint(x: (orgImg.size.width - clipSize.width) / 2,
                               y: (orgImg.size.height - clipSize.height) / 2)
      let cropped = orgImg.cropping(rect: CGRect(origin: clipOrigin, size: clipSize))
      let resized = cropped.resize(orgImg.size) // 切り抜いたら元画像のサイズへリサイズする
      image = resized
    } else { // カメラからでない場合
      image = info[.originalImage] as? UIImage
    }
  }
}


extension UIImage
{
  func resize(_ size: CGSize) -> UIImage?
  {
    return UIGraphicsImageRenderer(size: size, format: imageRendererFormat).image {
      _ in draw(in: CGRect(origin: .zero, size: size))
    }
  }
  
  func cropping(rect: CGRect) -> UIImage {
      let imgRef = cgImage?.cropping(to: rect)
      let trimImage = UIImage(cgImage: imgRef!, scale: scale, orientation: imageOrientation)
      return trimImage
  }
  
  func getCorrectOrientationUIImage() -> UIImage {
    var newImage = UIImage()
    let ciContext = CIContext()
      switch imageOrientation.rawValue {
      case 1:
        guard let orientedCIImage = CIImage(image: self)?.oriented(CGImagePropertyOrientation.down),
              let cgImage = ciContext.createCGImage(orientedCIImage, from: orientedCIImage.extent) else {
          print("Image rotation failed.")
          return self
        }
        newImage = UIImage(cgImage: cgImage)
      case 3:
        guard let orientedCIImage = CIImage(image: self)?.oriented(CGImagePropertyOrientation.right),
              let cgImage = ciContext.createCGImage(orientedCIImage, from: orientedCIImage.extent) else {
            print("Image rotation failed.")
          return self
        }
        newImage = UIImage(cgImage: cgImage)
      default:
        newImage = self
      }
      return newImage
  }
}

これでUIImagePickerControllerをカメラで利用した際の基本的なUIの機能を実装することができます。

あとは自作UIで覆ってしまいUI操作からこれらの機能へアクセスさせましょう。良かったですね。

参考文献

[iOS] UIGraphicsImageRendererを使用して画像のリサイズや着色を行う | DevelopersIO – dev.classmethod.jp

iPhoneで撮った写真の向きがおかしいとき、お手軽回転メソッドで全てを解決する – Qiita

iphone – Implementing zoom in\out using UIImagePickerController – Stack Overflow