本気ではじめるiPhoneアプリ作り〜黒帯エンジニアがしっかり伝える基本テクニック〜(随時更新)

本気ではじめるiPhoneアプリ作り〜黒帯エンジニアがしっかり伝える基本テクニック〜

Chapter 日付
Chapter2 4/11
Chapter4 4-1 4/5
4-2 4/5
4-3 4/10
4-4 4/11
4-5 4/12
Chapter5 5-1 4/12
5-2 4/15

作りたいアプリがあるのでPythonの息抜きに少しずつ勉強する。
実現したいことは、ユーザ登録とかしないで音声を録音して加工して流すだけだし多分そんな難しくない、と思ってる。


あとでちょこちょこ見返すとは思うけど1〜3はすっ飛ばす。
エリアの名前が多すぎて覚えられない問題だけある。Xcodeボタンとか色々多すぎ。
Swift云々もあるけど、Xcodeでどこのボタンをどう使えばいいのかわからなくて躓きそう。



Chapter2

Swift4の基本文法に関してはこちらのサイトを参考にする。
Swift4 の基本文法をマスターしよう

p60〜、Optional型の説明があるがQiitaの記事のまとめがわかりやすかったのでそちらを参考に。
どこよりも分かりやすいSwiftの"?"と"!"


・p62 関数で別名の指定

// 1 関数の定義
func 関数名(引数名: 型) -> 戻り値の型 {
}

// 2 別名を指定
func 関数名(関数の別名 引数名: 型) -> 戻り値の型 {
}

// 3 引数名の省略
func 関数名(_ 引数名: 型) -> 戻り値の型 {
}

Chapter4

4-1 アプリの使用と部品の配置

・Storyboardを選択し、FileInspectorのUseAutoLayoutのチェックを外すとその他3つのチェックも外れるが、これでiPhoneに最適化された状態になる。現段階ではなんのことだかさっぱり。後にAutoLayoutを学ぶために今はオフる。
・サイズが小さいディスプレイを基準に部品レイアウトを行うこと。
 →画面中央下の「View as: iPhone 8(vC hR)」を押し、SEの大きさに変更。
・Label:UILabelの「Autoshrink」プロパティの値を変更することでLabelのサイズに収まらないテキストを設定した場合に、テキストを自動的に縮小する。OSのメジャーバージョンの更新でフォントが変わって文字がはみ出る場合もあるので設定しておくと安心。
・Size Inspector > View
  > FrameRectangle = ビューの周囲を装飾する影などを含んだサイズ
  > Alignment Rectangle = 装飾を含まないサイズ
  ※ただし、iOS22用の標準パーツに影はないのでどちらでも影響は無い。macOSの部品などで使う。
・ObjectLibrary > TextField
  表示のみに使いiOSの標準入力をさせたくない場合、Attributes Inspector > Control > State > Enabledのチェックを外す。
  標準パーツでユーザの操作ができないものは背景色がグレーor半透明になる。
  デフォルトで表示させたいものはAttributes Inspector > Textに設定。


4-2 処理を実装する

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var priceField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

・エディターエリアにStoryboardとコードを並べたい場合、Storyboardを表示した状態でoptionを押しながら表示したいファイルをクリックする。
 右上にあるAssistant Editorももちろん使える(◯が2つ重なってるアイコン)。
viewDidLoad()
 ViewControllerで制御するビューの準備が整った後に呼び出される。追加する場合ここに書く。
didReceiveMemoryWarning()
 iOS本体のメモリ容量が圧迫されるたときに呼び出される。iOSはシステム安定化のためにメモリを使いすぎているアプリに矯正集の指示を出すことがあるが、ここにはアプリ内で利用していない情報を破棄する処理を記述する。
・override
 ViewControllerクラスの親であるUIViewControllerのメソッドをViewControllerで変更する。
super.viewDidLoad()
 ViewControllerのviewDidLoadメソッドではなく、親(UIViewController)のviewDidLoadを呼び出す。これによって既存の処理を行った後に追加で独自の処理を行うことが可能になる。
・Storyboardからコードへドラックした時の◎は関連付けされている、という印。
@IBOutlet
 Storyboard上で配置された部品で、コードから扱えるプロパティにつく。「@」は予約語、IBはインターフェイスビルダ(Storyboard登場前に使用されていた画面レイアウトのツール)の意味。
