【Firebase】庫存APP (上傳、下載文字 / 圖片)

在之前的firebase初體驗之後,對於它的Realtime功能相當的感興趣,所以想來做一個會發大財的APP來小試一下身手,做一個類似管理庫存的APP,不過這是我第二次使用它來寫東東,相信一定會碰到很多坑才是吧…XD,此外,還使用了Swift5的新功能 - Result,它在非同步的處理上相當好用,雖然之前自己有做了一個類似的功能,不過還是原生的比較有保障嘛,話不多說,讓我們繼續看下去吧。在這裡只會說明firebase的使用,關於畫面的部分就不多做說明了。

事前準備

資料庫的長相

  • 就一個很簡單的書籍列表,資料都是從博客來Copy來的,主要要注意的地方是,是以ISBN來當唯一的Key值,方便管理。

完成後的長相

Firebase小工具

取值小工具

  • 有鑑於之前的經驗(坑?),決定再把API進化一下,在只要使用一個function就可以選擇要不要及時去做更新,另外在使用realtime的時候會回傳一個UInt的值,可以用來關閉它。
// MARK: - 主工具
extension FIRDatabase {
    
    /// 取值 (單次 / 及時) => 回傳realtime的handle代號
    func childValueFor(type: RealtimeDatabaseType, path: String, result: @escaping (Result<Any?, Error>) -> Void) -> UInt? {
        
        switch type {
        case .single:
            
            let handleNumber = childValueForSingle(withPath: path) { (_result) in
                switch(_result) {
                case .failure(let error): result(.failure(error))
                case .success(let value): result(.success(value))
                }
            }
            return handleNumber
            
        case .realtime:
            
            let handleNumber = childValueForRealtime(withPath: path) { (_result) in
                switch(_result) {
                case .failure(let error): result(.failure(error))
                case .success(let value): result(.success(value))
                }
            }
            return handleNumber
        }
    }
    
    /// 移除Realtime的Handle
    func removeObserver(withHandle handleNumber: UInt?) {
        guard let handleNumber = handleNumber else { return }
        reference.removeObserver(withHandle: handleNumber)
    }
}

// MARK: - 小工具
extension FIRDatabase {
    
    /// 取值 (單次)
    private func childValueForSingle(withPath path: String, result: @escaping (Result<Any?, Error>) -> Void) -> UInt? {
        
        reference.child(path).queryOrdered(byChild: "\(BookField.ISBN)").observeSingleEvent(of: .value, with: { (snapshot) in
            result(.success(snapshot.value))
        }, withCancel: { (error) in
            result(.failure(error))
        })
        
        return nil
    }
    
    /// 取值 (及時) => 回傳realtime的handle代號
    private func childValueForRealtime(withPath path: String, result: @escaping (Result<Any?, Error>) -> Void) -> UInt? {
        
        let handleNumber = reference.child(path).queryOrderedByKey().observe(.value, with: { (snapshot) in
            result(.success(snapshot.value))
        }, withCancel: { (error) in
            result(.failure(error))
        })
        
        return handleNumber
    }
}

更新數據

  • 更新數據方面,有更新單一數值,跟多組數值的方式。
// MARK: - 主工具
extension FIRDatabase {
    
    /// 更新數據 (單個)
    func setChildValue(_ value: Any?, forKey key: String, path: String, result: @escaping (Result<Bool, Error>) -> Void) {
        
        reference.child(path).child(key).setValue(value) { (error, database) in
            if let error = error { result(.failure(error)); return }
            result(.success(true))
        }
    }
    
    /// 更新數據 (多個)
    func updateChildValues(withPath path: String, values: [String: Any], result: @escaping (Result<Bool, Error>) -> Void) {
        
        reference.child(path).updateChildValues(values) { (error, database) in
            if let error = error { result(.failure(error)); return }
            result(.success(true))
        }
    }

搜尋數據

  • 在這裡主要是搜尋單一值,在使用前一定要先排序才能使用喲。
// MARK: - 主工具
extension FIRDatabase {

