リーダブルコードで読みにくいコードを改善していく ~ 準備編

<p>どうも、ねこきち（<a href="https://mobile.twitter.com/nekokichi1_yos2">@nekokichi1_yos2</a>）です。</p>

僕が参加してる[アプリ道場サロン](https://community.camp-fire.jp/projects/view/281055)で

>昔流行った技術はもう使ってないが、アルゴリズムやデザインパターンの知識は今も役に立ってる

とサロンオーナーが仰っていました。

確かに、iOSアプリの言語であるObjective-C、広範囲で活用されたPerl、どれも昔は流行でしたが、今じゃ新しい言語の台頭で人気を失っています。

対して、
・リファクタリング
・コードを読む力
・アーキテクチャ
など、幅広い言語に精通する知識は、今も名前が上がっています。
（例えば、エンジニアが読むべき技術書、とか）

僕は現在、SwiftでiOSアプリ開発に励んでいますが、いつかAppleが新しい言語を発表したら、Swiftの知識は価値を失い、最も精通しているSwiftを手放さなければなりません。

**ですが、基礎となる知識や経験がなければ、別の言語に移っても、初心者の位置から再スタートすることになります。**

1つの言語ではなく、別の言語や技術に移った際に滞りなく開発スキルを磨くためには、普遍的なスキルが必要です。

**そこで、エンジニアの必読書として有名な[リーダブルコード](https://www.amazon.co.jp/%E3%83%AA%E3%83%BC%E3%83%80%E3%83%96%E3%83%AB%E3%82%B3%E3%83%BC%E3%83%89-%E2%80%95%E3%82%88%E3%82%8A%E8%89%AF%E3%81%84%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E6%9B%B8%E3%81%8F%E3%81%9F%E3%82%81%E3%81%AE%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%A7%E5%AE%9F%E8%B7%B5%E7%9A%84%E3%81%AA%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AF-Theory-practice-Boswell/dp/4873115655)を使って、コードを読みやすいように改善していきます**

## 目的と概要

**目的は、リーダブルコードを参考にして自分が書いたコードをきれいにすることです。**

エンジニアの間で人気とされる、リーダブルコード、を選んだのは、
・買ったままで実践してない
・ページ数やそこまで多くない
・ネット上で解説記事が多い
からです。

どんな本や技術書も、読んで、理解して、実践することで自分の力になります。

また、量や質から見て実践しやすく、既に中身を解説した記事が出回っているので、分からない箇所でつまづくことなく進められます。

**本記事を第1弾として、第2、第3と、リファクタリングする過程を実況中継していきます。**

理由は、記事として発信することで
・どこのコードを
・どのような形に改善し
・なぜそうしたか
を読者に説明でき、僕自身のためにもなるからです。

[アプリ道場サロン](https://community.camp-fire.jp/projects/view/281055)で、
>「エンジニアの実務では自分でコードを書くのと同じくらい、他人のコードを読んで理解する機会が多い」

と教えられました。

なので、
・リファクタリングのスキル
・読者に自分のコードを説明するスキル
を同時に磨こうと思ったのです。

## 使用するソースコード

使用するのは、**Realmを使用したメモアプリ**、です。

機能は、文字や画像を入力したTextViewを保存/取得/編集/削除、できることです。

GitHubにある過去のソースコードを候補に考えましたが、
・コードの量が多くない
・複雑なコードを用いている
・外部の技術（ライブラリなど）を用いている
の観点から、0から作りました。
↓
[https://github.com/CatLuck2/ReadbleCodeSample1/tree/development](https://github.com/CatLuck2/ReadbleCodeSample1/tree/development)
（ブランチはmaster、developmentの2つ。developmentの方を扱っていきます。）

### ストーリーボード

![スクリーンショット 2020-08-16 11.11.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284464/4807d461-8c5e-dd34-7fac-b55019c24460.png)
### ViewController

ホーム画面として、Realmからデータを取得し、取得したデータをAttributedTextに変換し、Stringに変換してtableViewに表示させています。

セルをタップすれば、indexPath.rowに該当する番号のデータを遷移先の画面に渡してます。

```swift
import UIKit
import RealmSwift

class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
    
    @IBOutlet weak var tableView: UITableView!
    
    var list:Results<MemoObject>!
    var text = [NSAttributedString]()
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        let realm = try! Realm()
        text = [NSAttributedString]()
        list = realm.objects(MemoObject.self)
        if list.count > 0 {
            for i in 0...list.count-1 {
                let attributeText = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(list![i].data) as! NSAttributedString
                text.append(attributeText)
            }
        }
        tableView.reloadData()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: "customcell")
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return list.count
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "customcell", for: indexPath) as! CustomCell
        cell.label.text = text[indexPath.row].string
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        performSegue(withIdentifier: "display", sender: nil)
    }
    
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        let realm = try! Realm()
        let object = list[indexPath.row]
        try! realm.write() {
            realm.delete(object)
        }
        tableView.reloadData()
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "display" {
            let vc = segue.destination as! DisplayMemo
            vc.memo = list[tableView.indexPathForSelectedRow!.row]
            vc.text = text[tableView.indexPathForSelectedRow!.row]
            vc.indexPath = tableView.indexPathForSelectedRow?.row
        }
    }

}
```

### AddMemo

Realmに保存するモデルは、
・data:入力したテキスト
・identifier:識別子
です。

identifierは、保存したデータを編集して再度保存する時に、クエリでどのデータかを識別するのに使用してます。

TextViewに文字を書き込み、長押しタップのジェスチャーで画像をTextAttachementでtextViewに貼り付けています。

Realmは保存できるデータの容量は16MBまでなので、
・圧縮
・リサイズ
の処理を施しています。

保存するのはAttributedTextなので、textViewのテキストをData型で保存してます。

```swift
import UIKit
import RealmSwift

class MemoObject: Object {
    @objc dynamic var data: Data!
    @objc dynamic var identifier: String!
}

class AddMemo: UIViewController {
    
    @IBOutlet private weak var memoTextView: UITextView!
    
    let picker = UIImagePickerController()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        picker.delegate = self
        picker.sourceType = .photoLibrary
    }
    
    @IBAction func leftButton(_ sender: Any) {
        let realm = try! Realm()
        let memo: MemoObject = MemoObject()
        let data = try! NSKeyedArchiver.archivedData(withRootObject: memoTextView.attributedText!, requiringSecureCoding: false)
        memo.data = data
        memo.identifier = String().randomString()
        try! realm.write{
            realm.add(memo)
        }
        self.navigationController?.popViewController(animated: true)
    }
    
    @IBAction func gesture(_ sender: UILongPressGestureRecognizer) {
        let alert = UIAlertController(title: "画像を添付", message: nil, preferredStyle: .actionSheet)
        let action = UIAlertAction(title: "OK", style: .default) { (action) in
            self.dismiss(animated: true, completion: nil)
            self.present(self.picker, animated: true, completion: nil)
        }
        let cancel = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)
        alert.addAction(action)
        alert.addAction(cancel)
        present(alert, animated: true, completion: nil)
    }
    
}

extension String {
    func randomString() -> String {
        let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        var len = Int()
        var randomCharacters = String()
        for _ in 1...9 {            len = Int(arc4random_uniform(UInt32(characters.count)))
            randomCharacters += String(characters[characters.index(characters.startIndex,offsetBy: len)])
        }
        return randomCharacters
    }
}

extension AddMemo: UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let image = info[.originalImage] as? UIImage {
            let fullString = NSMutableAttributedString(attributedString: memoTextView.attributedText)
            let pickerImage = image.resized(withPercentage: 0.1)!
            let imageWidth = pickerImage.size.width
            let padding: CGFloat = self.view.frame.width / 2
            let scaleFactor = imageWidth / (memoTextView.frame.size.width - padding)
            let imageAttachment = NSTextAttachment()
            imageAttachment.image = UIImage(cgImage: pickerImage.cgImage!, scale: scaleFactor, orientation: pickerImage.imageOrientation)
            let imageString = NSAttributedString(attachment: imageAttachment)
            fullString.append(imageString)
            memoTextView.attributedText = fullString
        }
        dismiss(animated: true, completion: nil)
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }
    

}

```

### DisplayMemo

ViewControllerで選択したセルのindexPath.rowで、表示させるデータを取り出しています。

```swift
import UIKit
import RealmSwift

class DisplayMemo: UIViewController {
    
    @IBOutlet weak var memoTextView: UITextView!
    
    var list:Results<MemoObject>!
    var memo: MemoObject = MemoObject()
    var text = NSAttributedString()
    var indexPath: Int!
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        memoTextView.attributedText = text
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "edit" {
            let vc = segue.destination as! EditMemo
            vc.memoObject = memo
            vc.text = memoTextView.attributedText
            vc.indexPath = indexPath
        }
    }
    

}
```

### EditMemo

AddMemoとほぼ同じです。



```swift
import UIKit
import RealmSwift

class EditMemo: UIViewController {
    
    @IBOutlet weak var memoTextView: UITextView!
    
    let picker = UIImagePickerController()
    var list:Results<MemoObject>!
    var memoObject: MemoObject = MemoObject()
    var text = NSAttributedString()
    var indexPath: Int!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        picker.delegate = self
        picker.sourceType = .photoLibrary
        let realm = try! Realm()
        list = realm.objects(MemoObject.self)
        memoTextView.attributedText = text
    }
    
    @IBAction func gesture(_ sender: UILongPressGestureRecognizer) {
        let alert = UIAlertController(title: "画像を添付", message: nil, preferredStyle: .actionSheet)
        let action = UIAlertAction(title: "OK", style: .default) { (action) in
            self.dismiss(animated: true, completion: nil)
            self.present(self.picker, animated: true, completion: nil)
        }
        let cancel = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)
        alert.addAction(action)
        alert.addAction(cancel)
        present(alert, animated: true, completion: nil)
    }
    
    @IBAction func complete(_ sender: Any) {
        let realm = try! Realm()
        let data2 = try! NSKeyedArchiver.archivedData(withRootObject: memoTextView.attributedText!, requiringSecureCoding: false)
        let memo = realm.objects(MemoObject.self).filter("identifier == %@", memoObject.identifier)
        try! realm.write {
            memo.setValue(data2, forKey: "data")
        }
        self.navigationController?.popToRootViewController(animated: true)
    }
        
}

extension EditMemo: UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let image = info[.originalImage] as? UIImage {
            let fullString = NSMutableAttributedString(attributedString: memoTextView.attributedText)
            let pickerImage = image.resized(withPercentage: 0.1)!
            let imageWidth = pickerImage.size.width
            let padding: CGFloat = self.view.frame.width / 2
            let scaleFactor = imageWidth / (memoTextView.frame.size.width - padding)
            let imageAttachment = NSTextAttachment()
            imageAttachment.image = UIImage(cgImage: pickerImage.cgImage!, scale: scaleFactor, orientation: pickerImage.imageOrientation)
            let imageString = NSAttributedString(attachment: imageAttachment)
            fullString.append(imageString)
            memoTextView.attributedText = fullString
        }
        dismiss(animated: true, completion: nil)
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }

}

extension UIImage {
    //データサイズを変更する
    func resized(withPercentage percentage: CGFloat) -> UIImage? {
        let canvas = CGSize(width: size.width * percentage, height: size.height * percentage)
        return UIGraphicsImageRenderer(size: canvas, format: imageRendererFormat).image {
            _ in draw(in: CGRect(origin: .zero, size: canvas))
        }
    }
}
```

### CustomCell

Labelを表示させるだけです。

```swift
import UIKit

class CustomCell: UITableViewCell {

    @IBOutlet weak var label: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
}
```

## デモ

### メモを追加

<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284464/7fbdccb7-b35d-fa75-c1a0-abfc31d43b13.gif">

### メモを編集


<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284464/4ca41ae5-17ea-ec6d-113c-6c142482275d.gif">


### メモを表示

<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284464/a49e91c6-ebfd-6654-d1c6-6a3706ed1aeb.gif">

## まとめ

明日からリーダブルコードを参考にコードをリファクタリングしていきます。

## 参考にした記事

[Realmを使ってTODOアプリを作ってみよう！/ Swift(Realmの使い方、初級編)](https://qiita.com/pe-ta/items/616e0dbd364179ca284b)
[[iOS] Realmを使ってみた 〜環境構築からCRUDまで〜](https://dev.classmethod.jp/articles/start-ios-realm-crud/)
[Realmでfilterの検索条件に変数を利用するには](https://ja.stackoverflow.com/questions/30370/realm%E3%81%A7filter%E3%81%AE%E6%A4%9C%E7%B4%A2%E6%9D%A1%E4%BB%B6%E3%81%AB%E5%A4%89%E6%95%B0%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%81%AB%E3%81%AF)
[RLMException - 'Binary too big'](https://stackoverflow.com/questions/33247653/rlmexception-binary-too-big/33249074)
[[Swift] UITextViewに画像を添付する方法](https://qiita.com/kyam_/items/f08ba6d47f233ef04860)
[How to add image and text in UITextView in IOS?](https://stackoverflow.com/questions/24010035/how-to-add-image-and-text-in-uitextview-in-ios)
[[Swift] UIImageからCGImageを作成すると、画像が90度など回転してしまう件への対策](https://www.yoheim.net/blog.php?q=20190102)
[（swift）画像のデータサイズを縮小する方法（UIImage）](https://qiita.com/Tsh-43879562/items/4883c433bb7297019a1f)