打造家用書庫系統

我家的書很多(但幾乎都不是我的),當書一多就會出現一個困擾,有時這本書家裡有或沒有實在會記不起來,又不想重複買,而有時也沒辦法回家確認後再跑一趟書店(像是門市限時特價之類的,沒辦法在網路上買的情況),若是可以有個資料庫可以登錄跟查詢家裡的書籍就太好了。

幾年前用網頁做過這個系統,但找書時每次都得先輸入ISBN,而且就算是用筆電也得要一大台帶到書架旁登錄,覺得實在不怎麼方便,如果可以用手機直接掃描barcode,並在app裡直接查找跟登錄就太好了,所以這次就是要完成這個系統的app版本。

  1. 畫面與流程
    i.這個部分並不複雜,首頁只需要一個輪入框及按鈕即可,成功找到書本就跳出該本書詳細資料,否則彈跳出找不到書的提示框
    ii.第二個部分則是已存書的書櫃,會列出目前已登錄的書本,並可以點選後再進到詳細頁,書本的詳細頁是可以共用的
    iii.保留一個設定的bar button,但目前只有版本資訊而已(想不到要設定什麼)

2. 總結一下使用的技術
delegate, data source, core data, json parse & json decode

找書及加入書櫃部分

因為要用相機來解barcode所以import AVFoundation 並遵從 AVCaptureMetadataOutputObjectsDelegate

import AVFoundation
import Foundation
class SearchBookViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {@IBOutlet weak var searchISBN: UITextField!
var captureSession: AVCaptureSession!
var previewLayer: AVCaptureVideoPreviewLayer!

當searchISBN傳入空值時就啟動相機,若有值就直接用此值搜尋。相機因為要解析的是barcode所以要設定類型

metadataOutput.metadataObjectTypes = [.ean8, .ean13, .pdf417]

而啟動相機後若是想放棄,因為它已經疊了一層在原來的view上,所以就算切到別的頁面再回來也還是維持在相機的模式,所以要多加一個放棄的按鈕,所以就直接加在navigation bar的空間上,值得注意的是因為相機是疊加一層layer而不是viewcontroller所以不能用dismiss,而是用removeFromSuperlayer()

let exec = UIBarButtonItem(title: "關閉", style: .plain, target: self, action: #selector(SearchBookViewController.backmainboard))navigationItem.leftBarButtonItem = exec@objc func backmainboard(){//stop capture
captureSession.stopRunning()
//remove layer
previewLayer.removeFromSuperlayer()
//clear navigation title & event
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}

得到ISBN碼後就要訪問API,這裡是先直接訪問google book api

//抓取 JSON 將 Data 變成 String 印出let urlStr = "https://www.googleapis.com/books/v1/volumes?q=isbn:"+isbn

找到資料後則要解析json的資料

URLSession.shared.dataTask(with: url) { (data, response , error) in
if
let
data = data,
let books = try?JSONDecoder().decode(Books.self, from: data)
{

而解析的json需要先定義,只需要定義需要的資料就好,用不到的資料就直接無視了

struct Books: Codable {
let items: [Book]
let totalItems: Int
}
struct Book: Codable {
let kind, id: String
let volumeInfo: VolumeInfo
}
struct VolumeInfo: Codable {
let title: String
let authors: [String]
let publisher, publishedDate, description: String
let pageCount: Int
let printType: String
let categories: [String]
let contentVersion: String
let imageLinks: ImageLinks
}
struct ImageLinks: Codable {
let thumbnail: String
}

解析完資料後打算把資料傳給書本詳細頁就出現錯誤:UIViewController must run in main thread 所以要再利用 DispatchQueue.main.async,切換到 main thread 執行即可

DispatchQueue.main.async {
let mainStoryBoard = UIStoryboard(name: "Main", bundle: nil)
//建立連線並轉型為 BookViewController
let bookViewController = mainStoryBoard.instantiateViewController(withIdentifier: "showBookDetail") as! BookViewController
//顯示書本資訊
bookViewController.book["isbn"] = isbn
bookViewController.book["adddate"] = ""
bookViewController.book["cover"] = book.volumeInfo.imageLinks.thumbnail
bookViewController.book["title"] = book.volumeInfo.title
bookViewController.book["authors"] = book.volumeInfo.authors.joined(separator: ",")
bookViewController.book["publisher"] = book.volumeInfo.publisher
bookViewController.book["publisheddate"] = book.volumeInfo.publishedDate
bookViewController.book["description"] = book.volumeInfo.description
bookViewController.book["mode"] = "present"
self.present(bookViewController, animated:true, completion:nil)
}

詳細資料這頁的處理就簡單了一點,透過book[“mode”]來確認連線方式,並把相關的按鈕設為顯示或隱藏,並用傳入的isbn到core data裡找看看資料是不是已經存在,若存在則讓按鈕文字及狀態改為更新資料,若否則是加入書庫,另外也用此方式決定是要用dismiss或是 self.navigationController?.popViewController(animated: true)離開這頁。

core data的設定不難,但CRUD就不是很懂了,但這只是暫且還沒串接線上資料庫前的權宜方法,所以就找到現成的model來處理,所以就只是利用select找看看資料是否存在,存在就顯示core data的資料,若否則應該會有從前頁傳來的資料,按鈕則會因為狀態不同決定是CREATE或是UPDATE,按下時再用switch來處理相對應的程式。

書櫃部分

numberOfSections部分只有一個sections所以直接回傳1,numberOfRowsInSection則是按core data找到的資料數回傳

比較重要的是cell部分,先新增BookListTableViewCell.swift處理cell的資料

let cell = tableView.dequeueReusableCell(withIdentifier: "Shelve", for: indexPath) as! BookListTableViewCell

然後就只現成的tableview列表下來

為了要將書加入書櫃後來到書櫃時可以馬上看到剛才加入的書,所以處理了viewWillAppear

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
getData()
}
@objc func getData() {
DispatchQueue.main.async {
self.tableView.reloadData()
self.refreshControl!.endRefreshing()
}
}

為了要做到pull to update所以在viewDidLoad增加了

refreshControl = UIRefreshControl()let attributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
refreshControl?.attributedTitle = NSAttributedString(string: "更新資料", attributes: attributes)
refreshControl?.tintColor = UIColor.white
refreshControl?.backgroundColor = UIColor.black
refreshControl?.addTarget(self, action: #selector(getData), for: UIControl.Event.valueChanged)
tableView.refreshControl = refreshControl

為了要在列表上滑動刪除資料,則將原本的

override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {

解開註解,本來只是想打開測試,所以沒有真的刪除資料,但只要執行就會閃退,看了錯誤訊息原來是如果刪除後的資料數量與刪除前的不同,就會被捉出來,看來是真的得刪資料了。

override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {if editingStyle == .delete {
// Delete the row from the data source
刪除資料 刪除資料 刪除資料
print("刪除資料成功")
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}

最後詳細頁面的畫面會是這樣

最後看看執行的樣子吧

--

--