Xcode 프로젝트에 이미지 자르기(crop) 기능 추가: iOS Swift 라이브러리 Mantis 사용

카메라 또는 사진을 선택하면 화면에 보여주는 프로젝트가 있는데, 여기서 선택 후 화면에 표시하기 전 크롭(crop) 기능을 이용해 이미지를 자르는 기능을 추가하고자 합니다.

 

직접 구현하려면 매우 어려운 기능이지만 다행히 Mantis라는 외부 라이브러리가 있어 크롭 기능을 손쉽게 추가할 수 있습니다.

 

아래 코드 목록에서 PhotoViewController.swift를 사용합니다.


import UIKit
import Photos
// MARK: – 사진 권한 물어보기
private func photoAuth(isCamera: Bool, viewController: UIViewController, completion: @escaping () -> ()) {
// 경고 메시지 작성
let sourceName = isCamera ? "카메라" : "사진 라이브러리"
let notDeterminedAlertTitle = "No Permission Status"
let notDeterminedMsg = "\(sourceName)의 권한 설정을 변경하시겠습니까?"
let restrictedMsg = "시스템에 의해 거부되었습니다."
let deniedAlertTitle = "Permission Denied"
let deniedMsg = "\(sourceName)의 사용 권한이 거부되었기 때문에 사용할 수 없습니다. \(sourceName)의 권한 설정을 변경하시겠습니까?"
let unknownMsg = "unknown"
// 카메라인 경우와 사진 라이브러리인 경우를 구분해서 권한 status의 원시값(Int)을 저장
let status: Int = isCamera
? AVCaptureDevice.authorizationStatus(for: AVMediaType.video).rawValue
: PHPhotoLibrary.authorizationStatus().rawValue
// PHAuthorizationStatus, AVAuthorizationStatus의 status의 원시값은 공유되므로 같은 switch문에서 사용
switch status {
case 0:
// .notDetermined – 사용자가 아직 권한에 대한 설정을 하지 않았을 때
simpleDestructiveYesAndNo(viewController, message: notDeterminedMsg, title: notDeterminedAlertTitle, yesHandler: openSettings)
print("CALLBACK FAILED: \(sourceName) is .notDetermined")
case 1:
// .restricted – 시스템에 의해 앨범에 접근 불가능하고, 권한 변경이 불가능한 상태
simpleAlert(viewController, message: restrictedMsg)
print("CALLBACK FAILED: \(sourceName) is .restricted")
case 2:
// .denied – 접근이 거부된 경우
simpleDestructiveYesAndNo(viewController, message: deniedMsg, title: deniedAlertTitle, yesHandler: openSettings)
print("CALLBACK FAILED: \(sourceName) is .denied")
case 3:
// .authorized – 권한 허용된 상태
print("CALLBACK SUCCESS: \(sourceName) is .authorized")
completion()
case 4:
// .limited (iOS 14 이상 사진 라이브러리 전용) – 갤러리의 접근이 선택한 사진만 허용된 경우
print("CALLBACK SUCCESS: \(sourceName) is .limited")
completion()
default:
// 그 외의 경우 – 미래에 새로운 권한 추가에 대비
simpleAlert(viewController, message: unknownMsg)
print("CALLBACK FAILED: \(sourceName) is unknwon state.")
}
}
// MARK: – 설정 앱 열기
private func openSettings(action: UIAlertAction) -> Void {
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
return
}
if UIApplication.shared.canOpenURL(settingsUrl) {
UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
print("Settings opened: \(success)") // Prints true
})
}
}
// MARK: – photoAuth 함수를 main 스레드에서 실행 (UI 관련 문제 방지)
private func photoAuthInMainAsync(isCamera: Bool, viewController: UIViewController, completion: @escaping () -> ()) {
DispatchQueue.main.async {
photoAuth(isCamera: isCamera, viewController: viewController, completion: completion)
}
}
// MARK: – 사진 라이브러리의 권한을 묻고, 이후 () -> () 클로저를 실행하는 함수
func authPhotoLibrary(_ viewController: UIViewController, completion: @escaping () -> ()) {
if #available(iOS 14, *) {
// iOS 14의 경우 사진 라이브러리를 읽기전용 또는 쓰기가능 형태로 설정해야 함
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
photoAuthInMainAsync(isCamera: false, viewController: viewController, completion: completion)
}
} else {
// Fallback on earlier versions
PHPhotoLibrary.requestAuthorization { status in
photoAuthInMainAsync(isCamera: false, viewController: viewController, completion: completion)
}
}
}
// MARK: – 카메라의 권한을 묻고, 이후 () -> () 클로저를 실행하는 함수
func authDeviceCamera(_ viewController: UIViewController, completion: @escaping () -> ()) {
let notAvailableMsg = "카메라를 사용할 수 없습니다."
if UIImagePickerController.isSourceTypeAvailable(.camera) {
AVCaptureDevice.requestAccess(for: .video) { status in
photoAuthInMainAsync(isCamera: true, viewController: viewController, completion: completion)
}
} else {
// 시뮬레이터 등에서 카메라를 사용할 수 없는 경우
DispatchQueue.main.async {
simpleAlert(viewController, message: notAvailableMsg)
}
}
}

