[SwiftUI] UIKit을 SwiftUI로 가져오기 (feat.UIImagePickerController)

이정훈·2023년 8월 2일
1

SwiftUI

목록 보기
3/4
post-thumbnail

SwiftUI에서 사진 라이브러리의 사진을 선택하여 첨부하는 방법으로 PhotosPicker view를 사용할 수 있다.

하지만 문제는 사용자가 직접 카메라로 찍은 사진을 바로 첨부하는 방법은 SwiftUI만으로는 불가능한듯 했다.

그래서 이것을 구현하기 위한 방법으로 UIKit에서 사진 촬영, 동영상 촬영, 라이브러리에서 이미지 선택을 위해 사용하는 UIImagePickerControllerSwiftUI로 가져와 사용하는 방법을 알아보고자 한다.


ViewController 인스턴스를 반환하는 구조체 생성

SwiftUI에서 UIKitUIImagePickerController를 사용하기 위해서 해당 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:)

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:)

updateUIViewController(_:context:)viewController에 영향을 미치는 App의 상태가 업데이트 되었을때 SwiftUI가 호출하는 메서드로 viewController의 구성(혹은 상태)를 업데이트 하기 위해 수행할 작업을 구현할 수 있으며 해당 메서드 내부의 구현은 선택 사항이므로 업데이트 할 사항이 없으면 메서드 내부를 구현하지 않아도 된다. (하지만 프로토콜 준수를 위해 메서드는 반드시 선언해야한다.)

이번 예제에서는 updateUIViewController(_:context:) 메서드 내부는 따로 구현하지 않았다.


SwiftUI에서 ImagePicker 생성

위의 과정까지 마치고 SwiftUI에서 ImagePicker 구조체를 생성하여 정상적으로 작동하는지 결과를 확인한다.

결과를 확인하기 위해 ContentView에 버튼을 생성하고 버튼이 눌렸을때 ImagePickerfullScreen 형식으로 보이도록 구현하였다.

그리고 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()
    }
}

실행 결과는 아래의 화면처럼 정상적으로 사진 라이브러리에서 사진을 선택하는 화면이 보여지는것을 확인할 수 있다.

Coordinator로 UIImagePickerControllerDelegate protocol 구현하기

위의 결과를 확인했을때, 사진 선택 화면은 정상적으로 나오지만 사진을 선택한 뒤에 어떤 작업을 수행할지 구현하지 않았기 때문에 사진을 선택한 뒤에 아무 반응이 일어나지 않는 것을 확인할 수 있다.

알다 싶이 UIKit의 많은 기능들을 delegate 패턴으로 구현하기 때문에 UIImagePickerController도 마찬가지로 delegate 패턴을 사용하여 사진을 선택한 이후에 수행할 작업들을 구현해야한다.

delegate 메서드 중에 imagePickerController(_:didFinishPickingMediaWithInfo:) 메서드를 통해 사용자가 선택한 사진에 대한 정보를 메서드의 parameter를 통해 가져올 수 있다.

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
 
}

그렇다면 어떻게 ViewController의 delegate 메서드로 가져온 선택된 사진에 대한 정보를 다시 SwiftUI로 전달 할 수 있을까?

이때 등장하는 것이 Coordinator라는 개념으로 Coordinator는 ViewController의 delegateSwiftUI를 연결해주는 역할을 한다.

시작에 앞서 Coordinator를 만들기 전에 선택한 이미지를 저장할 변수를 생성한다.

필자의 경우 선택한 이미지가 SwiftUI의 변수와 연동될 수 있도록 @Binding 형식으로 변수를 선언하였다.

추가적으로 이미지 선택 후 photo 라이브러리를 해제하기 위해 @EnvironmentpresentationMode 변수를 선언하고 이미지 선택이 완료된 후에 presentationMode.wrappedValuedismiss() 메서드를 사용하여 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 타입의 인스턴스를 반환한다.

마지막으로 UIImagePickerControllerdelegate를 지정해 주지 않았기 때문에 makeUIViewController(context:) 메서드 내부에 아래 코드와 같이 delegate를 지정해 준다.

imagePicker.delegate = context.coordinator

최종 실행 화면


카메라로 접근하기

이미지를 선택할 때 photo 라이브러리가 아닌 카메라를 직접 실행하여 찍은 사진을 가져오고 싶다면 아래의 코드와 같이 UIImagePickerControllersourceType.camera로 설정한다.

func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
    let imagePicker = UIImagePickerController()
    imagePicker.sourceType = .camera    //카메라로 접근
	...
 }

reference

https://www.appcoda.com/swiftui-camera-photo-library/

profile
새롭게 알게된 것을 기록하는 공간

2개의 댓글

comment-user-thumbnail
2023년 8월 2일

많은 도움이 되었습니다, 감사합니다.

1개의 답글