- バックエンド / リーダー候補
- PdM
- Webエンジニア(シニア)
- 他19件の職種
- 開発
- ビジネス
iOSエンジニアのみなさん、こんにちは!
WantedlyでiOSアプリ開発を担当してます、杉上です。
このブログでは新規でiOSのアプリ開発を開始するなら、どうなふうに作ろうかなと妄想してみました。なかなか仕事の現場では新規アプリ開発の機会はないので、こういう妄想を常に膨らませつつプライベートで実験的にアプリを作ってみたりしています。
( ここでご紹介する内容はiOSアプリを作るにあたっての最適解でもベストプラクティスではありません。プロジェクトのゴールや規模など多様な要因により構成もケースバイケースになると思うので、ご参考までに。)
Embedded Framework活用(ターゲット分割)
アプリの一部のコードをドメインごとにターゲットへ分けてEmbedded Frameworkとして利用することで以下のメリットがあります。アプリ開発が進んでからコードを分けるのは難しいので開発初期の段階で検討したいです。
### メリット
- Extention間でコード共有
- Share Extensions、Today、Extensions、Apple Watchなど各ターゲットで共有したいコードをEmbedded Frameworkの紐付けにより依存関係をシンプルに表現できる
- Embedded Frameworkを利用しない場合は、共有したいソースファイルごとに複数ターゲットに紐付ける煩雑な管理となる
- ターゲットが分離されるとコードの修正による差分コンパイルの適応スコープが小さくなりコンパイル速度が向上する
- Embedded Frameworkのコードが依存するライブラリを明示化できる
- Embedded Frameworkの単位でテストを記述できる
### Embedded Frameworkをどうわけるか
大きくわけるのならアーキテクチャのレイヤーごとにわけるのがよさそうです。ViewModel、ModelレイヤーはExtention間でもコード共有しやすく、ロジックが中心なのでViewにも依存しない単位でくくり出せてテストも行いやすくなります。
さらに小さく分ける場合は、API通信部分だけや、データオブジェクトと永続化だけを切り出したライブラリにすると機能単位になりテストや依存の見通しもよりよくなりそうです。またレイヤー横断的な共通的な関数や定数を集めたライブラリという観点でも切りだせそうです。
### 参考
mono0926さんのEmbedded Framework使いこなし術がとても参考になります。
オブジェクト指向とプロトコル指向
継承による抽象化(アブストラクトなクラス)は設けない方針です。とくにUIViewContllerへ共通的な機能を持たせるために、各画面のUIViewContllerから継承するようなApplicationViewContllerは作りません。これは以下のような課題があるためです。
- ApplicationViewContllerを継承するサブクラスで利用したい共通機能が網羅的にかつ最大公約数的に実装されるので、多様な責務が混在するクラスになってしまい影響範囲を類推しにくくなる
- 処理が親クラス、子クラスを行き来するとコードを読んだ時に処理の流れを把握しにくい
共通的な機能はその責務を担う専用のクラスを設けたり、プロトコル拡張により実装した上で、プロトコルの準拠や、プロトコルの制約、プロトコルの内包する型の準拠により限定的に適応することで、必要な実装を必要なスコープにのみ局所化できるので影響範囲が把握しやすくコードを理解しやすくります。
ネットワーク
どのようにAPIと通信を行うかはアプリのとても根幹的な部分だと思います。Siestaはドキュメントを読んだだけですが、その他のライブラリは実際に書いてみました。
- Moya
- APIKit
- Siesta
### Alamofire
READMEがとても充実しており記述も直感的でわかりやすいので、やりたいことに対してあまり困ることなく開発ができるのではと思います。REQUESTをログに出力するときにcurl形式で出力できるのがお気に入りです。開発中にAPI通信において何か問題が発生した場合にアプリ起因か、APIサーバ起因か、切り分けが容易になります。
let request = Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
debugPrint(request)
$ curl -i \
-H "User-Agent: Alamofire" \
-H "Accept-Encoding: Accept-Encoding: gzip;q=1.0,compress;q=0.5" \
-H "Accept-Language: en;q=1.0,fr;q=0.9,de;q=0.8,zh-Hans;q=0.7,zh-Hant;q=0.6,ja;q=0.5" \
"https://httpbin.org/get?foo=bar"
### Moya
ネットワークを抽象化して扱えるようにしてくれます。スタブデータを設定でき容易に切り替えができます。Network開始終了をハンドルするPluginを追加できる機構になっています。Community Extensionsが充実しており、ObjectMapperやSwiftyJSONなどのJSONマッピングが供給されています。
### APIKit
ishkawaさんが開発されているライブラリで、ライブラリの設計、開発者が記述するコード共に美しいです。リクエスト/レスポンスの定義は1箇所に集約されるため可読性が高く仕様を把握しやすいです。
### Siesta
リクエスト/レスポンスをリソースのオブザーブに置き換えたインタフェースで提供してます。独自のキャッシュ機構を備えたり他のライブラリよりも多機能です。 APIKit以外になりますが、Siestaが比較を行ってくれています。
画面遷移(ルーティング)
iOSアプリの画面遷移は遷移元のViewControllerから遷移先のViewControllerへ遷移元がその責務を担い、遷移に関する処理が分散していますがUniversal Links(DeepLink)の活用を考えると中央集権的なURIベースのルーティングを導入して、多様なURIによる動的な遷移や 一気に深い遷移にも容易に行える管理したくなります。また遷移元が遷移先に大きく依存した構造から、ルーティングを処理を切り出すことで各画面(ViewController)の独立性が高まります。
Enumを活用して自前実装でルーティングテーブルと遷移ロジックを記述するか、以下のライブラリを利用するのもよさそうです。
### JLRoutes
JLRoutesはもっとも有名なルーティングライブラですが、ObjCで書かれているのでSwiftからは少し使いにくいです。
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
JLRoutes.addRoute("/user/view/:userID") { (params: [NSObject : AnyObject]!) -> Bool in
let userID = params["userID"] as! String
let viewController = ProfileViewController(userID)
self.navigationController?.pushViewController(viewController, animated: true)
return true
}
}
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {
return JLRoutes.routeURL(url)
}
### Compass
CompassはSwiftで記述されたライブラリで、シンプルで理解しやすくルーティング間の依存関係を記述できるなど柔軟で同ドナ表現も可能です。
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
Compass.scheme = "compass"
Compass.routes = ["profile:{username}", "login:{username}", "logout"]
return true
}
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {
return Compass.parse(url) { route, arguments in
switch route {
case "profile:{username}":
let profileController = profileController(title: arguments["{username}"])
self.navigationController?.pushViewController(profileController, animated: true)
case "login:{username}":
let loginController = LoginController(title: arguments["{username}"])
self.navigationController?.pushViewController(loginController, animated: true)
case "logout":
logout()
default: break
}
}
}
画像通信・画像キャッシュ
画像はクラウド上のデータを表示するケースも多いので以下を一括して管理してくれるライブラリを活用したいところです。
- 画像データの非同期通信
- 画像データ通信のキャンセル
- 画像データのキャッシュ
- プリフェッチ
- 同時通信数制御
- 通信の優先度付け
- WebPのデコード ( オプショナル )
以下の2点が有名かと思います。
SDWebImageはObjCで実装され、KingFisherはonevcatさんによりSDWebImageを参考にSwiftで実装されています。どちらもよくメンテナンスされていますが、Swiftアプリを開発するのならSwiftネイティブなインターフェイスのKingFisherがいいかなと思います。
ただし画像形式にWebPを利用したい場合は、KingFisherでは対応されていないので(対応される見通はないそうです)、SDWebImageを利用する必要があります。
開発当初は以下の点が活用できれば十分ですが
- 画像データの非同期通信
- 画像データ通信のキャンセル
- 画像データのキャッシュ
よりアプリの体験を改善したい場合は、以下を駆使したり調整することでグッとユーザ体験が向上します。
- プリフェッチ
- ユーザスクロールして表示したタイミングで通信開始するのではなく、すでに画像を取得することで表示をユーザに待たせない
- 同時通信数制御
- 回線の帯域により同時接続数を動的に変更するなど
- 通信の優先度付け
- 今見えている画面の中でもより早く見せたい画像と、そのでない画面の優先度を調整する
- WebPのデコード ( オプショナル )
- 画像配信形式をPNGやJPEGからWebPに変更することで、データサイズを大きく削減でき表示が早くなる
データの永続化とキャッシュ
RealmとJSONのファイル書き出しを併用するのがいいかなと思います。
- Realm
- SQLite3(CoreData)
- JSONファイル書き出し
### Realm
SQLite3(CoreData)よりは、Realmのほうが扱いやすくパフォーマンスもよいのでRealmを選択したいです。とくに非同期で処理や、データのスレッド跨ぎはCoreDataで苦しかった(クラッシュしやすい)ですが、Realmは考え方がとてもシンプルなのでそういった状況でも実装しやすく(クラッシュしにくい)、ドキュメントがしっかりしていて、サポートも厚いのが嬉しいです。また5月25日にRealmがついにバージョン1.0.0となり、以前に比べて仕様変更によるブレーキングチェンジやリリース頻度も少なくなりそうなので、より利用しやすくなったと思います。アーキテクチャでも触れていますが、Realmの通知機能がすばらしく、Realmをデータの正とすることで、画面を跨ぐような場合のデータの一貫性を容易の担保でき、データの変更があった際も各画面に通知することで表示上の同期も容易になります。
### JSONファイル書き出しの活用
画面を表示するたびに通信したデータで置き換えて表示する場合は、通信したデータ(JSON)をそのままファイルに書き出し、次回は通信とあわせて、JSONファイルを読み込んで表示用に利用するとシンプルなキャッシュ構成になります。
### Realm
以下のようにより複雑なケースではJSONをファイルに書き出すより、RealmなどDBを活用するほうが管理しやすくなると思います。
通信するデータを最小限にしたいため、通信によりすでに取得したデータとの差分のみを次回通信する場合は、すでに取得したデータをキャッシュとしてRealmに保存し、追加で取得したデータをRealmへ容易に追加できるので管理しやすいです。
通信で取得したデータをユーザ操作によりデータが変更され、画面を跨いだ表示部分にも影響を与える場合、シングルトンでデータを共通化したり、変更操作を伝播して跨いだ表示部分も変更したりもできますが、DBのデータを常に正として表示の度にDBからデータを読みこみなおすとシンプルに解決できます。
ローカルのデータで一覧表示をソートしたり、検索したりする場合はRealmのフィルターやソートを活用できます。
永続化とキャッシュの両方をRealmで担いたい場合は、Configrationの設定によりファイルをそれぞれ分割することを検討します。キャッシュのデータは最悪捨ててしまってもよいので、必要があればアグレッシブなスキーマ変更を難しいマイグレーションを行わずファイルを削除することで行えます。( ちょっとした変更であれば、Realmのマイグレーションはとても簡単です。念のため難しいマイグレーションに備えてファイルを分けています。)
キーチェイン
キーチェインのAPIはレイヤーが低くて使いにくいので、岸川さんがSwiftから扱いやすくラップしてくれたKeychainAccessにお世話になっています。KeychainAccessはキーチェインへのデータの読み書きを簡単するだけではなく、キーチェインにまつわる以下の機能にも対応しています。サービスがアプリとWebどちらも提供しており、ユーザはログインが必要であればShared Web Credentialsを是非検討したいです。SafariでWebにログインしたIDとパスワードを、ユーザがアプリで再入力する手間を省いてくれます。
- Shared Web Credentials
- Touch ID integration
- Keychain Sharing
- iCloud
オートレイアウト
基本的にはインターフェイスビルダー(StoryBoardやXib)を活用してGUI上でオートレイアウトを組んでいきますが、オートレイアウトをアニメーションさせたい場合や、ビューの一部をLazyLoadingによる遅延作成する場合など、プログラムから制御したいケースを多々あります。そんなときにNSLayoutConstraintで記述するのはとても辛いので、SnapKitを活用したいです。SnapKitは大変記述しやすく慣れてくるとインターフェイスビルダーよりも速く記述できるようになります。またオートレイアウトがコードになるのでGithubのPullReqでレビューもしやすくなります。
SnapKitを利用したコードによるオートレイアウトに例です。
import SnapKit
class MyViewController: UIViewController {
lazy var box = UIView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(box)
box.snp_makeConstraints {
$0.width.height.equalTo(50)
$0.center.equalTo(view)
}
}
}
ロギング
NSLogやprint関数でも開発時のログ出力は行えますが、専用のロギングライブラリを活用すると以下のメリットがあります。
- ログの出力内容に応じて出力レベル(INFO, WARNIG, DEBUG, ERROR)を設定できる
- Build Configrationにより上記ログ出力レベルを変更できる
- ログ出力が非同期やバッファリングに対応しており、大量のログ出力時でもメインスレッドのリソースを消費せずアプリの動作遅延に影響を与えない
- Xcodeのコンソール出力以外にも任意のファイルへの出力が可能で、tailやgrepを活用できる
iOSで利用できるロギングライブラリはいくつかありますが、最近はSwiftyBeaverの人気があるようです。
- CocoaLumberjack Objcで実装されたライブラリ、Swiftのラップ版も提供している
- XCLogger Swift初期からあるライブラリ、自分もよくつかったていた
- SwiftyBeaver 開発中だけではなくクラウドを介してリリース後もログを収集できる
SwiftyBeaverはお洒落なログ表示アプリもあります。
トラッキング
ユーザ操作をトラッキングするためのサービスはいろいろありますが、アプリをリリースし運用するにつれて新たにトラッキングサービスを追加したいケースもあるかなと思います。そういうケースを見越してトラッキングサービスのSDKを直接呼び出さず、TrackingAdapterのような抽象化するレイヤーを設け、アプリのコードから特定のトラッキングサービスに依存しないようにしておきます。これによりトラッキングサービスの入れ替えや追加、複数のトラッキングサービスへ同時に送信することが容易になります。
### トラッキングサービス
最近はFirebaseが注目されていますね。
- GoogleAnalytics
- MixPannel
- Firebase
- etc
### トラッキング項目
トラッキングする項目は大きくわけて2種類あります。イベントとスクリーンです。
イベントはざっくり以下のような観点があるかと思います。
- ユーザの操作をトリガーとして、操作したことを記録する ( ex. ログインボタンを押下した )
- ユーザの操作による処理の結果を記録する ( ex. ログインAPIのコールが成功した )
- システムによる処理の結果を記録する ( ex. アプリを起動してから3分間利用している )
スクリーンはアプリのどの画面に遷移したかを記録します。UIViewController#viewDidAppearのコールバックがトリガーになる場合が多いかと思います。すべての画面のviewDidAppearにトラッキングを仕込むのは単調で骨が折れるので、アスペクト指向を活用してトラッキングを横断的関心事として織り込んできいきます。
### トラッキング(横断的関心事)の織り込み
アスペクト指向のライブラリはAspectsを利用します。以下はAnalyzableViewControllerプロトコルを設けて、それに準拠したUIViewControllerのみトラッキングする例です。
protocol AnalyzableViewController: class {}
typealias AspectBlock = (aspectInfo: AspectInfo) -> ()
struct TargetInfo {
let action: String
let options: AspectOptions
init(action: String, options: AspectOptions) {
self.action = action
self.options = options
}
}
func hookViewController(targetInfo: TargetInfo, block: AspectBlock) {
try! UIViewController.aspect_hookSelector(Selector(targetInfo.action), withOptions: targetInfo.options,
usingBlock: unsafeBitCast(block as @convention(block) (aspectInfo: AspectInfo) -> (), AnyObject.self))
}
let targetInfo = TargetInfo(action: "viewDidAppear:", options: .PositionBefore)
hookViewController(targetInfo) { (aspectInfo: AspectInfo) -> () in
if let vc = aspectInfo.instance() as? UIViewController {
if vc is AnalyzableViewController {
let subjectType = Mirror(reflecting: vc).subjectType
let className = "\(subjectType)"
let screenName = className.stringByReplacingOccurrencesOfString("ViewController", withString: "")
self.traking(screenName)
}
}
}
func traking(screenName: String) {
// Some traking code here.
}
ローカライズ
多言語化を見越してアプリを開発する場合は初期のリリースでは日本語言語のみに対応したとしても、当初から多言語化の枠組みの中で開発したほうが、断然多言語化に移行しやすいです。とくにローカライズ辞書ファイルのKEYは英語や日本語の文言とはせず、一意になる識別子をちゃんと設けたほうがよいです。英語をKEYとした場合、シチュエーションによってはある英語の文言から2種類以上の日本語に翻訳したいケースもあります。また識別子のほうが管理も煩雑になりません。
フォントアイコン
開発初期や、プロトタイプではFontAwesomeをフォントや画像として利用できるFontAwesomeKitを導入しておくと、都度デザイナーさんにアイコンを作ってもらわなくても暫定的なアイコン利用して開発スピードを早めることができます。
またフォントのほうが、プログラムからサイズや色を柔軟に設定できるのと、文言の中に容易に埋め込めれるため重宝するので、デザイナーさんがアイコンを作ってくるときに画像ではなくフォントとして提供してもらえるといいですね。
リソース管理
リソースファイルへの参照は以下のように文字列にて指定をするため、誤りをコンパイル時にはチェックできず実行時にクラッシュしてしまいます。
let image = UIImage(named: "hoge.png")!
そこでR.swiftを利用するとアセット情報を解析し、Swiftのコードを自動生成してくれるので以下のように安全にリソースを指定することができます。またXcodeのコード補完で入力支援の恩恵も受けれます。
R.image.hoge.png()
UIImage以外にも、R.swiftは以下のリソースにも対応しています。
- Storyboard
- Nib
- Reuse Identifier
- UIImage
- Segue
- etc.
ライブラリ管理
CocoaPodsを利用している方が多いと思いますが、Carthageも合わせて利用すると、すでにコンパイル済みのFreameworkとして利用できるので、アプリのコンパイル時にPodsのライブラリのように都度コンパイルをする必要がなくなり効率的に開発を行うことが出来ます。まだまだすべてのライブラリがCarthageに対応しているわけではないのでCocoaPodsと併用しつつ、コンパイルに時間がかかるSwift言語で記述されたライブラリをできるだけCarthageで利用するのがよいかなと思います。
また外部ライブラリの利用数が多いとアプリが起動しないことがあるバグがiOS 9.3.2以前にあるようなのでライブラリの利用数には気をつけたいところです。
参考
フリルさんのエンジニアブログより
これらの問題は、iOSのバグが原因でした。 iOS 9.3.2で修正されておりアップデートをすれば発生しなくなります。
アップデート後などのメモリ不足のタイミングで、多くのFrameworkを使用しているアプリでのみ発生するようです。
iOS9でアップデート後にアプリが起動できなくなる問題の原因と解決方法
ライブラリ選定
どのライブラリを利用するか取捨選択は悩ましい問題です。自分がライブラリを探すときに利用しているサイトや確認方法を紹介します。
### ライブラリ探し
- スクリーンショット付きでたくさんのライブラリやデモアプリを紹介してくれています。
- Cocoapodsのインクリメンタルサーチができます。 READMEもすぐ見れます。
- ライブラリに限らずiOS開発における有益な情報がまとめられています。
- Githubのリポジトリ検索です。Swift言語でフィルターしつつ、スターの多い順でよく見ます。
- Githubで人気のリポジトリです。注目されている新しいライブラリが発見できます。
### ライブラリお試し
ライブラリにExampleやDemoアプリが付随している場合は、git cloneをして動かしてみます。
ライブラリを利用してコードを少し書いてみたいときは、This Could Be Us But You Playingを利用します。ライブラリを簡単にPlaygroundで利用できる状態にしてくれるコマンドラインツールで、CocoaPodsの作成でもあるneonichuさんが開発されています。
以下はAlamofireをPlaygroundで利用する例です。
$ pod playgrounds Alamofire
以下のように複数のライブラリを同時にPlaygroundから利用することもできます。
$ pod playgrounds RxSwift,RxCocoa
テスト
### 単体テスト
単体テストはXcode標準のXCTestを利用するか、Quickを利用するか悩ましいところです。単体テストとしてやりたいことは双方網羅されれていると思うので、開発者がどのようにテストを記述したいかで選べばいいかなと思います。
QuickはXcode標準ではないOSSですがブライアン(modocache)さんをはじめ、ikesyoさんによりSwift2.3対応もされており、Swiftのバージョンアップにもタイムリーに追随してくださっています。
参考
JPMarthaさんによりドキュメントも日本語に翻訳してくださっています。
### 結合テスト(画面操作自動テスト)
結合テスト(画面操作自動テスト)は以前ではKIFが一番利用されていたかと思いますが、Xcode7から導入されたUI testingがとても使いやすく、UI操作をレコーディングしてテストコードも生成することができるので、UI testingがいいかなと思います。
### テスト補助ツール
iOS Simulatorでのテストはどうしても遅くなりがちですが、FacebookがiOS Simulatorを複数起動できるツールFBSimulatorControlを提供してくれています。( まだ検証できていないので試してみたいです )
クラッシュレポート
リリース後のクラッシュの頻度やクラッシュ時のスタックトレースをモニタリングするためにFabricのCrashlyticsを利用します。社内のテストで検知できなかった問題を認知できる意義は大きいです。発生頻度やユーザインパクトをかんがみつつ優先度付けして、できるだけ迅速に対応していきます。
CI
OSXが稼働できるCIは数が少ないですが、やはりTravisCIが一番有名でしょうか。以前はCircleCIを利用していましたが価格改正を機にTravisCIに移行しました。CircleCIのほうがCPUスペックが高いらしくテスト時間も2/3ぐらいでしたので高いだけのことはあるようです。
CIにおけるテスト実行やMasterへマージ時の自動配布処理はfastlaneを利用します。fastlaneがなかった時代は、各々の開発者が独自仕様のスクリプトを記述していましたが、スクリプトを定型化したのと便利なツール群を用意し組み合わせることで、開発者は少しの記述でやりたかったいろいろな自動化が行えるようになりました。
証明書、プロビジョニング管理
iOSアプリ開発に必要な証明書とプロビジョニングプロファイルをGitHubのリポジトリで一括管理することで、チームの各メンバーが異なる証明書やプロファイルを保持したり、誰かが知らないうちに作り直してしまったとりという煩雑な管理から脱却できす。(1人で開発する場合はあまりメリットがないかもです)
上記の管理ツールとしてfastlaneに付属するmatchというツールを利用します。
参考
iOSアプリ開発に必要な証明書とプロファイルをGitHubのリポジトリで一括管理するがとても参考になります。
フィードバックツール
開発者以外がアプリを利用(テスト)したときのフィードバックを効率的にかつその状況にまつわる情報を含めて取得できるサービスがあります。アプリの開発は想定した仕様を実装したことが完了ではなく、ユーザのフィードバックを知ることをもとに改善サイクルを回すことが重要です。
### Repro
Reproは録画されたユーザー利用動画を見ることで、アプリに何が足りていないかを深く知ることができます。
( Reproは上記以外にも多様な機能があります )
### Balto
Baltoはアプリから1アクションでスクリーンショットとフィードバックが送れるコミュニケーションツールです。
デザイン
アプリ開発は、エンジニアとデザイナーがより手と手を取り合い、お互い協調・協力して開発するシーンが増えてきました。そんなエンジニアとデザイナーとの架け橋となるサービスがZeplinです。ZeplinはSketchで作成したファイルから指示書やスタイルガイドなどを自動で生成できるサービスです。Sketchをインストールしていなくても、プロジェクトのメンバーとデザインを共有でき、画像の書き出しやレイアウトの座標の確認が行えます。
### Sketch
### Zeplin
まとめ
今回は新規でiOSのアプリ開発するにあたって、思いつくことを広く浅く紹介させていただきました。
新規開発を着手する前に、アプリの非機能な要件について全体像を把握しておくと、実装の手戻りが軽減できたり、工数の見積もりにも役に立てると思うので、さぁ新規アプリの開発を始めようというときは、是非1度妄想から入ってみてください。