Automatic Reference Counting(ARC)
 iOSのメモリ管理の仕組み。
 オブジェクトがオーナーシップが持っているかによって、メモリを破棄するかどうかを決める。
 参照項目
  strong:オブジェクトを生成したコードを持つオブジェクト
  weak:オブジェクトの参照のみの場合。strong以外はできるだけこっちで。

 strong、weak、unonwedについてはまだいまいちよく分かっていない…。
・Connectionの解除
 Connectionの種類を間違えたら、まずコードを削除して、「ユーティリティエリア > Connections Inspector > Referencing Outlets」の関連付けを「✗」ボタンをクリックして解除する。

    @IBAction func tap1Button(_ sender: Any) {
        let value = priceField.text! + "1" // 1
        if let price = Int(value){ // 2
            priceField.text = "\(price)"
        }
    }

・1
アンラップ処理を行い、nilでないことを宣言した上でtextを利用可能にしている。
・2
引数に設定する文字列が数値でない場合nilが返されてしまう。
そこで数値にした後でnilでないことを確認するためにOptional Binndingを使用する。
どこよりも分かりやすいSwiftの"?"と"!" - Qiita

4-3 割引パーセンテージ入力画面のレイアウトと実装

・新しい画面を追加したら「File > New > File... > Cocoa Touch Class」で、画面に対応するテンプレートファイルを追加する。
Cocoa Touch Class
 iOSアプリのUI部品などを操作するための環境のことをCocoa Touchといい、その環境を利用したテンプレート。
 画面を追加する時にはこのテンプレートを利用する。
・ファイルを作るときのテンプレートの種類
 p175参照
・ファイルを追加したらStoryboardの画面を選択し、「ユーティリティエリア > Identity Inspector > Custom Class > Class」にファイルと同じクラス名を入力し紐付けを行う。


4-4 計算結果画面のレイアウトと実装

計算方法に関してはそのまんまなので飛ばす。
わかってないのは次の受け渡し。



4-5 画面遷移の関連付けとパラメータの受け渡し

基本的な画面遷移の方法

 画面:金額を入力         アクション:「割引%を入力する」ボタンを押下
→画面:割引パーセンテージ入力画面

 Controlを押しながら「割引%を入力する」ボタンを「割引パーセンテージ入力画面」にドラック&ドロップ
 これで画面同士の関連付けが出来た。
 2つの画面を結んでいる矢印のSegueをクリックし「Modal」を選択。
 このAction Segueは後で変更可能
 レイアウトを自動設定してくれるTrait Variationsを使用しているときは使用するSegueの種類が異なる。

Actuon Segueの種類
項目名 内容 デフォルトのアニメーション
Push NavigationControllerで画面遷移を管理しながら画面を切り替える 画面が右に進む
Modal 遷移先の画面での作業に集中するために利用される 下から上に向かって表示される
Custom 独自の遷移方法を指定する -
3つ目(最後)の画面から1つ目の画面に戻る場合

 上とは違い、まず1つ目の画面のViewに最後の画面から戻ってきたときの処理(この場合金額を0にしておく)を定義しておく。
 次に3つ目の画面の遷移のためのボタンをStoryboard上部のExitボタン(一番右のオレンジ)にControlキーを押しながらドラッグ&ドロップし、「restart:」を選択。

 

パラメータの受け渡し

①遷移先の入力画面にパラメータを渡す
 金額入力画面の「割引%を入力する」ボタン押下→iOSはprepareメソッドを呼び出す

・prepareメソッドのパラメータ

項目名 内容
segue Storyboard上で画面と画面を繋いだときのSegue情報を持っている変数。遷移先、遷移元情報など、画面遷移に関する情報を持っている。別名としてforが設定されている。
sender 画面遷移が発生する要因になったオブジェクト。画面遷移が発生する要因はStoryboard上で開発者が自由に設定できる。ボタン、セルのタップであるので、この変数は特定の型を指定しないAny型。

destinationプロパティ
UIViewControllerを返す。ここで取得できるUIViewControllerの実態はPercentViewControllerだが、それをPercentViewController型に変換するためにas!でダウンキャストを行う。
ダウンキャスト
あるクラスをサブクラスにキャスト(型変換)すること。
今回はUIViewControllerの変数をUIViewControllerのサブクラスであるPercentViewControllerクラスにダウンキャストしている。

②金額表示フィールドの文字列を整数型に
「!」をつけてnilでないことを明示している。

③金額を次の画面に設定
これで受け渡し完了

