Firestore database는 1.5mb 이하의 데이터만 저장이 가능하기때문에 이미지를 업로드 하지 않고 일반 하드인 Storage에 upload해야 한다.
retrive시에는 Storage에 저장된 UUID로 생성한 post.id, image.id의 url을 불러와 database에 저장한다.
1) 구조체 초기화
import Foundation
import UIKit
struct Post: Hashable, Codable, Identifiable {
var id: String
var title: String
var contents: String
var likeCounts: Int
var imageName: String?
let createdAt: Double
var imagePath: String {
return "images/\(id)/\(imageName ?? "")"
}
var createdDate: String {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "ko_kr")
dateFormatter.timeZone = TimeZone(abbreviation: "KST")
dateFormatter.dateFormat = "yyyy-MM-dd" // "yyyy-MM-dd HH:mm:ss"
let dateCreatedAt = Date(timeIntervalSince1970: createdAt)
print(dateCreatedAt)
print(Date().timeIntervalSince1970)
return dateFormatter.string(from: dateCreatedAt)
}
}
struct PostImage: Identifiable, Hashable {
var id: String
var image: UIImage
var imgName: String
}
Post 작성시 들어갈 변수들을 생성해주며 Post별 이미지를 첨부하지 않을 수도 있기에 nil 값 처리하며
주의점으로 database에는 Storage의 imagePath를 넣어준다.
그리고 각 post별로 생성기준으로 정렬하기위해 DateFormatter를 선언한다.
2) Post 작성
@MainActor
func addPost(_ post: Post, _ postImage: PostImage) async {
self.postImage.append(postImage)
do {
uploadImage(postId: post.id, image: postImage.image, name: post.imageName ?? "")
// print("post.imagePath : \(post.imagePath)")
try await database.collection("Post")
.document(post.id)
.setData([
"title": post.title,
"contents": post.contents,
"likeCounts": post.likeCounts,
"imagePath": post.imagePath,
"createdAt": post.createdAt
])
fetchPost()
} catch {
print("\(error.localizedDescription)")
}
}
업로드 함수(fetchPost)를 만들고 함수 내에서 비동기 처리로 Post collection에 업로드할 data들을 생성한다.
이미지를 Storage에 저장하는 uploadImage함수는 image에 사진 데이터, name에는 사진 id를 넘겨준다.
3) Image 업로드
private func uploadImage(postId: String, image: UIImage, name: String) {
let storageRef = storage.reference().child("images/\(postId)/\(name)")
let data = image.jpegData(compressionQuality: 0.1)
let metadata = StorageMetadata()
metadata.contentType = "image/jpg"
if let data = data {
storageRef.putData(data, metadata: metadata) { (metadata, err) in
if let err = err {
print("err when uploading jpg\n\(err)")
}
if let _ = metadata {
}
}
}
}
4) 게시글 불러오기: fetchPost
func fetchPost() {
self.posts.removeAll()
self.uiImg.removeAll()
database.collection("Post")
.order(by: "createdAt", descending: true)
.getDocuments { [self] (snapshot, error) in
if let snapshot {
for document in snapshot.documents {
let id: String = document.documentID
let docData = document.data()
let title: String = docData["title"] as? String ?? ""
let contents: String = docData["contents"] as? String ?? ""
let likeCounts: Int = docData["likeCounts"] as? Int ?? 0
let imagePath: String = docData["imagePath"] as? String ?? ""
let createdAt: Double = docData["createdAt"] as? Double ?? 0.0
let post = Post(id: id, title: title, contents: contents, likeCounts: likeCounts, imageName: imagePath, createdAt: createdAt)
self.fetchImage(post: post)
self.posts.append(post)
}
}
}
}
ObservableObject로 구현된 ViewModel이므로 fetch시 document에 data를 할당하는 것이 목적이 된다.
5) Image를 Storage에서 fetch하기
func fetchImage(post: Post) {
let ref = storage.reference(forURL: "gs://mzdoll-920ea.appspot.com/\(String(describing: post.imageName!))")
ref.getData(maxSize: 20 * 1024 * 1024) { data, error in
if let error = error {
print("error while downloading image\n\(error.localizedDescription)")
return
} else {
let image = UIImage(data: data!)
self.uiImg.append(image!)
}
}
}
6) 게시물 삭제하기: deletePost
func deletePost(post: Post) {
// remove from firebase db (firestore)
database.collection("Post").document(post.id).delete() { err in
if let err = err {
print("Error removing document: \(err.localizedDescription)")
} else {
print("Document successfully removed!")
}
}
// remove photos from storage
let imagesRef = storage.reference().child("images/\(post.id)")
imagesRef.delete { error in
if let error = error {
print("Error removing image from storage\n\(error.localizedDescription)")
} else {
print("images directory deleted successfully")
}
}
// remove from posts list
let index = posts.firstIndex { currentPost in
return currentPost.id == post.id
} ?? 0
// remove post from posts variable
posts.remove(at: index)
}
uploadImage, fetchPost과 같이 reference 객체를 만든다. database의 document 참조 객체를 만들어서 delete() 함수를 이용하면 참조하고 있는 데이터가 삭제된다. Storage에는 이미지 data가 저장되어 있고 Post 객체의 images에는 UUID로 저장된 post.id, images.id의 url경로가 저장되어 있으므로 참조 객체를 만들고 이미지도 삭제한다. ViewModel 내부의 posts의 해당 객체를 제거해 주면 끝이다.
View: PhotoPicker &
PhotosPicker(selection: $selectedItems, matching: .any(of: [.images, .not(.videos)])) {
Label("Pick Photo", systemImage: "photo.artframe")
}
.onChange(of: selectedItems) { newValues in
Task {
selectedImages = []
for value in newValues {
if let imageData = try? await value.loadTransferable(type: Data.self), let image = UIImage(data: imageData) {
selectedImages.append(image)
photosName = image
}
}
}
}
TextField("제목", text: $title)
TextField("내용", text: $content)
Button {
let id = UUID().uuidString
let photoName = UUID().uuidString
let post = Post(id: id, title: title, contents: content, likeCounts: 0, imageName: photoName, createdAt: Date().timeIntervalSince1970)
let postImage = PostImage(id: id, image: photosName, imgName: photoName)
Task {
await postStore.addPost(post, postImage)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
isPresented.toggle()
}
}
} label: {
Text("등록")
}
}
}
PhotoPicker를 사용해 다중 이미지를 선택할 수 있고
'등록' 버튼을 누를시 firebase에 저장된 이미지 경로에서 Storage data를 불러오는 시간차이를 두기 위해 1sec delay 준다.
'iOS > Firebase' 카테고리의 다른 글
[SwiftUI] Firebase ViewModel 'Combine' snapShot Listener(파이어베이스 스냅샷 리스너) (1) | 2023.02.05 |
---|---|
[SwiftUI] Firebase Storage 다중이미지 upload & retrieve, 별점 리뷰 글 posting (0) | 2023.01.29 |