はじめに
SNSのようなプロフィール画像を設定する時に、丸く写真を切り抜きたいことが多いと思います。
ライブラリを使って楽に実装できたので備忘録として残しておこうと思います。
実際の画面はこのような感じです
ライブラリから

カメラ撮影から

使用ライブラリ
今回はこちらのライブラリを使用しました。
Objective-Cで書かれており、カスタマイズをしないなら十分使えそうです。
Podでインポートしました。
pod 'RSKImageCropper'
pod 'RxSwift'
pod 'RxCocoa'
※RxSwiftを使用しています
注意点
実機のカメラ機能を試す場合は、Info.plistで下記を追加するのを忘れずに
ソースコード
ViewController
import UIKit
import RSKImageCropper
import RxSwift
final class ProfileImageViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var profileImage: UIImageView!
@IBOutlet weak var addImageButton: UIButton!
private let viewModel: ViewModelType = ViewModel()
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
profileImage.layer.cornerRadius = 60
inputBind()
outputBind()
}
private func inputBind() {
addImageButton.rx.tap.bind { [weak self] in
let controller = UIAlertController(title: .none, message: .none, preferredStyle: .actionSheet)
let camera = UIAlertAction(title: "写真を撮る", style: .default, handler: { [weak self]_ in
self?.addCameraView()
})
let library = UIAlertAction(title: "ライブラリから選択", style: .default, handler: { [weak self]_ in
self?.addImagePickerView()
})
let cancel = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)
controller.addAction(camera)
controller.addAction(library)
controller.addAction(cancel)
self?.present(controller, animated: true)
}.disposed(by: disposeBag)
}
private func outputBind() {
viewModel.output.successImageSet.bind { [weak self] image in
self?.dismiss(animated: true, completion: nil)
self?.setCrop(image: image)
}
}
}
//MARK: - RSKImageCropper
extension ProfileImageViewController: RSKImageCropViewControllerDelegate {
private func setCrop(image: UIImage) {
let imageCropVC = RSKImageCropViewController(image: image, cropMode: .circle)
imageCropVC.moveAndScaleLabel.text = "切り取り範囲を選択"
imageCropVC.cancelButton.setTitle("キャンセル", for: .normal)
imageCropVC.chooseButton.setTitle("完了", for: .normal)
imageCropVC.delegate = self
present(imageCropVC, animated: true)
}
//キャンセルを押した時の処理
func imageCropViewControllerDidCancelCrop(_ controller: RSKImageCropViewController) {
dismiss(animated: true, completion: nil)
}
//完了を押した後の処理
func imageCropViewController(_ controller: RSKImageCropViewController, didCropImage croppedImage: UIImage, usingCropRect cropRect: CGRect, rotationAngle: CGFloat) {
dismiss(animated: true)
profileImage.image = croppedImage
}
}
//MARK: - UIImagePicker
extension ProfileImageViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate{
// カメラの利用
private func addCameraView() {
// シミュレーターでカメラを使用するとアラート表示させる
if !UIImagePickerController.isSourceTypeAvailable(.camera) {
let alertController = UIAlertController.init(title: nil, message: "Device has no camera.", preferredStyle: .alert)
let okAction = UIAlertAction.init(title: "Alright", style: .default, handler: {(alert: UIAlertAction!) in
})
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
} else {
//imagePickerViewを表示する
let pickerController = UIImagePickerController()
pickerController.sourceType = .camera
pickerController.delegate = self
self.present(pickerController, animated: true, completion: nil)
}
}
// ライブラリーの利用
private func addImagePickerView() {
//imagePickerViewを表示する
let pickerController = UIImagePickerController()
pickerController.sourceType = .photoLibrary
pickerController.delegate = self
pickerController.modalPresentationStyle = .fullScreen
self.present(pickerController, animated: true, completion: nil)
}
// pickerの選択がキャンセルされた時の処理
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
// 画像が選択(撮影)された時の処理
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let selectedImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage? else {return}
viewModel.input.setCrop(image: selectedImage)
}
}
ViewModel
import Foundation
import RxSwift
import RxCocoa
protocol ViewModelInput {
func setCrop(image: UIImage)
}
protocol ViewModelOutput {
var successImageSet: PublishRelay<UIImage> { get }
}
protocol ViewModelType {
var input: ViewModelInput { get }
var output: ViewModelOutput { get }
}
final class ViewModel: ViewModelInput, ViewModelOutput, ViewModelType {
var input: ViewModelInput { return self }
var output: ViewModelOutput { return self }
//input
func setCrop(image: UIImage) {
successImageSet.accept(image)
}
//output
var successImageSet = PublishRelay<UIImage>()
}
解説
まず、ViewControllerでViewModelを定義します。
inputとoutputを明確に分けたいので、ViewModelTypeに準拠させます。
private let viewModel: ViewModelType = ViewModel()
private let disposeBag = DisposeBag()
そしてカメラとライブラリを起動させるため、extensionでUIImagePickerControllerDelegate, UINavigationControllerDelegateを準拠させます。
extension ProfileImageViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate{
// カメラの利用
private func addCameraView() {
// シミュレーターでカメラを使用するとアラート表示させる
if !UIImagePickerController.isSourceTypeAvailable(.camera) {
let alertController = UIAlertController.init(title: nil, message: "Device has no camera.", preferredStyle: .alert)
let okAction = UIAlertAction.init(title: "Alright", style: .default, handler: {(alert: UIAlertAction!) in
})
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
} else {
//imagePickerViewを表示する
let pickerController = UIImagePickerController()
pickerController.sourceType = .camera
pickerController.delegate = self
self.present(pickerController, animated: true, completion: nil)
}
}
// ライブラリーの利用
private func addImagePickerView() {
//imagePickerViewを表示する
let pickerController = UIImagePickerController()
pickerController.sourceType = .photoLibrary
pickerController.delegate = self
pickerController.modalPresentationStyle = .fullScreen
self.present(pickerController, animated: true, completion: nil)
}
// pickerの選択がキャンセルされた時の処理
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
// 画像が選択(撮影)された時の処理
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let selectedImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage? else {return}
viewModel.input.setCrop(image: selectedImage)
}
}
シュミレーターでカメラを起動するとクラッシュしてしまうので、シュミレーターで起動した場合はアラートが出るようにしてあります。
そして「アイコン画像を設定」ボタンを押すとカメラorライブラリか選ぶアクションシートを出します。
private func inputBind() {
addImageButton.rx.tap.bind { [weak self] in
let controller = UIAlertController(title: .none, message: .none, preferredStyle: .actionSheet)
let camera = UIAlertAction(title: "写真を撮る", style: .default, handler: { [weak self]_ in
self?.addCameraView()
})
let library = UIAlertAction(title: "ライブラリから選択", style: .default, handler: { [weak self]_ in
self?.addImagePickerView()
})
let cancel = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)
controller.addAction(camera)
controller.addAction(library)
controller.addAction(cancel)
self?.present(controller, animated: true)
}.disposed(by: disposeBag)
}
addCameraView()
addImagePickerView()
どちらかで分岐させます。
そして写真撮影,ライブラリどちらかで写真が選択された時に呼ばれるコードが下記です。
// 画像が選択(撮影)された時の処理
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let selectedImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage? else {return}
viewModel.input.setCrop(image: selectedImage)
}
ここでviewModelのsetCropメソッドに,選択された写真を渡しつつ呼び出していますね。
ViewModelを見て見ましょう。
//input
func setCrop(image: UIImage) {
successImageSet.accept(image)
}
//output
var successImageSet = PublishRelay<UIImage>()
setCropメソッドで渡ってきたselectedImageをoutputのsuccessImageSetに流しています。
そしてViewControllerのoutputBindでバインディング、受け取ります。
private func outputBind() {
viewModel.output.successImageSet.bind { [weak self] image in
self?.dismiss(animated: true, completion: nil)
self?.setCrop(image: image)
}
一度画面を閉じて、そこから画像カットをカットするためのメソッドを呼び出して選択された画像を渡し
extension ProfileImageViewController: RSKImageCropViewControllerDelegate {
private func setCrop(image: UIImage) {
let imageCropVC = RSKImageCropViewController(image: image, cropMode: .circle)
imageCropVC.moveAndScaleLabel.text = "切り取り範囲を選択"
imageCropVC.cancelButton.setTitle("キャンセル", for: .normal)
imageCropVC.chooseButton.setTitle("完了", for: .normal)
imageCropVC.delegate = self
present(imageCropVC, animated: true)
}
//キャンセルを押した時の処理
func imageCropViewControllerDidCancelCrop(_ controller: RSKImageCropViewController) {
dismiss(animated: true, completion: nil)
}
//完了を押した後の処理
func imageCropViewController(_ controller: RSKImageCropViewController, didCropImage croppedImage: UIImage, usingCropRect cropRect: CGRect, rotationAngle: CGFloat) {
dismiss(animated: true)
profileImage.image = croppedImage
}
}
キャンセルが押された時、完了を押した時の処理を記載します。
画面に配置したprofileImageにcroppedImage(カットしたimage)を渡して画面を閉じてあげれば終了です。
最後にリポジトリも上げておきますので、サッと見たい方はこちらから↓
終わりに
RxSwiftを使用したので、若干複雑になってしまいましたが流れは追えたかなと思います。
カットするだけなら全然使えるライブラリではないかなと感じました。
写真を設定した後、firestoreに保存する所まで記載しようと思いましたが長くなりそうだったので次回に書きたいと思います!
修正点などあればご教授いただけると嬉しいです。