2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-16

どうも、ねこきち(@nekokichi1_yos2)です。

僕が参加してるアプリ道場サロン

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

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

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

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

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

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

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

そこで、エンジニアの必読書として有名なリーダブルコードを使って、コードを読みやすいように改善していきます

目的と概要

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

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

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

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

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

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

アプリ道場サロンで、

「エンジニアの実務では自分でコードを書くのと同じくらい、他人のコードを読んで理解する機会が多い」

と教えられました。

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

使用するソースコード

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

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

GitHubにある過去のソースコードを候補に考えましたが、
・コードの量が多くない
・複雑なコードを用いている
・外部の技術(ライブラリなど)を用いている
の観点から、0から作りました。

https://github.com/CatLuck2/ReadbleCodeSample1/tree/development
(ブランチはmaster、developmentの2つ。developmentの方を扱っていきます。)

ストーリーボード

スクリーンショット 2020-08-16 11.11.35.png

ViewController

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

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

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型で保存してます。

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で、表示させるデータを取り出しています。

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とほぼ同じです。

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を表示させるだけです。

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

デモ

メモを追加

メモを編集

メモを表示

まとめ

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

参考にした記事

Realmを使ってTODOアプリを作ってみよう!/ Swift(Realmの使い方、初級編)
[iOS] Realmを使ってみた 〜環境構築からCRUDまで〜
Realmでfilterの検索条件に変数を利用するには
RLMException - 'Binary too big'
[Swift] UITextViewに画像を添付する方法
How to add image and text in UITextView in IOS?
[Swift] UIImageからCGImageを作成すると、画像が90度など回転してしまう件への対策
(swift)画像のデータサイズを縮小する方法(UIImage)

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?