circlecircle
원형
circlecircle
전체 방문자
오늘
어제
  • 분류 전체보기 (11)
    • Flutter (2)
      • Firebase (1)
    • iOS (9)
      • swiftUI (6)
      • UIKit (0)
      • Firebase (3)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • SPM
  • LazyVStack
  • StateObject
  • Kingfisher
  • AppStorage
  • Combine
  • 다중이미지
  • propertyWrapper
  • SwiftUI
  • EnvironmentObject
  • Cloud FireStore
  • userdefaults
  • SDWebImage
  • ObservableObject
  • storage
  • ios
  • Firebase
  • Stack

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
circlecircle

원형

iOS/Firebase

[SwiftUI] Firebase ViewModel 'Combine' snapShot Listener(파이어베이스 스냅샷 리스너)

2023. 2. 5. 04:15

Combine 사용전인 전 글 '[SwiftUI] Firebase Storage 다중이미지 upload & retrieve, 별점 리뷰 글 posting' 을 보면 비교를 쉽게 확인할 수 있다. 

 

먼저 등록할 가게정보의 Store 구조체를 만들어준다.

import Foundation
import SwiftUI
import UIKit
import FirebaseFirestore
import FirebaseFirestoreSwift


struct Store: Codable, Hashable, Identifiable {
    
    @DocumentID var id: String?
    var storeName: String
    var storeAddress: String
    var coordinate: GeoPoint
    var storeImages: [String]
    var menu: [String : String]
    var description: String
    var countingStar: Double
    var foodType: [String] //국밥 타입: ex:순대,돼지국밥
    //    var viewCount: Int// 장소 조회수
    static func == (lhs : Store, rhs: Store) -> Bool{
        lhs.id == rhs.id
    }
    
    
    
}

extension Store {
    static var test: Store = .init(storeName: "test", storeAddress: "test", coordinate: GeoPoint(latitude: 37, longitude: 125), storeImages: [], menu: [:], description: "test", countingStar: 0.5, foodType: ["순대국밥"])
}

StoreViewModel

firebase 'Store' 컬렉션에, Storage ' 등록, 불러오는 ViewModel을 작성한다.

View에서 사용하는 PhotosPicker ViewModel에서 함수로 빼 주었다.

import Foundation
import SwiftUI
import Combine
import PhotosUI

import Firebase
import FirebaseFirestore
import FirebaseStorage

final class StoreRegistrationViewModel: ObservableObject {
    @Published var store: Store
    @Published var latitude: String = ""
    @Published var longitude: String = ""
    
    @Published var selectedImages: [PhotosPickerItem] = []
    @Published var selectedImageData: [Data] =  []
    @Published var convertedImages: [UIImage] =  []
    
    @Published var stores: [Store] = []
    @Published var storeImages: [String : UIImage] = [:]
    
    @Published var modified = false
    
    private var cancellables = Set<AnyCancellable>()
    private var database = Firestore.firestore()
    private var storage = Storage.storage()
    
    
    init(store: Store = Store(storeName: "",
                              storeAddress: "",
                              coordinate: GeoPoint(latitude: 0, longitude: 0),
                              storeImages: [],
                              menu: [:],
                              description: "",
                              countingStar: 0.0,
                              foodType: ["순대국밥"]
    )) {

        self.store = store
        self.$store
            .dropFirst()
            .sink { [weak self] store in
                self?.modified = true
            }
            .store(in: &self.cancellables)
    }
    

    
    private func convertToUIImages() {
        if !selectedImageData.isEmpty {
            for imageData in selectedImageData {
                if let image = UIImage(data: imageData) {
                    convertedImages.append(image)
                }
            }
        }
    }
    
    private func makeImageName() -> [String] {
        var imgNameList: [String] = []
        // iterate over images
        for img in convertedImages {
            let imgName = UUID().uuidString
            imgNameList.append(imgName)
            uploadImage(image: img, name: (store.storeName + "/" + imgName))
        }
        return imgNameList
    }
    
    
    private func addStoreInfo() {
        do {
            self.convertToUIImages()
            self.store.storeImages = makeImageName()
            //위도 경도값을 형변환해서 넣어주기
            self.store.coordinate = GeoPoint(latitude: Double(self.latitude) ?? 0.0, longitude: Double(self.longitude) ?? 0.0)
            
            let _ = try database.collection("Store")
                .addDocument(from: self.store)
        }
        catch {
            print(error)
        }
    }
    
    private func updateStoreInfo(_ store: Store) {
        
        if let documentId = store.id {
            do {
                try database.collection("Store")
                    .document(documentId)
                    .setData(from: store)
            }
            catch {
                print(error)
            }
            
        }
    }
    
    
    private func removeStoreInfo() {
        if let documentId = self.store.id {
            database.collection("Store").document(documentId).delete { error in
                if let error = error {
                    print(error.localizedDescription)
                }
            }
        }
    }
    
    
    private func updateOrAddStoreInfo() {
        if let _ = store.id {
            self.updateStoreInfo(self.store)
        }
        else {
            addStoreInfo()
        }
    }
    
