見出し画像

iOSアプリ開発の難しいところ

iOSアプリ開発を始めて7年、仕事にして2年経ちました。
7年もしていれば色々とできることも増えてきたわけですが、それ以上に「アプリ開発難しすぎでは…」と感じるタイミングも増えてきました。

普通に考えたら徐々に技術力も上がってきて、難しさを感じるタイミングは減りそうなものです。
ですが、経験が増えるにつれて気になること、気にしないとダメなことが増えてきました。

この記事では筆者がiOSアプリ開発をする上で気になること、気にしてること、そしてその難しさについて書きたいと思います。
iOSアプリ開発に慣れていない人には「あ、アプリ開発してる人ってこんなこと考えてるんだ」って感じてもらえるような内容にしたいと思います。
iOSアプリ開発に慣れている人は「わかるわかる」と思って読んでいただける内容にしたいと思います。

 注意点
この記事は筆者がiOSアプリ開発を通じて感じた主観に基づく感想です。
「そんなことは難しくない」や「もっと難しい要素がある」といったこともあるかもしれませんが暖かい目で見守ってほしいです。
また、筆者はネイティブのiOSアプリ開発の経験しかありません。Android/React Native/Flutter/Web/Serverその他諸々の経験はありません。他のプラットフォームと比べての難しさではなく、純粋にネイティブのiOSアプリ開発をする上で感じた難しさについて書いていきたいです。

1. 基本編  (「2. プラットフォーム編」まで 読み飛ばし可)

まず最初に初心者が感じそうな難しそうな点について説明します。
エンジニアならこの辺りで難しさを感じる方は少ないと思います。
(注) あくまで"基本"の知識の話です

1-1. プログラミングの"基本"知識

「プログラミングって難しそう」
未経験の人がまず最初に想像するiOSアプリ開発の難しさはこれではないでしょうか?
ただ、Swiftは比較的後発の言語ということもあり、他の言語の良い点を吸収してきた言語なので、色々とコードが書きやすいと感じています。
ある程度コードが書けるようになると、ここに難しさを感じるエンジニアは少ないと思います。

1-2. UIの"基本"知識

iOSアプリにはUIが存在します。なので、UIに関する知識は必須になってきます。
現在iOSのUIをネイティブで実現するには大きく分けてUIKitとSwiftUIの2つの方法があります。
最近だとSwiftUIがかなり注目されていますが、UIKitを触らずにiOSアプリ開発をするのはまだ難しいと思います。
SwiftUIを使っていると「UIKitだったら簡単にできるのに...」と感じる瞬間もよくあります。
なのでSwiftUIだけでなくUIKitも学ぶ必要がありますが、UIKitのたくさんの機能を適切に把握するのはそれはそれで初心者には難しいと思います。
特にアプリ開発でほぼ必須なUITableViewやUICollectionViewはCellの再利用やサイズの計算については、初見だと苦しむ内容な気がしています。

またAutoLayoutも初見だと理解するのに結構な時間がかかると思います。

余談ですが、書店に「アプリ開発入門」みたいな本でSwiftUIを紹介していますが、初心者がああいった本を見て「SwiftUI簡単!」となるのかは若干疑問です。
SwiftUIはSwiftの言語としての機能をかなり多く使っています。
struct, propertyWrapper, associated type, ResultBuilder, someなど結構モリモリでそれが何か分からないと「なぜbodyの中ではfor文を使わずにForEachを使うのか」などが分からないのでは、と思います。

最初にも書きましたが、ここまでの内容ならiOSアプリ開発を仕事にしている人ならさほど難しさを感じないかもしれません。
言い換えればこのあたりに難しさを感じないようでしたら、アプリ開発を仕事にするだけの最低限の技術力はあると個人的には思います。

2. プラットフォーム編

ここからはもう少し踏み込んだ、ある程度経験を積まないと実感しにくい難しさについて説明していきます。

2-1. iOSの仕様は変わる

まず大前提です。iOSの仕様は変わります。
「何をそんな当たり前なことを」と思うかも知れませんが、これを意識できているかいないかは大事なことです。

例えば、iOS12までは「iOSにウインドウはひとつ」という (暗黙的な) 仕様がありました。
(自分も含めた) 多くの開発者はこの仕様を信じていたのではないのでしょうか?
iOSにウインドウがひとつしかないという仕様に基づくと様々な便利メソッドを用意することができます。
例えば今表示されている画面に関わらず、歯車がくるくる回るインジケーターをオーバーレイで画面の中央に表示させる実装を考えてみましょう。
多くの開発者はを自前で実装したりサードパーティーのライブラリを使用していたのではないでしょうか?
そして、その呼び出しのコードはこんな感じではなかったでしょうか?

