【Firebase】庫存APP (上傳、下載文字 / 圖片)
在之前的firebase初體驗之後,對於它的Realtime功能相當的感興趣,所以想來做一個會發大財的APP來小試一下身手,做一個類似管理庫存的APP,不過這是我第二次使用它來寫東東,相信一定會碰到很多坑才是吧…XD,此外,還使用了Swift5的新功能 - Result,它在非同步的處理上相當好用,雖然之前自己有做了一個類似的功能,不過還是原生的比較有保障嘛,話不多說,讓我們繼續看下去吧。在這裡只會說明firebase的使用,關於畫面的部分就不多做說明了。
事前準備
資料庫的長相
完成後的長相
- 主要是利用掃瞄Barcode來實現這個功能,當然一定是利用別人寫好的 - QRCodeReader.swift功能直接來使用囉。
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上。另外有發現一些錯誤,比如說會回傳兩次值?然後不會排序?然後沒有上傳圖片的功能?相信這些問題在下一次都會有相當程度的解決方法才是,共勉之。