iOS) 포토 라이브러리에서 사진 가져오기

2021. 7. 23. 23:55iOS

보통 앱에서 회원가입을 할때 프로필 사진용으로 사진업로드 기능을 지원한다. 이 포스팅에서는 포토 라이브러리에서 사진을 가져오거나, 카메라를 사용하여 직접 사진을 올리는 방법에 대해 설명한다. 

 

 

내가 구현한 회원가입 화면의 스크린샷이다. 사람모양의 아이콘이 있고 이것을 클릭하면 사진관련 동작을 실행할수 있도록 한다.

 

1. 이미지뷰 생성 및 배치

일단 UIImageView를 만들어 위 사진처럼 배치해주는 것에서 시작한다. 

 

private let imageView: UIImageView = {

        let imageView = UIImageView()

        imageView.image = UIImage(named: "person")

        imageView.tintColor = .gray

        imageView.contentMode = .scaleAspectFit

        imageView.layer.masksToBounds = true

        imageView.layer.borderWidth = 2

        imageView.layer.borderColor = UIColor.lightGray.cgColor

        return imageView

    }()

 

UIImage(named: "person") 은 Assets에 person이라는 이름으로 사람모양의 아이콘을 미리 등록해놓았다. scaleAspectFit 타입으로 비율에 맞게 이미지뷰를 채우고 layer.masksToBounds로 잘리는 부분은 영역에서 보이지 않게한다.

 

이렇게 imageView 변수에 UIImageView 설정을 저장했다. 이제 이것을 상위뷰에 배치시켜야 화면에 보인다.

위에서 설명하지 않았지만 현재 화면은 UIView위에 UIScrollView로 한번 덮고 나머지 요소들을 그 위에 배치하고 있는 상태이다.

 

 

이를 위해 viewDidLoad에 다음처럼 뷰 위에 스크롤뷰를 추가하고, 스크롤뷰 위에 다른 서브뷰를 추가하고 있다.

     override func viewDidLoad() {
        super.viewDidLoad()
        title = "Register"
        view.backgroundColor = .white
        
        ...
        
        // Add subviews
        view.addSubview(scrollView)
        scrollView.addSubview(imageView)
        
        ...
        
        imageView.isUserInteractionEnabled = true
        
        let gesture = UITapGestureRecognizer(target: self, action: #selector(didChangeProfilePic))
        imageView.addGestureRecognizer(gesture)
    }

 

여기에서 나중에 이미지를 뷰를 터치할 때 실행될 action을 Target-Action 으로 연결하고 있다. 

 

override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        scrollView.frame = view.bounds
        
        let size = scrollView.width/3
        imageView.frame = CGRect(x: (scrollView.width-size)/2,
                                 y: 20,
                                 width: size,
                                 height: size)
        imageView.layer.cornerRadius = imageView.width/2.0
  
        ...
}

 

배치를 위해서는 imageView의 frame을 정의해야 한다. imageView의 frame을 설정하기에 앞서 imageView의 상위뷰인 scrollView의 frame을 먼저 설정하면 좋다. 먼저 scrollView의 frame을 이 화면의 VC의 bounds로 설정하고 (화면전체크기), size를 대충 전체 화면길이의 1/3 정도로 설정해놓는다. 이것을 이용하여 가로정렬 좌표를 구하게 되는데 (화면의 가로길이 - 이미지의 가로길이) / 2 를 하면 가로 좌표를 구할 수 있다.

 

 

이제 이미지뷰를 화면에 띄웠으니 아까 Target-Action으로 연결했던 메소드를 완성하면 된다.

 

