【Swift 初心者向け】自分が理解できなかったのでメモアプリを1からコード解説してみた

初めまして。
Swift勉強し始めて3ヶ月程度の初心者です。
自分が勉強していく中でわかりにくかった部分を噛み砕いて共有できたらなと...
(自分の備忘録です笑)

今回は[Swiftで作れる簡単メモ帳アプリ](https://qiita.com/SHOBLOG/items/b5107eb1da0fdebae8d3)の記事を参考にしながら、やっていきます。

あと、自分で作ったこのメモアプリのコードをgithubにあげてるのでよかったらみてみてください。zipをダウンロードして貰えば起動するはず（多分笑）　→ [SimpleMemoApp](https://github.com/miyu0001/SimpleMemoApp)

Storybordやファイル作成についてはたくさん記事があるので他の記事を参考にしてみてください。
ということで、コードについて詳しく説明したいと思います。

#メインのメモ一覧画面

<img width=30% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1381539/729e16e3-5127-3a19-634f-d88ca5298284.jpeg">
写真だとこの画面です。メインのViewControllerですね。

##とりあえず実際のコード

```swift
import UIKit

class ViewController: UIViewController ,UITableViewDelegate,UITableViewDataSource{
  
    @IBOutlet weak var memoTableView: UITableView!
    
    //String型の空の配列を作成
    var memoArray = [String]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        memoTableView.delegate = self
        memoTableView.dataSource = self
        
        
    }
    
    override func viewWillAppear(_ animated: Bool) {
        loadMemo()
    }

    //セルに何個表示させるか
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return memoArray.count
    }
    
    //セルに表示させる内容
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        //tavleViewCellの登録
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "MemoCell") else {
            //例外処理
            return UITableViewCell()
        }
        //cellのtextにmemoArrayを順番に入れる
        cell.textLabel?.text = memoArray[indexPath.row]
        //最後にcellを返してあげるよ！
        return cell
    }
    
    //セルを押した時に呼ばれる関数
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        //identifierを使って画面遷移
        self.performSegue(withIdentifier: "toDetail", sender: nil)
        //おされたら押した状態を解除
        tableView.deselectRow(at: indexPath, animated: true)
    }

    //userdefaultsからデータを呼び出す
    func loadMemo(){
        let ud = UserDefaults.standard
        //メモが入っていたら
        if ud.array(forKey: "memo") != nil {
            memoArray = ud.array(forKey: "memo") as! [String]
            //データを呼び出した後にtableViewをリロードしてくれる
            memoTableView.reloadData()
        }
    }

    //次の画面にデータを渡す
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "toDetail" {
            //次の画面をUIViewController型からDetailviewController型にダウンキャスト（型変換）して取得する
            let detailViewController = segue.destination as! DetailViewController
            //遷移前にtableViewで今選ばれているセルを取得する,次の画面にselectedIndexpathのrow番目を渡してあげる
            let selectedIndexPath = memoTableView.indexPathForSelectedRow!
            //次の画面(DetailViewController)のselectedMemoにmemoArrayのselectedIndexPath.row番目の値を入れてあげる→値を渡せてる
            detailViewController.selectedMemo = memoArray[selectedIndexPath.row]
            //何番目のセルが押されたのかという情報も一緒に値を渡す
            detailViewController.selectedRow = selectedIndexPath.row
        }
    }   
}
```
かなりコメントアウトしていますが早速コードを解説していきます。上から見ていきましょう。

##コード解説

###記入したメモを入れておくための配列を作成
まずはアプリ内でメモに何か書いたときに入れておくための空の配列を作って置いてあげます

```swift
var memoArray = [String]()
```

###TableViewを使うための宣言的な
これはTableViewの使い方の話になるのでここがわからない人は[この記事](https://qiita.com/pe-ta/items/cafa8e20029047993025)を見てみてください

```swift
memoTableView.delegate = self
memoTableView.dataSource = self
```


###loadMemoという関数の呼び出し
これはviewDidLoadに書いてもいいですがviewWillAppearに書くことで毎回この画面になるたびにloadMemo()という関数が呼び出されるようにしています。ここのviewDidLoadとviewWillAppearの違いがわからない人は[この記事](https://qiita.com/entaku0818/items/87d4e7225e23db0b01ff)見てみてください。
このloadMemo()に関しては後で解説しますのでとりあえずこの関数を呼び出してるんだなと思ってください。

```swift
override func viewWillAppear(_ animated: Bool) {
　　　　loadMemo()
}
```

###セルに表示させる個数
これはコメントアウトの通りTableViewのセルに何個表示させるかです。
今はさっき作ったmemoArrayという配列に入ってるメモの数だけ表示させて欲しいのでmemoArray.countとなります。もし10個だけ表示させたいのであれば　return 10　とすればいいです。

```swift
//セルに何個表示させるか
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return memoArray.count
}
```
###セルに表示させたい内容
TableViewのcellForRowAtというのは表示させたいセルの内容を記入するところです。
これは最後にreturnで値を返してあげるルールになってるのでreturnがないとエラーになります。

####セルの登録
まずはセルの登録をしていきます。コンピューターはどのセルが使われているのかなんてわからないのでIdentifierで名前をつけてそれをコードでも登録してあげます。IdentifierはStoryboardの方で最初に設定するので、ここがわからんって人は[参考記事](https://qiita.com/SHOBLOG/items/b5107eb1da0fdebae8d3)のセルのIdentifierに名前をつけてるところを見てみてください。

```swift
//tavleViewCellの登録
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MemoCell") else {
　　　　//例外処理
　　　　return UITableViewCell()
}
```

では、なぜここでguardletを使ってるのでしょうか。
正直自分はここら辺が最初全くわかりませんでした。（今もわかってるのかは微妙）
これは、スコープのメソッドの戻り値がUITableViewCell（非オプショナル型）なのでunwrapしてあげないといけないみたいです。
とりあえずここではnilの判定が必要みたいです。guard letで書かずに！で強制アンラップしても平気ですが！はあまり良くないみたいなのでこっちで書いてみました。

####メモの内容をセルに表示
```swift
//cellのtextにmemoArrayを順番に入れる
cell.textLabel?.text = memoArray[indexPath.row]
```
セルの中には元々Labelが入ってるのでそのラベルのテキストにmemoArrayに入ってるものを代入することで表示させてあげます。
indexPath.rowというのは表示させるメモがそれぞれ何行目かにアクセスして順番に表示させています。
最後はcell return で値を返してあげます。


###セル（メモ）を押した時
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){}というのはセルを選択した時に呼び出される関数なので、{}内にセルを選択したら次にどうして欲しいのかを書いていきます。

####詳細画面に画面遷移

```swift
//identifierを使って画面遷移
self.performSegue(withIdentifier: "toDetail", sender: nil)
```
選択した時に詳細画面に遷移するようにコードで記入してあげます。遷移するコードはperformSegueでIdentifierは遷移するときのSegueにつけた名前ですね。

####セルが押された状態の解除
```swift
tableView.deselectRow(at: indexPath, animated: true)
```
これはそのままですが書いた時と書いてない時で試してみたらわかると思います。書かない場合、セルがずっと選択状態になってしまいます。

###loadMemo()
やっと最初に話したloadMemo()のところに来ました。これは自分で作った関数なので名前はなんでもいいです。
今回はuserdefaultsからデータを呼び出すのでloadMemoって感じです。

####userdefaultsを用意

```swift
let ud = UserDefaults.standard
```
userdefaultsを用意してあげるコード。これは毎回用意してあげるからよく出てくる。

####メモがuserdefaultsに入ってる時だけ考える
メモが入っていない時はデータから取り出す必要がないので、メモが入ってる時だけ考えます。

####memoArrayに値を入れる

```swift
memoArray = ud.array(forKey: "memo") as! [String]
memoTableView.reloadData()
```

userdefaultsから値を取り出してString型に変換したものをmemoArrayに代入します。
この値が入った状態でTableViewをもう一回リロードすればok!

###詳細画面にデータを渡す
データを渡すときのコードはprepare(for segue ...)です。
どこにデータを渡すかはっきりしとくためにif 文で詳細画面に行くように設定しておきます。

####次の画面を取得
元々はUIViewController型なので、これを次の画面のDetailViewController型に直して代入してあげます。

```swift
let detailViewController = segue.destination as! DetailViewController
```


####何番目の情報か
メモの削除機能があるので、選択しているセルが何番目なのかという情報も一緒に渡してあげます。それを、detailViewControllerの中で定義したselectedRowに代入してあげる。

```swift
let selectedIndexPath = memoTableView.indexPathForSelectedRow!
detailViewController.selectedRow = selectedIndexPath.row
```


#メモ追加画面

<img width=30% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1381539/773d9d5d-76a5-e449-e43b-fc843d28c23e.jpeg">

##とりあえず実際のコード

```swift
import UIKit

class AddMemoViewController: UIViewController {

    @IBOutlet weak var memoTextView: UITextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        //他のところをタッチしたらキーボードが閉じる
        let tapGR: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        tapGR.cancelsTouchesInView = false
        self.view.addGestureRecognizer(tapGR)
    }

    @IBAction func save(_ sender: Any) {
        //memoInputに入力したテキストを代入
        var memoInput = memoTextView.text
        //userdefaultsを用意
        let ud = UserDefaults.standard
        
        //前にメモを入力したことがあれば元々入ってるuserdefaultsの値を取り出す
        if ud.array(forKey: "memo") != nil {
            //SavedMemoArrayにAny型からString型としてud.arrayから値を取り出して代入
            var savedMemoArray = ud.array(forKey: "memo") as! [String]
            //memoInputにメモが入ってるかどうか
            if memoInput != nil {
                savedMemoArray.append(memoInput!)
            } else {
                print("何も入力されていません")
            }
            //配列savedMemoを保存
            ud.set(savedMemoArray, forKey: "memo")
        //前にメモを入力したことがなければ
        } else {
            var newMemoArray = [String]()
            //memoInputにメモが入ってるかどうか
            if memoInput != nil {
                newMemoArray.append(memoInput!)
            } else {
                print("何も入力されていません")
            }
            ud.set(newMemoArray, forKey: "memo")
        }
        //即時保存
        ud.synchronize()
        self.navigationController?.popViewController(animated: true)
    }
    
    //キーボード下げる関数
    @objc func dismissKeyboard() {
        self.view.endEditing(true)
    }
}

```

##コード解説

###画面をタッチしたらキーボードを下げる
```swift
//他のところをタッチしたらキーボードが閉じる
let tapGR: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
tapGR.cancelsTouchesInView = false
self.view.addGestureRecognizer(tapGR)

//キーボード下げる関数
@objc func dismissKeyboard() {
    self.view.endEditing(true)
}
```
他のところをタッチしたらキーボードが下がるようにするコードは最後に書いてある関数とセットでこのままコピペでいいです。（うちも覚えてない）
これ書かないと実機で試した時キーボードがずっとあるので保存ボタンが押せなくなります。

記入したメモをとりあえず代入して、userdefaultsを用意するところまではさっきと同じです。

###メモを保存する関数の作成
####メモを前に書いたことがあるとき

```swift
//SavedMemoArrayにAny型からString型としてud.arrayから値を取り出して代入
var savedMemoArray = ud.array(forKey: "memo") as! [String]
//memoInputにメモが入ってるかどうか
if memoInput != nil {
    savedMemoArray.append(memoInput!)
} else {
    print("何も入力されていません")
}
//配列savedMemoを保存
ud.set(savedMemoArray, forKey: "memo")
```

以前にすでにメモを書いたことがあるのか、ないのかをif文をつかってみていきます。もし前にメモを書いたことがあるのであれば、userdefaultsから前のメモを取ってきて、そこに新しいメモを追加してあげればいいです。ただ、今回新しく追加するメモがちゃんとテキストに入力されているのかも確認してあげないといけません。(テキストに入力されてなければ追加できないのでアプリがクラッシュしてしまう）
メモがちゃんと書かれていれば新たなメモをそのまま追加してあげればいいです。入力されていないなら、本当はアラートとかを出してあげたらいいのかなと思いますが今回はprintしておきます。
ud.setで前のメモと一緒にuserdefaultsに保存されます。

####初めてメモを書くとき

```swift
var newMemoArray = [String]()
//memoInputにメモが入ってるかどうか
if memoInput != nil {
    newMemoArray.append(memoInput!)
} else {
    print("何も入力されていません")
}
ud.set(newMemoArray, forKey: "memo")
```
初めてメモを書く場合は新しい配列を作ってそこに追加してあげればいいです。ここでも、メモがちゃんとテキストに入力されているのかチェックしないといけませんが。
最後にud.setでメモがuserdefaultsに保存されます。

#メモ詳細画面
<img width=30% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1381539/60af4a01-7e2d-6c00-2882-ed2cae400e82.jpeg">

##とりあえず実際のコード
```swift
import UIKit

class DetailViewController: UIViewController {

    //メモを削除するときのために作る
    var selectedRow : Int = 0
    
    //値を入れた状態で渡すので絶対にnilにはならない
    var selectedMemo : String = ""
    
    @IBOutlet weak var memoTextView: UITextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        memoTextView.text = selectedMemo
        
    }
    
    @IBAction func delatememo(_ sender: Any) {
        //userdefaultsから値を取り出す
        let ud = UserDefaults.standard
        //何かしらメモが入ってる時
        if ud.array(forKey: "memo") != nil {
            //String型の配列に直してsavedMemoArrayに代入
            var saveMemoArray = ud.array(forKey: "memo") as! [String]
            //selectedRow何番目を削除する
            saveMemoArray.remove(at: selectedRow)
            //削除した後の配列を保存し直す
            ud.set(saveMemoArray, forKey: "memo")
            //即時保存
            ud.synchronize()
            self.navigationController?.popViewController(animated: true)
        }
    }
}
```

##コード解説
###変数を定義

```swift
var selectedRow : Int = 0
var selectedMemo : String = ""
```
メイン画面から値を渡してもらうために箱を用意しといてあげます。


###メモの削除
```swift
//何かしらメモが入ってる時
if ud.array(forKey: "memo") != nil {
    //String型の配列に直してsavedMemoArrayに代入
    var saveMemoArray = ud.array(forKey: "memo") as! [String]
    //selectedRow何番目を削除する
    saveMemoArray.remove(at: selectedRow)
   　　//削除した後の配列を保存し直す
   　　ud.set(saveMemoArray, forKey: "memo")
　　　　　　　　//即時保存
   　　ud.synchronize()
```

メモを削除するための関数を作成します。
メモがないときは消すものがないので、if文でメモがuserdefaultsに入ってる時と指定してあげましょう。
削除をするときにコンピューターにはどの行のメモかを指定してあげなければいけません。そのためにselectedRowの情報を値渡ししたので、これで選択したところが削除できます。
削除した後にuserdefaultsを保存しなおせば完璧ですね。
ud.synchronize()は良くわかりませんが即時保存できるらしいのでこのまま覚えておきます。

###削除と同時に画面が一つ前の画面に戻る
```swift
self.navigationController?.popViewController(animated: true)
```
navigationControllerを使ってる場合の画面を一つ前に戻すコードです。



#最後に
なかなか長くなってしまいましたが、ちょっとでも参考になれば嬉しいです。
これからも、自分が勉強していく中でわからんことをどんどん発信していきます。
間違ってるとことか、わからんってとこあったら教えてください。