ProgressIndicator.show()

当時は良さそうに見えました。
しかし、iOS13以降はiPadでマルチウインドウが登場し、今まで開発者が信じていた「iOSにウインドウはひとつ」という仕様はなくなりました。
すると上のコードは途端に動かなくなります。なぜなら、どのウインドウにそのインジケーターを表示するべきかという情報が、このインターフェースには完全に無いからです。
つまり上のコードは「iOSにウインドウはひとつ」という仕様に依存したコードだったわけです。

もしかすると「ウチのアプリはiPad対応する必要がないからマルチウインドウのことを考える必要はないよ」と思う方もいると思います。
もちろん、実際にiPadに対応するかどうかはケースバイケースで、マストではないので問題ないかもしれませんが、その考え自体も「iPhoneではウインドウはひとつ」という仕様に依存していることに気を付けてください。

もちろんまだ見ぬiOSの仕様を予測することは難しいですし、無理やりiOSの仕様に依存せずにコードを書いたりすることはYAGNIの観点からやりすぎな可能性もあります。
大事なのは、今自分が書いているそのコードはiOSの仕様に依存しているのかどうかをちゃんと意識することです。
上のインジケーターがiOSの仕様に依存しているものだと認識できていたらプロジェクトで広く使うのは避けようとしたかもしれません。
トレードオフを考慮して、導入するかどうかの検討をすることもできます。
少なくとも「iOSではウインドウはひとつと決まっているので全く問題なし」とノーリスクだと信じ、深く考えずに導入することはなくなります。

2-2. iPad対応

iOSアプリはiPadで動かすこともできます。
iPadに最適化されていないアプリでも、iPhoneの画面を引き伸ばしたようなデザインでアプリを使うことはできますが、最近はiPadに最適化されていることが当たり前になりつつあります。

iPadに最適化するにはどのようなことを考える必要があるでしょうか?

まず最初に思い浮かぶのはデザインだと思います。
iPadの場合、単純に面積が広くなっただけでなく、縦長の画面や横長の画面も考慮する必要があります。
SplitViewを使って画面を半分にしたり、1/3にしたり、全画面にしたりをシームレスにサポートした方がiPadOSに自然に適合できていると思います。

次にキーボードやマウスに対する最適化です。
iPhoneでもキーボードなどは使えますが、ユニバーサルコントロールが登場した今、iPadをよりキーボードやマウスで操作する機会は増えました。
マウスに対するUIは比較的フレームワーク側でサポートされますが、キーボードショートカットなどは開発者側で対応する必要があります。

これ以外にも、iPadOSにはiPadOSとしての守るべきUIの作法があり、iOSと同じ感覚で作ってもiPadOSに最適化されたアプリを作るのは難しくなりつつあります。
また先ほど述べたマルチウインドウに対応する必要もありますが、それは後で説明します。

2-3. AppleWatch対応

iOSアプリはAppleWatchに対応することもできます。
AppleWatchに対応するのは決して難しくないですが、iOSアプリに含まれているコードを再利用してWatchアプリを作成したい場合、結構難しくなります。
(多くはないですが) iOSアプリでしか使えないフレームワークにアプリ全体が強く依存していた場合、そのコードを再利用することは難しくなります。
無理やり対応しようと#if os(iOS)のようにOSで分岐するマクロを使うこともできますがコードは途端に複雑になるのであまりしたくないという思いもあります。

またFirestoreは現在AppleWatchをサポートしていません。
Firestoreを採用することとAppleWatchをサポートすること、どちらが重要かはケースバイケースです。
ただし、少なくともFirestoreを採用する際に「今後AppleWatchのサポートが技術的な理由でできなくなる」ということが許容できるかどうかは検討してもいいかと思います。

2-4. Siri対応

iPhoneにはSiriが搭載されています。
Siriは声で操作されるものと捉えがちですが、実は違います。
例えば「Siriからの提案」ではユーザの行動を学習し、適切なタイミングで適切なアクションを提案することができます。
もしくはURLや画像を友達に共有するときに適切な連絡先を提案してくれるようになります。
またWidgetを適切なタイミングで提案することもできます。
このように、Siriを通じてアプリの"外"でもユーザとアプリがコミュニケーションを取ることができます。
これはビジネスの観点からもプレゼンスを高めることができるので非常に有効なことです。
これらの機能を実現するためにはアプリとSiriが適切なコミュニケーション取れている必要があります。

