3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Swift]SNS風プロフィール画像設定時、円形に切り抜く方法

Last updated at Posted at 2022-07-25

はじめに

SNSのようなプロフィール画像を設定する時に、丸く写真を切り抜きたいことが多いと思います。

ライブラリを使って楽に実装できたので備忘録として残しておこうと思います。

実際の画面はこのような感じです

ライブラリから

カメラ撮影から

使用ライブラリ

今回はこちらのライブラリを使用しました。

Objective-Cで書かれており、カスタマイズをしないなら十分使えそうです。

Podでインポートしました。

Podfile
  pod 'RSKImageCropper'
  pod 'RxSwift'
  pod 'RxCocoa'
 

※RxSwiftを使用しています

注意点

実機のカメラ機能を試す場合は、Info.plistで下記を追加するのを忘れずに
スクリーンショット 2022-07-25 12.05.00.png

ソースコード

ViewController

ProfileImageViewController.swift

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

ViewModel.swift
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に保存する所まで記載しようと思いましたが長くなりそうだったので次回に書きたいと思います!
修正点などあればご教授いただけると嬉しいです。

3
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?