view raw

PhotoAuth.swift

hosted with ❤ by GitHub


import UIKit
class PhotoViewController: UIViewController {
@IBOutlet weak var imgView: UIImageView!
// 1) 이미지 피커 컨트롤러 추가
let imagePickerController = UIImagePickerController()
override func viewDidLoad() {
super.viewDidLoad()
// 2) 이미지 피커에 딜리게이트 생성
imagePickerController.delegate = self
}
@IBAction func btnActCamera(_ sender: UIButton) {
// 5-1) 권한 관련 작업 후 콜백 함수 실행(카메라)
authDeviceCamera(self) {
self.imagePickerController.sourceType = .camera
self.present(self.imagePickerController, animated: true, completion: nil)
}
}
@IBAction func btnActPhotoLibrary(_ sender: UIButton) {
// 5-2) 권한 관련 작업 후 콜백 함수 실행(사진 라이브러리)
authPhotoLibrary(self) {
// .photoLibrary – Deprecated: Use PHPickerViewController instead. (iOS 14 버전 이상 지원)
self.imagePickerController.sourceType = .photoLibrary
self.present(self.imagePickerController, animated: true, completion: nil)
}
}
}
// 3) 이미지 피커 관련 프로토콜을 클래스 상속 목록에 추가
extension PhotoViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
// 4) 카메라 또는 사진 라이브러리에서 사진을 찍거나 선택했을 경우 실행할 내용 작성
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.originalImage] as? UIImage {
imgView.image = image
}
dismiss(animated: true, completion: nil)
}
}


import UIKit
func simpleAlert(_ controller: UIViewController, message: String) {
let alertController = UIAlertController(title: "Caution", message: message, preferredStyle: .alert)
let alertAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(alertAction)
controller.present(alertController, animated: true, completion: nil)
}
func simpleAlert(_ controller: UIViewController, message: String, title: String, handler: ((UIAlertAction) -> Void)?) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let alertAction = UIAlertAction(title: "OK", style: .default, handler: handler)
alertController.addAction(alertAction)
controller.present(alertController, animated: true, completion: nil)
}
func simpleDestructiveYesAndNo(_ controller: UIViewController, message: String, title: String, yesHandler: ((UIAlertAction) -> Void)?) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let alertActionNo = UIAlertAction(title: "No", style: .cancel, handler: nil)
let alertActionYes = UIAlertAction(title: "Yes", style: .destructive, handler: yesHandler)
alertController.addAction(alertActionNo)
alertController.addAction(alertActionYes)
controller.present(alertController, animated: true, completion: nil)
}
func simpleYesAndNo(_ controller: UIViewController, message: String, title: String, yesHandler: ((UIAlertAction) -> Void)?) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let alertActionNo = UIAlertAction(title: "No", style: .cancel, handler: nil)
let alertActionYes = UIAlertAction(title: "Yes", style: .default, handler: yesHandler)
alertController.addAction(alertActionNo)
alertController.addAction(alertActionYes)
controller.present(alertController, animated: true, completion: nil)
}

 

1) Mantis 라이브러리를 CocoaPods, Swift Packages 등을 이용해 설치합니다.

 

pod 'Mantis'

 

2) 설치가 완료되었다면 프로젝트를 열고 뷰 컨트롤러에 Mantisimport 합니다.
import UIKit
import Mantis

 

