SwiftとiOS開発で学んだこと
原文は Ellie Huxtable により に公開されました。 このブログを購読する
最初に断っておきますが、私は決して専門家ではありません。プログラミング自体は長くやってきましたが、SwiftやiOSの世界は初心者で、モバイルアプリを作るのも今回が初めてです :) これは、Pillionの初代iOS版を作る中でつまずきながら学んだことのすべてです!
Pillionはバイクの駐車場探しを手助けするアプリです — 世界中に約10,000件の駐車場所を掲載しており、一部はOpenStreetMapsから、一部はオンラインデータセットから、残りはユーザーからの投稿です。駐車場情報のほか、走行記録の機能も提供しています。
多くはまだ手探りで進めているときに見つけたことなので、「正しい」やり方ではなかったケースもたくさんあります。より良い方法を学べたところでは、両方のアプローチを併記しています。
もし好評であれば、今後各セクション(特にMapBox)についてより詳しく掘り下げた記事を書くかもしれません。
間違いにお気づきの際は、ぜひ教えてください 😊
SwiftUI
最初から、iOS 13でAppleがリリースした新しいUIシステムであるSwiftUIを使うことにしました。注意点として、対応するのはiOS >= 13を搭載したデバイスのみです — とはいえ、これはiPhone 6S以降のすべての機種にあたります。
長年Reactを使ってきた身としては、SwiftUIはとても心地よく感じます。違いはあるものの、コンポーネントや子要素、stateの扱い方が自然に感じられます。個人的には、Reactのstateの扱いよりもSwiftUIのstateバインディングの方が好みで、コードもかなりすっきりすると感じています。
UIKitは使ったことがないのであまり比較はできませんが、見てきた限りではSwiftUIの方がはるかにすっきりしています。
SwiftUIではいくつか問題にも遭遇しましたが、ほとんどはまだ成熟していないことに起因しているようです — この点については後述します。
残念ながら、GitHubで見つかる素敵なUIコンポーネントのほとんどはUIKit専用でした。いずれラップしてみるかもしれませんが、SwiftUIのエコシステムはまだ少し手薄で、未成熟さを如実に物語っています。覚えておくといいでしょう。
デバッグ
XCodeのデバッガは嬉しい驚きでした!普段はPythonでipdbセッションを使っているのですが、コードがコンパイルされて実機で動いていることを考えると、ここまで柔軟なものは期待していませんでした。
デバッガは値を難なくインスペクトでき、Swiftの式を評価でき、やりたいことは何でもできました!タブ補完もとても快適に動きます。ツールの出来には万歳です :)
たまに少し重くなることもありますが、それは仕方ないでしょう。
UIスレッド周り
私のアプリの処理の多くは、基本的にAPIにリクエストを送って結果を描画するものです。バックグラウンドスレッド(HTTPレスポンスの非同期ハンドラなど)からUIを更新しようとするとエラーになることがわかりました。代わりに、次のようなパターンを使うべきです:
DispatchQueue.main.async {
// do ui
}これでUIスレッドをブロックせずに済むのがとても簡単になります 😁
これは実はSwiftを使い始めてかなり早い段階で読んだことでした。その後、データの取得や更新には、可能な限りObservableObjectsとPublishedプロパティを使うのが定石だと学びました。ただ、念のためここに残しておきます。プロジェクト内でのHTTPリクエストの扱いにはまだあまり満足していないので、将来的に見直すことになると思います。
キーボードを隠す
ちょっとしたことですが、特定の操作でキーボードを隠せるようにしたかったのです。例えば、検索バーにフォーカスがある状態でリスト内の項目をタップしたときに、キーボードを閉じるといった具合です。
extension UIApplication {
func hideKeyboard() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}UIApplication.shared.hideKeyboard()Just
SwiftでHTTPリクエストを送るのはそこまで大変ではありませんが、Pythonのrequestsに慣れているので、似たようなものを探してみました。
Justがまさにそれにぴったりでした!シンプルで軽量、高速で、こんなふうに書けます:
Just.get("https://example.com") { r in
print(r.text)
}…と、とても手軽です!
また、ヘッダーをあらかじめ設定したセッションを作ることもでき、認証が必要なHTTPリクエストに便利です。
Mapbox
このプロジェクトの大部分でMapBox Maps SDK for iOSを使ってきました。MapboxはJavaScriptでは使っていて楽しいのですが、正直なところiOS版は少し物足りません。カスタマイズが面倒なこともあり、ドキュメントもあまり良くありません。何か一つのことをするのにも複数の方法が見つかり、なぜそうするのかという説明がほとんどないことがよくありました。ただ、Android版はここはもう少し良いようです!
悪いと言っているわけではなく、JS版や、私が見た限りでのAndroid版と比べると、少し未完成というか、作り込みが足りないように感じるということです。Androidアプリを作ったら比較してみるつもりです。
また、ドキュメントの一部はUIKit向けのものが多いという点もあります — SwiftUIがまだ新しいことを考えれば当然ですが!念頭に置いておくとよいでしょう。とはいえ、SwiftUI用にマップをラップする方法を示したドキュメントもあります。
Swiftの型チェック
Rustから来た身としては、Swiftの型チェッカーは少し…物足りなく感じます。時には一見明らかな型の推論にあからさまに失敗したり、時間がかかりすぎて開発者にコードを細かく分けるよう求めてきたりします。大きな問題というわけではありませんが、少し煩わしいです。ただ、常に改善されているようです!
場合によっては、実際には問題のない箇所にエラーを表示することもあります。コードを削除すると、本当の原因となっているエラーが表示されます。これはSwiftUIのDSLに関係しているのだと思います。DSL内で型エラーがあると、型を推論できない場合に全体がなんとなく崩れてしまうのです。今は良い例が見つかりませんが、見つけ次第追記します。
Catalinaと最新のXCodeにアップデートした後、それまで存在しなかった大量の「ambiguous member」エラーが出るようになりました。Swiftは誤解を招くエラーメッセージを出す傾向があるようで、これは…不思議です。特にこれほど大きな企業が背後にいるモダンな言語で、同じようなことをするものは他に使ったことがありません。
最終的には、Swiftが理解できるコードを書くのがだんだん上手くなりました。以前書いていたコードも、私の知る限り意味的には正しかったのですが。
SwiftFormat
他のプロジェクトでフォーマッタを使い慣れていたので、ここでも探してみました。SwiftFormatが良さそうです。
SwiftUIのDSLの問題
SwiftUIでは、ViewのbodyはちょっとしたDSLのようなもので宣言します。
var body: some View {
ComponentOne {
AnotherOne {
// so much nesting :O
}
}
}…と、これはこれで問題ありません。
しかし、条件付きレンダリングをしようとすると、DSLはまだ十分に対応できていないようです。
if thing.ouch {
RenderMe(thing.ouch.thing)
}ただ、執筆時点ではif letのようなものは動きません。
if let o = thing.ouch {
RenderMe(o.thing)
}必須ではありませんが、できれば欲しいところです!
DSLには他にも細かな問題がいくつかあり、まだ整理が必要です。ただ、SwiftUIはとても新しい/ベータ版なので、驚きはありません。問題点を除けば、使っていてとても楽しいものです。うまく動くときは、本当に気持ちよく動きます!動かないときは別ですが ;)
Auth0
現在はGoogleとAppleのOAuthログインでAuth0 Lockを使っています。実際には今は自作のソリューションに置き換えましたが、まだ参考になると思うのでここに残しておきます。
Appleの方は本当にクールです!ユーザーはメールアドレスを隠すためにAppleの転送用アドレスを選べますし、FaceIDでログインもできます。
LockはSwiftUI用にセットアップされていませんでしたが、ラップするのはとても簡単です:
struct Auth0Lock: UIViewControllerRepresentable {
typealias UIViewControllerType = LockViewController
var lock: Lock = Lock.passwordless().withOptions {
$0.passwordlessMethod = .magicLink
}
.onAuth { credentials in
print(credentials)
// save access token and use it to do stuff :)
}
.onError{ error in
print(error)
// you should probably do something other than just print errors
}
func makeUIViewController(context: UIViewControllerRepresentableContext<Auth0Lock>) -> LockViewController {
return lock.controller
}
func updateUIViewController(_ uiViewController: LockViewController, context: UIViewControllerRepresentableContext<Auth0Lock>) {
// there isn't really much updating I need to do atm
}
}Auth0には、リフレッシュトークンの処理や新しいアクセストークンの取得、トークンの保存・検証などを担うクレデンシャルマネージャーが付属しています。なので、ユーザーのログイン状態を維持するためにそれほど手間をかける必要はありません!
Auth0をやめた理由を少し補足すると、認証の扱いについてより柔軟性が欲しかったことと、結局バックエンドをDjangoアプリとして書き直したので、Djangoがすでに持っているものと統合するのがとても簡単だったからです。面白いアーキテクチャのことを考えるのに時間を費やさず、プロダクトをより速く作るためにDjangoに切り替えました。
Apple Developer登録
会社(Pillion Software Ltd)名義でApple Developerアカウントに登録するのに、おそらく1〜2週間かかりました — オンラインのフォーム入力自体は早かったのですが、承認されるまで少し時間がかかり、電話もかかってきました。ただ、その後はすべて順調でした!
Fastlaneを使ってスクリーンショットを撮ろうとしましたが、シミュレータやテストがあまりうまく動きませんでした(UIテストでMapboxがクラッシュする)。そのため、実際に機能した唯一の方法は、手動で数枚撮ることでした。あまり良くないですし、いずれ何とかしたいと思っています。いずれ対応するつもりです。私のノートパソコンが非常に遅く、少し古い(主にRAM不足の)こともあり、シミュレータがとても使いづらいというのも一因です。
初めてApp Storeに提出した後、アプリの機能が十分ではないと言われました。がーん。調べてみると、ウェブアプリになり得るものは、Appleはあまり承認したがらないようです。そこで、ロードマップにあったいくつかの機能を前倒しすることにしました。
実際に、あるバージョンでは審査プロセス全体(私が「提出」を押してから公開されるまで)が約7分で完了したこともあり、とても感心しました。
まとめ
振り返ってみると、昔のウェブ版と同じ技術を使うことに飛びつく前に、他の選択肢をもっと調べておけばよかったと思います。もしかしたらMapKitで十分だったかもしれませんし、そうすればMapBoxを使わずに済んだかもしれません。
とはいえ、この過程で多くを学べたので、あまり変えることはないと思います。近々同じものをAndroid向けにも作り始める予定です(Android開発もまだほとんど知らないのですが ;P)、どうなるか見ものですね!今回のような投稿をまた期待していてください。
とにかく、PillionがApp Storeで公開されました!
バイクに乗る方で、試してみようと思った方がいれば、ぜひ感想を聞かせてください — 遠慮なくご連絡ください!
記事をランダムに読む
コメント
ログインしてコメントする