Lessons learned with Swift and iOS development

Ellie Huxtable

SwiftとiOS開発で学んだこと

まず最初にお伝えしておきたいのですが、私は決してエキスパートではありません。プログラミング自体は長くやってきましたが、SwiftやiOSの世界は初心者で、モバイルアプリを作るのも今回が初めてです :) これは、PillionのiOS版を初めて作る中でつまずいたり学んだりしたことのすべてです!

Pillionはバイクの駐輪場探しを手伝うアプリです。世界中に約10,000件の駐輪場情報を掲載しており、一部はOpenStreetMap、一部はオンラインのデータセット、そして残りはユーザーからの投稿によるものです。駐輪場探しのほか、走行記録の機能も提供しています。

その多くはまだ手探りで進めていた頃に見つけたことなので、だいぶ「正しい」やり方とは違うことをしていたケースも多くあります。より良いやり方が分かったところでは、両方の方法を併記しています。

反響があれば、今後各セクション(特にMapBox)について詳しく掘り下げた記事を書くかもしれません。

もし間違いにお気づきの際は、ぜひ教えてください 😊

SwiftUI

最初からSwiftUIを使うことにしました。SwiftUIはAppleがiOS 13でリリースした新しいUIシステムです。注意点として、対応するのは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を使い始めてかなり早い段階で読んだことでした。その後、データの取得や更新には、可能な限りObservableObjectsPublishedプロパティを使うのが定石だと学びました。ただ、必要になる場合もあるかと思い、ここに残しておきます。プロジェクト内の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

現在はAuth0 Lockを使ってGoogleとAppleのOAuthログインに対応しています。実際には今は自作の仕組みに置き換えていますが、参考になると思うのでここに残しておきます。

Appleのログインは本当にクールです!ユーザーはメールアドレスを隠すためのAppleの転送用アドレスを選べますし、Face IDでログインもできます。

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がクラッシュするなど)。そのため、結局手作業で数枚撮るしか実行可能な方法がありませんでした。あまり良くないですし、いずれ何とかしたいと思っています。いずれ対応するつもりです。さらに、私のノートPCがとても遅くて少し古い(主にメモリ不足)ことも手伝って、シミュレータの動きはかなりひどいものでした。

初めてApp Storeに申請した後、アプリの機能が十分ではないと言われました。痛いですね。調べてみると、ウェブアプリになり得るものはAppleがあまり承認したがらないようです。そこで、ロードマップにあったいくつかの機能を前倒しで実装することにしました。

実際に、申請ボタンを押してから公開されるまでの審査プロセス全体が約7分で完了したバージョンもあり、とても感心しました。

まとめ

振り返ってみると、以前のウェブ版と同じ技術をそのまま使う前に、他の選択肢をもっと調べておけばよかったと思います。もしかしたらMapKitでも十分だったかもしれませんし、そうすればMapBoxを使わずに済んだかもしれません。

とはいえ、この過程で多くのことを学べたので、あまり変えたいとは思いません。近々同じものをAndroid向けにも作り始める予定です(Android開発もまだほとんど知らないのですが ;P)。どうなるかお楽しみに!今回のような記事をまた投稿する予定です。

ともあれ、Pillionは現在App Storeで公開中です!

バイクに乗る方で、試してみようと思った方がいらっしゃれば、ぜひ感想を聞かせてください。お気軽にご連絡ください!

原文は Ellie Huxtable により に公開されました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。