[SwiftUI] TwitterClone: ProfilePhotoSelectorView

Junyoung Park·2022년 11월 17일
0

SwiftUI

목록 보기
105/136
post-thumbnail
post-custom-banner

🔴 Let's Build Twitter with SwiftUI (iOS 15, Xcode 13, Firebase, SwiftUI 3.0)

TwitterClone: ProfilePhotoSelectorView

구현 목표

  • 이미지 서버 저장 및 데이터베이스 등록

구현 태스크

  • PHPickerViewController를 통한 이미지 등록 뷰 구현
  • 파이어베이스 스토리지 함수 구현: 프로필 선택 이미지 스토리지 업로드 및 URL 정보 리턴
  • 파이어스토어 함수 구현: 프로필 URL 정보 데이터베이스 저장

핵심 코드

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            picker.dismiss(animated: true)
            guard
                let itemProvider = results.first?.itemProvider,
                itemProvider.canLoadObject(ofClass: UIImage.self) else { return }
            itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
                guard
                    let image = image as? UIImage,
                    error == nil else { return }
                self?.parent.selectedImage = image
            }
        }
  • SwiftUI 프레임워크 내에서 UIKit 컴포넌트를 사용하기 위한 커스텀 UIViewControllerRepresentable 클래스
  • 해당 클래스의 델리게이트 함수를 통해 선택된 이미지를 리턴
.sheet(isPresented: $showImagePicker, onDismiss: loadImage) {
                ImagePicker(selectedImage: $selectedImage)
            }
  • 현재 뷰에서 해당 이미지 피커를 사용한 결과값을 사용하도록 onDismiss 함수에서 적용
private func loadImage() {
        guard let selectedImage = selectedImage else { return }
        profileImage = Image(uiImage: selectedImage)
    }
  • 선택된 이미지가 있다면 profileImage에 값을 줌
func uploadProfileImage(with image: UIImage) {
        guard let uid = tempUserSession?.uid else { return }
        ImageUploader.uploadImage(image: image) { result in
            switch result {
            case .success(let urlString):
                let data = ["profileImageURL": urlString]
                Firestore.firestore().collection("users")
                    .document(uid)
                    .setData(data, merge: true) { [weak self] error in
                        if let error = error {
                            print(error.localizedDescription)
                            print("Profile Upload Did Fail")
                        } else {
                            self?.userSession = self?.tempUserSession
                            self?.tempUserSession = nil
                        }
                    }
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
  • 선택된 이미지가 생긴다면 버튼 클릭 가능
  • 주어진 이미지를 파이어베이스 스토리지에 등록 후 해당 URL 정보를 리턴하는 컴플리션 핸들러 작성

소스 코드

import SwiftUI
import PhotosUI

struct ImagePicker: UIViewControllerRepresentable {
    @Binding var selectedImage: UIImage?
    @Environment(\.dismiss) var dismiss
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> some UIViewController {
        let configuration = PHPickerConfiguration()
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = context.coordinator
        return picker
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        
    }

}

extension ImagePicker {
    class Coordinator: NSObject, UINavigationControllerDelegate, PHPickerViewControllerDelegate {
        let parent: ImagePicker
        
        init(_ parent: ImagePicker) {
            self.parent = parent
        }
        
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            picker.dismiss(animated: true)
            guard
                let itemProvider = results.first?.itemProvider,
                itemProvider.canLoadObject(ofClass: UIImage.self) else { return }
            itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
                guard
                    let image = image as? UIImage,
                    error == nil else { return }
                self?.parent.selectedImage = image
            }
        }
    }
}
  • 기존 UIKit 프레미워크의 기본 컴포넌트를 사용하는 클래스를 핸들링
  • Coordinator 등 프로토콜을 따르는 데 필요한 함수를 적용한 뒤 서용
  • 현재 클래스를 사용하고 있는 뷰에서 바인딩으로 넘겨받은 selectedImage 값을 변경함으로써 해당 뷰에서 이미지를 사용 가능
import SwiftUI

struct ProfilePhotoSelectorView: View {
    @State private var showImagePicker = false
    @State private var selectedImage: UIImage?
    @State private var profileImage: Image?
    @EnvironmentObject private var viewModel: AuthViewModel
    var body: some View {
        VStack {
            AuthenticationHeaderView(title: "Create your account\nAdd a profile photo")
            Button {
                showImagePicker.toggle()
            } label: {
                if let profileImage = profileImage {
                    profileImage
                        .resizable()
                        .scaledToFill()
                        .frame(width: 180, height: 180)
                        .clipShape(Circle())
                } else {
                    Image("tweet_plus")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 180, height: 180)
                }
            }
            .sheet(isPresented: $showImagePicker, onDismiss: loadImage) {
                ImagePicker(selectedImage: $selectedImage)
            }
            .padding(.top, 44)
            
            if let selectedImage = selectedImage {
                Button {
                    viewModel.uploadProfileImage(with: selectedImage)
                } label: {
                    Text("Continue")
                        .font(.headline)
                        .foregroundColor(.white)
                        .frame(width: 340, height: 50)
                        .background(Color(.systemBlue))
                        .clipShape(Capsule())
                        .padding()
                }
                .shadow(color: .gray.opacity(0.5), radius: 10, x: 0, y: 0)
            }

            Spacer()
        }
        .ignoresSafeArea()
    }
}

