ikeh1024のブログ

ZennやQiitaに書くにはちょっと小粒なネタをこちらに書いています

iOS_Apprentice_Section2_Checklistsの解説(Chapter18まで)

概要

iOS Apprenticeの第2章で作成するチェックリストアプリのコード・構成の解説を忘備録として記載。 この本は作っては壊し、を繰り返すので(大幅なリファクタリング)、実用的であろうアプリ作成過程を体験できるので初心者→中級者の橋渡しとしてはいいなと感じる。

GitHub

github.com

以下、

<Code> <解説> の順番で記述していく。

データモデル

DataModel -> Checklist -> ChecklistItemの順に小さくなる

画面構成は下記の通り。1View1Controllerの構成。

-w861

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:リストのタイトル itemsChecklistItem.swiftの配列

init(name: String) {
  self.name = name
  super.init()
}

nameを指定してオブジェクトの作成を行う。

DataModel.swift

var lists = [Checklist]()

listsChecklist.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()
  }
}

UserDefaultsFirstTimeを読み込み、初回起動であれば、見本のリストを作成する。

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の一番始めのviewControllerAllListsViewControllerである。(これはStoryboardで一目瞭然だが、2番目以降はどう取るのだろうか?)

AllListsViewControllerdataModelに先程作成した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)

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

-w511

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の場合は、遷移先のコントローラにperformSeguesenderで受け取った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を自身に設定する。

// 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

下記の通りセルに表示する内容を設定する。

  1. reusableCellの作成
  2. セルに対応したデータを取り出す
  3. セルに上記のデータの値を設定する

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を表示する

-w1011

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.indexChecklistオブジェクトを渡すことで、そのオブジェクトの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先のメソッドを呼ぶ。 delegatenilの可能性があるのでオプショナル。

@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で変更があったときに呼ばれるメソッド。
  • NSRangeRangeに変換する必要がある。
  • また、引数のstringは変更された文字のみが渡されている。なので、これをoldTextに挿入することで、newTextを作成する。
  • doneは空ならばdisable。ifを使わずにこう書けることは1つキーポイント。

textFieldShouldClear(_ textField: UITextField) -> Bool

doneBarButton.isEnabled = false
return true
  • textFieldの「x」ボタンが押されたときに呼ばれるメソッド。
  • doneボタンをdisableにする。
  • return trueでキーボードを再表示しないようになる。

TextFieldのバインド

-w627

  • 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設定

-w781

セルのAccessoryボタンを押下時のSegue設定

-w790

  • Accessory Action >> Showを選択すること、
    • Selection Segue >> Showではない

-w1116

prepare(for segue: UIStoryboardSegue, sender: Any?)

if segue.identifier == "AddItem" {
  let controller = segue.destination as! ItemDetailViewController
  controller.delegate = self
} 
  • segue実行時、identifierによって処理を分岐させる。
  • AddItemのとき、遷移先のItemDetailViewControllerdelegateを設定するのみ。
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のとき、上記と同様に遷移先のItemDetailViewControllerdelegateを設定する。
    • また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内のオブジェクトを取得している。

-w1100

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を設定しておく。(下図)
  • データから該当するデータを抜き出す
  • configureTextconfigureCheckmarkによりデータとセルの状態を同期させる

-w1102

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で返される数
  • insertRowsIndexPathの配列を渡すことが必要
  • 処理が終われば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)

-w618

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で変更があったときに呼ばれるメソッド。
  • NSRangeRangeに変換する必要がある。
  • また、引数のstringは変更された文字のみが渡されている。なので、これをoldTextに挿入することで、newTextを作成する。
  • doneは空ならばdisable。ifを使わずにこう書けることは1つキーポイント。

textFieldShouldClear(_ textField: UITextField) -> Bool

doneBarButton.isEnabled = false
return true
  • textField内の「x」ボタンの押下時
  • doneボタンをdisableにする
  • return trueで再度キーボードを表示しない