一から勉強させてください

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

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

(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 はいろんな言語のいいとこ取りしてるような感じで面白い言語だなぁという印象でした。

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

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

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

作成したアプリ

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

f:id:d_animal141:20150228000945g:plain

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

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

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

CoreData tutorial in Swift 5 using NSFetchedResultsController – Ravi Shankar

基本的に上記チュートリアルを参考にしながら、改造していくスタイルで開発しました(チュートリアルに載っているコード、たまに動かなかったので気をつけてください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 諦めてそちらも勉強していきます。。

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

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

参考