3) CropViewControllerDelegate를 구현하는 아래 extension을 추가합니다.
extension PhotoViewController: CropViewControllerDelegate {

    func cropViewControllerDidCrop(_ cropViewController: CropViewController, cropped: UIImage, transformation: Transformation, cropInfo: CropInfo) {

        // 이미지 크롭 후 할 작업 추가

        cropViewController.dismiss(animated: true, completion: nil)
    }
    
    func cropViewControllerDidCancel(_ cropViewController: CropViewController, original: UIImage) {

        cropViewController.dismiss(animated: true, completion: nil)
    }
        
    private func openCropVC(image: UIImage) {
        
        let cropViewController = Mantis.cropViewController(image: image)
        cropViewController.delegate = self
        cropViewController.modalPresentationStyle = .fullScreen
        self.present(cropViewController, animated: true)
    }
}
  • cropViewControllerDidCrop(...)
    • 필수적으로 구현해야 하는 프로토콜 스텁(Protocol stub)이며, 이미지 크롭 작업 완료 후 실행할 내용을 작성합니다.
    • 크롭뷰 컨트롤러(cropViewController)를 사라지게 하는 dismiss를 추가합니다.
    • 이 함수가 실행되는 영역은 cropViewController이므로 cropViewController 대신 self를 넣어도 됩니다.
  • cropViewControllerDidCancel(...)
    • 필수 프로토콜 스텁이며 이미지 크롭 작업이 취소(cancel)되었을 때 해야할 작업을 지정합니다.
    • 취소 후 따로 할 일이 없으므로 컨트롤러(cropViewController)를 사라지게 합니다.
  • openCropVC(image: UIImage)
    • 카메라 촬영 후 또는 이미지 선택 후 그 이미지를 바탕으로 이미지 크롭 컨트롤러를 실행하는 코드입니다.
    • iOS 13 이후 버전은 cropViewController.modalPresentationStyle = .fullScreen 를 지정해야 합니다.

 

4) 기존 이미지 피커 컨트롤러 부분에서 크롭 뷰 컨트롤러를 띄우도록 변경합니다.
// 3) 이미지 피커 관련 프로토콜을 클래스 상속 목록에 추가
extension PhotoViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    
    // 4) 카메라 또는 사진 라이브러리에서 사진을 찍거나 선택했을 경우 실행할 내용 작성
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        
        if let image = info[.originalImage] as? UIImage {
//            imgView.image = image // <- 삭제하고 cropViewControllerDidCrop에서 실행
            
            dismiss(animated: true) {
                self.openCropVC(image: image)
            }
            
        }
        dismiss(animated: true, completion: nil)
    }
}
  • 기존에 하던 작업을 삭제하고 cropViewControllerDidCrop 함수로 옮깁니다.
  • dismiss 후 클로저 함수로 self.openCropVC(image: image) 를 실행해야 합니다. 별도로 분리할 경우 크롭뷰 컨트롤러가 뜨지 않습니다.

 

5) 4번에서 삭제한 부분을 cropViewControllerDidCrop로 옮깁니다.
func cropViewControllerDidCrop(_ cropViewController: CropViewController, cropped: UIImage, transformation: Transformation, cropInfo: CropInfo) {
    
    // 기존 작업 부분
    imgView.image = cropped
    
    cropViewController.dismiss(animated: true, completion: nil)
}

 

좀 더 자세한 사용법은 Mantis 페이지에서 볼 수 있습니다. 비율 강제 고정, 커스텀 비율 추가, 다양한 모양으로 자르기, 로컬라이징 등이 가능합니다.

예를 들어 원래는 크롭 뷰 컨트롤러에서 다양한 이미지 비율을 지원하지만, 이를 무시하고 강제적으로 이미지 자르기 비율을 1:1로 고정하려면 openCropVC 함수에 아래와 같은 코드를 추가하면 됩니다. 사용자 프로필 사진 등 비율이 강제되는 사진 자르기 기능에 적합합니다.

cropViewController.config.presetFixedRatioType = .alwaysUsingOnePresetFixedRatio(ratio: 1.0)

 

실행 화면

Animated GIF - Find & Share on GIPHY

문의 | 코멘트 또는 yoonbumtae@gmail.com


카테고리: Swift


0개의 댓글

답글 남기기

Avatar placeholder

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다