読者です 読者をやめる 読者になる 読者になる

一から勉強させてください( ̄ω ̄;)

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです。

iPhoneアプリ開発初心者がSwift + CoreDataでシンプルなTODOアプリをつくってみた

iOS Swift

(2015/11/7追記)Swift2.0に対応しつつ、リネームやリファクタリングなど修正を加えたものをGitHubにあげております。この記事内で説明している通りに実装しても動作しないと思うので、もしよろしければGitHubのコードも参考にしてみてください。

僕は普段、RailsとかJavaScriptとか触っているようなへっぽこWebエンジニアでして、iOSAndroidアプリの開発経験はありません。

HogeClass *hoge = [[HogeClass alloc] init]; // (゚Д゚≡゚Д゚)マジ?

て感じでした。

でも最近はObjectiveなんたらの100億倍書きやすそうなSwiftという言語が出ていたりするので、楽しそうだしちょっと手を出してみるかと。

そこでまずiOS開発勉強用のサンプルとしてSwiftでシンプルなTODOアプリをつくってみたので、今回はその時の実装メモを書きたいと思います。

iOS開発とか1ナノもわからない状態からスタートしたので、同じような境遇の人のお役に立てたら幸いです。

事前準備

Swiftどころか、ObjectiveなんたらもCもまともにやったこと無い人間だったので、さすがにいきなり開発はツラいと。

そこでまずは鉄板のドットインストールさんで事前にSwiftやらiOSの開発がどんな感じかをざっくり学びました。(プレミアム会員なりましたよ)

僕は「ストーリーボードって何ですの?」レベルだったのでかなり参考になりました。

その後、Swiftの言語仕様を学びたい欲が高まってきたので、以下の本を読み始めました。

詳解 Swift

詳解 Swift

個人的にSwiftはいろんな言語のいいとこ取りしてるような感じで面白い言語だなぁという印象でした。

あとハマるのはツラいので最低限のデバッグ方法をなんとなく調査。

こちらこちらを見る感じだとブレークポイント仕込んで、po ◯◯ってやれば値をいじくれそうな気配がします。 JSのDEBUGをChromeでやるような感じでなんとなくできそうです。po!!

そんな感じで本を読みながら、ブレークポイントを仕込みながら、楽しくTODOアプリ開発スタートです。

作成したアプリ

今回作成したものはこんな感じです。

f:id:d_animal141:20150228000945g:plain

コードはこちらに公開しております。

TODOを新規に登録できて、それを編集できて、スワイプしたらDELETEボタン出てきて削除できて、みたいな。

いい感じのチュートリアルを見つけたのでそちらをモロパクリした次第です(以下、チュートリアルはこの記事のことを指すものとします)。英語です。

http://rshankar.com/coredata-tutoiral-in-swift-using-nsfetchedresultcontroller/

基本的に上記チュートリアルを参考にしながら、改造していくスタイルで開発しました(チュートリアルに載っているコード、たまに動かなかったので気をつけてくださいw)

あとチュートリアルと僕の作成したもので命名がちょいちょい違うところあると思いますが、特に深い理由はないのでお気になさらず。

ちなみにCoreDataなるものを使っていて割とそこがツラかったりしましたが、まあ何とかなると思いますw

プロジェクト作成

Create a new File -> New -> Projectでテンプレートを選択します。

今回はMaster-Detail Applicationではなく、Single View Applicationで一から作っていきます。

あとUse Core Dataのチェックを忘れずに。

UI開発

チュートリアルに図が豊富に載っているし、ドットインストールでストーリーボードの使い方も勉強したので、UIまわりはほとんど詰まることなく進めることができました。

強いて言うなら最初、プロジェクト作成後に自動で作られるViewControllerを消して、新たにTableViewControllerを追加するところ。 僕はviewControllerのルートを表すストーリーボードの→マークが消えて「どこいったぁーーーー」と荒れ狂ったのですが、そういう人は新しく追加したtableViewControllerを選択した状態で、

attributes inspector -> is initial view controller

