怠慢プログラマーの備忘録

怠慢でナマケモノなプログラマーの備忘録です。

既存プロジェクトへSwiftUIの導入する時の話

2021年も早いもので既に3ヶ月が経過しようとしている中で、2020年は主にAndroidのお仕事が多かったのですが、年明けからなぜかシフトしてiOSを見ることが多くなりました。

最近さまざまなところで顔を出していますが、既存アプリにSwiftUIを実際にプロダクション導入することも増えてきたので、SwiftUI導入するにあたって感じたことを好き勝手に書いていきます。

なぜSwiftUIに移行するのか

今現在世に出ているアプリはほとんどがUIKitベースかと思いますが、これを今SwiftUI化を必ずしなくてはならないかで言うとそうではありません。

というより、サポートOSバージョンを13以上にあげられないアプリも多いかと思います(後述しますがiOS13と14でも使用できるコンポーネントが異なります)。

では今SwiftUIへの移行をまったく考慮しなくてもよいかというと、そういう話ではありません。

個人的にSwiftUIを導入することへのメリットや導入を考慮しているチームは以下のことを考えているかと思います。

可読性は格段に上がる

SwiftUIとUIKitでの可読性でいうと、慣れてしまえば確実にSwiftUIの方がラクに感じるかと思います。巷で「宣言的UI」と言われていますがReactライクな作法でUIを構築できるのは大きなメリットかと思います。 従来はUITableView/UICollectionViewなどを含めたxibを含むコンポーネントの接続にUIKit独自の接続文法が存在して配慮する箇所が多かったように思いますが、この煩わしさが解消されています。

また具体的な文法は異なるもののReactあるいはAndroidJetpack Composeの作法と酷似している箇所がある点では他のプラットフォームのプログラマーとコードベースのコミュニケーションが取りやすくなる未来を想像しています。

developer.android.com

DataBinding(ObservableObject)が使用できる

従来のUIKitベースの設計ではViewの状態を表現するデータ群とViewコンポーネントとのDataBindingが他の言語やプラットフォームとは独立した思想で、個人的には他と比較するとあまり綺麗なものではなかったようにも思います。 それらを解決する為RxSwift/RxCocoaを使用していましたが、これを1度行うとReactive Extensions前提の構成となり依存することとなります。

それを解決するのが後述するCombineフレームワークや@Stateや@ObservedObjectを含むProperty Wrapperです。

これらを使用することでViewの状態を管理するオブジェクトや変数が見やすくなる恩恵があります。

Combineとの相性がいい

SwiftUIと同時に標準の非同期フレームワークであるCombineが追加されました。Combine自体が現状RxSwiftの全てを表現できるまでには少々至ってはおりませんが、非同期フレームワークを長年サードパーティーライブラリに頼っていた歴史を考えると大きな進歩で、積極的に使用していってもいいかと思います。

ただ現状全てをCombineに置き換えるべきかというと個人的には一旦保留にしたい気持ちがあり、上述したようにまだCombine自体がまだ不完全であることと、async/awaitのリリースを待ってから見極めていきたいと思っています。

github.com

UIViewRepresentableという逃げ道

UIViewRepresentableプロトコルを使用することで、どうしてもUIKitでないと表現できないようなレイアウトやデザインが出現した場合にUIKit UIViewをSwiftUI上で表現することができます。

WebViewやMapView、LottieのAnimationViewなどはUIViewRepresentableに閉じ込めて置くことでSwiftUI上での実装が可能となります。

iOS13ではSwiftUIの全てのコンポーネントを使用することができない

iOS13とiOS14ではSwiftUIの使用できるコンポーネントが異なってきます。

例で言うとUIKitのUICollectionViewのように2カラム以上のグリッドを表現したい場合はLazyVGrid/LazyHGridを使用することが考えられるのですが残念ながらAvailability iOS14.0+なのでサポートOSが13も含む場合は、13向けにForEachなどで頑張るしか手段がありません。 (外部ライブラリは複数あります)

developer.apple.com developer.apple.com

またリスト系でよく用いられるPull-to-refreshはSwiftUIのListやScrollViewにはありませんので別途実装が必要です。

yutaabe200.hatenablog.com

ObservableObject/EnvironmentObjectはProtocolでそのまま使えない

@loveeさんのスライドでもありますが、ObservableObject/EnvironmentObjectの内部ではObjectWillChangePublisherが宣言されており単純にProtocolで使用することができません。

今のところObservableObject/EnvironmentObjectとしたProtocolの型をジェネリクスにすることで使用できます。

MVVMなどでViewModelなどInput/Outputで定義して分けている場合などは上記の注意が必要でした。

protocol ViewModelInputs {}

protocol ViewModelOutputs {}

protocol ViewModelType: ObservableObject {
    var inputs: ViewModelInputs { get }
    var outputs: ViewModelOutputs { get }
}

final class ViewModel: ObservableObject, ViewModelInputs, ViewModelOutputs, ViewModelType {
   ...
}
struct ContentView<T: ViewModelType>: View {
    @ObservedObject var viewModel: T
}

みたいな感じになりました。

最後に

これからも気づいたことはこちらに追記・修正していきますので、何か間違ってたり他にありましたらご意見ください🙇‍♂️

また現状iOSアプリにSwiftUI化されたいお仕事も募集してます💪

ご相談はTwitterのDMyutaabe200@gmail.comまでお願いします。

twitter.com