extension ProfilePhotoSelectorView {
    private func loadImage() {
        guard let selectedImage = selectedImage else { return }
        profileImage = Image(uiImage: selectedImage)
    }
}
  • 선택받은 이미지가 존재하다면 해당 이미지를 렌더링
  • 버튼 클릭을 통해 해당 이미지를 뷰 모델 함수를 통해 업로드
func uploadProfileImage(with image: UIImage) {
        guard let uid = tempUserSession?.uid else { return }
        ImageUploader.uploadImage(image: image) { result in
            switch result {
            case .success(let urlString):
                let data = ["profileImageURL": urlString]
                Firestore.firestore().collection("users")
                    .document(uid)
                    .setData(data, merge: true) { [weak self] error in
                        if let error = error {
                            print(error.localizedDescription)
                            print("Profile Upload Did Fail")
                        } else {
                            self?.userSession = self?.tempUserSession
                            self?.tempUserSession = nil
                        }
                    }
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
  • 회원가입 이후 userSession 값으로 넘겨받은 임시 변수를 통해 데이터베이스에 접근 가능
import Foundation
import SwiftUI
import FirebaseStorage

struct ImageUploader {
    static func uploadImage(image: UIImage, completion: @escaping(Result<String, Error>) -> Void) {
        guard let imageData = image.jpegData(compressionQuality: 0.80) else { return }
        let fileName = UUID().uuidString
        let ref = Storage.storage().reference(withPath: "/profile_image/\(fileName)")
        ref.putData(imageData) { _, error in
            if let error = error {
                print(error.localizedDescription)
                print("Failed to upload image with error")
                completion(.failure(error))
                return
            }
            ref.downloadURL { imageURL, error in
                if let error = error {
                    completion(.failure(error))
                    return
                } else if let imageURLString = imageURL?.absoluteString {
                    completion(.success(imageURLString))
                    return
                } else {
                    completion(.failure(URLError(.badURL)))
                    return
                }
            }
        }
    }
}
  • static 함수를 통해 해당 클래스의 타입으로 접근 가능
  • Result<> 타입을 리턴하는 컴플리션 핸들러

구현 화면

파이어베이스 스토리지에 사진을 업로드하는 중 인터렉션을 막기 위해 버튼 클릭 이벤트를 막거나, 도중 스피너(ProgressView) 등을 사용할 수 있을 것이다.

profile
JUST DO IT
post-custom-banner

0개의 댓글