- 概要
- GitHub
- データモデル
- AppDelegate
- ViewControllers
- AllListViewController.swift
- クラスの設定
- Property
- viewDidLoad()
- 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
- Protocol
- クラス設定
- 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のバインド
- 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
- Protocol
- クラス設定
- Property
- viewDidLoad()
- viewWillAppear(_ animated: Bool)
- @IBAction func cancel()
- @IBAction func done()
- tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath?
- TextFieldの設定(storyboard)
- textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
- textFieldShouldClear(_ textField: UITextField) -> Bool
- AllListViewController.swift
概要
iOS Apprenticeの第2章で作成するチェックリストアプリのコード・構成の解説を忘備録として記載。 この本は作っては壊し、を繰り返すので(大幅なリファクタリング)、実用的であろうアプリ作成過程を体験できるので初心者→中級者の橋渡しとしてはいいなと感じる。
GitHub
以下、
<Code>
<解説>
の順番で記述していく。
データモデル
DataModel -> Checklist -> ChecklistItemの順に小さくなる
画面構成は下記の通り。1View1Controllerの構成。
ChecklistItem.swift
import Foundation class ChecklistItem: NSObject, Codable { var text = "" var checked = false func toggleChecked() { checked = !checked } }
text
:TODO項目のタイトル
checked
:チェックマークをつけるか否か
NSObject
の継承:オブジェクトの比較のため。
Codable
:NSUserDefaultsで保存時にバイナリ変換をするため
toggleChecked
のようにデータ側でプロパティを変更する作りにするのがオブジェクト指向的なポイント。
Checklist.swift
class Checklist: NSObject, Codable {
ChecklistItem.swift
と同じ理由。
name
:リストのタイトル
items
:ChecklistItem.swift
の配列
init(name: String) { self.name = name super.init() }
name
を指定してオブジェクトの作成を行う。
DataModel.swift
var lists = [Checklist]()
lists
:Checklist.swift
の配列
init() { loadChecklists() registerDefaults() handleFirstTime() print("\(dataFilePath())") // for dubug }
色々初期化を行っている。(後述)
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にチェックリストの項目を保存する。
func 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)") } } func loadChecklists() { let path = dataFilePath() if let data = try? Data(contentsOf: path) { let decoder = PropertyListDecoder() do { lists = try decoder.decode([Checklist].self, from: data) } catch { print("Error decoding list array: \(error.localizedDescription)") } } }
Checklist.plist
への保存とロードを行う。
これをinit
で呼び出すことで、前回のデータを読み込んでいる。
実際にオブジェクトが作成されるタイミングは、AppDelegate内のlet dataModel = DataModel()
である。
func registerDefaults() { let dictionary = [ "ChecklistIndex": -1, "FirstTime": true ] as [String : Any] // 異なる型を登録する場合はキャストが必要 UserDefaults.standard.register(defaults: dictionary) // register:設定されていない場合に値を登録する。 set:上書き登録 }
UserDefaults
に、まだ保存されていない場合、作成したdictionary
を登録する。
"ChecklistIndex": -1
:前回最後に選択されたチェックリスト
"FirstTime": true
:初回起動かどうか
func 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
を読み込み、初回起動であれば、見本のリストを作成する。
indexOfSelectedChecklist = 0 // 初回起動時はindex0が開かれるようにする
は後述のComputedプロパティを使用している。
userDefaults.synchronize()
はすぐに書き込みをするため。
// このメソッドで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
AppDelegate
let dataModel = DataModel() // インスタンスはAppDelegateで作成して、これをAllListsViewControllerへ渡す
ここでdataModel
の作成を行い、後にAllListViewController
へ渡す。
func 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
を渡してやる。
// MARK:- Helper Methods func saveData() { dataModel.saveChecklists() } func applicationDidEnterBackground(_ application: UIApplication) { saveData() } func applicationWillTerminate(_ application: UIApplication) { saveData() }
dataModel.saveChecklists()
で情報をUserDefaults
に登録するだけのメソッド。これらをAppDelegate
で、アプリがsuspendや終了するタイミングで呼ぶようにする。
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)
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
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) // Update cell information let checklist = dataModel.lists[indexPath.row] cell.textLabel!.text = checklist.name cell.accessoryType = .detailDisclosureButton return cell
下記の通りセルに表示する内容を設定する。
- reusableCellの作成
- セルに対応したデータを取り出す
- セルに上記のデータの値を設定する
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)
let newRowIndex = dataModel.lists.count dataModel.lists.append(checklist) let indexPath = IndexPath(row: newRowIndex, section: 0) let indexPaths = [indexPath] tableView.insertRows(at: indexPaths, with: .automatic) navigationController?.popViewController(animated: true)
Checklist
が追加された場合に呼び出されるメソッド。
newRowIndex
は新しく要素が追加されるindexで、これはちょうどデータのcountにより計算できる。
deleteのときと同様に、データに追加するのと同時にtableViewにも追加処理をする。
終わったら1つ前の画面に戻る。
listDetailViewController(_ controller: ListDetailViewController, didFinishEditing checklist: Checklist)
if let index = dataModel.lists.index(of: checklist) { let indexPath = IndexPath(row: index, section: 0) if let cell = tableView.cellForRow(at: indexPath) { cell.textLabel!.text = checklist.name } } navigationController?.popViewController(animated: true)
既存のChecklist
が変更された場合に呼び出されるメソッド。
dataModel.lists.index
にChecklist
オブジェクトを渡すことで、そのオブジェクトのindexが取得できる(dateModel
に含まれている場合、同じオブジェクトを指しているかどうかで判断しているのだろう)- indexPathがあればオプショナルバインディングにより、cellのテキストを書き換えてやる。
終わったら1つ前の画面に戻る。
ListDetailViewController
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 {
UITextFieldDelegate
はTextFieldの入力変更を受け取るため。今夏はテキストフィールドが空のときにDoneボタンをDisableにするために使う。
weak var delegate: ListDetailViewControllerDelegate? var checklistToEdit: Checklist?
delegateは強参照を避けるためにweak
である。
checklistToEdit
は、これが呼び出し前にセットされていれば現在編集モードと判断するためにも使う(nilならば追加モード)。
viewDidLoad()
super.viewDidLoad() if let checklist = checklistToEdit { title = "Edit Checklist" textField.text = checklist.name doneBarButton.isEnabled = true }
checklistToEdit
がある場合は編集モード。
- タイトルを変更
- テキストフィールドに編集項目の名前をプリセット
done
ボタンをEnableにする(textFieldDelegateメソッドはこのとき呼ばれないため)
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! delegate?.listDetailViewController(self, didFinishEditing: checklist) } else { let checklist = Checklist(name: textField.text!) delegate?.listDetailViewController(self,didFinishAdding: checklist) }
checklistToEdit
がある場合(編集モードの場合)- オブジェクトにテキストを設定
- delegate先の編集完了時のメソッドを呼ぶ
checklistToEdit
がない場合(新規追加の場合)- 新たに
Checklist
オブジェクトを作成、このときのinit関数にtextFieldの値をセットする - delegate先の追加完了時のメソッドを呼ぶ
- 新たに
tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
return nil
セルの選択を許可しない
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押下時の設定。
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
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
weak var delegate: ItemDetailViewControllerDelegate? var itemToEdit: ChecklistItem?
- delegateプロパティはweakかつオプショナル型
itemToEdit
、これが存在すれば編集モード、無ければ追加モードである
viewDidLoad()
super.viewDidLoad() if let item = itemToEdit { title = "Edit Item" textField.text = item.text doneBarButton.isEnabled = true }
itemToEdit
、これが存在すれば編集モード、無ければ追加モード- 編集モードの場合
- Viewのタイトルを変更
- textFieldに項目名を設定
- doneボタンをenableにする(textFieldDelegateが呼ばれないため)
viewWillAppear(_ animated: Bool)
super.viewWillAppear(animated)
textField.becomeFirstResponder()
- viewの表示時にtextFieldを選択状態にする
@IBAction func cancel()
delegate?.itemDetailViewControllerDidCancel(self)
- キャンセルボタン押下時に呼ばれる
- delegateは設定されていない場合があるのでオプショナル型
@IBAction func done()
- Doneボタン押下時
if let itemToEdit = itemToEdit { itemToEdit.text = textField.text! delegate?.itemDetailViewController(self, didFinishEditing: itemToEdit) }
- 編集モードであれば、渡された
itemToEdit
オブジェクトを編集して、delegate先のメソッドを呼び出す
else { let item = ChecklistItem() item.text = textField.text! delegate?.itemDetailViewController(self, didFinishAdding: item) }
- 追加モードであれば、新規に
ChecklistItem
オブジェクトを作成して値を設定し、delegate先のメソッドを呼び出す。
tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath?
return nil
tableViewの選択を許可しない
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
で再度キーボードを表示しない