打造家用書庫系統
我家的書很多(但幾乎都不是我的),當書一多就會出現一個困擾,有時這本書家裡有或沒有實在會記不起來,又不想重複買,而有時也沒辦法回家確認後再跑一趟書店(像是門市限時特價之類的,沒辦法在網路上買的情況),若是可以有個資料庫可以登錄跟查詢家裡的書籍就太好了。
幾年前用網頁做過這個系統,但找書時每次都得先輸入ISBN,而且就算是用筆電也得要一大台帶到書架旁登錄,覺得實在不怎麼方便,如果可以用手機直接掃描barcode,並在app裡直接查找跟登錄就太好了,所以這次就是要完成這個系統的app版本。
- 畫面與流程
i.這個部分並不複雜,首頁只需要一個輪入框及按鈕即可,成功找到書本就跳出該本書詳細資料,否則彈跳出找不到書的提示框
ii.第二個部分則是已存書的書櫃,會列出目前已登錄的書本,並可以點選後再進到詳細頁,書本的詳細頁是可以共用的
iii.保留一個設定的bar button,但目前只有版本資訊而已(想不到要設定什麼)
2. 總結一下使用的技術
delegate, data source, core data, json parse & json decode
找書及加入書櫃部分
因為要用相機來解barcode所以import AVFoundation 並遵從 AVCaptureMetadataOutputObjectsDelegate
import AVFoundation
import Foundationclass 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
}}
最後詳細頁面的畫面會是這樣
最後看看執行的樣子吧