    /// 搜尋數據
    func queryChildValue(_ value: Any?, withPath path: String, forKey key: String, result: @escaping (Result<Any?, Error>) -> Void) {
        
        reference.child(path).queryOrdered(byChild: key).queryEqual(toValue: value).observeSingleEvent(of: .value, with: { (snapshot) in
            result(.success(snapshot.value))
        }, withCancel: { (error) in
            result(.failure(error))
        })
    }
}

移除數據

// MARK: - 主工具
extension FIRDatabase {

    /// 移除資料
    func removeChildValue(withPath path: String, forKey key: String, result: @escaping (Result<Bool, Error>) -> Void) {
        
        reference.child(path).child(key).removeValue { (error, database) in
            if let error = error { result(.failure(error)); return }
            result(.success(true))
        }
    }
}

頁面功能

庫存功能

  • 主要就是及時去取得資料,跟移除單一資料。
/// MARK: - 小工具
extension StockViewController {
    
    /// 取得firebase上的數值 (回傳handle代號)
    private func bookInfomation(withType type: RealtimeDatabaseType, result: @escaping (Result<Any?, Error>) -> Void) -> UInt? {
        
        let handleNumber = FIRDatabase.shared.childValueFor(type: type, path: "\(BookField.BarCode.realPath())") { (_result) in
            switch(_result) {
            case .failure(let error): result(.failure(error))
            case .success(let value): result(.success(value))
            }
        }
        
        return handleNumber
    }
    
    /// 移除書籍
    private func removeBook(withPath path: String, forKey key: String, result: @escaping (Result<Bool, Error>) -> Void) {
        
        FIRDatabase.shared.removeChildValue(withPath: path, forKey: key) { (_result) in
            
            switch (_result) {
            case .failure(let error): result(.failure(error))
            case .success(let isOK): result(.success(isOK))
            }
        }
    }
}

新增、修改功能

  • 這裡主要的功能都是利用ISBN的值去取得資料來做為準則,然後因為有很多相似的功能,所以這四頁都是同一個頁面。
// MARK: - 小工具
extension ProductActionViewController {
    
    /// 加入新書
    private func appendBook(_ sender: UIBarButtonItem) {
        
        guard let _isbn = isbnTextField.text,
              let _count = countTextField.text,
              let title = titleTextField.text,
              let isbn = Int(_isbn),
              let count = Int(_count),
              let timestamp = Optional.some(Int(Date().timeIntervalSince1970))
        else {
            return
        }
        
        FIRDatabase.shared.queryChildValue(isbn, withPath: "\(BookField.BarCode.realPath())", forKey: "\(BookField.ISBN)") { (_result) in
            
            KRProgressHUD.show()
            
            switch (_result) {
            case .failure(let error):
                KRProgressHUD.showError(withMessage: error.localizedDescription)
                
            case .success(let value):
                
                let isOK = self.checkValue(value, withType: self.tabBarType)
                
                if (!isOK) { return }
                
                let product = Book(isbn: isbn, title: title, url: nil, icon: nil, count: count, timestamp: timestamp).dictionary() as [String: Any]
                let realPath = BookField.BarCode.realPath(withBarCode: isbn)
                
                FIRDatabase.shared.updateChildValues(withPath: realPath, values: product) { (result) in
                    switch (result) {
                    case .failure(let error): KRProgressHUD.showError(withMessage: error.localizedDescription)
                    case .success(_): KRProgressHUD.showSuccess()
                    }
                }
            }
        }
    }

    /// 更新書本數量
    private func updateBookCount(_ count: String?, barCode: String?, type: TabBarType) {
        
        guard let _count = count,
              let _barCode = barCode,
              let count = Int(_count),
              let barCode = Int(_barCode)
        else {
            return
        }
        
        let path = "\(BookField.BarCode.realPath(withBarCode: barCode))"
        var saveCount = 0
        
        switch type {
        case .plus: saveCount = (originalCount ?? 0) + count
        case .minus: saveCount = (originalCount ?? 0) - count
        case .cart, .fix, .stock: return
        }
        
        FIRDatabase.shared.setChildValue(saveCount, forKey: "\(BookField.Count)", path: path) { (result) in
            
            switch (result) {
            case .failure(let error):
                KRProgressHUD.showError(withMessage: error.localizedDescription)
            case .success(let isOK):
                if (isOK) { KRProgressHUD.showSuccess() }
                KRProgressHUD.showError()
            }
        }
    }
    
