普段はLaravel、Vue.jsで開発しているPHPerですが、SwiftでiOSアプリを作ることになりました。
色々と苦労しましたが、意外となんとかなったので、メモ程度に残します。
目次
まず何から始めればいいの...?
開発環境の準備
ベース構築
開発開始
まとめ
まず何から始めればいいの...?
モバイルアプリについて何もしらなかったので、ここからスタートです。。
少し調べると、ネイティブとクロスプラットフォームがあり、クロスプラットフォームはiOSでもAndroid両方対応している。
ネイティブ
Swift
クロスプラットフォーム
Flutter(Dart) 、React Native(JavaScript)
どの言語にする?
今回の要件として、装置とBluetooth接続する必要がある。
クロスプラットフォームだと、iOSとAndroid両方でBluetooth通信が上手くいく気が全くせず、Swiftを選定。
フレームワーク
UIKitとSwiftUIがある。
UIKitは複雑なことができるけれど、データバインディングが大変そうという印象。そしてなぜかMVCが推奨されている(MVVMじゃないの?)。
下記の理由からSwiftUIを選定。
- アプリの規模が小さめ
- Swift自体に不慣れ
- 開発スピード重視
- SwiftUIの方が最新
アーキテクチャは何があるのか
調べた限り、MVVM(Model + View + ViewModel)が有力候補。
2番手として、MV(Model + ViewModel)も見かけました。というのも、2019年にSwiftUIが発表されたことで、宣言的UIになったことが関係している。宣言的UIの理解は今でもふわっとしていますが、感覚としては「UI(View)を先に作る」ぐらいの感覚。。
PHPだとMVCが有名ですが、MVVMではControllerの代わりがViewModel。
そもそもRequestがないので、そりゃそうかって感じでした。
Controller -> Requestを受け取る
ViewModel -> 画面のイベントなどを検知する
ただ、今回の案件の規模は大きくない、Swiftの知見がない、ということから、バインディングで手こずる危険性もあったためMVとしました。
キャッチアップ開始
会社で書籍購入制度があるため、ひとまずSwiftUIの書籍を購入して8割ぐらい読みました。全て読み終えるまでに開発に入ってしまったので、途中で終わってしまっています。
基本的なことはこの1冊でなんとかなりました。
開発環境の準備
基本的なところはわかったので、次は開発環境を準備しました。
エディタ
基本的にはXcodeです。
プロジェクトの設定、デバッグ、ファイルの追加・削除、アプリのビルド(スマホにアプリをインストール、更新などを行う)はXcodeで行うようにしました。
ただ、下記の理由からコードはVSCodeで書いてました。
- Copilotが使えない(設定すれば使えるかも)
- コメントアウトすると「//」が行の先頭について、戻すとインデントがずれる
- コードの入力中にエラーが出て面倒
- Xcodeが日本語対応していないため、わかりづらい
ライブラリ
Composer的なものがあれば楽できるかも(そんな都合のいいものはない)と思い調べていると、CocoaPodsがメジャーみたいなので、とりあえず使える状態にする。
https://cocoapods.org/
整形ツール
SwiftFormatを使用。Apple公式なので安心?
CocoaPodsでインストールして、VSCodeの拡張機能をインストール・設定したら使用できるようになりました。
Git
プロジェクト直下から管理対象にしました。
.gitignore はまだ開発初期でよくわからなかったので、ネットで検索して適当なものを記述しました。
Apple開発アカウントの問題だと思いますが、プロジェクトの設定を変更すると、自分以外はビルド時にエラーが発生。開発アカウントを選択し直せばエラー解消できるものだったため、最後までゴリ押ししました。。
ベース構築
開発の超ざっくり方針、開発環境は整ったので、メンバー間で混乱しないようベース構築を開始。
ディレクトリ構成
- Components
ヘッダー、フッター、トーストなど - DataTypes
データクラス(プロパティを定義したクラス) - Delegates
Delegate(InterFace的なもの) - Extensions
拡張した構造体を格納 - Info.plist
プロジェクト作成時に作成される - Managers
Bluetoothなどを制御するクラス(Delegateメソッドが大量にある) - Models
-- Entities
-- Enums
-- Services - Preview Content
プロジェクト作成時に作成される - Resources
Assetsなど - Shared
全体で共有するEnviromentオブジェクト(Bluetoothのクラスなどはシングルトンが望ましいため) - Utilities
helperクラスなど - ViewModels
ViewModelを格納(VMでは厳しくなり、最終的に一部にのみ採用ViewModelを採用) - Views
構造体を格納
DB接続
今回はDBに保存する程のデータがないため、UserDefaultsのラッパークラスを作成。
Utilities
日付取得(JP)や、環境変数取得クラス、ライフサイクル管理クラスなどを定義
Model関連
Modelクラス、Enum、Serviceクラスを1つずつ、サンプルとして定義
ライフサイクル
AppDelegate、SceneDelegateの2つのライフサイクル取得方法がある。
なぜ2つあるかというと、UIKitがAppDelegateを使用していたためだと思います。
SceneDelegateは画面を開いたことにより検知されるため、キル状態で短時間だけ起動したとき(リージョン検知、プッシュ通知)には検知されないため注意が必要。
※ここでめちゃくちゃハマりました
キル状態で起動する必要があったため、両方定義しました。
開発開始
だいたいの方針は決まったので、開発を進めていきます。
PHPの感覚だと違和感があった点、悩んだ点などを書いていきます。
nil
nilです。nullじゃありません。
名前の違いだけならいいのですが、PHPとは違って扱いが厳しいです(PHPStanのレベルによっては同じかも)。nilが許可されているプロパティは、必ず下記でアンラップしないとエラーになります。
アンラップでnilではないことを確認します。
ロジックをどこに記述するか
今回はMVを採用しています。ロジックはServiceクラスへの記述になります。開発の規模が大きいなら、MVVMを採用し、ViewModelからUsecaseなどが良いのだと思います。
実際、今回の規模が大きくありませんでしたが、データの取り回しが厳しくて最終的には一部ViewModelを採用しました。
共通データは使用していましたが、Model以外で使用していました。それとは別にModelのプロパティを監視したかったので、ViewModelを途中から採用しました。
Environmentオブジェクト
プロジェクト全体で使用できるクラスです。使用するときにObservableオブジェクトとして使用するため、データ変化を監視できます。
ReactやVueの状態管理と同じ考え方だと思います。
State(StateObject)の限界
Enviromentオブジェクトとは別に、一時的な用途だとこちらを使用します。
このStateですが、数が多くなると辛くなってきます。子コンポーネントに値を渡し、もしそれが孫コンポーネントでも使用されたりすると訳がわからなくなります。
さらに、値が監視されるのでデータ変化で再描画が走ります。Stateの数が多いと、なぜ再描画されているのかわからなくなり、制御不能に陥りました。
この問題があって、Stateの多いところはViewModelへのリファクタが発生しました。
構造体とクラスとは
構造体(Struct)とクラス(Class)が存在します。
構造体は値型、クラスは参照型です。基本的にはStructを使用するというのがApple公式では言われているようです。
Model関連, Managerはクラスで定義していて、それ以外はほとんど構造体で定義しました。
init
コンストラクタです。
Viewではあまり使用しない方がいいのかなと思いました。Stateなどの扱いが複雑になります。
極力使用せずに、ViewだとonApper()を使用しました。
Delegate
インターフェースです。
例えばBluetoothを使用する場合は、Appleが用意しているBluetoothのDelegateを実装(implements)して使用します。
ドキュメント
PHPに比べると少ないです。
これが結構辛くて、公式もわかりやすいとは感じなかったです。公式を読んでわからなければ、ChatGPTを使用するのをお勧めします。
ただ、ChatGPTは2019年9月以降のデータを持っていない(2023年9月時点)ので、直近1〜2年ぐらいの記事を探すのもあり。
画面遷移の方法
NavagationStack + Destinationか、sheetを利用する方法があります。
最近のiOSアプリだとsheetをよく見かけますし、UXの観点からsheetが適切ならsheetを使用するのが良いと思います。
画面遷移のフラグ管理
NavigationStackを記述して、NavagationStackのDestinationの引数(Bool)で外部から画面の切り替えが可能です。
引数はEnvironmentオブジェクトで管理するのがよいのかなと思いました。
画面遷移時の注意点
Viewの表示、非表示時はonAppear、onDisappearが対応していますが、onAppearは子Viewから戻った時も実行されます(NavaigationStack + Destinationで遷移しているからかも)。
戻るボタンをカスタムで作成し、カスタムしたボタンにクロージャーを渡して回避しました。
NavagationLink
iOS14以降で、NavigationLinkは非推奨で警告が出ます。NavigationStackを使用すれば大丈夫です。
gestureの優先順位
リストがあり、リストはスクロールするリストです。リストの要素はタップ、ロングタップで選択します。
ここで、スクロール + タップ + ロングタップのgestureを共存させ、さらに優先順位を制御するのに苦労しました。
タップ、ロングタップを定義して、下記を使用しました。
https://developer.apple.com/documentation/swiftui/gesture/exclusively(before:)
トースト
トーストはなかったので自作しました。
overlayのToastViewを作成して、設定秒後に自動で消えるようにしています。
どのViewでも表示して欲しいので、トップのViewで、ZStackの中にToastViewを記述しました。
ダイアログ
ダイアログもトーストと同じ要領で追加しました。
Alertで良いところはAlertを使用しています。
権限管理
細かく権限のリクエストが必要です。ユーザーとしては安心ですね。
ちなみに、権限をリクエストするときはインスタンスを作成すると勝手にリクエストされるものあり、リクエストのタイミングは工夫する必要があります。
extension の命名規則
私が知らないだけかもしれませんが、独特だなと思いました。
色の指定をするとき、Colorの16進数で指定できないため、Color構造体を拡張しました。
命名としては下記のように「+」で繋ぐのが一般的なようです。
Color+Hex.swift
.env
最後まで何が一般的なのかわからなかったのですが、デフォルトでは存在しません。
Xcode上で、GUIで指定できるものがあったので設定してみたのですが、デバッグのときしか使用できないものでした(気付くのに時間がかかった)。
結局自作しました。
ログ出力
これも最後まで何が一般的なのかわからなかったのですが、デフォルトでは存在しません。
Logクラスを自作して、ファイル生成、ログ出力するようにしました。iPhoneから確認できる場所にファイルを生成して、iPhoneからリアルタイムにログを確認できるようにしました。
サーバーからの通知を受け取るなど、キル状態でも動作するアプリの場合は必要かと思います。
※.envの状態を見て出力は制御しています
ライトモード・ダークモード
使用する色はXcode上で、GUIで追加できます。
具体的にはAssetsから追加でき、ライトモード・ダークモードの色指定にも対応しています。
Assetsでも指定しなくても、プログラム上でどちらのモードか判定できる変数は元々用意されているのですが、Viewが煩雑になるためAssetsで指定しました。
画像
画像もAssetsで保存します。画像ファイルを直接設置するのではなく、Assetsへ保存しました。
試していませんが、ディレクトリに画像を設置することも可能な気がします。
ただし、アプリのアイコン画像だけはAssetsでで設定する必要があります。
まとめ
初めてのSwiftで、自分なりに調べながら進めました。
反省点
一番の反省点としては、MVVMを始めから採用しておけばよかったことです。SwiftUIだとデータのバインディングが容易なので、学習コストもほとんどないと思います。
調べていたときはUIKitの記事を見ていて、これは学習コストが高そうだと思ってしまったのが失敗でした。
ライブラリについてもっと知りたい
Webで当たり前にあるものがなく自作したけれど、もっといいやり方があったのかもしれません。
CocoaPodsもSwiftFormatしか使用しませんでした。もちろん自作のメリットもありますが、axiosのようにメジャーなものなら採用したいし、そういうものがあるなら知りたいなと思いました。
UIKitについて
SwiftUIフレームワークが最新とはいえ、細かいカスタマイズはUIKitがよいという意見をよく見かけます。確かに細かいカスタマイズはUIKitでないとできないことがありましたが、このクラスでのみUIKitを使用するといったことは可能で、SwiftUIとの共存は可能です。
データバインディングはSwiftUIだとシンプルにできるので、大規模プロジェクトであろうとSwiftUIでよいのではと思っています。
ただ、UIKitを使用したらまた意見も変わると思います。
まだまだ知らないことが多い
開発が終わった後、Swiftの学習サイトなどを見ると知らない単語が多かったです。StoryBookとかよく見かけますが、よくわかりません(jsのライブラリと同じ感じ?)。特にUIKitに関連するものが抜け落ちている気がします。
今回はWebサーバーとの通信、DBなしでそのあたりもよくわかっていません。
今後
今後はAndroidアプリ開発があるため、そちらもキャッチアップが必要です。。
Androidアプリが終わったら、ReactNativeも個人的に触ってみようかなと思います。