- 概要
- GitHub
- データモデル
- AppDelegate
- AppDelegate.swift
- クラス設定
- Import library
- Protocol
- Property
- application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
- saveData()
- userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)
- AppDelegate.swift
- ViewControllers
- AllListViewController.swift
- クラスの設定
- Property
- viewDidLoad()
- viewWillAppear(_ animated: Bool)
- viewWillAppear(_ animated: Bool)
- Segueの設定(Storyboard)
- prepare(for segue: UIStoryboardSegue, sender: Any?)
- navigationController:(UINavigationController )navigationController willShowViewController:(UIViewController )viewController animated:(BOOL)animated;
- tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
- tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
- tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
- tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
- tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath)
- listDetailViewControllerDidCancel(_ controller: ListDetailViewController)
- listDetailViewController(_ controller: ListDetailViewController, didFinishAdding checklist: Checklist)
- listDetailViewController(_ controller: ListDetailViewController, didFinishEditing checklist: Checklist)
- ListDetailViewController.swift
- Protocol
- クラス設定
- Property
- viewDidLoad()
- viewWillAppear(_ animated: Bool)
- @IBAction func cancel()
- @IBAction func done()
- tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
- textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
- textFieldShouldClear(_ textField: UITextField) -> Bool
- TextFieldのバインド
- iconPicker(_ picker: IconPickerViewController, didPick iconName: String)
- prepare(for segue: UIStoryboardSegue, sender: Any?)
- ChecklistViewController.swift
- クラス設定
- Property
- viewDidLoad()
- Segueの設定(Storyboard)
- prepare(for segue: UIStoryboardSegue, sender: Any?)
- configureCheckmark(for cell: UITableViewCell, with item: ChecklistItem)
- configureText(for cell: UITableViewCell, with item: ChecklistItem)
- tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
- tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
- tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
- tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
- itemDetailViewControllerDidCancel(_ controller: ItemDetailViewController)
- itemDetailViewController(_ controller: ItemDetailViewController, didFinishAdding item: ChecklistItem)
- itemDetailViewController(_ controller: ItemDetailViewController, didFinishEditing item: ChecklistItem)
- ItemDetailViewController.swift
- Import Library
- Protocol
- クラス設定
- Property
- viewDidLoad()
- viewWillAppear(_ animated: Bool)
- // MARK:- Actions
- // MARK:- Table View Delegates
- tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath?
- tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
- tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
- tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
- tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
- tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int
- // MARK:- Text Field Delegates
- // MARK:- Helper Methods
- Assets.xcassets
- IconPickerViewController.swift
- 起動時の画面
- AllListViewController.swift
概要
iOS Apprenticeの第2章で作成するチェックリストアプリのコード・構成の解説を忘備録として記載。 この本は作っては壊し、を繰り返すので(大幅なリファクタリング)、実用的であろうアプリ作成過程を体験できるので初心者→中級者の橋渡しとしてはいいなと感じる。
GitHub
以下、
<Code>
<解説>
の順番で記述していく。
データモデル
DataModel -> Checklist -> ChecklistItemの順に小さくなる
画面構成は下記の通り。1View1Controllerの構成。
Checklist.swift
class Checklist: NSObject, Codable {
ChecklistItem.swift
と同じ理由。
name
:リストのタイトル
items
:ChecklistItem.swift
の配列
init(name: String, iconName: String = "No Icon") { self.name = name self.iconName = iconName super.init() }
name
とiconName
を指定してオブジェクトの作成を行う。iconName
はdefaultを与えているため、初期化の際には省略可能
countUncheckedItems() -> Int
return items.reduce(0) { cnt, item in cnt + (item.checked ? 0 : 1) }
- 関数型プログラミング機能の一端。
reduce()
はそれぞれの要素を見て{}
内の動作を実行する
ChecklistItem.swift
クラス設定
class ChecklistItem: NSObject, Codable {
NSObject
の継承:オブジェクトの比較のため。
Codable
:NSUserDefaultsで保存時にバイナリ変換をするため
Property
var text = "" var checked = false var dueDate = Date() var shouldRemind = false var itemID = -1
text | TODO項目のタイトル |
---|---|
text | TODO項目のタイトル |
checked | チェックマークをつけるか否か |
dueDate | 通知時刻 |
shouldRemind | 通知を行うか否か |
itemID | 通知のidentifierをして使用する。 各アイテムにユニークな数字。 UserDefaultsに保存した値をインクリメントしていく。 0からスタート。 |
init()
super.init() itemID = DataModel.nextChecklistItemID()
- DataModelのクラスプロパティからユニークな
itemID
を取得する
deinit
removeNotification()
- オブジェクトの開放時に呼ばれる特殊なメソッド。
- 今回の例だとChecklistItem・Checklistが削除された場合に呼ばれる
toggleChecked()
checked = !checked
toggleChecked
のようにデータ側でプロパティを変更する作りにするのがオブジェクト指向的なポイント。
scheduleNotification()
- object oriented programmin→自分のことはできるだけ自分で行う
removeNotification() // すでにitemID名の通知がある場合は削除する
- 上記の通りに実装することで、上書きやRemindスイッチをOFFにしたとき等色々と条件があるのが、簡潔にかける様になる。
if shouldRemind && dueDate > Date() {
- 「RemindスイッチがON」かつ「通知時刻が未来の場合」
- ここは
early return
にすると見通しが良くなるかと個人的に思う
- ここは
// 通知の中身を作成 let content = UNMutableNotificationContent() content.title = "Reminder" content.body = text content.sound = UNNotificationSound.default
// dueDateから日時の要素を取り出す let calendar = Calendar(identifier: .gregorian) let components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: dueDate)
- それぞれの地域に合わせた時刻に自動で変換される(時差の考慮は不要)
// いつ実行するかのトリガを作成 let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
// itemIDを通知のidentifierとして使っている。通知依頼書を作成的な…。 let request = UNNotificationRequest(identifier: "\(itemID)", content: content, trigger: trigger)
- 通知の
identifier
にはitemID
を使用する
// UNUserNotificationCenterに新しいnotificationを登録する let center = UNUserNotificationCenter.current() center.add(request)
removeNotification()
let center = UNUserNotificationCenter.current() center.removePendingNotificationRequests(withIdentifiers: ["\(itemID)"])
- 通知オブジェクトを取得
- 自身の
itemID
のidentifierを持つ通知を削除する
DataModel.swift
Property
var lists = [Checklist]()
lists
:Checklist.swift
の配列
// このメソッドでUserDefaultの値を読み書きできる(他クラスでUserDefaultを意識しないで良くなる) var indexOfSelectedChecklist: Int { get { return UserDefaults.standard.integer(forKey: "ChecklistIndex") } set { UserDefaults.standard.set(newValue, forKey: "ChecklistIndex") } }
Computedプロパティ。UserDefaults
をこのクラス内で隠蔽することで、疎な作りにすることができる(オブジェクト指向)。
使用例は下記の通り
書き込み:dataModel.indexOfSelectedChecklist = -1
読み込み:let index = dataModel.indexOfSelectedChecklist
UserDefault
は例えば以下の場所に保存されている。
/Users/<ユーザ名>/Library/Developer/CoreSimulator/Devices/8A2A393D-0538-48CF-8D0A-B9F72F50788C/data/Containers/Data/Application/94934470-B24A-4BAF-9928-B2C7757E55FD/Library/Preferences/com.razeware.Checklists.plist
init()
loadChecklists() registerDefaults() handleFirstTime()
色々初期化を行っている。(後述)
registerDefaults()
let dictionary = [ "ChecklistIndex": -1, "FirstTime": true ] as [String : Any] // 異なる型を登録する場合はキャストが必要 UserDefaults.standard.register(defaults: dictionary) // register:設定されていない場合に値を登録する。 set:上書き登録
UserDefaults
に、まだ保存されていない場合、作成したdictionary
を登録する。"ChecklistIndex": -1
:前回最後に選択されたチェックリスト- -1は選択なし
"FirstTime": true
:初回起動かどうか
handleFirstTime()
let userDefaults = UserDefaults.standard let firstTime = userDefaults.bool(forKey: "FirstTime") if firstTime { let checklist = Checklist(name: "List") lists.append(checklist) indexOfSelectedChecklist = 0 // 初回起動時はindex0が開かれるようにする userDefaults.set(false, forKey: "FirstTime") userDefaults.synchronize() }
UserDefaults
に登録されているFirstTime
の値を読み込み初回起動であれば、見本のリストを作成する。Checklist
のオブジェクトを作成し、配列へ追加indexOfSelectedChecklist = 0
はComputedプロパティを使用している。- 次回以降は初回起動ではないので
FirstTime
の値にfalse
をセットする userDefaults.synchronize()
はすぐに書き込みをするため。
Get Plist path
func documentsDirectory() -> URL { let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return paths[0] } func dataFilePath() -> URL { return documentsDirectory().appendingPathComponent("Checklists.plist") }
Checklist.plist
のパスを取得するための関数。このplistにチェックリストの項目を保存する。
saveChecklists()
let encoder = PropertyListEncoder() do { let data = try encoder.encode(lists) try data.write(to: dataFilePath(), options: Data.WritingOptions.atomic) } catch { print("Error encoding list array: \(error.localizedDescription)") }
Checklist.plist
への保存を行う。- データの書き込みは失敗する可能性があるのでtry-catch
loadChecklists()
let path = dataFilePath() if let data = try? Data(contentsOf: path) { let decoder = PropertyListDecoder() do { lists = try decoder.decode([Checklist].self, from: data) sortChecklists() } catch { print("Error decoding list array: \(error.localizedDescription)") } }
- plistから保存してあるデータを読み込む
- これを
init
で呼び出すことで、前回のデータを読み込んでいる。- 実際にオブジェクトが作成されるタイミングは、AppDelegate内の
let dataModel = DataModel()
である。
- 実際にオブジェクトが作成されるタイミングは、AppDelegate内の
sortChecklists()
lists.sort(by: { list1, list2 in return list1.name.localizedStandardCompare(list2.name) == .orderedAscending })
lists.sort(by: { /* the sorting code goes here */ })
{}
はclosure- 大文字小文字を区別しない
nextChecklistItemID() -> Int
let userDefaults = UserDefaults.standard let itemID = userDefaults.integer(forKey: "ChecklistItemID") userDefaults.set(itemID + 1, forKey: "ChecklistItemID") userDefaults.synchronize() return itemID
userDefaults
から"ChecklistItemID"のキーの値を取り出す- ここには次に使用すべき
itemID
の値が格納されている - 登録されていない場合は「0」が帰ってくるので問題ない(
itemID
は0始まり)
- ここには次に使用すべき
- 次に登録する
itemID
をインクリメントで登録する userDefaults.synchronize()
ですぐにUserDefaults
へ書き込むよう
AppDelegate
AppDelegate.swift
クラス設定
Import library
import UserNotifications
- ローカル通知用のライブラリをインポート
Protocol
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
- ローカル通知を受け取るために
UNUserNotificationCenterDelegate
を設定
Property
let dataModel = DataModel() // インスタンスはAppDelegateで作成して、これをAllListsViewControllerへ渡す
ここでdataModel
の作成を行い、後にAllListViewController
へ渡す。
application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let navigationController = window!.rootViewController as! UINavigationController let controller = navigationController.viewControllers[0] as! AllListsViewController controller.dataModel = dataModel
まずnavigationController
を取得する。
navigationController
の一番始めのviewController
はAllListsViewController
である。(これはStoryboardで一目瞭然だが、2番目以降はどう取るのだろうか?)
AllListsViewController
のdataModel
に先程作成したdateModel
を渡してやる。
// Notification set up let center = UNUserNotificationCenter.current() center.delegate = self
saveData()
dataModel.saveChecklists()
func applicationDidEnterBackground(_ application: UIApplication) { saveData() } func applicationWillTerminate(_ application: UIApplication) { saveData() }
dataModel.saveChecklists()
で情報をUserDefaults
に登録するだけのメソッド。これらをAppDelegate
で、アプリがsuspendや終了するタイミングで呼ぶようにする。
userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)
print("Received local notification \(notification)")
- User Notification Delegates
- アプリが起動中にlocal notificationがポストされたときに呼ばれる
- 今回はログを表示するだけ
ViewControllers
AllListViewController.swift
クラスの設定
class AllListsViewController: UITableViewController, ListDetailViewControllerDelegate, UINavigationControllerDelegate {
ListDetailViewControllerDelegate
ListDetailViewController
からの変更を受け取るためのDelegate
UINavigationControllerDelegate
- NavigationControllerでこの画面に戻ってきた通知を受け取るためのプロトコル
- navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)
- NavigationControllerでこの画面に戻ってきた通知を受け取るためのプロトコル
Property
let cellIdentifier = "ChecklistCell"
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)
のときに使用するためのidentifier
var dataModel: DataModel!
Checklist.swift
の配列。これをTableViewに表示している。
viewDidLoad()
super.viewDidLoad() tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier) }
再利用するためのセルを登録
viewWillAppear(_ animated: Bool)
super.viewWillAppear(animated)
tableView.reloadData()
- delegateを使う手もあるが、簡単な方法を考える。
reloadData
により、tableViewにinsertするよりもぐっと楽に書けるようになった。- 今回はテーブルもせいぜい14個なので、tableViewの
reloatData
で対応できる。(表示されているのが100よりも大きいのであればdelegateを考え始める)
viewWillAppear(_ animated: Bool)
navigationController?.delegate = self // navigationControllerDelegateメソッドを受け取るために自分をセット
navigationControllerのDelegateに自分を設定。 「!」だとnilの場合にCrashするので「?」で安全に、ない場合は移行が処理されないだけ。
let index = dataModel.indexOfSelectedChecklist
Computed PropertyによりUserDefaultから保存されているindexを取得。indexが保存されている場合は次のメソッドで遷移する。
if index >= 0 && index < dataModel.lists.count { // indexが-1ではない、かつindexが範囲外ではない(plistとUserDefaultは同期していないため)= indexがvalidかどうか let checklist = dataModel.lists[index]; performSegue(withIdentifier: "ShowChecklist", sender: checklist) }
checklist
オブジェクトを取得し、遷移時に渡す。
遷移時の動作はprepare
を参照。
またdataModel
の読み込みはAppDelegate
で行っている。
Segueの設定(Storyboard)
ShowChecklist
AddChecklist
「+」ボタンからListDetailViewController
へバインドする。
Control-drag from this new bar button to the Add Checklist scene below to add a new Show segue.
EditChecklist
Control-drag from the yellow cirlce at the top of the All Lists scene to the new scene. Select Show from the Manual Segue section of the pop-up menu.
prepare(for segue: UIStoryboardSegue, sender: Any?)
if segue.identifier == "ShowChecklist" { let controller = segue.destination as! ChecklistViewController controller.checklist = sender as? Checklist }
segueのidentifier
によって遷移時の処理を分けている。
ShowChecklist
の場合は、遷移先のコントローラにperformSegue
のsender
で受け取ったChecklist
をセットしている。
例えば下記の通り。
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { ... performSegue(withIdentifier: "ShowChecklist", sender: checklist) }
else if segue.identifier == "AddChecklist" { let controller = segue.destination as! ListDetailViewController controller.delegate = self }
AddChecklist
の場合は新規項目追加なので、再びAllListsViewController
に戻ってきたときにDelegateメソッドで項目を受け取るため、遷移先のコントローラのdelegateを自身に設定する。
navigationController:(UINavigationController )navigationController willShowViewController:(UIViewController )viewController animated:(BOOL)animated;
// MARK:- Navigation Controller Delegates // 戻るボタンが押されてAllListsViewControllerが表示される場合に呼ばれる if viewController === self { dataModel.indexOfSelectedChecklist = -1 }
AllListsViewController
が表示されているならば、UserDefaultにChecklistを選択されていない状態を登録する。
tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return dataModel.lists.count
そのSectionのRowの数を返す。
tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
// Get cell let cell: UITableViewCell! if let c = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) { cell = c } else { cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier) }
- 今、subtitleのcellを使っているので、オプショナル型の
detailTextlaben
は!
でunwrapできる - Optional Bindingでもかけるが、前者のほうがスッキリしていて筆者の好み
- reusableCellがあれば利用するようにしている理由がちょっとわからない。
// Update cell information let checklist = dataModel.lists[indexPath.row] cell.textLabel!.text = checklist.name cell.accessoryType = .detailDisclosureButton let count = checklist.countUncheckedItems() cell.detailTextLabel!.text = count == 0 ? "All Done" : "\(count) Remaining"
- セルの更新
- セルに対応したデータを取り出す
- セルに上記のデータの値を設定する
cell.imageView!.image = UIImage(named: checklist.iconName) return cell
- 標準の
.subtitle
は左にUIImageView
を持つ
tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
dataModel.lists.remove(at: indexPath.row) let indexPaths = [indexPath] tableView.deleteRows(at: indexPaths, with: .automatic)
セルをスライドしたときに削除できるようにするためのメソッド。
データから削除するだけでなく、tableViewからも削除する必要があることに注意。
tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
dataModel.indexOfSelectedChecklist = indexPath.row // 選択された列をUserDefaultsに記録しておく let checklist = dataModel.lists[indexPath.row] performSegue(withIdentifier: "ShowChecklist", sender: checklist)
セルが選択されたときに呼ばれるメソッド。
対応するChecklist
オブジェクトを取得し、それを持ってsegueを行う。
tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath)
セルのaccessoryButtonがタップされたときに呼ばれるメソッド。
let controller = storyboard!.instantiateViewController(withIdentifier: "ListDetailViewController") as! ListDetailViewController controller.delegate = self let checklist = dataModel.lists[indexPath.row] controller.checklistToEdit = checklist navigationController?.pushViewController(controller, animated: true)
下記はstoryboard上でsegueでしていることと同じことを、コード内で行っている。
- storyboardからIDが
ListDetailViewController
(設定は下記の通り)のクラスを取得。 - 戻ってくるときのためにdelegateを自分に設定。
Checklist
オブジェクトを設定- ControllerのViewを表示する
listDetailViewControllerDidCancel(_ controller: ListDetailViewController)
navigationController?.popViewController(animated: true)
ここから3つはメソッド名の通り、listDetailViewControllerDelegate
プロトコルのメソッド。
Checklist
の追加・編集キャンセル時のコードは1画面前に戻るだけ。(ListDetailViewControllerを閉じる)
listDetailViewController(_ controller: ListDetailViewController, didFinishAdding checklist: Checklist)
dataModel.lists.append(checklist) dataModel.sortChecklists() tableView.reloadData() navigationController?.popViewController(animated: true)
Checklist
が新しく追加された場合に呼び出されるメソッド。- 処理の流れは以下の通り
- データにアイテムを追加
- データをソート
- tableViewを再読込
- 終わったら1つ前の画面に戻る。
listDetailViewController(_ controller: ListDetailViewController, didFinishEditing checklist: Checklist)
dataModel.sortChecklists() tableView.reloadData() navigationController?.popViewController(animated: true)
- 既存の
Checklist
が変更された場合に呼び出されるメソッド。 - 前は編集されたオブジェクトのインデックスを取得して…とやっていたが、
sortChecklist
とreloadData
によりそれすらも必要がなくなる。- 個別にcellのUIを編集をせずにtableVIew全体を更新しているため
- 終わったら1つ前の画面に戻る。
ListDetailViewController.swift
Protocol
protocol ListDetailViewControllerDelegate: class { func listDetailViewControllerDidCancel(_ controller: ListDetailViewController) func listDetailViewController(_ controller: ListDetailViewController, didFinishAdding checklist: Checklist) func listDetailViewController(_ controller: ListDetailViewController, didFinishEditing checklist: Checklist) }
それぞれキャンセル時、追加時、既存のChecklist
オブジェクトの変更時に呼ばれる。
クラス設定
class ListDetailViewController: UITableViewController, UITextFieldDelegate, IconPickerViewControllerDelegate
UITextFieldDelegate
はTextFieldの入力変更を受け取るため。今夏はテキストフィールドが空のときにDoneボタンをDisableにするために使う。
IconPickerViewControllerDelegate
は、アイコン選択画面からアイコン選択情報を受け取るためのプロトコル。
Property
@IBOutlet weak var textField: UITextField! @IBOutlet weak var doneBarButton: UIBarButtonItem! @IBOutlet weak var iconImage: UIImageView! weak var delegate: ListDetailViewControllerDelegate? var checklistToEdit: Checklist? var iconName = "Folder"
- delegateは強参照を避けるために
weak
である。 checklistToEdit
は、これが呼び出し前にセットされていれば現在編集モードと判断するためにも使う(nilならば追加モード)。iconName
はcheckList
オブジェクトが作成されていない場合に一時保存するための変数- デフォルトは
Folder
としてある
- デフォルトは
viewDidLoad()
super.viewDidLoad() if let checklist = checklistToEdit { title = "Edit Checklist" textField.text = checklist.name doneBarButton.isEnabled = true iconName = checklist.iconName } iconImage.image = UIImage(named: iconName)
checklistToEdit
がある場合は編集モード。- タイトルを変更
- テキストフィールドに編集項目の名前をプリセット
done
ボタンをEnableにする(textFieldDelegateメソッドはこのとき呼ばれないため)- アイコン名をセット
- アイコンを表示する
- 編集モードであれば既存の項目のアイコン、追加モードであれば
iconName
のデフォルトFonder
アイコンが表示される
- 編集モードであれば既存の項目のアイコン、追加モードであれば
viewWillAppear(_ animated: Bool)
textField.becomeFirstResponder()
表示時にtextFieldを編集状態にする
@IBAction func cancel()
delegate?.listDetailViewControllerDidCancel(self)
cancel
ボタン押下時の動作。delegate先のメソッドを呼ぶ。
delegateはnilの可能性があるのでオプショナル。
@IBAction func done()
if let checklist = checklistToEdit { checklist.name = textField.text! checklist.iconName = iconName delegate?.listDetailViewController(self, didFinishEditing: checklist) } else { let checklist = Checklist(name: textField.text!, iconName: iconName) delegate?.listDetailViewController(self,didFinishAdding: checklist) }
checklistToEdit
がある場合(編集モードの場合)- オブジェクトにテキストを設定
- delegate先の編集完了時のメソッドを呼ぶ
iconName
には、iconPicker(_ picker: IconPickerViewController, didPick iconName: String)
で取得した値を入れる- ここで初めて本ちゃんのデータであるプロパティに保存される
checklistToEdit
がない場合(新規追加の場合)- 新たに
Checklist
オブジェクトを作成 - delegate先の追加完了時のメソッドを呼ぶ
- 新たに
tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
return indexPath.section == 1 ? indexPath : nil
- sectionが0のとき、セルの選択を許可しない
- sectionが1、即ちアイコンのrowの場合は選択を許可する
textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
let oldText = textField.text! let stringRange = Range(range, in:oldText)! let newText = oldText.replacingCharacters(in: stringRange, with: string) doneBarButton.isEnabled = !newText.isEmpty return true
- textFieldで変更があったときに呼ばれるメソッド。
NSRange
をRange
に変換する必要がある。- また、引数の
string
は変更された文字のみが渡されている。なので、これをoldText
に挿入することで、newText
を作成する。 done
は空ならばdisable。ifを使わずにこう書けることは1つキーポイント。
textFieldShouldClear(_ textField: UITextField) -> Bool
doneBarButton.isEnabled = false return true
- textFieldの「x」ボタンが押されたときに呼ばれるメソッド。
- doneボタンをdisableにする。
return true
でキーボードを再表示しないようになる。
TextFieldのバインド
- delegateをコントローラに設定。
Did End On Exit
はキーボードのdone押下時の設定。
iconPicker(_ picker: IconPickerViewController, didPick iconName: String)
self.iconName = iconName // 追加モードのときはこの時点でchecklistオブジェクトが存在しないので、propertyに一時保存する iconImage.image = UIImage(named: iconName) navigationController?.popViewController(animated: true)
iconPickerViewController
でアイコンが選択された場合に呼ばれる- アイコン名を一時的にプロパティに保存し、アイコンの画像をUIに表示する
- その後
iconPickerViewController
を閉じる
prepare(for segue: UIStoryboardSegue, sender: Any?)
if segue.identifier == "PickIcon" { let controller = segue.destination as! IconPickerViewController controller.delegate = self; }
- 自分を
IconPickerViewController
のdelegateに設定してsegueで遷移する
ChecklistViewController.swift
クラス設定
class ChecklistViewController: UITableViewController, ItemDetailViewControllerDelegate {
ItemDetailViewController
からの返答を取得するために、ItemDetailViewControllerDelegate
プロトコルを設定。
Property
var checklist: Checklist!
ChecklistItem
を配列に持つChecklist
をプロパティとして有する。- オプショナルなのは、
ShowChecklist
で遷移時に常にオブジェクトは設定されているが、viewを作成した直後、例えばviewDidLoad
以前にはnilの可能性があるため。
viewDidLoad()
title = checklist.name
- 画面上部のタイトルをリストの名前に変更
ViewController
はいくつかプリセットとしてpropertyを持っており、title
はその内の1つ。- NavigationBarはこれを探してnavigation bar内のテキスト自動で書き換える。
Segueの設定(Storyboard)
Addボタンを押したときのSegue設定
セルのAccessoryボタンを押下時のSegue設定
Accessory Action >> Show
を選択すること、Selection Segue >> Show
ではない
prepare(for segue: UIStoryboardSegue, sender: Any?)
if segue.identifier == "AddItem" { let controller = segue.destination as! ItemDetailViewController controller.delegate = self }
- segue実行時、identifierによって処理を分岐させる。
AddItem
のとき、遷移先のItemDetailViewController
にdelegateを設定するのみ。
else if segue.identifier == "EditItem" { let controller = segue.destination as! ItemDetailViewController controller.delegate = self if let indexPath = tableView.indexPath(for: sender as! UITableViewCell) { controller.itemToEdit = checklist.items[indexPath.row] } }
EditItem
のとき、上記と同様に遷移先のItemDetailViewController
にdelegateを設定する。- また
sender
で受け取ったUITableViewCell
、これはtableViewの選択されたcellである。
- また
configureCheckmark(for cell: UITableViewCell, with item: ChecklistItem)
let label = cell.viewWithTag(1001) as! UILabel if item.checked { label.text = "√" } else { label.text = "" }
- cellのチェックマークを表示するかどうかの設定をするためのメソッド。
- このメソッドによりデータとviewの表示間で同期を取れる。
- 引数のcellは例えば、下記の通りResuableCellにより作成している。
let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem", for: indexPath)
- cell内のオブジェクトにtagを予め振り分けておき(下図)、
viewWithTag
により、cell内のオブジェクトを取得している。
configureText(for cell: UITableViewCell, with item: ChecklistItem)
let label = cell.viewWithTag(1000) as! UILabel label.text = item.text
configureCheckmark
と同様、cellのテキストを設定をするためのメソッド。- このメソッドによりデータとviewの表示間で同期を取れる。
tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return checklist.items.count
- tableViewのrowの数を設定する
tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem", for: indexPath) let item = checklist.items[indexPath.row] configureText(for: cell, with: item) configureCheckmark(for: cell, with: item) return cell
- tableViewに表示するcellを設定する。
- ReusableCellを使用するために、予めPrototypeCellsにidentifierを設定しておく。(下図)
- データから該当するデータを抜き出す
configureText
・configureCheckmark
によりデータとセルの状態を同期させる
tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
checklist.items.remove(at: indexPath.row) let indexPaths = [indexPath] tableView.deleteRows(at: indexPaths, with: .automatic)
- cellをスライドして削除できるようにする
- データとtableViewの両方から削除を行うこと
tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
if let cell = tableView.cellForRow(at: indexPath) { let item = checklist.items[indexPath.row] item.toggleChecked() configureCheckmark(for: cell, with: item) } tableView.deselectRow(at: indexPath, animated: true)
- セルが選択されたときに呼ばれるメソッド
- 選択されたcellを取得
- 選択されたcellのデータを取得し、チェックマークの状態を反転させる
configureCheckmark
でcellの状態を書き換え、データとcell表示の同期をとる- 最後にtableViewのcell選択を解除する
itemDetailViewControllerDidCancel(_ controller: ItemDetailViewController)
navigationController?.popViewController(animated:true)
- キャンセルボタン押下時
- 画面を閉じる処理だけ
itemDetailViewController(_ controller: ItemDetailViewController, didFinishAdding item: ChecklistItem)
let newRowIndex = checklist.items.count checklist.items.append(item) let indexPath = IndexPath(row: newRowIndex, section: 0) let indexPaths = [indexPath] tableView.insertRows(at: indexPaths, with: .automatic) navigationController?.popViewController(animated:true)
itemDetailViewController
にて項目の追加が行われた場合に呼ばれる- データとtableViewの両方に項目を追加する
newRowIndex
は追加する項目のindexになる、これはちょうどcountで返される数insertRows
はIndexPath
の配列を渡すことが必要- 処理が終われば
itemDetailViewController
のviewを閉じる
itemDetailViewController(_ controller: ItemDetailViewController, didFinishEditing item: ChecklistItem)
if let index = checklist.items.index(of: item) { let indexPath = IndexPath(row: index, section: 0) if let cell = tableView.cellForRow(at: indexPath) { configureText(for: cell, with: item) } } navigationController?.popViewController(animated:true)
itemDetailViewController
にて項目の編集が行われた場合に呼ばれるchecklist.items.index(of: item)
により、戻ってきたデータが、checklist
プロパティのどのindexに相当するかを調べる- tableViewのcellの参照をとってきて、
configureText
でcellの表示を変更する- データの編集は
itemDetailViewController
に参照渡しをしている?ので向こうで編集済み
- データの編集は
- 処理が終われば
itemDetailViewController
のviewを閉じる
ItemDetailViewController.swift
Import Library
import UserNotifications
- 通知オブジェクトを作成するため
Protocol
protocol ItemDetailViewControllerDelegate: class { func itemDetailViewControllerDidCancel(_ controller: ItemDetailViewController) func itemDetailViewController(_ controller: ItemDetailViewController, didFinishAdding item: ChecklistItem) func itemDetailViewController(_ controller: ItemDetailViewController, didFinishEditing item: ChecklistItem) }
それぞれキャンセル時・項目追加時・項目編集時にdelegate先から呼び出すためのメソッドをProtocolで用意する
クラス設定
class ItemDetailViewController: UITableViewController, UITextFieldDelegate {
textField
の変更を取得するために、UITextFieldDelegate
のプロトコルを設定。
Property
@IBOutlet weak var doneBarButton : UIBarButtonItem! // 編集完了ボタン @IBOutlet weak var textField : UITextField! // item名の入力欄 @IBOutlet weak var shouldRemindSwitch : UISwitch! // リマインド有無のスイッチ @IBOutlet weak var dueDateLabel : UILabel! // 期限日を表示するラベル @IBOutlet weak var datePickerCell : UITableViewCell! // datePickerCellのオブジェクト @IBOutlet weak var datePicker : UIDatePicker! // datePickerのオブジェクト
weak var delegate : ItemDetailViewControllerDelegate? var itemToEdit : ChecklistItem? var dueDate = Date() // UILabelから簡単に情報を取れないので、変数で保持する var datePickerVisible = false
- delegateプロパティはweakかつオプショナル型
itemToEdit
、これが存在すれば編集モード、無ければ追加モードである- dueDateは
shouldRemindSwitch
と異なりUILabelから簡単に情報を取れないので、変数で保持する
viewDidLoad()
super.viewDidLoad() if let item = itemToEdit { // 編集モードであれば、UIをその情報に変更する title = "Edit Item" textField.text = item.text doneBarButton.isEnabled = true shouldRemindSwitch.isOn = item.shouldRemind dueDate = item.dueDate } updateDueDateLabel()
itemToEdit
、これが存在すれば編集モード、無ければ追加モード- 編集モードの場合
- doneボタンをenableにする(textFieldDelegateが呼ばれないため)
- 日付は
item
のpropertyからは簡単に変更できないので、更新用に作成したupdateDueDateLabel()
を使用する
viewWillAppear(_ animated: Bool)
super.viewWillAppear(animated)
textField.becomeFirstResponder()
- viewの表示時にtextFieldを選択状態にする
// MARK:- Actions
@IBAction func cancel()
delegate?.itemDetailViewControllerDidCancel(self)
- キャンセルボタン押下時に呼ばれる
- delegateは設定されていない場合があるのでオプショナル型
@IBAction func done()
- Doneボタン押下時
if let itemToEdit = itemToEdit { itemToEdit.text = textField.text! itemToEdit.shouldRemind = shouldRemindSwitch.isOn itemToEdit.dueDate = dueDate itemToEdit.scheduleNotification() delegate?.itemDetailViewController(self, didFinishEditing: itemToEdit) }
- 編集モードの場合
- 渡された
itemToEdit
オブジェクトを、UI上の情報に編集 - スケジュールの通知設定を行う
- delegate先のメソッドを呼び出す
- 渡された
else { let item = ChecklistItem() item.text = textField.text! item.checked = false item.shouldRemind = shouldRemindSwitch.isOn item.dueDate = dueDate item.scheduleNotification() delegate?.itemDetailViewController(self, didFinishAdding: item) }
- 追加モードの場合
- 新規に
ChecklistItem
オブジェクトを作成して値を設定 - スケジュールの通知設定を行う
- delegate先のメソッドを呼び出す。
- 新規に
dateChanged(_ datePicker: UIDatePicker)
dueDate = datePicker.date
updateDueDateLabel()
- データピッカーの値が変更された場合に呼ばれる
- Propertyの期限日時を変更し、UIも更新する
shouldRemindToggled(_ switchControl: UISwitch)
textField.resignFirstResponder() if switchControl.isOn { let center = UNUserNotificationCenter.current() center.requestAuthorization(options: [.alert, .sound]) { granted, error in // do nothing } }
- Remindのスイッチが押された場合に呼ばれる
textField
の編集状態を解除する- RemindスイッチがONの場合
- 通知の権限を取得するダイアログを表示する
// MARK:- Table View Delegates
tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath?
if indexPath.section == 1 && indexPath.row == 1 { return indexPath // dateのrowのみ選択可能とする } else { return nil }
dueDateLabel
のあるcellの選択のみ許可をする- このcellが選択された場合にdatePickerを表示するため
tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
- cellが選択された場合に呼ばれる
- 今回は
dueDateLabel
のcellが選択された場合に限られる
- 今回は
tableView.deselectRow(at: indexPath, animated: true) textField.resignFirstResponder()
- 選択状態を解除
textField
の編集状態を解除
if indexPath.section == 1 && indexPath.row == 1 { if !datePickerVisible { // datePickerが表示されていない場合 showDatePicker() } else { hideDatePicker() } }
datePicker
の表示状態を反転させる
tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
if indexPath.section == 1 && indexPath.row == 2 { return datePickerCell // データピッカーセルを返す } else { return super.tableView(tableView, cellForRowAt: indexPath) // 他のcellひは影響がないようにする }
- static cellの場合はcellForRowAtのメソッドを通常持たないが、overrideすることができる(取扱は注意が必要)
- datePickerが表示されるcellの場合、特別に予め別に用意していたcellを返してやる
tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
if section == 1 && datePickerVisible { return 3 } else { return super.tableView(tableView, numberOfRowsInSection: section) }
- 「sectionが1」かつ「datePicker」が表示されている場合
- rowは3である
tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
if indexPath.section == 1 && indexPath.row == 2 { return 217 } else { return super.tableView(tableView, heightForRowAt: indexPath) }
- datePickerのcellに関して、自動ではresizeされないのでハードコーディングで指定してやる必要がある
tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int
var newIndexPath = indexPath if indexPath.section == 1 && indexPath.row == 2 { newIndexPath = IndexPath(row: 0, section: indexPath.section) } return super.tableView(tableView, indentationLevelForRowAt: newIndexPath)
- static cellのdata sourceをいじったのでoverrideする必要のあるメソッド
- storyboard上で設定されていないstatic cellがあたかも存在するかのように処理を書き換える(インデントに関する設定っぽい)
- indexPathの指定した値の理由はわからないが…
// MARK:- Text Field Delegates
TextFieldの設定(storyboard)
textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
let oldText = textField.text! let stringRange = Range(range, in:oldText)! let newText = oldText.replacingCharacters(in: stringRange, with: string) doneBarButton.isEnabled = !newText.isEmpty return true
- textFieldで変更があったときに呼ばれるメソッド。
NSRange
をRange
に変換する必要がある。- また、引数の
string
は変更された文字のみが渡されている。なので、これをoldText
に挿入することで、newText
を作成する。 done
は空ならばdisable。ifを使わずにこう書けることは1つキーポイント。
textFieldShouldClear(_ textField: UITextField) -> Bool
doneBarButton.isEnabled = false return true
- textField内の「x」ボタンの押下時
- doneボタンをdisableにする
return true
で再度キーボードを表示しない
textFieldDidBeginEditing(_ textField: UITextField)
hideDatePicker() // textFieldの編集中はdatePickerを隠す
// MARK:- Helper Methods
updateDueDateLabel()
let formatter = DateFormatter() // for converting the Date to String formatter.dateStyle = .medium formatter.timeStyle = .short dueDateLabel.text = formatter.string(from: dueDate)
- プロパティに保存されている
dueDate
の値でUIの表示を更新する
showDatePicker()
datePickerVisible = true let indexPathDatePicker = IndexPath(row: 2, section: 1) tableView.insertRows(at: [indexPathDatePicker], with: .fade) datePicker.setDate(dueDate, animated: false) // dueDateプロパティの日時をdatePickerに表示させる dueDateLabel.textColor = dueDateLabel.tintColor // 編集中のときのみ日付ラベルをtintColorにする
datePicker
のセルを表示させるためのメソッドtableView
にinsertRows
することでUIが更新される?- 通常のデータ配列に項目を追加する代わりに、
datePickerVisible = true
で処理を分岐させている
- 通常のデータ配列に項目を追加する代わりに、
datePicker
にはdueDate
の値を表示させておくdatePicker
の表示中は日時ラベルをtintColorにする
hideDatePicker()
if datePickerVisible { datePickerVisible = false let indexPathDatePicker = IndexPath(row: 2, section: 1) tableView.deleteRows(at: [indexPathDatePicker], with: .fade) dueDateLabel.textColor = UIColor.black // 日付ラベルを元の色に戻す }
- showDatePickerと逆の動き
Assets.xcassets
Icons
- アイコンをimport
- iOS12以上はRetinaなので1xは必要ない
IconPickerViewController.swift
Protocol
protocol IconPickerViewControllerDelegate: class { func iconPicker(_ picker: IconPickerViewController, didPick iconName: String) }
Property
weak var delegate: IconPickerViewControllerDelegate? let icons = [ "No Icon", "Appointments", "Birthdays", "Chores", "Drinks", "Folder", "Groceries", "Inbox", "Photos", "Trips" ]
- アイコンは不変なのでlet。これが
tableViewに表示する内容
tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return icons.count
tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
let cell = tableView.dequeueReusableCell(withIdentifier: "IconCell", for: indexPath) let iconName = icons[indexPath.row] cell.textLabel!.text = iconName cell.imageView!.image = UIImage(named: iconName) return cell
ReusableCell
を用いる
tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
if let delegate = delegate { let iconname = icons[indexPath.row] delegate.iconPicker(self, didPick: iconname) }
- 選択された
indexPath
のrow
からiconName
を取得し、delegate先に渡して処理する。
起動時の画面
- 今回は
Launchboard
ファイルは削除し、Main画面を起動時の画面とした- 読み込みが爆速な感じを出せる