// 1つ目の画面:ViewController.swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    // ①遷移先の画面を取り出す
    let viewController = segue.destination as! PercentViewController

    // ②金額フィールドの文字列を数値に変換する
    if let price = Int(priceField.text!) {
        // ③数値に変換した金額を次の画面に設定する
        viewController.price = price
    }
}
// 2つ目の画面:PercentViewController.swift
// 渡すパラメータが、priceとpercentの2つになっている
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    // 次の画面を取り出す
    let viewController = segue.destination as! ResultViewController

    // 次の画面に現在保持している金額を設定する
    viewController.price = price
    if let percent = Int(percentField.text!) { // 文字列を数値に変換する
        // 次の画面に現在保持しているパーセンテージを設定する
        viewController.percent = percent
    }
}

4-6 Auto Layoutを使う

図が多いので割愛。





Chapter5

5-1 データ保存と永続性

・p211
iOSバイスのデータ保存場所が書かれてある。必要なときに読み返す。
バックアップされるデータはディレクトリによって異なる。

UserDefaults

iOSアプリのデータ永続性の仕組み。
データ形式:key=value形式(大量のデータ保存には向いていない)
・特徴   :保存可能でコード量が少なくて処理が高速
・      保存したデータはアプリ内部にplist(プロパティリスト形式)で保存される。実態はXML
・毎回plist形式のファイルにアクセスするのではなくメモリのキャッシュを使用する。
・メモリにはアプリの起動時に事前読み込みをする。
・アプリが終了するとキャッシュはクリアされる
 →アプリ終了前にplist形式のファイルにキャッシュ内容を反映させる処理が必要
  これはUserDefaultsのデフォルト動作で実現される。
  また、反映後、キャッシュとplistファイルとの値の同期をとる動作をとる。
  同期をwh取る前にアプリがクラッシュするとデータが保存されない場合がある。
・これを防ぐために、キャッシュの値を変更後、明示的にファイルに書き込む処理を加えることが出来るが、これには時間がかかるので、指示出しのタイミングや回数に注意すること。
・保存できる値、ファイルの量に上限は無い。
・キャッシュ動作があるため大きすぎる値の保存には不向き。
・plistファイルからデータをメモリに読み込むため、plistファイルのサイズが大きいとメモリへの値のコピーに時間がかかりアプリの起動が遅くなる。
・大きいデータはデータ永続性の仕組みを利用すること!

→ p236:plist形式の中身

Core Data

・データをレコード形式で保存するデータ永続性の仕組み。
データ形式:レコード形式(テーブルという表を用いる)
・特徴   :UserDefaultsのように起動時に全てのデータを参照しない。
       高機能であるため、利用方法が複雑。

iCloud

・アプリが保存するデータをサーバに配置する。
・ユーザの目に触れないデータも保存出来る。
データ形式:key=value形式

Keychain

iOS内部のセキュアな場所に保存される。
データ形式:key=value形式
・通常、保存データを他のアプリから参照出来ない(アクセス設定によっては可能)。
・汎用性が高いがkei=valueなので大量のデータ保存に不向き。
・アプリを削除してもiOSにデータが残り続けるので、再インストールしても以前のデータが使用可能。

データ形式 学習コスト 特徴
UserDefaults key=value 少量のデータ保存に向いている
機密性の高いものは向いていない
Core Data RDB 大量保存に向いている
iCloud key=value ドキュメント単位の保存向き
異なるデバイスでデータを共有可能
Keychain key=value 機密性の高いデータ保存向き
同一開発者が提供しているアプリ間でのデータ共有が可能
アプリを削除しても、再インストール後にアクセス可能

Core Dataを利用するときはOSSであるRealmや、MagicalRecordを利用することも検討。



5-2 シンプルな値の保存処理

電卓アプリは値が1種類で、テキストデータで、サイズも小さいのでUserDefaultsを利用する。

値の紐付け

テキストフィールド、ボタンをCtrlでドラック&ドロップして紐付ける。
テキストは、ConnectionがOutlet。
ボタンは、ConnectionがAction。


値保持の流れ

やりたいこと:
ボタンをタップしたときにテキストフィールドに入力した値をUserDefaultsに保存し、アプリ起動時にその値をUserDefaultsから読み出したい。
処理はアプリ起動時に保存されている値を表示するもの(1〜5)と、
ボタン押下時に値を保存する処理(6〜8)の2つに分けられる。

