どうも、ねこきち(@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の方を扱っていきます。)
ストーリーボード
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)