ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • PhotoPicker를 구현해 ProfileImage 뷰 구현하기
    SwiftUI 2023. 6. 10. 20:21

    iOS 14.0 에서 PHPickerViewController 가 추가되었습니다. 해당 ViewController를 이용해서 SwiftUI에서 사용할 수 있는 PhotoPicker를 구현하고 이를 이용해 프로파일 이미지 뷰를 구현해 보겠습니다. 기본적으로 ViewController를 SwiftUI에서 사용하기 위해서는 UIViewControllerRepresentable 프로토콜을 구현해 주어야 합니다. 해당 프로토콜은 아래의 두 함수를 구현하도록 하고 있습니다. 

    PHPickerViewController 를 이용해 PhotoPicker 구현하기 

    func makeUIViewController(context: Self.Context) -> Self.UIViewControllerType
    func updateUIViewController(Self.UIViewControllerType, context: Self.Context)

    대부분의 뷰가 딜리게이트를 통해서 결과를 받아오는 데 이런 경우 Coordinator를 추가해서 이를 수행할 수 있습니다. 구현해야 할 함수는 아래와 같다. 

    func makeCoordinator() -> Self.Coordinator

    우선 PhotoPicker를 UIViewControllerRepresentable 를 준수하도록 선언합니다. 

    struct PhotoPicker: UIViewControllerRepresentable {

    이제 makeUIViewController(context: Context) 가 PHPickerViewController를 생성해 반환하도록 함수를 생성합니다. 

    여기서는 이미지만 검색하므로  필터를 이미지로 선택합니다. 

    	func makeUIViewController(context: Context) ->  PHPickerViewController {
            var configuration = PHPickerConfiguration()
            configuration.filter = .images
            let controller = PHPickerViewController(configuration: configuration)
            controller.delegate = context.coordinator
            return controller
        }

    여기에 PHPickerViewController 이지지를 선택할 때 호출될 delegate를 할당하는데 이때 coordinator가 필요합니다. 이를 위해 추가로 coordinator를 아래와 같이 구현하고 makeCoordinator 함수를 구현합니다. 

    	class Coordinator: PHPickerViewControllerDelegate {
            private let parent: PhotoPicker
            init(_ parent: PhotoPicker) {
                self.parent = parent
            }
            
            func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
                picker.dismiss(animated: true)
                self.parent.results = results
    
            }
        }
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }

    이제 마지막으로 updateUIViewController()를 구현하는 데 해당 뷰 컨트롤러는 update 시 아무일도 하지 않기 때문에 빈 함수로 둡니다. 

    	func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
        }

    대부분의 UIViewController를 SwiftUI로 뷰로 변경하는 건 위와 같은 방법을 통해 이루어집니다. 해당 패턴을 익혀 두면 SwiftUI에서 아직 지원하지 않는 ViewController를 SwiftUI를 통해서 사용 가능합니다.  이제 가장 중요한 부분이 남았습니다. PHPickerViewController를 통해서 우리가 하고자 하는 일은 선택된 이미지를 SwiftUI 쪽에서 알게하는 거니까요. 이를 위해 바인딩 객체를 하나 생성합니다. 그리고 이미지를 선택 시 호출되는 delegate 함수가 호출될 때 전달된 result 값을 바인딩된 results 값에 할당합니다.

    @Binding var results: [PHPickerResult]?

    이제 아래와 같은 방법으로 PHPickerViewController의 결과 값을 받을 수 있습니다. 

    @State var selection: [PHPickerResult]?
    
    PhotoPicker(results: $selection)

    PhotoPicker를 이용해 ProfileImage 뷰 구현하기 

    여기서는 위에 구현된 PhtoPicker를 이용해 Image를 0.5 초가 누르면 PhotoPicker를 열어 선택된 이미지를 Image에 보이도록 구현해 보겠습니다. 우선 디폴트 이미지 와 로딩, 실패와 관련된 상태에 따라 뷰가 변경되도록 상태값을 표현하는 ProfileViewStatus를 정의합니다. 

    enum ProfileViewStatus {
        case success(Image)
        case empty
        case loading
        case failure(Error)
        
        var image: Image? {
            if case let .success(image) = self {
                return image
            }
            return nil
        }
    }

    해당 상태는 ProfieViewModel에서 관리하고 변경될 때 마다 뷰가 변경될 수 있도록 @Published 속성을 추가합니다. 또 PhotoPicker 값을 ProfileViewModel에서 관리하게 위해  @Published 속성으로 selections: [PHPickerResult]?를 정의합니다. 

    class ProfileViewModel: ObservableObject {
        @Published var status: ProfileViewStatus = .empty
        @Published var selections: [PHPickerResult]? = nil {
            didSet {
                guard let provider = selections?.first?.itemProvider,
                      provider.canLoadObject(ofClass: UIImage.self) else {
                    self.status = .empty
                    return
                    
                }
                status = .loading
                provider.loadObject(ofClass: UIImage.self) { image, _ in
                    guard let image = image as? UIImage else {
                        self.status = .empty
                        return
                    }
                    self.status = .success(Image(uiImage: image))
                }
            }
        }
    }

    selections 가 값이 할당되면 didSet을 이용해 결과에서 이미지를 로딩해 상태를 .success(Image(uiImage: image))를 통해서 상태를 변경합니다. 이 때 실패할 때는 .empty로 처리하고 있지만 .failure로 처리해 실패 했을 때 다른 상태로 변경해 줄 수 있습니다. 

    이제 위 두 개의 개체를 이용해 ProfileImage를 구현해 보겠습니다. 

     

    우선 상태에 따라 뷰에 표현할 뷰와 해당 뷰를 누를 때 픽커를 띄우는 뷰 두개로 분리해 구현합니다. 상태에 따라 변경되는 뷰를 LoadImageView로 정의하고 아래와 같이 구현합니다. 

    struct LoadImageView: View {
        var status: ProfileViewStatus
        var body: some View {
            switch status {
            case .success(let image):
                image.resizable()
                    .aspectRatio(contentMode: .fit)
            case .loading:
                ProgressView()
            case .empty:
                Image(systemName: "person.fill")
                    .font(.system(size: 40))
                    .foregroundColor(.white)
            case .failure(_):
                Image(systemName: "exclamationmark.triangle.fill")
                    .font(.system(size: 40))
                    .foregroundColor(.white)
            }
        }
    }

    상태에 따라 .success 일 시 받아온 이미지를, .loading 일 때 프로그래스뷰를 .empty 와 , .failure 일 때 각각의 시스템 이미지를 화면에 보여 줍니다. 이제 이 뷰를 LongPressGesture를 지원하는 뷰를 아래와 같이 구현합니다. 

    struct ProfileImage: View {
        @State var isPresented: Bool = false
        @ObservedObject var state = ProfileViewModel()
        @GestureState private var isDetectingLongPress = false
        @State private var completedLongPress = false
        
        var body: some View {
            LoadImageView(status: state.status)
                .scaledToFill()
                .frame(maxWidth: 100, maxHeight: 100)
                .background(
                    LinearGradient(
                        colors: [.white, .gray.opacity(0.5)],
                        startPoint: .topLeading,
                        endPoint: .bottomTrailing
                        )
                )
                .overlay(
                    Circle().stroke(
                        LinearGradient(
                            colors: [.white, .gray.opacity(0.5)],
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing
                        ), lineWidth: 1
                    )
                )
                .clipShape(Circle())
                .shadow(radius: 4)
                .onLongPressGesture(minimumDuration: 0.5) {
                    isPresented = true
                }
                .sheet(isPresented: $isPresented) {
                    PhotoPicker(results: $state.selections)
                }
        }
    }

    여기까지 ProfileView를 만들어 보았습니다. 여기서 SwiftUI를 개발하면서 많이 볼 UIViewControllerRepresentable를 통해서 ViewController를 SwiftUI 뷰로 변경하는 방법과 뷰 내 상태를 정의해 상태에 따라 뷰가 전이되도록 하는 방법에 대해 보여주기 위해 ProfileImage 뷰를 구현해 보았습니다. 해당 코드는 gitub에서 확인 할 수 있습니다. 

    https://github.com/hjpark0724/CacheAsyncImage/tree/main/CacheAsyncImage

     

    GitHub - hjpark0724/CacheAsyncImage

    Contribute to hjpark0724/CacheAsyncImage development by creating an account on GitHub.

    github.com

     

    댓글

Designed by Tistory.