こんにちは
カメラで撮った写真やアルバムの画像を正方形で自由に切り抜けるビューの需要があったので自作してみました。
UIKitに既にあるこのような要件を満たすコンポーネントには UIImagePickerController
がありますが、これを使って撮影した写真の切り抜き範囲に問題があり、写真の端っこを含めることができないようです。(参考)
大事なところ
-
UIView#convert(_:from:)
メソッドでViewとViewの重なっている範囲を取得できる -
UIGraphicsBeginImageContext
でコンテキストを作成してUIImageViewの現在表示範囲でクロップ
(下記コード内で一部Extensionでメソッド生やしたりしているところがあります、宣言内容について詳しくはページ最後にあるGitHubへのリンクから参照してください。)
UIScrollViewとUIImageViewをコードで生成
プロパティの設定がめんどいので設定済みのUIScrollViewのサブクラスを作っておきます
class CropScrollView: UIScrollView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .black
clipsToBounds = false
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
layer.borderWidth = 1
layer.borderColor = UIColor.lightGray.cgColor
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
今回切り取りは正方形固定で行うので、画面上のそれ以外の部分をグレーで覆うビューも作っておきます。
class CropOverlayView: UIView {
init() {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
super.init(frame: CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight))
isUserInteractionEnabled = false
let overlayViewHeight = (screenHeight - screenWidth) / 2
let upper = UIView(frame: CGRect(x: 0, y: 0, width: screenWidth, height: overlayViewHeight))
let lower = UIView(frame: CGRect(x: 0, y: screenWidth + overlayViewHeight, width: screenWidth, height: overlayViewHeight))
upper.isUserInteractionEnabled = false
lower.isUserInteractionEnabled = false
upper.backgroundColor = UIColor.ex.cropViewOverlay
lower.backgroundColor = UIColor.ex.cropViewOverlay
addSubview(upper)
addSubview(lower)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
ここまで準備ができたら、クロップ用のビューコントローラを作っていきます。
import UIKit
protocol CropViewControllerDelegate: class {
func cropViewControllerDidFinishTask(_ image: UIImage)
}
class CropViewController: UIViewController, UIScrollViewDelegate {
private var scrollView: UIScrollView!
private var imageView: UIImageView!
private var image: UIImage!
weak var delegate: CropViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
// ImageViewを準備
imageView = UIImageView(image: image)
// ScrollViewを準備
scrollView = CropScrollView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width))
scrollView.center = view.center
scrollView.delegate = self
// ImageViewとScrollViewをビューに追加
scrollView.addSubview(imageView)
view.addSubview(scrollView)
// 最初は写真全体を表示
setZoomScale()
// 切り取り領域ビューの準備
view.addSubview(CropOverlayView())
// 決定ボタンを準備
let confirmButton = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
confirmButton.setTitle("決定", for: .normal)
confirmButton.setTitleColor(.white, for: .normal)
confirmButton.addTarget(self, action: #selector(didTapConfirmButton), for: .touchUpInside)
view.addSubview(confirmButton)
confirmButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint(item: confirmButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 100).isActive = true
NSLayoutConstraint(item: confirmButton, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 66).isActive = true
NSLayoutConstraint(item: confirmButton, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: -8).isActive = true
NSLayoutConstraint(item: confirmButton, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: -8).isActive = true
// キャンセルボタンを準備
let cancelButton = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
cancelButton.setTitle("キャンセル", for: .normal)
cancelButton.setTitleColor(.white, for: .normal)
cancelButton.addTarget(self, action: #selector(dismissSelf), for: .touchUpInside)
view.addSubview(cancelButton)
cancelButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint(item: cancelButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 100).isActive = true
NSLayoutConstraint(item: cancelButton, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 66).isActive = true
NSLayoutConstraint(item: cancelButton, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 8).isActive = true
NSLayoutConstraint(item: cancelButton, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: -8).isActive = true
}
func prepareView(image: UIImage) {
self.image = image
}
func didTapConfirmButton() {
// TODO
}
func dismissSelf() {
dismiss(animated: true)
}
private func setZoomScale() {
let imageViewSize = imageView.bounds.size
let scrollViewSize = scrollView.bounds.size
let widthScale = scrollViewSize.width / imageViewSize.width
let heightScale = scrollViewSize.height / imageViewSize.height
scrollView.minimumZoomScale = min(widthScale, heightScale)
scrollView.zoomScale = min(widthScale, heightScale)
}
// MARK: - UIScrollViewDelegate
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let imageViewSize = imageView.frame.size
let scrollViewSize = scrollView.bounds.size
let verticalPadding = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0
let horizontalPadding = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0
scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}
これで、 prepareView(image:)
を使って UIImage
をなげつけるとその画像を自由にパンしたりズームしたりできる画面ができるはずです。
クロップ処理
スクロールビューの現在の表示領域を scrollView.bounds
で取得して、その領域をImageViewから切り取ります。
その後、画像生成用のコンテキスト内に描画してあげます。
func didTapConfirmButton() {
// クロップ
let width = scrollView.bounds.width
let height = width
let x = scrollView.bounds.origin.x
let y = scrollView.bounds.origin.y
let cropBounds = CGRect(x: x, y: y, width: width, height: height)
let visibleRect = imageView.convert(cropBounds, from: scrollView)
// 画像生成
UIGraphicsBeginImageContext(visibleRect.size)
let drawRect = CGRect(x: -visibleRect.origin.x, y: -visibleRect.origin.y, width: image!.size.width, height: image!.size.height)
image!.draw(in: drawRect)
let croppedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
// 呼び出し元に渡す
delegate?.cropViewControllerDidFinishTask(croppedImage)
dismiss(animated: true)
}
Code
今回制作したビューを含めた全コードはこちらに掲載しています。
keisei1092/CropViewControllerPractice
https://github.com/keisei1092/CropViewControllerPractice