にチェックを入れると幸せになれると思います。

とりあえずTODO一覧画面、詳細画面を作成して、+ボタンで遷移できるところまで作ったとします。また、一覧、詳細のviewControllerはそれぞれTodosViewController, TodoDetailViewControllerとします。

CoreDataの準備

続いてCoreDataの準備にとりかかります。

といっても1ナノも使い方がわからないので、まずはざっとCore Data プログラミングガイドこちらの記事に目を通します。

とりあえず

  • CoreDataはORマッピングフレームワークであり、永続ストアには主にSQLiteを使用する
  • 今回特に使いそうなのはNSManagedObject、NSManagedObjectContext、NSEntityDescription、NSFetchRequest、NSFetchedResultsControllerあたり

ということがわかりました。

そして今回使いそうなクラスについて

  • NSManagedObject

モデルクラス。CoreDataで永続化するオブジェクトは必ずこのクラス、またはこのクラスのサブクラスのオブジェクトでなければならない。

  • NSManagedObjectContext

データ操作を行うためのコンテキスト(作業領域)。NSManagedObjectに対する操作はすべてこのコンテキストを介して行われる。CRUD操作、Undo Redo、Validationなどを担当。

  • NSEntityDescription

エンティティ記述という、エンティティの定義を管理するクラス。エンティティとはCoreDataで永続化の対象となるデータのこと。エンティティとNSManagedObjectオブジェクトの関係はDBでいうところのテーブルとレコードの関係みたいな感じですかね。

  • NSFetchRequest

データの検索条件を管理するクラス。ここで指定した条件が SQL に変換されてデータ検索に使われる。

  • NSFetchedResultsController

NSManagedObjectオブジェクトを監視するコントローラークラス。NSManagedObjectオブジェクトが挿入、変更、削除された時にNSFetchedResultsControllerDelegateオブジェクトに通知する。

ふむ。

プロジェクト作成時にUse Core Dataをチェックした時点でCoreDataを使うための設定はある程度やってくれているみたいですね。

では上記を踏まえ、チュートリアルに沿って進めていきます。

まずはxcdatamodeldファイルを更新します。チュートリアルではエンティティをTasksとしていますが、僕はTodosとして、AttributesはString型のcontentを持つものとしました。

Todos.swiftはとりあえずこんな感じになります。

import Foundation
import CoreData

class Todos: NSManagedObject {

    @NSManaged var content: String

}

ちなみにここで@NSManagedというものがついていますが、もともとObjectiveなんたらではNSManagedObjectのサブクラスのプロパティは@dynamicという属性をつけていたらしく、「ここはゲッターセッターの自動生成は無しにして、NSManagedObjectの実装に任せましょう」みたいな意味だったらしいです。(参考)

これがSwiftではNSManagedObjectのサブクラスにのみ利用可能な@NSManagedという属性が使われるようになったようです。

では、いよいよTODOを登録するところを実装していきます。

TODOの登録

TODOを登録できるようにするためにTodoDetailViewControllerを修正していきます。

まずNSManagedObjectContextオブジェクトを用意します。

let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext

iOS では1アプリにつき必ず1つの UIApplication が存在しており、そのシングルトンオブジェクトはUIApplication.sharedApplicationで取得できます。(参考)

そいつからAppDelegateインスタンスmanagedObjectContextを取得します。

そしてテキストフィールドに入力したテキストを保存できるようにメソッドを追加していきます。

func createTodo() {
    let entity = NSEntityDescription.entityForName("Todos", inManagedObjectContext: managedObjectContext!)
    let todo = Todos(entity: entity!, insertIntoManagedObjectContext: managedObjectContext!)
      
    todo.content = self.textField.text
    managedObjectContext!.save(nil)
}

func dismissViewController() {
    self.navigationController?.popViewControllerAnimated(true)
}

@IBAction func save(sender: AnyObject) {
    self.createTodo()
    self.dismissViewController()
}

おそらくお作法なのだろうと、真似して保存するところまで書いてみます。(エラーハンドリング等はとりあえず無視)

