SwiftUI에서 UIImagePickerController를 활용해서 이미지를 고르고 그 이미지를 다시 SwiftUI로 만든 View에 띄우는 기능을 구현하는 것이 오늘의 목표입니다.
메인뷰는 간단합니다. 이미지와 버튼 1개만 구현해두면 됩니다.
다만 UIImage 하나를 selectedImage라는 변수로 선언해둡니다. 해당 변수는 하위뷰인 ImagePicker에 Binding으로 전달할 것입니다. 하위 뷰인 ImagePicker에서 이 변수에 선택된 이미지를 할당할 것입니다. UIImage로 선언한 이유는 UIImagePickerController는 UIKit 프레임워크에 속해있기 때문에 선택된 이미지의 타입이 UIImage이기 때문입니다.
SwiftUI의 Image로 변환할 때는 UIImage를 인자로 받는 initializer를 사용하면 됩니다.
struct MainView3: View {
@State private var selectedImage: UIImage? //👉 image picker에서 선택한 이미지
@State var shouldShowImagePicker: Bool = false //👉 이미지 picker를 띄우기 위한 Bool 값
var body: some View {
VStack {
if let selectedImage = selectedImage { //👉 이미지가 있을 때만 이미지를 띄운다.
Image(uiImage: selectedImage)
.resizable()
.frame(width: 100, height: 100)
}
Button { //👉 ImagePicker를 모달로 띄우는 버튼
shouldShowImagePicker = true
} label: {
Text("Pick Image")
}
}
.sheet(isPresented: $shouldShowImagePicker) {
ImagePicker(image: $selectedImage)
}
}
}
UIViewControllerRepresentable 프로토콜은 UIViewController를 SwiftUI의 View로 감쌀 수 있게 해주는 프로토콜입니다.
안에는 총 3가지의 메소드를 정의해야하는데요. 3가지 메소드는 각각 아래와 같은 역할을 가지고 있습니다.
struct ImagePicker: UIViewControllerRepresentable {
//✅ SwiftUI의 context 안에서 사용할 VC를 만드는 메소드
func makeUIViewController(context: Context) -> some UIViewController {
}
//✅ context에 Coordinator를 정의하는 함수
//👉 Coordinator: VC에서 발생한 동작을 SwiftUI로 연결해주는 역할을 함. (delegate 역할)
func makeCoordinator() -> Coordinator {
}
//✅ VC를 업데이트할 때 사용하는 함수
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Coordinator는 UIViewController에서 일어나는 동작과 SwiftUI의 뷰를 연결해주는 역할을 합니다. 일종의 Delegate라고 보면 됩니다. delegate의 역할을 할 View를 parent로 전달합니다. 그리고 Coordinator는 NSObject와 UIImagePickerController의 상위 뷰가 상속해야하는 프로토콜들 (UINavigationControllerDelegate, UIImagePickerControllerDelegate)를 상속합니다.
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
//✅ 이미지 골랐을 때 수행할 메소드 정의 (UIImagePickerControllerDelegate protocol에 정의되어 있음)
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
}
}
struct ImagePicker: UIViewControllerRepresentable {
@Binding var image: UIImage? //👉 binding으로 상위의 state 변수와 연결
@Environment(\.presentationMode) var mode
func makeUIViewController(context: Context) -> some UIViewController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator //👉 coordinator를 delegate로 등록한다.
return picker //👉 VC를 리턴한다.
}
func makeCoordinator() -> Coordinator {
Coordinator(self) //👉 스스로를 parent로 가지는 Coordinator 리턴
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
//🤔 업데이트를 안 할 것이면 비워 두어도 된다.
}
//✅ ImagePicker에서만 사용할 클래스이므로 내부에 정의
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image = info[.originalImage] as? UIImage else { return } //👉 오리지널 이미지를 UIImage로 캐스팅해서
self.parent.image = image //👉 binding 변수에 할당한다.
self.parent.mode.wrappedValue.dismiss() //👉 모달을 dismiss
}
}
}
버튼을 누르면 SwiftUI의 모달로 감싸진 UIImagePickerController가 나타납니다. 여기서 이미지를 고르면 coordinator에 정의된 delegate 메소드가 실행되어 nil이었던 image가 선택된 UIImage로 할당됩니다. 결과적으로 @State 변수가 업데이트되면서 뷰가 다시 랜더링 되고 결과적으로 이미지가 뷰에 나타납니다.