1. デフォルトの画面表示処理
viewDidLoadで画面の表示に関する初期設定を行う。
オーバーライドしているメソッドなので、オーバーライド前の本来の処理を実行するために、「super.」をメソッドの前につけて独自の処理を作成する前に本来の処理を実行する!

2. UserDefaultsインスタンスの参照
UserDefaultsクラスを用いて値を操作するには、UserDefaultsクラスのインスタンス参照にstandardというプロパティを呼び出す。これをuserDefaultsという変数に格納して参照出来るようにする。

3. UserDefaultsクラスからtextキーに対応する値を取り出す
userDefaults変数からtextのキーに対応しているvalueを取り出す。保存されているString型の値を取り出すにはstringメソッドを使用する。
UserDefaultsで保存、参照するメソッドは型ごとに異なる。

4. 取り出した結果は存在しているのか
取り出した結果が存在しているのか検証する。
stringメソッドでは値が存在する場合はString型の変数が返され、そうでない場合はnilが返ってくるのでif letを使用する。

5. 値をTextFieldに反映
textFieldnoプロパティのtextはString型なのでそのままvalueを反映する。

・p60 if let文
Optional型でラップされた変数を利用する際にはnilでは無いことを明示する必要がある。
nilで無いことを保証された状態にすることをアンラップするという。
ラップされた変数をletで宣言した変数に代入し、その変数の真偽をifで判定する。

6. UserDefaultsインスタンスの参照
画面表示のときと同じで、userDefaultsという変数名でインスタンスを参照

7. UserDefaultsにtextFieldの文字列を保存
textFieldのtextプロパティの値をtextキーに対応する値としてUserDefaultsに保存。
userDefaults変数のset:forKeyメソッッド:キーに対応する値の保存

8. UserDefaultsの値の同期指示
UserDefaultsに設定した値はメモリに反映される。
この値を永続性のあるファイルに同期するための処理は、システムがタイミング見て行う。
その同期処理が開始される前にアプリがクラッシュした場合値が永続化されないので、ここで明示的に同期処理を行う。
UserDefaults.synchronizeメソッド:明示的な同期処理