また最近のiOSは色々な機能をSiriを通じて提供するようになりつつあります。
iOSとアプリが深いところでSiriを通じて結びつくことはより一層重要になってきています。

もちろんSiriに対応するかどうかもマストではないので、しないという選択肢もありです。
ですがPMやPlannerはSiriに対して詳しいわけではないので、iOSエンジニアが常に「この機能はSiriとの相性はどうだろうか?」と考え続け、時にはエンジニア側からSiriの機能を提案する必要があります。

またSiriの機能はデバッグが難しいものが多いです。
知見や情報もほとんどネットにはなく、挙動はブラックボックスになっていて、エンジニアですら、なんでうまく動かないのか分からない時も少なくないです。

そして影響範囲を適切に把握することも難しいです。
SiriはiOSの裏で繋がっているので、Siriショートカットの実装をしているとその情報が別のSiriの機能に影響を与えることもあります。

このようにSiriの機能をしっかりと把握し、提案することもiOSエンジニアが注意するべきことだと思います。

3. UI編

iOSアプリはGUIアプリケーションですが、このUIもアプリ開発を複雑にしていく要因です。

3-1. パフォーマンス

iPhoneのリフレッシュレートは60Hz、ProMotionだと120Hzの時もあります。
これは、言い方を変えると、1秒間に60回、または120回画面が更新されるということを意味します。
さらに言い方を変えると0.016秒(1/60秒)、もしくは0.008秒(1/120秒)に一回画面を更新する必要があります。

なのでUIに関してシビアなパフォーマンスが求められます。
例えばあなたのアプリの中の画面をスクロールした時に、スクロールのポジションに応じて何か特別な処理をしたとします。
その処理が0.016秒以内に終わるものであれば全く問題ないのですが、もし0.016秒以上の時間がかかったとしたら、画面はどうなるでしょうか?
答えは簡単で、画面は固まります。
そして処理が終わった瞬間、画面のスクロール位置が一気に変わります。
これは固まらなかったら移動していたであろうスクロール位置に、固まった後に一気にジャンプするからです。
この現象のことをScroll Hitchといいます。

「うちのアプリ、スクロールすると時々アニメーションが止まって綺麗じゃないんだよなぁ」と思ったならそれはHitchが発生しているからかもしれません。
Hitchについてはこれ以上詳しくは説明しないので、興味がある人はAppleの動画を見てください。

iOSエンジニアはUIに対して正しく実装する責任があるので、Hitchは可能な限り無くすべきです。
重たい処理などをメインスレッドではなくバックグラウンドスレッドに渡してあげることで、ある程度は解消できますが「UIに関するデリゲートメソッドを同期的に処理する必要があるせいでバックグラウンドスレッドに渡せない」など色々な事情で完全に取り除くのはなかなか難しいです。

3-2. マルチウインドウ

このあたりから難易度がさらに上がります。
iPadについて説明した際にマルチウインドウの例を出しました。
適切にマルチウインドウに対応するのはかなり難しいです。

マルチウインドウの何が難しいのでしょうか?
まずは一つ目にシングルトンが使えないシーンがかなり増える、という点です。
これは比較的簡単に回避できます、シングルトンを使わないように気をつければ解決できる問題です。(そもそも僕はシングルトンは作りたくないですが…)
ただすでにViewManager.shared.currentViewControllerみたいなものが広く使われていたりすると、対応には苦労するかもしれません。

もっと難しい問題として、アプリケーションの状態の同期です。
例えば初回起動時にログインを要求するアプリを考えます。
初回起動したタイミングでAとBの2つのウインドウを用意すると、それぞれにログイン画面が表示されます。

2つのログイン画面が表示されているiPad

この時、Aのウインドウでログインして画面遷移した時、Bのウインドウはどう振る舞う必要があるでしょうか?
おそらく、AのウインドウでログインしたタイミングでBのウインドウもログインしたことにして、Bのウインドウでも遷移する必要があります。
もしログインに2段階認証が必要だったら、画面遷移のたびに全てのウインドウでの同期が必要になります。
さらに、ログインボタンを押している間は画面全体にインジケータを表示するUIの場合、すべてのウインドウでインジケータを表示する必要があります。

このようにアプリケーション全体の状態に応じてウインドウ間での同期が必要になります。
案外これは見落とされがちなケースで、それなりに有名なアプリでも挙動がおかしくなることがあります。