let gesture = UITapGestureRecognizer(target: self, action: #selector(didChangeProfilePic))
imageView.addGestureRecognizer(gesture)
@objc private func didChangeProfilePic() {
  presentPhotoActionSheet()
}

 

#selector의 didChangeProfile을 objc func 로 구현한뒤 presentPhotoActionSheet() 메소드를 호출한다. 

여기에서 사진선택과 사진찍기 등의 기능을 관련 델리게이트 패턴으로 구현하면 된다.

전체구현 코드는 다음과 같다.

 

extension RegisterViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func presentPhotoActionSheet() {
        let actionSheet = UIAlertController(title: "Profile Picture", message: "How would you like to select a picture?", preferredStyle: .actionSheet)
        actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        actionSheet.addAction(UIAlertAction(title: "Take Photo", style: .default, handler: { [weak self] _ in
            self?.presentCamera()
        }))
        actionSheet.addAction(UIAlertAction(title: "Choose Photo", style: .default, handler: { [weak self] _ in
            self?.presentPhotoPicker()
        }))
        
        present(actionSheet, animated: true)
    }
    
    func presentCamera() {
        let vc = UIImagePickerController()
        vc.sourceType = .camera
        vc.delegate = self
        vc.allowsEditing = true
        present(vc, animated: true)
    }
    
    func presentPhotoPicker() {
        let vc = UIImagePickerController()
        vc.sourceType = .photoLibrary
        vc.delegate = self
        vc.allowsEditing = true
        present(vc, animated: true)
    }
    
    // when user takes a photo
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        picker.dismiss(animated: true, completion: nil)
        
        guard let selectedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage else {
            return
        }
        self.imageView.image = selectedImage
    }
    
    // when user choose photo
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }
}

 

UIImagePickerControllerDelegate, UINavigationControllerDelegate 두개를 동시에 채택하고 있음에 유의해야한다. 왜 동시에 두가지 프로토콜을 채용해야 할까?

 

  • UIImagePickerControllerDelegate의 delegate 속성은 UIImagePickerControllerDelegate와 UINavigationControllerDelegate 프로토콜을 모두 구현하는 객체로 정의되어있다.
  • 위 코드의 vc.delegate = self에서 self를 vc.delegate에 할당하려면 self는 UINavigationControllerDelegate 타입이어야 한다.
  • UIImagePickerController의 delegate를 UINavigationControllerDelegate에 위임해준 것인데, 대리자는 사용자가 이미지나 동영상을 선택하거나 ImagePickerController 화면을 종료할 때 알림을 받는다.

 

 

   func presentCamera() {
        let vc = UIImagePickerController()
        vc.sourceType = .camera
        vc.delegate = self
        vc.allowsEditing = true
        present(vc, animated: true)
    }
    
    func presentPhotoPicker() {
        let vc = UIImagePickerController()
        vc.sourceType = .photoLibrary
        vc.delegate = self
        vc.allowsEditing = true
        present(vc, animated: true)
    }

 

다시 코드로 돌아가서, 먼저 이미지를 클릭하면 actionSheet로 사진을 찍을것인지 사진첩에서 이미지를 선택할 것인지 옵션을 제공한다. 사진을 찍을 것이라고 선택하면 presentCamera() 를, 사진첩을 불러올 것이라고 선택하면 presentPhotoPicker() 를 호출한다. 두 메소드를 보면 큰 차이는 없고 다만 UIImagePickerController.sourceType이 .camera 냐 .photoLibrary냐의 차이만 있을 뿐이다. 이때 위임자로는 현재화면의 View Controller를 지정해야 한다. 

 

    // when user takes a photo
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        picker.dismiss(animated: true, completion: nil)
        
        guard let selectedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage else {
            return
        }
        self.imageView.image = selectedImage
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }

 

imagePickerController(picker:, info:) 메소드는, 사진을 찍거나 불어오거나 아무튼 이미지를 선택했을때 자동으로 위임자 (=아까 설정했던 self) 에게 사용자가 이미지 선택을 완료했다고 말해준다. 

 

일단 PickerController(사진첩이나 사진기)를 화면에서 내린다. picker.dismiss(animated:, completion:)

그리고 사용자가 선택한 이미지를 구한다. 이때 두번째 인자인 Info 의 타입을 잘 보면

[UIImagePickerController.InfoKey : Any] 라고 되어있는데, InfoKey의 타입에는 아래 사진처럼 정말 많은 것들이 지원되고 있다.

 

 

우리는 이미지를 리턴할거니까 editiedImage를 선택한다.

그리고 또 Info 타입을 잘보면 Value의 타입이 Any 이다. UIImage로 타입캐스팅을 낭낭하게 걸어주자. Dictionary Subscript(:)는 Optional을 반환하므로 if let 문법을 사용하여 safe unwrapping 해주고, 현재 이미지 뷰의 이미지 정보에 방금 구한 이미지를 넣어주면 끝이다.