まずエンティティを取得して、それを使ってNSManagedObjectオブジェクトを取得。そいつにテキストフィールドの値を入れてNSManagedObjectContextオブジェクトにて保存すると。

@IBAction func save(sender: AnyObject)でSaveボタンを押したときの処理が書けるので、TODOの保存と詳細画面を閉じる処理を追加します。

登録したTODOの取得

次にTodosViewControllerの修正です。

こちらのviewControllerでもCoreDataをインポートし、NSFetchedResultsControllerDelegateプロトコルに適合させていきます。またデータ操作のためのNSManagedObjectContextオブジェクトも必要です。

import CoreData

class TodosViewController: UITableViewController, NSFetchedResultsControllerDelegate {

    let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
    var fetchedResultController: NSFetchedResultsController = NSFetchedResultsController()

これだけだとfetchedResultControllerは無力なので、getFetchedControllerメソッドを定義して、viewDidLoadの中でゴニョゴニョやります。(viewDidLoadがいつ呼ばれるか等、iOSのイベントに関してはこちらを参考にさせて頂きました)

override func viewDidLoad() {
    super.viewDidLoad()

    fetchedResultController = self.getFetchedResultController()
    fetchedResultController.delegate = self
    fetchedResultController.performFetch(nil)
}

func getFetchedResultController() -> NSFetchedResultsController {
    fetchedResultController = NSFetchedResultsController(fetchRequest: self.todoFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
    return fetchedResultController
}

func todoFetchRequest() -> NSFetchRequest {
    let fetchRequest = NSFetchRequest(entityName: "Todos")
    let sortDescriptor = NSSortDescriptor(key: "content", ascending: true)
    fetchRequest.sortDescriptors = [sortDescriptor]
    return fetchRequest
}

まずtodoFetchRequestにて検索条件を作成します。そしてそいつをセットしてNSFetchedResultsControllerオブジェクトを作成。performFetchで検索条件をもとにデータ取得といった感じでしょうか。 あーキツくなってきたぞw

登録したTODOの一覧表示

次、一覧表示しますよ。まず、ストーリーボードのTodosViewControllerのCellを選択した状態で、identifierをCellとしてください。

そんでからチュートリアルのコードをパクります。どん。

//こいつは必要なさそうな?
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    let numberOfSections = fetchedResultController.sections?.count
    return numberOfSections!
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let numberOfRowsInsection = fetchedResultController.sections?[section].numberOfObjects
    return numberOfRowsInsection!
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
    let todo = fetchedResultController.objectAtIndexPath(indexPath) as Todos
    cell.textLabel?.text = todo.content
    return cell
}

func controllerDidChangeContent(controller: NSFetchedResultsController!) {
    tableView.reloadData()
}

こちらの記事を参考にtableViewまわりのメソッドを整理していきます。

  • numberOfSectionsInTableView

テーブル全体のセクションの数を返すように実装する。

  • tableView(tableView: UITableView, numberOfRowsInSection section: Int)

セクション無しのテーブルを作成する場合はこのメソッドでセルの数を返すように実装する。テーブルにセクションを付ける場合はnumberOfRowsInSectionで指定されたセクションの項目数を返すように実装する。

  • tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) cellForRowAtIndexPathで指定したインデックスパスのセル(UITableViewCell)を作成し、そのインスタンスを返すように実装する。

ただ今回、別にセクション分けされたtableViewを作っているわけではないのでnumberOFSectionsInTableViewは要らない気がしました。実際消しても何も問題起きないので僕は削除しました。

あと、tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)の以下の部分。

let todo = fetchedResultController.objectAtIndexPath(indexPath) as Todos

ここでSwiftにはNameSpaceがあるので、このダウンキャストはモジュール名.クラス名じゃないと使えないと。

てなわけでxcdatamodeldファイルを再度開き、Data Model Inspectorから上記に従ってクラス名を変更します。僕はiOSTodosというプロジェクト名だったのでiOSTodos.Todosです。