3-3. Dynamic Type

アクセシビリティの機能としてiOSにはDynamic Typeがあります。
Dynamic Typeはユーザの設定に応じて文字や画像の大きさが変わる機能です。
通常の文字のサイズだと字が小さくて読めない、という方に向けた機能です。

一見、ただ文字の大きさが変わるだけで、影響はほとんどないように思います。
ですが、この機能の難しいところはレイアウトが簡単に崩れてしまうことです。
「文字の大きさが変わる」ということは文字の高さが変わることを意味します。
みなさんのアプリのデザインは文字の高さを決め打ちで実装していませんか?
また、1行に表示できる文字数も変わります。
「ここにはn文字の言葉しか入らないし、1行で大丈夫」みたいな判断をしていませんか?

例えばAppleの設定アプリをみてみましょう。
これは普段僕が使っているフォントサイズで表示した設定アプリです。
僕はフォントサイズを一番小さいサイズにしているのでこのように表示されます。

小さいフォントサイズの設定アプリの画像

次にフォントサイズを大きくした設定アプリがこちらです。

大きいフォントサイズの設定アプリの画像

設定アプリはちゃんとDynamic Typeに対応しているので、上の画像のようにフォントサイズを大きくしても複数行になって表示されますし、セルの大きさも正しく変わります。

しかし、これを実現するのは決して簡単なことではありません。
上の設定アプリは簡単なレイアウトでしたが、複雑なレイアウトになったときに文字サイズが変わっても、適切なレイアウトを維持することは簡単なことではありません。
Appleはプラットフォーマーということもあり、多くの画面でアクセシビリティ対応していますが、サードパーティのアプリのほとんどはDynamic Typeや他のアクセシビリティに対応しきれていません。

また「最初に一つのフォントサイズでデザインをして、それを後からフォントサイズを変えたものに展開していく〜」となりがちですが、その方法ではうまくいかない、もしくはできてもかなり複雑な仕様になることが多いです。
できるだけ最初のデザインを決定する段階でDynamic Typeのことを意識する必要があります。

UIのデザインをする人がDynamic Typeを意識していればいいのですが、そうでないケースも多いため、エンジニアもデザインが渡されたタイミングで「これのDynamic Typeの対応はどうしますか?」と一度聞くと良いと思います。

アクセシビリティ対応が必須でないと考える方がいるかもしれませんが、これは立場を逆にして考えてみてください。
もしみんなが話題にしている文書が点字のみで書かれていたら多分僕は「文字が印刷しされた文書も用意して!」と叫びます。
ユーザー数を考えてマジョリティな層に届けることも大事ですが、利用したい人が利用できるアプリ、ということも大事だと思います。

4. 設計編

アプリを開発する上で、そのアプリがスムーズに成長できる状態を維持することも当然必要になってきます。
なのでユーザーや他の部署の人からは見えないアプリの設計についても、一定の注意を払う必要が出てきます。

iOSアプリ開発ではたびたび設計についての議論になりますが、その理由として設計は以下の課題とそれぞれ密接に関係しているからだと個人的には思います。(もちろん他にももっとあると思います)

  • ビルド時間の削減

  • テストのしやすさ

  • Xcode Preview

  • ミニアプリ

  • 環境への依存

アプリの設計とこれらの課題がどう関係しているのか、なぜそれが難しいのか説明していきたいと思います。

4-1. ビルド時間の削減

アプリを作り始めた時点では気にならなかったビルドのスピードが、アプリの成長とともに徐々に遅くなっていき、いつの間にかビルド時間がボトルネックになってしまう、というのはよくある問題です。
この問題の解決方法の一つとして、キャッシュを使うことが考えられます。

モジュール単位でキャッシュされるため、キャッシュを有効的に使うためには1つのアプリを複数のモジュールに分割する必要があります。

また、あるモジュールを変更した際、そのモジュールに依存している他のモジュールも再ビルドする必要があります。
再ビルドされるモジュールは少ない方がいいので、モジュールの依存関係もビルド時間に影響を与えます。

つまり設計を考える上で、ビルド時間のことを考えて、適切な単位、適切な依存関係でモジュールに分割することを考慮する必要があります。

4-2. テストのしやすさ

テストがしやすい、ということは他の開発同様、iOSアプリ開発でも重要になってきます。
テストが簡単にできるように適切な依存関係を保ったり、インターフェースの切り出しなどは一見簡単そうですが、難しい要素です。
特に開発者はシングルトンの誘惑と常に戦う必要があります。
シングルトンはどこからでもアクセスできるので、開発のスピード自体は(その場では)上がりますが、テストをすることはかなり難しくなってきます。

