SwiftUI에서 사진 라이브러리의 사진을 선택하여 첨부하는 방법으로 PhotosPicker
view를 사용할 수 있다.
하지만 문제는 사용자가 직접 카메라로 찍은 사진을 바로 첨부하는 방법은 SwiftUI만으로는 불가능한듯 했다.
그래서 이것을 구현하기 위한 방법으로 UIKit에서 사진 촬영, 동영상 촬영, 라이브러리에서 이미지 선택을 위해 사용하는 UIImagePickerController
를 SwiftUI로 가져와 사용하는 방법을 알아보고자 한다.
SwiftUI에서 UIKit의 UIImagePickerController
를 사용하기 위해서 해당 ViewController 인스턴스를 반환할 수 있는 구조체를 선언해야 한다.
방법은 아래의 코드와 같이 구조체가 UIViewControllerRepresentable
protocol을 채택하고 해당 protocol을 준수 하도록 makeUIViewController(context:)
메서드와 updateUIViewController(_:context:)
메서드를 구현하면 된다.
import SwiftUI
import UIKit
struct ImagePicker: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
// UIImagePickerController 인스턴스 반환
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
makeUIViewController(context:)
메서드는 ImagePicker
구조체가 생성될때 호출되는 메서드로 최종적으로 우리의 목적인 UIImagePickerController
를 반환하는 메서드이다.
makeUIViewController(context:)
메서드 내부 구현은 다음과 같다.
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
// UIImagePickerController 인스턴스 반환
let imagePicker = UIImagePickerController()
imagePicker.sourceType = .photoLibrary //이미지 소스 선택
imagePicker.allowsEditing = false //이미지 편집기능 여부
return imagePicker
}
updateUIViewController(_:context:)
는 viewController에 영향을 미치는 App의 상태가 업데이트 되었을때 SwiftUI가 호출하는 메서드로 viewController의 구성(혹은 상태)를 업데이트 하기 위해 수행할 작업을 구현할 수 있으며 해당 메서드 내부의 구현은 선택 사항이므로 업데이트 할 사항이 없으면 메서드 내부를 구현하지 않아도 된다. (하지만 프로토콜 준수를 위해 메서드는 반드시 선언해야한다.)
이번 예제에서는 updateUIViewController(_:context:)
메서드 내부는 따로 구현하지 않았다.
위의 과정까지 마치고 SwiftUI에서 ImagePicker
구조체를 생성하여 정상적으로 작동하는지 결과를 확인한다.
결과를 확인하기 위해 ContentView
에 버튼을 생성하고 버튼이 눌렸을때 ImagePicker
가 fullScreen 형식으로 보이도록 구현하였다.
그리고 ImagePicker
에서 선택한 사진을 ContentView
에서 확인할 수 있도록 아래 코드와 같이 Image view도 생성하였다.
import SwiftUI
struct ContentView: View {
@State private var image: UIImage = UIImage()
@State private var showImagePicker: Bool = false
var body: some View {
VStack {
Image(uiImage: image)
.resizable()
Button(action: {
showImagePicker.toggle()
}, label: { Text("이미지 선택") })
.buttonStyle(.borderedProminent)
}
.fullScreenCover(isPresented: $showImagePicker, content: {
ImagePicker()
})
.padding()
}
}
실행 결과는 아래의 화면처럼 정상적으로 사진 라이브러리에서 사진을 선택하는 화면이 보여지는것을 확인할 수 있다.
위의 결과를 확인했을때, 사진 선택 화면은 정상적으로 나오지만 사진을 선택한 뒤에 어떤 작업을 수행할지 구현하지 않았기 때문에 사진을 선택한 뒤에 아무 반응이 일어나지 않는 것을 확인할 수 있다.
알다시피 UIKit의 많은 기능들을 delegate 패턴으로 구현하기 때문에 UIImagePickerController
도 마찬가지로 delegate 패턴을 사용하여 사진을 선택한 이후에 수행할 작업들을 구현해야한다.
delegate 메서드 중에 imagePickerController(_:didFinishPickingMediaWithInfo:)
메서드를 통해 사용자가 선택한 사진에 대한 정보를 메서드의 parameter를 통해 가져올 수 있다.
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
}
그렇다면 어떻게 ViewController의 delegate 메서드로 가져온 선택된 사진에 대한 정보를 다시 SwiftUI로 전달 할 수 있을까?
이때 등장하는 것이 Coordinator라는 개념으로 Coordinator는 ViewController의 delegate와 SwiftUI를 연결해주는 역할을 한다.
시작에 앞서 Coordinator를 만들기 전에 선택한 이미지를 저장할 변수를 생성한다.
필자의 경우 선택한 이미지가 SwiftUI의 변수와 연동될 수 있도록 @Binding
형식으로 변수를 선언하였다.
추가적으로 이미지 선택 후 photo 라이브러리를 해제하기 위해 @Environment
로 presentationMode
변수를 선언하고 이미지 선택이 완료된 후에 presentationMode.wrappedValue
의 dismiss()
메서드를 사용하여 photo 라이브러리를 해제한다.
@Binding var selectedImage: UIImage //선택된 이미지를 bindging 형식으로 저장
@Environment(\.presentationMode) private var presentationMode //photo 라이브러리 해지를 위한 변수
Coordinator의 역할을 하는 class를 만들기 위해 ImagePicker
구조체 내부에 Coordinator
class를 생성하고 NSObject
class를 상속 받고 UIImagePickerControllerDelegate
protocol과 UINavigationControllerDelegate
protocol을 채택하도록 한다.
struct ImagePicker: UIViewControllerRepresentable {
@Binding var selectedImage: UIImage //선택된 이미지를 bindging 형식으로 저장
@Environment(\.presentationMode) private var presentationMode //photo 라이브러리 해지를 위한 변수
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var imagePicker: ImagePicker
init(imagePicker: ImagePicker) {
self.imagePicker = imagePicker
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
imagePicker.selectedImage = image
}
imagePicker.presentationMode.wrappedValue.dismiss() //사진 라이브러리 해제
}
}
...
}
Coordinator
class 내부에 구현한 imagePickerController(_:didFinishPickingMediaWithInfo:)
메서드는 이미지가 선택 되었을때 delegate를 통해 호출되는 메서드로 info
parameter를 통해 선택한 이미지를 가져올 수 있다.
Coordinator
클래스 구현이 완료되면 해당 클래스의 인스턴스를 반환할 수 있는 메서드를 ImagePicker
내부에 구현한다.
func makeCoordinator() -> Coordinator {
return Coordinator(imagePicker: self)
}
해당 메서드는 makeUIViewController(context:)
메서드 호출전 호출되어 Coordinator
타입의 인스턴스를 반환한다.
마지막으로 UIImagePickerController
의 delegate를 지정해 주지 않았기 때문에 makeUIViewController(context:)
메서드 내부에 아래 코드와 같이 delegate를 지정해 준다.
imagePicker.delegate = context.coordinator
이미지를 선택할 때 photo 라이브러리가 아닌 카메라를 직접 실행하여 찍은 사진을 가져오고 싶다면 아래의 코드와 같이 UIImagePickerController
의 sourceType
을 .camera
로 설정한다.
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.sourceType = .camera //카메라로 접근
...
}
많은 도움이 되었습니다, 감사합니다.