ここまでで新規登録と登録したTODOの一覧表示はできるかと。あーーキツイ!!

登録したTODOの削除

スワイプしたらDELETEボタンが出てきて、クリックしたら削除できるあれですね。

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    let managedObject: NSManagedObject = fetchedResultController.objectAtIndexPath(indexPath) as NSManagedObject
    managedObjectContext?.deleteObject(managedObject)
    managedObjectContext?.save(nil)
}

こんな感じになりました。コンテキストを介して、対象のNSManagedObjectオブジェクトを削除して保存。新規登録と同じ感じですね。

登録したTODOの編集

TODOを編集できるようにします。一覧画面からセルを選ぶと詳細画面に遷移して、編集、保存を行えるようにします。

まずストーリーボードのTodosViewControllerのCellからTodoDetailViewControllerに向かってセグエを作成します。あとでセグエを特定するためにidentifierが必要になるので、identifierはeditとします。

次にTodosViewControllerの修正をします。

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "edit" {
        let cell = sender as UITableViewCell
        let indexPath = tableView.indexPathForCell(cell)
        let todoDetailController: TodoDetailViewController = segue.destinationViewController as TodoDetailViewController
        let todo: Todos = fetchedResultController.objectAtIndexPath(indexPath!) as Todos
        
        todoDetailController.todo = todo
    }
}

prepareForSegueは画面遷移が始まった時に呼ばれるメソッドですね。

クリックされたCellをもとにindexPathを取得してTODOを特定します。そして、segue.destinationViewControllerで遷移先のviewControllerが取得できるのでそいつにTODOを渡しています。 TodoDetailViewControllerのほうでtodoを定義しないといけません。

TodoDetailViewControllerの修正です。

var todo: Todos?

override func viewDidLoad() {
    super.viewDidLoad()
    
    if todo != nil {
        self.textField.text = todo?.content
    }
}

一覧画面からTODOが渡ってきた場合、テキストフィールドにそいつのcontentをセットしています。

そして編集メソッド

func editTodo() {
    todo?.content = self.textField.text
    managedObjectContext!.save(nil)
}

入力したテキストフィールドの値で上書き保存します。

Saveボタンをおした時の挙動が新規登録、編集の2パターンになったので修正します。

@IBAction func save(sender: AnyObject) {
    if todo != nil {
        self.editTodo()
    } else {
        self.createTodo()
    }
    self.dismissViewController()
}

これで新規登録、一覧表示、削除、更新、ひと通りの処理ができるようになりました。キツかった。。

テキストフィールドでReturnした時にキーボードが閉じるようにする

現状ではテキストを入力し終わってドヤ顔でReturnしてもキーボードが閉じず、恥ずかしい思いをしてしまいます。

TodoDetailViewControllerを修正していきます。

class TodoDetailViewController: UIViewController, UITextFieldDelegate {
    略

    func textFieldShouldReturn(textField: UITextField!) -> Bool {
        self.view.endEditing(true)
        return false
    }

UITextFieldDelegateプロトコルに適合して、Returnした時に呼ばれるtextFieldShouldReturnを実装しておきます。これでキーボードが閉じるはず。

まとめ

以上、チュートリアルを参考にTODOアプリを作ってみました。作る過程で様々な本や記事に触れ、少しだけですがiPhoneアプリ開発のことを知ることができました。やはり今までやったことない開発は楽しいし、Swiftという言語自体にもかなり惹かれているので今後も勉強を続けていきたいなと思いました。ただどうしても現状Objectiveなんたらの知識が求められる場面が多いのでそこはツラいですねw 諦めてそちらも勉強していきます。。

ちなみに他にもテキスト空の状態で登録できないようにしたり、ちょいちょい改良はしてみたのですが、長くなるので後はこちらで見ていただければと思います。

また本記事や公開しているコードにおいて、初心者ゆえの間違いも多々あるかもしれません。僕は非常に傷つきやすいので、もし発見された方は優しくコメントして頂けると幸いです。

参考