본문 바로가기

프로젝트/Twitter Clone App (w∕Firebase)

10. 기능 구현하기 #2

기능 구현하기 #2
프로필 이미지 설정하기


ImagePicker

SwiftUI를 사용하는 앱에서는 iOS16 혹은 동세대의 OS 이상을 사용하는 경우 PhotosUI를 사용하도록 개선이 이루어졌다.
물론 iOS16 이상을 대상으로만 서비스를 한다면 문제가 없겠지만 세상은 그리 녹록지 않기 마련이다.
이전까지는 PHPickerViewController나 UIKit의 UIImagePickerController를 사용해야만 한다.

영상에서는 UIImagePickerController를 사용하는 방식을 사용했으므로 이에 대해 나열한다.

Utils 폴더를 하나 만들어 주고, 그 안에 ImagePicker를 구현한다.

import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
    @Binding var selectedImage: UIImage?
    @Environment(\.dismiss) var dismiss

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }

    func makeUIViewController(context: Context) -> some UIViewController {
        let picker = UIImagePickerController()
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {

    }
}

extension 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
            }
            parent.selectedImage = image
            parent.dismiss()
        }
    }
}

UIKit의 PhotoPicker를 사용할 때 주의할 점은 Delegate 패턴을 필수적으로 구현해야 한다는 것이다.
그 역할은 Coordinator 클래스가 담당한다.
결론적으로 PhotoPicker를 표시하며, 선택한 이미지는 selectedImage에 저장하고 dismiss로 PhotoPicker를 닫는다.
해당 코드는 호출하기만 하면 되는 완벽히 모듈화 된 코드로서 다른 프로젝트에서도 충분히 재활용할 수 있다.

 

ProfilePhotoSelectorView

struct ProfilePhotoSelectorView: View {
    var body: some View {
        VStack {
            AuthHeaderView(upperTitle: "Create your account", lowerTitle: "Add a profile photo")

            Button {
                print("image picker")
            } label: {
                Image(systemName: "plus.square.dashed")
                    .resizable()
                    .frame(width: 180, height: 180)
                    .padding(.top, 50)
            }


            Spacer()

        }
        .ignoresSafeArea()
    }
}

기본적인 UI는 위와 같다.
화면 중앙의 파란색 + 아이콘을 터치해 PhotoPicker를 호출하고,
사진을 선택하면 해당 사진을 원형으로 표시한다.

struct ProfilePhotoSelectorView: View {
	@State private var showImagePicker = false
	@State private var selectedImage: UIImage?
	@State private var profileImage: Image?
	
	@EnvironmentObject var viewModel: AuthViewModel
	
	@Environment(\.dismiss) var dismiss

ImagePicker를 사용하기 위해 3개의 State 변수를 선언한다.

  • showImagePicker
    ImagePicker를 표시하기 위한 flag로 사용한다.
  • selectedImage
    선택된 이미지를 저장한다.
  • profileImage
    선택된 이미지를 화면에 프로필 사진의 형태로 표시한다.

ImageUpload
| ImageUploader

import FirebaseStorage

struct ImageUploader {
	static func uploadImage(image: UIImage, completion: @escaping(String) -> Void) {
		guard let imageData = image.jpegData(compressionQuality: 0.5) 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("debug: failed to upload image with \(error.localizedDescription)")
				return
			}
			
			ref.downloadURL { imageUrl, error in
				guard let imageUrl = imageUrl?.absoluteString else { return }
				completion(imageUrl)
			}
		}
	}
}

Image 등의 큰 데이터는 FirebaseStorage에 저장한다.
uploadImage 메서드를 선언하고, 이름은 무작위로 지정하기 위해 UUID를 사용하고, ref에는 저장 경로를 지정한다.
업로드가 완료되면 completion Handler로 업로드가 완료된 imageUrl을 전달한다.

ImageUpload
| 방금 생성한 계정 인증

프로필 사진은 방금 회원가입 완료한 계정의 정보에 해당한다.
따라서 해당 계정의 정보를 사용할 필요가 있다.

class AuthViewModel: ObservableObject {
	@Published var userSession: FirebaseAuth.User?
	@Published var didAuthenticateUser = false
	
	private var tempUserSession: FirebaseAuth.User?
	
