最近のiOSの設計について
XCodeは毎年、新機能が追加されそれに伴いアプリの開発、設計手法も変わるでしょう。
そこで現時点の自分の設計方法をまとめてみました。
ボクは個人でたくさんのアプリを設計・開発をしています。
その為効率と改善速度に重点を置いています。
すっごく大きい規模のアプリになるとこれが正しいかはわかりません。アプリの種類にもよるでしょう。
自分が3年ほどアプリを作ってみた途中経過の設計だと思ってください。
Utility系
DL数30万
画面数40程度
のようなケースでは問題なく日々改善できております。
今のところ闇(わけの分からない箇所)の実装はない感じです。
コード設計
既存のクラスに機能をextensionしていきます。例えば文字列の制御はString、明らかにViewcontrollerでしか実行しない処理はUIViewControllerへ。
以下のリンクのようにprefixをつけるという手もありますがRxSwiftなどを入れた時点で使えないので注意が必要
https://tech.starttoday-tech.com/entry/swift_modern_extensions
結果実装が追いやすく楽しくなりました。
Modelに多くの機能を詰め込まず極力既存のクラスにExtensionしてprocedureを呼ぶような作り。
ControllerもざっくりつくりViewから呼び出すときも一行程度でまとまるように。
Cocoaが予め持っている役割に自分のアプリの機能を付け加えるような考え方でしょうか。
RxSwiftについて
MVVM支援系機能などは特に使用しません。(iOSアプリ開発ではバインディング機構が公式にサポートされたものでは無く個人的には使い勝手良く感じないなどの理由により)。自分はRealmInMemoryで十分でした。ただ近年Realmのコミュニティが静かになっているので代替の物があればほしい。
ただしチャット機能などが入った場合迷わず投入する事をお勧めします。
API叩いて取得してViewへ反映するような、ニュース系のアプリケーションで入れる必要性はないように感じます。
MVCはfatになりがちという記事を多く見ますが、未だにTopは200行以内に収まっています。
テストコードについて
XCtestを使用しています。
パスコードロックの実装部分や入力フォーム部分のみ。
API部分やRepository層についてバックエンド側テスコードを書きフロント側では書きません。
画面間の値渡しにdelegateは原則的には使わない
segueなど便利なものがある為シンプルに
画面間の値渡しは
・segue(一番使う)
・KVO(ここぞというとき)
・notification(ここぞという時)
を使用すれば特に困ることは無いはず。
delegateは便利が故にコードの可読性が落ち、密結合になりがちです。
遷移自体はコードで。
汎用UIのようなもの
AlertやActionSheetなど必ずViewControllerで実装するようなものは
extension UIViewController {
//確認などのダイアログで使用
func showAlertDialog(title title: String, message: String, buttonTitle: String,okFunc: @escaping () -> ()) {
let alert: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: buttonTitle, style: .default) { action in
okFunc()
}
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
}
//複数選択肢があるもの
func showAlertSelect(title: String, message: String, titles: [String], type: Int, preferredStyle: UIAlertControllerStyle, function: @escaping (_ index: Int, _ type: Int ) -> Void) {
let alert: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: preferredStyle)
for (index, item) in titles.enumerated() {
let action = UIAlertAction(title: item, style: .default) { _ in
function(index, type)
}
alert.addAction(action)
}
let cancel = UIAlertAction(title: "キャンセル", style: .default)
alert.addAction(cancel)
present(alert, animated: true, completion: nil)
}
}
使いたいときはViewControllerで
showAlertDialog(title: "お知らせ", message: "Alert出現のお知らせ", buttonTitle: "OK",okFunc: okFunc)
という感じで呼び出す。
APIなどコールバックで処理を行うケース
予めAPIというクラスを作っておき
それにExtensionしていく
例は簡単なSpreadsheetのtableに書かれたものを呼び出す
(AlamoFireやObjectMapperなど適宜 説明上シンプルに書きました)
func fetch(requestUrl: String,callback:@escaping (_ data: Data,_ responseObject: Any)->()) {
let req = URLRequest(url: URL(string: requestUrl))
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate:nil, delegateQueue:OperationQueue.main)
let task = session.dataTask(with: req, completionHandler: {
(datas, response, error) -> Void in
guard let res = response else {
return
}
callback(datas,res)
})
task.resume()
}
APIKitを使うなら必要ありませんがちょっとした疎通確認なら上記がおすすめ。
https://github.com/ishkawa/APIKit
Parseについて以前ObjectMapperを使用していましたが現在はCodableで十分。
使い方
API().fetch(requestUrl: "\(AppSetting().SpreadsheetUrl)\(AppSetting().SpreadsheetKey)/exec") { data,responsObject in
//受け取った値をViewへ反映
}
APIで受け取ったModelをRealmのinMemoryへ
ModelをRealmのinMemoryを使用することでObserverが超簡単に実装できる。
例えばAPIを受け取ったときにTableViewをいちいちReloadとか面倒ですよね。
これを使用すればModelは監視してくれてpropertyが変わったらViewへ反映してくれます。
プログラマがReloadを意識する機会が減ります。
もう一つのメリットはprint文でlist構造であっても中身を表示できる。
let token = realm.addNotificationBlock { notification, realm in
viewController.updateUI()
}
token.stop()
ViewControllerにTableViewがある場合
煩雑になりやすいTableViewですがextensionでViewControllerとしっかり分けることができすっきり。
extension MainViewController: UITableViewDataSource,UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableDataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath as IndexPath) as! TextTableViewCell
return cell
}
}
URLの画像配置
Nukeも良いですがこれに落ち着きました。
https://github.com/pinterest/PINRemoteImage
画面レイアウト
StackViewとAutolayoutを使用します。
ざっくり大枠はAutolayoutで組み。細かい部分はStackView
画面遷移
画面遷移は現在のプロジェクトで使えるかどうか別にしてStoryboardReferenceが最強だと思いました。
少し前までviewControllerにxibを呼び出してsegueで遷移していました。
これに出会ってもう戻れないくらいの便利さです。(iOS9以降でないと導入できない。)
幸い今の環境が過去のiOSバージョンをじゃんじゃん切っていく感じなので感謝しています。
StoryboardReference+segueが今のところ一番シンプルでベストな設計ではないのかと思いました。
自分はよくチーム開発をやっているのですが
StoryboardReferenceを使用していると
一番怖いStoryboardの競合が高い確率で防げます。
少ない数のStoryboardでぎっちり組むといざスケールアップしてある程度の人数がアサインされた時
設計を見直す必要があるでしょう。
画面間の値渡しは全てprepareを使用
渡さない場合はコードで遷移
全てコードで書くような現場もあると思いますが、StoryboardならデザイナーとUIの相談をする時いちいちbuildする必要もないですしStoryboardを使えるデザイナーも増えてきています。そのうち分業できるのでは?
人数が多ければ多いほどStoryboardは便利。
ただコードで書いておくと置換やViewの管理など容易なのでメリットもあります。
一斉操作をする場面が頻繁に発生する段階でリニューアルも必要なのかと....
値の永続化 UserDefaults
取り回しの良さからUserdefaultが便利です。
class DataCenter {
static let ud = UserDefaults.standard
class user {
struct Default {
static let name: String = ""
}
class var name: String {
get {
ud.register(defaults: ["userName": Default.name])
return ud.object(forKey: "userName") as String
}
set(newValue) {
ud.set(newValue, forKey: "userName")
ud.synchronize()
}
}
}
}
使い方
DataCenter.user.name = "Tom"
//アプリをターミネート後に実行
DataCenter.user.name -> "Tom"
自分のプロジェクトではせいぜい5箇所くらいしか無いため増えてきたら
SwiftyUserDefaultsなどで管理すると良いです。
あくまで10以内なら上記それ以外は
https://github.com/radex/SwiftyUserDefaults
値の永続化 Realm
これも定番ですが便利ですよね。会社では使う機会がない。
(説明しやすく強制アンラップしてます。実際はもう少し例外入れたり便利な機能を入れますが説明しやすく分岐など最低限のCRUDだけ)
実際に使うときは protocol
extension
などで書くとスッキリかけます。
import RealmSwift
class CoreRealm {
let realm = try! Realm()
func save(object: Object){
try! realm.write {
realm.add(object, update: true)
}
}
func find<T: Object>(_ type: T.Type, query: String? = nil,sort: String? = nil,ascending: Bool? = true) -> Results<T>? {
if let value = query {
let predicate: NSPredicate = NSPredicate(format: value)
if let sortValue = sort {
return self.realm.objects(type).filter(predicate).sorted(byKeyPath: sortValue, ascending: ascending)
} else {
return self.realm.objects(type).filter(predicate)
}
} else {
if let sortValue = sort {
return self.realm.objects(type).sorted(byKeyPath: sortValue, ascending: ascending)
} else {
return self.realm.objects(type)
}
}
}
func update<T: Object>(type:T.Type, post: [String: Any]){
try! realm.write {
realm.create(type.self, value:post, update: true)
}
}
func delete<T: Object>(object: Results<T>?){
try! realm.write {
realm.delete(object!)
}
}
}
使い方
データの保存
let animalReaml = AnimalReaml()
animalReaml.name = "いぬ"
CoreRealm().save(object: animalReaml)
データの呼び出しから削除
//呼び出し
let animalReaml = CoreRealm().find(animalReaml.self, query: "name == 'いぬ'")
//削除
CoreRealm().delete(object: animalReaml)
値の共有
離れたViewの値共有は構造体を使用(名前空間なと命名設計しっかりと)
struct Animal {
static var name = "いぬ"
static var age = 32
}
使い方
名前空間さえしっかり設計しておけば大変便利
実際は shared
などを挟むのが一般的です。
Animal.age -> 32
Animal.name = "ねこ"
Animal.name -> "ねこ"
定数
Alertのタイトルやラベルのテキスト、URLやカラーなど
SwiftGen使いましょう。
iOS / macOS アプリ開発の補助ツールです。リソース(アプリ内の画像やテキストなど)の扱いが楽になるソースコードを自動生成してくれます。
https://github.com/SwiftGen/SwiftGen
Firebase
FirebaseDBやFireStoreがないプロジェクトでも入れておいたほうが良いです。
CrashlyticsとAnalyticsはgrowthするにはとても強力で手放せないでしょう。
プッシュ通知
FirebaseCloudMessageを使いましょう。
アプリ配信
deploygateにしておきましょう
https://deploygate.com/?locale=ja
CI
現時点だとbitriseが良いでしょう。
fastlaneでrelease、配信等自動化。
https://www.bitrise.io/?utm_source=brandAW&utm_campaign=brandAW&gclid=EAIaIQobChMIqOaptY_W3AIVyquWCh1YDAEOEAAYAiAAEgII3_D_BwE
Lint
現時点だとSwiftLint
https://github.com/realm/SwiftLint