    /// 取得firebase上的數值 (回傳handle代號)
    private func bookInfomation(withType type: RealtimeDatabaseType, barCode: Int ,result: @escaping (Result<Any?, Error>) -> Void) -> UInt? {
        
        let path = "\(BookField.BarCode.realPath(withBarCode: barCode))"
        
        let handleNumber = FIRDatabase.shared.childValueFor(type: type, path: path) { (_result) in
            switch(_result) {
            case .failure(let error): result(.failure(error))
            case .success(let value): result(.success(value))
            }
        }
        
        return handleNumber
    }
}

Cloud Storage小工具

資料結構

  • 其實就跟一般的資料夾一樣,而在規則方面就要去看看文件說明了

程式說明

  • 簡單的上傳就是以Data去做處理,而下載的方法有分直接下載跟使用網址二種
// MARK: - Storage工具
class FIRStorage: NSObject {
    
    /// 圖片類型
    enum ImageType: String {
        
        var metadata: String { return getMetadata() }
        
        case jpeg = ".jpg"
        case png = ".png"
        
        /// 取得Metadata
        private func getMetadata() -> String {
            switch self {
            case .jpeg: return "image/jpeg"
            case .png: return "image/png"
            }
        }
    }
    
    public static let shared = FIRStorage()
    public static let imageFolder = "Books"

    private let reference = Storage.storage().reference()
    private let maxSize: Int64 = 1024 * 1024
    
    private override init() { super.init() }
}

// MARK: - 主工具
extension FIRStorage {
    
    /// 下載圖片 (記憶體)
    func imageWithName(_ name: String, forFolder fold: String, result: @escaping (Result<Any?, Error>) -> Void) {
        
        reference.child(fold).child(name).getData(maxSize: maxSize) { (data, error) in
            if let error = error { result(.failure(error)); return }
            result(.success(data))
        }
    }
    
    /// 下載圖片 (網址)
    func downloadImageWithName(_ name: String, forFolder fold: String, result: @escaping (Result<URL?, Error>) -> Void) {
        
        reference.child(fold).child(name).downloadURL { (url, error) in
            if let error = error { result(.failure(error)); return }
            result(.success(url))
        }
    }
    
    /// 上傳圖片
    func uploadImageWithName(_ name: String, forFolder fold: String, image: UIImage?, type: ImageType, result: @escaping (Result<StorageMetadata?, Error>) -> Void) -> StorageUploadTask? {
        
        guard let image = image,
              let imageData = imageData(image, withType: type),
              let metadata = Optional.some(StorageMetadata()),
              let nameWithType = Optional.some(name + type.rawValue)
        else {
            return nil
        }
        
        metadata.contentType = type.metadata
        
        let task = reference.child(fold).child(nameWithType).putData(imageData, metadata: metadata) { (metadata, error) in
            if let error = error { result(.failure(error)); return }
            result(.success(metadata))
        }
        
        return task
    }
    
    /// 刪除圖片
    func removeImage(withName name: String, forFolder fold: String, result: @escaping (Bool) -> Void) {
        reference.child(fold).child(name).delete { (error) in
            if let _ = error { result(false); return }
            result(true)
        }
    }
}

// MARK: - 主工具
extension FIRStorage {
    
    /// 圖片壓縮 -> Data
    private func imageData(_ image: UIImage, withType type: ImageType) -> Data? {
        switch type {
        case .jpeg: return image.jpegData(compressionQuality: 0.5)
        case .png: return image.pngData()
        }
    }
}

範例程式碼下載

後記

  • 其實做了這支APP也花了好幾天的時間在做,但主要的精神是在於firebase的使用,所以寫起來在文字上不是著墨很多,主要都集中在Code上。另外有發現一些錯誤,比如說會回傳兩次值?然後不會排序?然後沒有上傳圖片的功能?相信這些問題在下一次都會有相當程度的解決方法才是,共勉之。