Xcode 프로젝트에 이미지 자르기(crop) 기능 추가: iOS Swift 라이브러리 Mantis 사용
카메라 또는 사진을 선택하면 화면에 보여주는 프로젝트가 있는데, 여기서 선택 후 화면에 표시하기 전 크롭(crop) 기능을 이용해 이미지를 자르는 기능을 추가하고자 합니다.
직접 구현하려면 매우 어려운 기능이지만 다행히 Mantis라는 외부 라이브러리가 있어 크롭 기능을 손쉽게 추가할 수 있습니다.
아래 코드 목록에서 PhotoViewController.swift
를 사용합니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) 설치가 완료되었다면 프로젝트를 열고 뷰 컨트롤러에 Mantis
를 import
합니다.
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)
0개의 댓글