	init() {
		self.userSession = Auth.auth().currentUser
		
		print("debug: user session is \(self.userSession?.uid)")
	}

AuthViewModel에 tempUserSession 변수를 하나 선언한다.

func register(withEmail email: String, password: String, fullname: String, username: String) {
    Auth.auth().createUser(withEmail: email, password: password) { result, error in
        if let error = error {
            print("debug: failed to register \(error.localizedDescription)")
            return
        }

        guard let user = result?.user else {
            return
        }
        self.tempUserSession = user

        let data = ["email": email,
                    "username": username.lowercased(),
                    "fullname": fullname,
                    "uid": user.uid]

        Firestore.firestore().collection("users").document(user.uid).setData(data) { _ in
            self.didAuthenticateUser = true
        }
    }
}

해당 변수에는 방금 회원 가입한 계정의 정보를 임시로 저장한다.

ImageUpload
| uploadProfileImage

func uploadProfileImage(_ image: UIImage) {
    guard let uid = tempUserSession?.uid else {
        return
    }

    ImageUploader.uploadImage(image: image) { profileImageUrl in
        Firestore.firestore().collection("users").document(uid).updateData(["profileImageUrl": profileImageUrl]) { _ in
            self.userSession = self.tempUserSession
            self.fetchUser()
        }
    }
}

프로필 사진을 설정하는 과정은 계정 가입이 완료되고, 해당 계정의 임시 정보가 존재할 때 진행된다.
Firebase의 ImageUploader 메서드에 이미지를 업로드하고, 업로드 한 이미지의 위치를 colsure로 받아 온다.
현재 로그인된 UserSession을 확인해 가져온 uid로 새로운 document를 생성하고,
profileImageUrl 이란 이름으로 이미지의 주소를 저장한다.

struct ProfilePhotoSelectorView: View {
    @State private var showImagePicker = false
    @State private var selectedImage: UIImage?
    @State private var profileImage: Image?

    @EnvironmentObject var viewModel: AuthViewModel

    @Environment(\.dismiss) var dismiss

    var body: some View {
        VStack {
            AuthHeaderView(upperTitle: "Setup account", lowerTitle: "Add a profile photo")

            Button {
                showImagePicker.toggle()
            } label: {
                if let profileImage = profileImage {
                    profileImage
                        .resizable()
                        .scaledToFill()
                        .frame(width: 180, height: 180)
                        .clipShape(Circle())
                } else {
                    Image(systemName: "plus.square.dashed")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 180, height: 180)
                }
            }
            .sheet(isPresented: $showImagePicker, onDismiss: loadImage) {
                ImagePicker(selectedImage: $selectedImage)
            }
            .padding(.top, 50)

            if let selectedImage = selectedImage {
                Button {
                    viewModel.uploadProfileImage(selectedImage)
                    dismiss()
                } label: {
                    Text("Continue")
                        .font(.headline)
                        .foregroundColor(.white)
                        .frame(width: 340, height: 50)
                        .background(Color(.systemBlue))
                        .clipShape(Capsule())
                        .padding()
                }

            }

            Spacer()

        }
        .ignoresSafeArea()
    }

    func loadImage() {
        guard let selectedImage = selectedImage else {
            return
        }
        profileImage = Image(uiImage: selectedImage)
    }
}

이후엔 버튼을 눌렀을 때 showImagePicker 변수를 toggle 하고,
해당 변수를 사용해 sheet 방식으로 ImagePicker를 호출하기만 하면 된다.
selectedImage가 존재하지 않는다면 + 버튼을 표시하고,
존재한다면 UIImage를 Image로 캐스팅해 가공해 화면에 표시함과 동시에 설정을 완료할 수 있는 버튼을 표시한다.
Continue라는 이름으로 표시되는 이 버튼은 AuthViewModel의 uploadProfileImage 메서드를 호출해 서버에 이미지를 저장한다.

선택한 이미지가 표시되고, 회원가입이 마무리되는 걸 확인할 수 있다.

새로 가입한 Test777의 uid와 동일한 데이터가 추가됐고, imageUrl 프로필 사진의 ImageUrl도 잘 저장된 걸 확인할 수 있다.

'프로젝트 > Twitter Clone App (w∕Firebase)' 카테고리의 다른 글

12. DB와 연결하기 #1  (0) 2023.01.05
11. 기능 구현하기 #3  (0) 2022.12.22
09. 기능 구현하기 #1  (0) 2022.12.13
08. 기본 UI 구현하기 #8  (0) 2022.12.02
07. 기본 UI 구성하기 #7  (0) 2022.12.02