iPhoneアプリ開発初心者がSwift + CoreDataでシンプルなTODOアプリをつくってみた
(2015/11/7 追記)Swift2.0 に対応しつつ、リネームやリファクタリングなど修正を加えたものをGitHubにあげております。この記事内で説明している通りに実装しても動作しないと思うので、もしよろしければ GitHub のコードも参考にしてみてください。
僕は普段、Rails とか JavaScript とか触っているようなへっぽこ Web エンジニアでして、iOS や Android アプリの開発経験はありません。
HogeClass *hoge = [[HogeClass alloc] init]; // (゚Д゚≡゚Д゚)マジ?
て感じでした。
でも最近は Objective なんたらの 100 億倍書きやすそうな Swift という言語が出ていたりするので、楽しそうだしちょっと手を出してみるかと。
そこでまず iOS 開発勉強用のサンプルとして Swift でシンプルな TODO アプリをつくってみたので、今回はその時の実装メモを書きたいと思います。
iOS 開発とか1ナノもわからない状態からスタートしたので、同じような境遇の人のお役に立てたら幸いです。
事前準備
Swift どころか、Objective なんたらも C もまともにやったこと無い人間だったので、さすがにいきなり開発はツラいと。
そこでまずは鉄板のドットインストールさんで事前に Swift やら iOS の開発がどんな感じかをざっくり学びました。(プレミアム会員なりましたよ)
- 【旧版】iPhoneアプリ開発入門 (全13回) - プログラミングならドットインストール
- 【旧版】iOSレイアウト入門 (全12回) - プログラミングならドットインストール
- 【旧版】Swift入門 (全24回) - プログラミングならドットインストール
僕は「ストーリーボードって何ですの?」レベルだったのでかなり参考になりました。
その後、Swift の言語仕様を学びたい欲が高まってきたので、以下の本を読み始めました。
個人的に Swift はいろんな言語のいいとこ取りしてるような感じで面白い言語だなぁという印象でした。
あとハマるのはツラいので最低限のデバッグ方法をなんとなく調査。
こちらやこちらを見る感じだとブレークポイント仕込んで、po ◯◯
ってやれば値をいじくれそうな気配がします。
JS の DEBUG を Chrome でやるような感じでなんとなくできそうです。po!!
そんな感じで本を読みながら、ブレークポイントを仕込みながら、楽しく TODO アプリ開発スタートです。
作成したアプリ
今回作成したものはこんな感じです。
コードはこちらに公開しております。
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 諦めてそちらも勉強していきます。。
ちなみに他にもテキスト空の状態で登録できないようにしたり、ちょいちょい改良はしてみたのですが、長くなるので後はこちらで見ていただければと思います。
また本記事や公開しているコードにおいて、初心者ゆえの間違いも多々あるかもしれません。僕は非常に傷つきやすいので、もし発見された方は優しくコメントして頂けると幸いです。
参考
- 【旧版】iPhoneアプリ開発入門 (全13回) - プログラミングならドットインストール
- 【旧版】iOSレイアウト入門 (全12回) - プログラミングならドットインストール
- 【旧版】Swift入門 (全24回) - プログラミングならドットインストール
- http://mitaplus.sakura.ne.jp/wordpress/?p=335
- [iOS] デバッグ中に変数の値を変更する | DevelopersIO
- CoreData tutorial in Swift 5 using NSFetchedResultsController – Ravi Shankar
- Core Data プログラミングガイド
- サルでもわかる Core Data 入門【概念編】 - A Day In The Life
- SwiftでCoreData - Qiita
- iOS アプリの構造がどのようになっているか紐解いてみる - A Day In The Life
- iOS のイベント駆動をライフサイクルイベントとユーザアクションイベントにわけて理解する - A Day In The Life
- objectivec-iphone.com - objectivec iphone リソースおよび情報
- Obejctive-C 3分クッキング - A Day In The Life