class ViewController: UIViewController {
    // ================================
    // アプリ起動時に保存されている値を表示する
    // ================================
    @IBOutlet weak var textField: UITextField!
    override func viewDidLoad() {
        super.viewDidLoad()  // 1. デフォルトの画面表示処理

        // 2. UserDefaultsの参照
        let userDefaults = UserDefaults.standard

        // 3. 4. textというキーを指定して保存していた値を取り出す
        if let value = userDefaults.string(forKey: "text") {
            // 5. 取り出した値をテキストフィールドに設定
            textField.text = value
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // ================================
    // ボタン押下時に値を保存する
    // ================================
    @IBAction func tapActionButton(_ sender: Any) {
        // 6. UserDefaultsの参照
        let userDefaults = UserDefaults.standard
        // 7. textというキーで値を保存する
        userDefaults.set(textField.text, forKey: "text")
        // 8. UserDefaultsへの値の保存を明示的に行う
        userDefaults.synchronize()
    }
}

5-3 独自クラスの値を保存できる処理

p224:UserDefaultsで保存出来るデータ型の種類と、対応メソッド一覧表

UserDefaultsで保存出来るデータ型に該当しない型はどうやって保存するのか?

UserDefaultsにオブジェクトを保存する場合、そのオブジェクトにはisNSObjectメソッドが実装されてる必要がある

シリアライズ

UserDefaults型はData型の保存をサポートしている
→独自クラスをData型にすれば保存可能!この変換がシリアライズ

シリアライズ
オブジェクトの内容をバイナリに変換すること。
シリアライズすることでオブジェクトはシリアライズされたデータを扱うことが出来るData型に変換される。
↔デシリアライズ

使用するクラス
クラス 処理をするメソッド
シリアライズ NSKeyedArchiverクラス encodingWithCode
シリアライズ NSKeyedUnarchiverクラス initWithCoder

独自クラスの中にNSKeyArchiverクラスのシリアライズ処理ではNSCodingプロトコルのメソッドである「encodingWithCode」が呼び出される。独自クラスのここでシリアライズ処理が行われる。
つまり、独自クラス内で2つのメソッドを定義し、シリアライズ、デシリアライズのときは定義したこのメソッドが呼び出される。



p230 いざ実装

1. NSCodingを適用
まずはNSCodingプロトコルを実装する。適用するためには「クラス名: NSCoding」。
しかしプロトコルを適用してもエラーが出る。
プロトコルの定義を見るために「⌘ + Control」を押したままNSCodingにカーソルをあわせる。それが青色になるのでクリック。

public protocol NSCoding {
    public func encode(with aCoder: NSCoder)
    publiv init?(coder aDecoder: NSCoder) // Objectibe-CのinitWithCoder()に該当
}

つまり、encode、initメソッドという2つのメソッドが定義されなくてはならない。
NSCoder:NSObjectクラスを継承したクラス。シリアライズ、デシリアライズに関するメソッド郡が定義されている。NSObjectというクラスはObjective-CのものだがSwiftでも利用可能。
その為、クラス宣言する際には、NSObjectも適用する必要がある

2. encodeとinitを定義
NSCodingプロトコルのメソッドを定義する。それぞれのメソッド内でシリアライズ、デシリアライズ処理で呼ばれるエンコード、デコード処理を実装する。エンコード、デコード処理のメソッドは型ごとに異なる。
文字列のエンコードにはencodeメソッド、デコードにはdecodeメソッドを使用する。

// 変数vallueStringを、キーである"valueString"を指定してエンコード
aCoder.encode(valueString, forKey: "valueString") 

// キーであるvalueStringをデコードしてString型に
valueString = aDecoder.decodeObject(forKey: "valueString") as? String

initメソッドはイニシャライザと呼ばれる。これは、クラスを生成するときの処理。引数なしのinitはデフォルトイニシャライザとしてクラス内での実装処理を省略可能だが、引数月のイニシャライザを実装した場合、引数なしのinitは存在しないものとみなされる。
引数なしのinitが存在しない場合は、クラスのインスタンスを引数なしで生成できなくなる。
let data = MyData()
みたいに。
引数なしのinitメソッドを明示的に宣言する場合には、引数なしのinitメソッドをオーバーライドする。
つまり、、、処理の呼び出し方は

// シリアライズ処理
let シリアライズ結果 = NSKeyedArchiver.archiverdData(withRootObject: 対象データ)

// デシリアライズ処理
let デシリアライズ結果 = NSKeyedUnarchiver.unarchiverdData(with: 対象データ)
// MyData.swift
import Foundation

// 1.NSCodingをクラスに適用させる
class MyData: NSObject, NSCoding {
    var valueString: String?

    override init() {
        
    }

    required init?(coder aDecoder: NSCoder) {
        valueString = aDecoder.decodeObject(forKey: "valueString") as? String
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(valueString, forKey: "valueString")
    }

}


valueStringに文字列"test"を設定し、UserDefaultsに保存
→文字列"test"を再度読み出して表示
という処理を行うとする。

1. UserDefaultsの生成
2. 独自クラスのインスタンスを生成し、valueStringに文字列設定
3. シリアライズ処理
 NSKeyedArchiverクラスのarchivedDataメソッドにシリアライズ対象のデータを渡しシリアライズ
 データはData型の変数として変数に設定される。
4. UserDefaultsにシリアライズしたデータを保存
 Data型の変数archiveDataを、キー"data"に対応する値としてUserDefaultsに保存し、明示的に同期させる。
5. UserDefaultsからシリアライズされたデータを取得
 キー"data"で保存してある値を取り出し、Data型に変換しstoredDataに格納
6. シリアライズされたデータをデシリアライズして独自クラスの型に復元
7. 独自クラスのインスタンスのvalueStringの文字列を出力
 valueStringプロパティはOptional型なのでアンラップをして出力

// ViewController.swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // 画面表示時にデータを格納
        let userDefaults = UserDefaults.standard // 1
        let data = MyData()                      // 2
        data.valueString = "test"                // 2

        // シリアライズ処理
        let archiveData = NSKeyedArchiver.archivedData(withRootObject: data) // 3
        userDefaults.set(archiveData, forKey: "data")                        // 4
        userDefaults.synchronize()                                           // 4

        // デシリアライズ処理
        if let storedData = userDefaults.object(forKey: "data") as? Data {   // 5
            if let unarchivedData = NSKeyedUnarchiver.unarchiveObject(with: storedData) as? MyData {  // 6
                if let valueString = unarchivedData.valueString {            // 7
                    print("デシリアライズデータ:" + valueString)
                }
            }
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

5-4 ToDoリストアプリの基本動作を作る

・p241 「+」ボタン押下時に画面を遷移させる
 XcodeメニューのEditor > Embed in > Navigation Controller
・Cellに対して「Attribute Inspector > Identifier」を設定することでプログラム内からも利用可能にする。
 Idedntifierの指定はドキュメントアウトラインにも反映される。
・Table View Cellの「Attribute Inspector > Accessory」
 TableViewCellには様々なスタイルが有りテンプレートがある。「Style」にはラベルや詳細を表示する位置などの選択肢がある。
 「Accessory」はセルの状態表示や補足情報への導線提供、タップしたときに詳細画面に遷移するかどうかを視覚的に伝えるアイコンを表示する。
・Bar Button Itemを「Attribute Inspector > System Item > Add」にすることで「+」ボタンに変更される。



p243 UI部品とコードの関連付け

ここでは、UITableViewクラスのプロトコルの以下2つを学ぶ
 ・UITableViewDataSource:テーブルに表示するデータの内容を定義
 ・UITableViewDelegate  :テーブルに対する振る舞いを定義



・今回はUIViewControllerにUITableViewDelegateプロトコル・UITableViewDataSourceプロトコルを実装する。
・この2つの処理を行うクラスであるViewControllerをUIViewControllerの継承先クラスに指定する。
 1. ドキュメントアウトラインの「View Controller Scene」のツリーで「View Controller Scene > View Controller > View > Table View」を選択肢Controlを押しながら上位のView Controllerにドラック&ドロップ。
 2. OutletはTableViewと、ViewControllerの関係について以下2つを指定する。
  dataSource:UITableViewDataSourceの実装先の指定
  delegate  :UITableViewDelegateの指定
  どちらViewControllerに対して指定することでViewControllerがUITableViewDataSourceと、Delegateの処理の実装先として定義されたことになる。
  今回は両方共関連付けるために同じ処理を2回繰り返し、dataSource、delegateのどちらも指定する。
  TableViewを選択したまま「ユーティリティエリア > Connections Inspector > Outlets」をクリックして関連付けを確認可能。

実装

・UITableViewDataSource、UITableViewDelegateプロトコルを利用する
・「+」ボタン押下時にToDoを入力するためにアラートダイアログを表示する

1. プロトコルを利用する宣言
2. ToDoプロパティの宣言
 「+」ボタンが押されたときに表示されるアラートダイアログに入力されたテキストはToDoとしてこの配列に追加される。
3. UITableViewのoutletを定義
 行を追加した時の通知などでUITableViewに対して操作を行うために定義。
4. アラートダイアログ生成
 ToDoテキストを入力するアラートダイアログを表示
 ダイアログにOK、CANCELボタンを追加
5. ToDoの配列の先頭に入力値を挿入
 OKボタンをタップした際に配列の先頭にテキストが挿入されることによって、新規追加されたToDoが常にテーブルの一番上に表示されるようになる。
6. 行が追加されたことをテーブルに通知
 todoListプロパティに値を挿入したら、UITableViewリストに対してテーブル項目が挿入されたことを通知する。
 これによってUITableViewの再描画処理が行われ、実際の画面に新しい項目が追加されることになる。
 UITableViewに通知を行うときは、sectionとrowIndexを指定する必要がある。
 UITableViewのStyle(Plain or Grouped)によって見た目が異なる。

 tabAddButton関数は「+」をタップした時に呼び出される。
 UITableViewDelegate、DataSourceを利用すると、プロトコルに準拠したメソッドを実装するまでエラーが表示される。

// 1
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    // 2 ToDoを格納した配列
    var todoList = [MyTodo]()

    // 3
    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // 保存しているToDoの読み込み処理
        let userDefaults = UserDefaults.standard
        if let storedTodoList = userDefaults.object(forKey: "todoList") as? Data {
            if let unarchiveTodlList = NSKeyedUnarchiver.unarchiveObject(with: storedTodoList) as? [MyTodo] {
                todoList.append(contentsOf: unarchiveTodlList)
            }
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    // +ボタンをタップしたときに呼ばれる処理
    @IBAction func tapAddButton(_ sender: Any) {
        // 4 アラートダイアログを生成
        let alertController = UIAlertController(title: "TODO追加", message: "TODOを入力してください", preferredStyle: UIAlertControllerStyle.alert)
        // 4 テキストエリアを追加
        alertController.addTextField(configurationHandler: nil)

        // OKボタンがタップされた時の処理
        let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.default) { (action: UIAlertAction) in
            // OKボタンがタップされた時の処理
            if let textField = alertController.textFields?.first {
                // 5. ToDoの配列に入力値を挿入。先頭に挿入する
                let myTodo = MyTodo()
                myTodo.todoTitle = textField.text!
                self.todoList.insert(myTodo, at: 0)

                // テーブルに行が追加されたことをテーブルに通知
                self.tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: UITableViewRowAnimation.right)

                // ToDoの保存処理
                let userDefaults = UserDefaults.standard
                // Data型にシリアライズする
                let data = NSKeyedArchiver.archivedData(withRootObject: self.todoList)
                userDefaults.set(data, forKey: "todoList")
                userDefaults.synchronize()
            }
        }

        // 4 OKボタンを追加
        alertController.addAction(okAction)

        // CANCELボタンがタップされた時の処理
        let cancelButton = UIAlertAction(title: "CANCEL", style: UIAlertActionStyle.cancel, handler: nil)
        // 4 CANCELボタンを追加
        alertController.addAction(cancelButton)

        // アラートダイアログを表示
        present(alertController, animated: true, completion: nil)
    }

    // テーブルの行数を返却する
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // ToDoの配列の長さを返却する
        return todoList.count
    }

