【Swift】バナナ情報がどこでも手軽にメモできるアプリを作った。
ダーヤス.comのバナナ部のページにて、自分の食べたバナナの感想をメモしているのですが、それをメモするのが面倒なので、どこでもアプリでメモできたらいいなと思ってそういうアプリをswiftで作ってみました。
まあただのメモアプリです。
ちなみに、この感想のメモは去年で止まっております。
毎日食べているのですが。。。最近は三平で買う完熟王がコスパ高いんじゃないかと感心しております。
以下、去年で止まっているそのメモ。
商品名 | お店 | コク | キレ | 値段 | 糖度 |
---|---|---|---|---|---|
完熟王 | サミット クイーンズ伊勢丹 三平 |
5 | 2 | サミ:3 クイ:3 三:2 |
|
セブンプレミアム フィリピン産高地栽培バナナ |
セブンイレブン | 5 | 2 | 2 | |
バナージュ | サミット | 3 | 3 | 1.5 | |
仙人1986 (仙人種バナナ) |
いなげや | 5 | 2 | 2 | 22.3 |
バナナ美人 | サミット | 4 | 2 | 2 | |
エクアドル産おいしい有機バナナ | サミット | 4 | 2 | 3.5 | |
バナンボ | 生活彩家 | 3 | 3 | 1 | |
ドール極撰バナナ | クイーンズ伊勢丹 | 5 | 2 | 3 | 23.5 |
グレイシオバナナ | クイーンズ伊勢丹 | 3 | 3 | 1 | |
エナジーナ | いなげや | 4 | 2 | 2 | 22.0 |
台湾バナナ | 阿佐ヶ谷高野青果 | 4 | 2 | 1 | |
ミディオ | サミット | 4 | 2 | 1 | |
フィリピン産こだわり栽培バナナ | ファミリーマート | 5 | 2 | 2 | |
チキータプレシャス | 高野青果 | 4 | 2 | 2 | |
完熟王ゴールドプレミアム | プレッセプレミアム東京ミッドタウン店 | 4 | 2 | 2 | 22.5 |
さて、アプリはこんなイメージです。(小さいので画像クリックして拡大しないとわかりにくいです。)
各バナナの詳細を記入するメモ。
キレ、価格等の項目がすでに入っています。
バナナ名を選ぶ画面が出せます。
この画面でバナナを選んで、先ほどの画面で詳細を記入します。
ここでは、今までいなげやでしか見たことのないエナジーナを追加。リストにエナジーナが表示されました。
core dataにメモ情報を記録しているので、アプリを落としても記憶情報は保持されます。
ところで、今思ったのが、このアプリでメモした内容はアプリ側でローカル保存されるだけですが、
書いたものをサーバ側に配列形式のJSONデータで転送しておくプログラムにしておいて、
webホームページ側のリストでは、JavaScriptでそれを取得するプログラムにしておけば、同期がとれるなあ。
そのうちやってみようかな。
さて、以下、xcodeで作る手順を雑に書きます。
プロジェクトをMaster-Detail Applicationで作ります。
.xcdatamodeldファイルを開いて、「Add Entity」でエンティティを追加。
エンティティ名は「Name」と「Status」とかで。
ネームの方に商品名を入れて、ステータスの方にそのバナナの詳細情報(買ったお店、コク、キレ、値段、糖度)を入れるイメージです。
Nameエンティティにnameアトリビュートを追加。typeはString型で。
Statusエンティティにstore,rich,sharpness,price,sweetnessアトリビュートを追加。いずれもタイプはstring型で。
次に関連については、Nameエンティティの方ではnameを設定。DestinationはStatusで。inverseはnameを。
Statusエンティティ側ではstatusを設定。DestinationはNameで。inverseはstatusを。
あと、NameクラスとStatusクラスは1対多の関係になるので、statusのTypeを右側のコラムの部分でTo manyに設定。
その後、メニューバーのEditorからCreate NSmanagedObject Subclass...を選べば、
Name.swift
Name+CoreDataProperties.swift
Status.swift
Status+CoreDataProperties.swift
の4ファイルが作成されるので、これは放置で。
デフォルトで置いてあるDetail Sceneを削除してView Controllerをストーリーボードに追加。
Navigation Controller SceneのNavigation Controllerから、新規作成したTable View Controller SceneのTable View ControllerまでControlキーを押しながらドラッグで接続。
Table View Controller Sceneのクラスを、右側コラムのCustom ClassでDetailViewControllerに変更。
Table View CellのIdentifierを"Cell"と設定。
Prototype Cells にラベルとテキストフィールドを追加。
心持ち大きめにしておいた方がいいかもしれません。
それぞれビュータグを1、2をしておきます。
あと、テキストフィールドのuser interactionをyesにしておくのを忘れずに。
コードの説明はちょっと長くなりそうなので時間があるときにでも追記するかもしれません。
以下、コードです。
MasterViewController.swift
import UIKit import CoreData class MasterViewController: UITableViewController, NSFetchedResultsControllerDelegate { var detailViewController: DetailViewController? = nil var managedObjectContext: NSManagedObjectContext? = nil override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. self.navigationItem.leftBarButtonItem = self.editButtonItem() let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "alert") self.navigationItem.rightBarButtonItem = addButton if let split = self.splitViewController { let controllers = split.viewControllers self.detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController } } func alert() { let alertController = UIAlertController(title: "新規バナナの追加", message: "バナナ名を入力してください", preferredStyle: .Alert) alertController.addTextFieldWithConfigurationHandler(nil) let cancelAction = UIAlertAction(title: "キャンセル", style: .Cancel, handler: nil) let otherAction = UIAlertAction(title: "OK", style: .Default) { [unowned self] action in if let textFields = alertController.textFields{ let textField = textFields[0] as UITextField self.insertNewObject(textField.text!) } } alertController.addAction(cancelAction) alertController.addAction(otherAction) self.presentViewController(alertController, animated: true, completion: nil) } override func viewWillAppear(animated: Bool) { self.clearsSelectionOnViewWillAppear = self.splitViewController!.collapsed super.viewWillAppear(animated) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } func insertNewObject(name: String) { let context = self.fetchedResultsController.managedObjectContext let entity = self.fetchedResultsController.fetchRequest.entity! let person = NSEntityDescription.insertNewObjectForEntityForName(entity.name!, inManagedObjectContext: context) as NSManagedObject let address = NSEntityDescription.insertNewObjectForEntityForName("Status", inManagedObjectContext: context) as NSManagedObject // If appropriate, configure the new managed object. // Normally you should use accessor methods, but using KVC here avoids the need to add a custom class to the template. person.setValue(name, forKey: "name") person.setValue(address, forKey: "status") // Save the context. do { try context.save() } catch { // Replace this implementation with code to handle the error appropriately. // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. //print("Unresolved error \(error), \(error.userInfo)") abort() } } // MARK: - Segues override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "showDetail" { if let indexPath = self.tableView.indexPathForSelectedRow { let object = self.fetchedResultsController.objectAtIndexPath(indexPath) let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController controller.detailItem = object as! Name controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem() controller.navigationItem.leftItemsSupplementBackButton = true } } } // MARK: - Table View override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return self.fetchedResultsController.sections?.count ?? 0 } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let sectionInfo = self.fetchedResultsController.sections![section] return sectionInfo.numberOfObjects } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) self.configureCell(cell, atIndexPath: indexPath) return cell } override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { // Return false if you do not want the specified item to be editable. return true } override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if editingStyle == .Delete { let context = self.fetchedResultsController.managedObjectContext context.deleteObject(self.fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject) do { try context.save() } catch { // Replace this implementation with code to handle the error appropriately. // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. //print("Unresolved error \(error), \(error.userInfo)") abort() } } } func configureCell(cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) { let object = self.fetchedResultsController.objectAtIndexPath(indexPath) cell.textLabel!.text = object.valueForKey("name")!.description } // MARK: - Fetched results controller var fetchedResultsController: NSFetchedResultsController { if _fetchedResultsController != nil { return _fetchedResultsController! } let fetchRequest = NSFetchRequest() // Edit the entity name as appropriate. let entity = NSEntityDescription.entityForName("Name", inManagedObjectContext: self.managedObjectContext!) fetchRequest.entity = entity // Set the batch size to a suitable number. fetchRequest.fetchBatchSize = 20 // Edit the sort key as appropriate. let sortDescriptor = NSSortDescriptor(key: "name", ascending: true) let sortDescriptors = [sortDescriptor] fetchRequest.sortDescriptors = [sortDescriptor] // Edit the section name key path and cache name if appropriate. // nil for section name key path means "no sections". let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: nil, cacheName: "バナナ名") aFetchedResultsController.delegate = self _fetchedResultsController = aFetchedResultsController do { try _fetchedResultsController!.performFetch() } catch { // Replace this implementation with code to handle the error appropriately. // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. //print("Unresolved error \(error), \(error.userInfo)") abort() } return _fetchedResultsController! } var _fetchedResultsController: NSFetchedResultsController? = nil func controllerWillChangeContent(controller: NSFetchedResultsController) { self.tableView.beginUpdates() } func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) { switch type { case .Insert: self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade) case .Delete: self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade) default: return } } func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) { switch type { case .Insert: tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade) case .Delete: tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade) case .Update: self.configureCell(tableView.cellForRowAtIndexPath(indexPath!)!, atIndexPath: indexPath!) case .Move: tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade) tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade) } } func controllerDidChangeContent(controller: NSFetchedResultsController) { self.tableView.endUpdates() } /* // Implementing the above methods to update the table view in response to individual changes may have performance implications if a large number of changes are made simultaneously. If this proves to be an issue, you can instead just implement controllerDidChangeContent: which notifies the delegate that all section and object changes have been processed. func controllerDidChangeContent(controller: NSFetchedResultsController) { // In the simplest, most efficient, case, reload the table view. self.tableView.reloadData() } */ }
DetailViewController.swift
import UIKit class DetailViewController: UITableViewController { var detailItem: Name? let headers: [[String: String]] = [ ["id": "name", "header": "バナナ名"], ["id": "status.store", "header":"買ったお店"], ["id": "status.rich", "header": "コク"], ["id": "status.sharpness", "header": "キレ"], ["id": "status.price", "header": "価格"], ["id": "status.sweetness", "header": "糖度"] ] override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } override func viewDidDisappear(animated: Bool) { super.viewDidDisappear(animated) for (i, header) in self.headers.enumerate() { let indexPath = NSIndexPath(forRow: i, inSection: 0) if let cell = self.tableView.cellForRowAtIndexPath(indexPath) { let key = header["id"] if let editView = cell.contentView.viewWithTag(2) as? UITextField { self.detailItem?.setValue(editView.text, forKeyPath: key!) } } } if let item = self.detailItem { do { try item.managedObjectContext!.save() } catch _ { } } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.headers.count; } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) let header = headers[indexPath.row] as Dictionary if let headerLabel = cell.contentView.viewWithTag(1) as? UILabel { headerLabel.text = header["header"] } if let editView = cell.contentView.viewWithTag(2) as? UITextField { let key = header["id"] if let item = self.detailItem { if let obj: AnyObject = item.valueForKeyPath(key!) { editView.text = obj.description } } } return cell } }