ダーヤス.com プレミアム

ダーヤス.comにようこそ。プレミアムな情報で、ワークライフバランスの充実を図りませんか。

【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
    }
    
}

 - ごはん, アプリ開発