またiOSアプリ開発ではUIというテストしにくいものを対象に開発する必要があります。
できるだけUIの中で状態を管理するのではなくて、外でUIの状態を管理する設計にする必要もあります。

4-3. Xcode Preview

UIのテストはしにくいと書きましたが、ではUIのデバッグはどうするかというと、現状Xcode Previewが有効な方法です。
Xcode Previewは (触ったことないのでわからないのですが) FlutterのHotReloadのようなもので、コードの変更をPreview画面に表示されているUIに対してリアルタイムに反映することができます。
SwiftUIのための機能と思われがちですが、これはUIKitのコードに対しても動きます。

しかしXcode PreviewはUIに対するデバッグ手法であり、他の要素 (DBやビジネスロジック) をデバッグするための良い手法ではありません。
なのでUIから直接他の要素に依存を持つのはあまり良い状態ではありませんし、Xcode Previewを使うハードルも上がります。
XcodePreviewを有効に使うため、UIをできるだけUIのみで完結させる必要がありますが、これも適切な設計ができている必要があります。

4-4. ミニアプリ

ミニアプリとは、アプリの一部の機能のみを取り出して動かせる小さいアプリのことをここでは意味しています。

ミニアプリには様々なメリットがあります。
例えば、単にビルド対象のソースコードが少なくなるのでビルド時間が早くなる、起動直後デバッグしたい画面に遷移させる、などがあります。

このミニアプリを実現するためには、ミニアプリにしたい機能同士が依存することができません。
例えばタブAとタブBの2つのタブをもつアプリに対して、タブAのみのミニアプリ、タブBのみのミニアプリ、の2つのミニアプリを用意することを考えます。
それぞれが独立したアプリとして成り立つためには、タブAとタブBのソースコードがお互いに独立したモジュールになっている必要があります。

しかし最終的な成果物であるアプリが1つのアプリである限り、アプリの仕様としては相互のタブが独立せずに連動する必要があります。
タブAのボタンを押すとタブBに遷移する、であったり、タブAでのアクションがタブBにのコンテンツに影響を与える、などタブ同士の連携はアプリとしては必須です。

なのでミニアプリを実現するためには、アプリ全体を支える共通のモジュール、そして各機能を構成するモジュールを適切に分割し、適切な依存関係にすることが、アプリを設計する際に求められます。

4-5. 環境への依存

最後に環境への依存の問題です。ここでの環境とは、OSやAppExtensionのことを指します。

上で述べたようにiOSアプリ開発をする際に、AppleWatchの対応をすることもあると思います。
さらにアプリによってはMac版のアプリも存在するでしょう。
AppleWatchやMacに対応しなくても、WidgetのようなAppExtensionに対応するアプリは結構多いのではないでしょうか?

そんな場合、やはりできるだけソースコードを共通のものにしたいです。
なので、実行環境に依存するモジュールと実行環境に依存しないモジュールに分割しておきたいです。

アプリの設計を考える上で、どのモジュールは環境に依存したらダメなのか、どのモジュールは環境に依存したらダメなのかを考える必要がでてきます。

4-6. 設計のバランスと断続的な更新

ここまで色々とアプリの設計をする上で考えるべきことについて説明しました。
個々の要素だけ見るとそれほど難しくないものもありますが、同時に全ての要素を実現しようとするのは結構難しいです。

なのでプロダクトに応じて適切なバランスで、どの要素に力を入れるのかを考えるべきです。

また、未来を予知して完全な設計を最初に作ることは難しいです。
中には全く想定していない方向にアプリが成長することもあります。
ですが、こまめに設計を見直し適切に設計を修正していれば、大きな破壊的な変更にはそう簡単にならないと思います(それでもそうなってしまう時はありますが…)

なので開発者は機能を追加したりするたびに、「今の設計が本当にベストなのか」を絶え間なく考え続ける必要があります。

おわりに

iOSアプリ開発をする上で開発者がどんなことに難しさを感じるのか、そしてどんなことに注意する必要があるのかを説明しました。
どれかひとつはできても、これらを同時に全て完全にこなすのはかなり難しいです。というか筆者自身、できていないことだらけです。
やはりiOSアプリ開発は難しいなぁって思います。

みなさんの思うiOSアプリ開発の難しいところもぜひ教えてください。

この記事が気に入ったらサポートをしてみませんか?