    // テーブルの行ごとのセルを返却する
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // Storyboardで指定したtodoCell識別子を利用して再利用可能なセルを取得する
        let cell = tableView.dequeueReusableCell(withIdentifier: "todoCell", for: indexPath)

        // 行番号に合ったToDoの情報を取得
        let myTodo = todoList[indexPath.row]
        // セルのラベルにToDoのタイトルをセット
        cell.textLabel?.text = myTodo.todoTitle
        // セルのチェックマーク状態をセット
        if myTodo.todoDone {
            // チェックあり
            cell.accessoryType = UITableViewCellAccessoryType.checkmark
        } else {
            // チェックなし
            cell.accessoryType = UITableViewCellAccessoryType.none
        }
        return cell
    }

    // セルをタップした時の処理
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let myTodo = todoList[indexPath.row]
        if myTodo.todoDone {
            // 完了済みの場合は未完了に変更
            myTodo.todoDone = false
        } else {
            // 未完の場合は完了済みに変更
            myTodo.todoDone = true
        }

        // セルの状態を変更
        tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.fade)
        // データ保存。Data型にシリアライズする
        let data: Data = NSKeyedArchiver.archivedData(withRootObject: todoList)
        // UserDefautlsに保存
        let userDefaults = UserDefaults.standard
        userDefaults.set(data, forKey: "todoList")
        userDefaults.synchronize()
    }

    // セルが編集可能であるかどうかを返却する
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    // セルを削除した時の処理
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        // 削除処理かどうか
        if editingStyle == UITableViewCellEditingStyle.delete {
            // ToDoリストから削除
            todoList.remove(at: indexPath.row)
            // セルを削除
            tableView.deleteRows(at: [indexPath], with: UITableViewRowAnimation.fade)
            // データ保存。Data型にシリアライズする
            let data: Data = NSKeyedArchiver.archivedData(withRootObject: todoList)
            // UserDefautlsに保存
            let userDefaults = UserDefaults.standard
            userDefaults.set(data, forKey: "todoList")
            userDefaults.synchronize()
        }
    }
}

// 独自クラスをシリアライズする際には、NSObjectを継承し
// NSCodingプロトコルに準拠する必要がある
class MyTodo: NSObject, NSCoding {
    // ToDoのタイトル
    var todoTitle: String?
    // ToDoを完了したかどうかを表すフラグ
    var todoDone: Bool = false
    // コンストラクタ
    override init() {

    }

    // NSCodingプロトコルに宣言されているデシリアライズ処理。デコード処理とも呼ばれる
    required init?(coder aDecoder: NSCoder) {
        todoTitle = aDecoder.decodeObject(forKey: "todoTitle") as? String
        todoDone = aDecoder.decodeBool(forKey: "todoDone")
    }

    // NSCodingプロトコルに宣言されているシリアライズ処理。エンコード処理とも呼ばれる
    func encode(with aCoder: NSCoder) {
        aCoder.encode(todoTitle, forKey: "todoTitle")
        aCoder.encode(todoDone, forKey: "todoDone")
    }
}