    private func uploadImage(image: UIImage, name: String) {
        let storageRef = storage.reference().child("storeImages/\(name)")
        let data = image.jpegData(compressionQuality: 0.1)
        let metadata = StorageMetadata()
        metadata.contentType = "image/jpg"
        
        // uploda data
        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 = metadata {
                    print("metadata: \(metadata)")
                }
            }
        }
        
    }

        // MARK: - UI 핸들러
    
    func handleDoneTapped() {
        self.updateOrAddStoreInfo()
    }
    
    func handleDeleteTapped() {
        self.removeStoreInfo()
    }
    
 
}//StoreViewModel

 


StoresViewModel 

Store 구조체의 정보를 모두 가져오는 ViewModel을 구분지어 따로 만들어준다.

StoreViewModel을 확인하면 firebase에 등록하는 함수만 있고 불러오는 함수는 없다. 

firebase에서 데이터를 한번 가져오는 것 이외도 Cloud Firestore는 소위 스냅샷 리스너를 사용하여 앱에 업데이트를 제공하는 것도 지원한다. 컬렉션 or 쿼리에 스냅샷 리스너를 등록 할 수 있으며, Cloud Firesotre는 있을 때마다 리스너를 호출한다.

매핑된 문서를 포함하는 로컬 배열을 업데이트할 필요가 없다. X

스냅샷 수신기의 코드에서 처리되기 때문이다.

import Foundation
import UIKit

import Firebase
import FirebaseFirestore
import FirebaseStorage

// 스토어의 정보를 모두 가져오는 뷰모델
@MainActor
final class StoresViewModel: ObservableObject {
    @Published var stores: [Store] = []
    @Published var storeTitleImage: [String : UIImage] = [:]
    
    private var database = Firestore.firestore()
    private var storage = Storage.storage()
    private var listenerRegistration: ListenerRegistration?
    
    //Store정보 구독취소
    //Store정보가 필요한 뷰에서
    //.onDisappear { viewModel.unsubscribeStores() } 하면 실행됨
    func unsubscribeStores() {
        if listenerRegistration != nil {
            listenerRegistration?.remove()
            listenerRegistration = nil
        }
    }
    
    //Store정보 구독
    //Store정보가 필요한 뷰에서
    //.onAppear { viewModel.subscribeStores() } 하면 실행됨
    func subscribeStores() {
        if listenerRegistration == nil {
            listenerRegistration =  database.collection("Store")
                .addSnapshotListener { (querySnapshot, error) in
                    guard let documents = querySnapshot?.documents else {
                        print("There are no documents")
                        return
                    }
                    
                    //FirebaseFireStoreSwift 를 써서 @Document 프로퍼티를 썼더니 가능
                    self.stores = documents.compactMap { queryDocumentSnapshot in

                        let result = Result { try queryDocumentSnapshot.data(as: Store.self) }

                        switch result {
                        case .success(let store):
                            for imageName in store.storeImages {
                                Task.init{
                                    do{
                                        try await self.fetchImages(storeId: store.storeName, imageName: imageName)
                                    }
                                    catch{
                                        
                                    }
                                }
                               
                            }
                            return store
                        case .failure(let error):
                            print(#function, "\(error.localizedDescription)")
                            return nil
                        }
                    }
                }
        }
    }
    
    
    func fetchImages(storeId: String, imageName: String) async throws -> UIImage {
        let ref = storage.reference().child("storeImages/\(storeId)/\(imageName)")

        let data = try await ref.data(maxSize: 1 * 1024 * 1024)
        let image = UIImage(data: data)
        
        self.storeTitleImage[imageName] = image
        
        return image!
    }
}

마지막 View에서 ViewModel을 사용할 때 

View에서 StateObject로 ViewModel을 불러오며 onAppear & onDisappear시 호출만 하면 된다.

    .onAppear {
            Task{
                storesViewModel.subscribeStores()
            }
        }
        .onDisappear {
            storesViewModel.unsubscribeStores()
        }

 

'iOS > Firebase' 카테고리의 다른 글

[SwiftUI] Firebase Storage 다중이미지 upload & retrieve, 별점 리뷰 글 posting  (0) 2023.01.29
[SwiftUI] Firebase Storage Post Image upload & retrieve 단일이미지  (0) 2022.12.18
    'iOS/Firebase' 카테고리의 다른 글
    • [SwiftUI] Firebase Storage 다중이미지 upload & retrieve, 별점 리뷰 글 posting
    • [SwiftUI] Firebase Storage Post Image upload & retrieve 단일이미지
    circlecircle
    circlecircle

    티스토리툴바