SwiftUIで実装した新しいホーム画面をリリースしました
こんにちは!iOSエンジニアの高橋です。
今回は、4月中旬頃にリリースをした新しいホーム画面について紹介いたします。また、技術面ではSwiftUIを活用した事例・感想など共有できればと思います!
1. 新しくなったホーム画面
これまでのホーム画面では、多くの情報が溢れており本当に必要な情報が受け取りにくい状態でした。今回はその点を改善するために、シンプルかつ「受動的」なコンテンツを提供できるような設計にリニューアルしました。
こちらでAndroidの取り組みについても紹介されています。
2. SwiftUIの活用
SwiftUIはAppleがWWDC19で発表した、UI作成のための新しいフレームワークになります。UIKitでの実装と比べ、以下のようなメリットがあります。
・よりシンプルなコードで直感的にUI構築が可能
- Viewに対してpaddingやframeなどの情報を付与することで分かりやすいUI実装となる
・実装中のコードからをプレビューを表示することが可能
- これまでシュミレータで動作確認が必要だったデバッグ作業が短縮され、作業効率UPが見込める
2-1. 導入前の検討
先にあげたようなSwiftUIのメリットを活用したいところですが、実際にプロダクトに導入することができるのか検証する必要があります。そのため、SwiftUIを採用するにあたり以下の内容を注目して検討を行いました。
・対応iOSバージョンについて
・SwiftUIで構築可能か?
・開発速度などのメリットについて
2-1-1. 対応iOSバージョンについて
まず、SwiftUIを導入するためにはDeployment Targetを最低iOS13.0以上にする必要があります。(LazyVStackなどを使用する場合は14.0以上が必要です。)ですが、対応前のスペースマーケットアプリはiOS11までサポートしていたため、こちらを変更する必要がありました。そのため、iOSバージョン別の利用率を確認・検討を行い、iOS13以上まで上げることが可能であることを確認しました。また、今回の実装ではLazyVStackなどを使用しているため、今後の対応バージョンのメンテナンスのスケジュール調整も行いました。
2-1-2. SwiftUIで構築可能か?
デザインを確認しながら、SwiftUIで実装可能であることを確認していきました。全てを確認することはしていませんが、実装に不安がある点などは事前に確認して、一通りイメージ通りに動作することが確認できればOKとしました。また、もし実装中やリリース後に予期せぬ不具合があった場合に出戻りを少なく対応できるように、SwiftUIで作成した画面はあくまでViewとして活用することで柔軟に対応できるようにしました。(詳しくは2-2で紹介しています)
2-1-3. 開発速度などのメリットについて
ある程度メリットがなければ、導入する魅力が薄れてしまうため実際にSwiftUIを活用してUI構築などの操作感を確認しました。また、将来的にどこかでSwiftUIに切り替える必要が発生するため、なるべく早い段階でキャッチアップできるというメリットも考慮して検討しました。
2-1-4. まとめ
・対応iOSバージョンについて
☑️ Development TargetをiOS13以上にすることが可能
・SwiftUIで構築可能か?
☑️ 要件に対して全体的に実装可能
☑️ ViewとしてUIKitと組み合わせることで柔軟に対応できることが可能
・開発速度などのメリットについて
☑️ 作業効率UPが見込める
☑️ 将来的にキャッチアップが必要な技術の検証・実用例ができる
以上の内容などを検討した結果、実装できる見込みが立ったためSwiftUIを活用してみることにしました。
また、実装の検討から実装までこちらの記事を参考にさせていただきました!
2-2. 実用例
SwiftUIを活用したいものの、まだ機能不十分な部分があるため不安が残ります。そのため、不具合などに対応しやすいようにController処理はViewControllerに任せ、SwfitUIはあくまでViewとして活用することにしました。(SwiftUIで作成した画面の機能を制限する)
画面の構成としては以下の図のようになります。
SwiftUIで作成した画面のアクションや値の更新などはViewController側で行うため、それぞれDelegateやDataSourceを定義して使用します。
2-2-1. Delegate
ここでは例として、キャンペーンバナーのタップ処理を例にしてDelegate処理を実装していきます。動作としては、バナー画像をタップ→該当ページへ遷移という流れになります。コードではざっくりと以下のようになります。
・BannerView(SwiftUI)
/// Delegateを定義
protocol BannerViewDelegate: AnyObject {
func didSelectBanner(url: String)
}
struct BannerView: View {
weak var delegate: BannerViewDelegate?
var body: some View {
// バナー画像に対してonTapGestureを設定
Image(”バナー画像”)
.onTapGesture {
self.delegate?.didSelectBanner(url: "")
}
}
}
・ViewController
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// SwiftUI.Viewを読み込む
let bannerView = BannerView(delegate: self)
if let view = UIHostingController(rootView: bannerView).view {
// addSubview, Constraintなどの初期設定
}
}
}
// BannerViewDelegateの処理
extension ViewController: BannerViewDelegate {
func didSelectBanner(url: String) {
// 画面遷移の処理
}
}
このようにDelegateを作成することで、アクションに対する処理をViewController側に譲渡しています。
2-2-2. DataSource
上記の図のように、SwiftUIで作成した画面へデータ受け渡しを行うためにObservedObjectを使用します。ObservedObjectはクラス内の変数(@Published)の更新通知を受け取り、Viewの状態を更新するためのプロパティラッパーになります。実装では以下のようになります。
・Banner Model
/// バナーモデル
struct Banner {
var url: String
var imageUrl: String
}
・BannerView(SwiftUI)
struct BannerView: View {
// ObservableObjectプロトロルを継承したクラスで定義する
class DataSource: ObservableObject {
@Published var banners: [Banner] = []
}
@ObservedObject var dataSource: DataSource = DataSource()
var body: some View {
// とりあえず、画像を横並びにするだけの処理で実装しています
HStack {
// bannersが更新された場合、再描画が行われる
ForEach($dataSource.banners.indices, id: \.self) { banner in
Image(”バナー画像”)
.onTapGesture {
// 該当するurlを返す
self.delegate?.didSelectBanner(url: banner.url)
}
}
}
}
}
・ViewController
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// SwiftUI.Viewを読み込む
let bannerView = BannerView(delegate: self)
if let view = UIHostingController(rootView: bannerView).view {
// addSubview, Constraintなどの初期設定
....
// 外部からバナー情報をフェッチ(結果: result: [Banner])
// 結果をdataSource.bannersに流す
view.dataSource.banners = result
}
}
}
細かいUI構築部分は省いていますが、大まかなデータの受け渡しはこちらのような実装になるかと思います。以下のように実装すると、SwiftUI側からViewController側へデータを渡す(ViewController側で購読する)ことも可能です。
import Combine
class ViewController: UIViewController {
private var cancellableSet: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
// SwiftUI.Viewを読み込む
let bannerView = BannerView(delegate: self)
if let view = UIHostingController(rootView: bannerView).view {
// addSubview, Constraintなどの初期設定
....
// 購読処理
view.dataSource.$banners
.sink { banners in
// 処理
}
.store(in: &cancellableSet)
}
}
}
2-3. 良かった点
・UI構築しやすい
従来のStorybordでの実装では、独特な制約や実際にビルドを通してみないと想定通りの画面にできていのかが分からないことが多くありましたが、SwiftUIでの実装では、宣言的にUI構築ができるだけでなくプレビューで画面を確認することができるので、作業効率が上っていると感じることができました。
・UI構築作業の分担がしやすくなった
SwiftUIで構築する場合は、プレビューなどで実装状況を確認ができるだけでなく、分かりやすい制約を追加することで実装ができるためデザイナーに協力してもらいながらアプリ作成を進めることがやりやすくなりました。また、差分も分かりやすくなったのでレビューもしやすくなったと感じました。
2-4. 苦労した点
・UIKitと比べて機能不足
UIKitと比べてSwiftUIには足りない機能があります。足りない部分は、UIKitの処理を呼び出すこともできるのですが、コードが複雑になりがちです。また、うまく処理が呼び出せないこともあるため工夫が必要になることも多くあると思います。その一例としてカルーセルの実装を別記事にまとめています。
・iOSバージョンによって動作が異なる場合がある
iOS14では正常に動作するが、iOS13ではクラッシュするという現象を体験しました。原因としては、frame(width: .infinity)など不要な処理を設定している場合に現象が発生するようでした。最新のバージョン(iOS14)で正常に動作しているので、なかなか気づきにくい部分であるため、念の為iOS13, iOS14共に動作確認する必要がありそうです。
・テストカバレッジを表示したままではプレビューが表示できない
SwiftUI導入初期の段階でプレビューが表示できない不具合が発生しました。原因としては、テストカバレッジを常時表示する設定を行なっていたためでした。テスト実行時にのみ有効にするルールでの運用や下記記事で紹介されているような設定を行うなど、チームの運用ルールによっては苦労する点になるかと思います。
3. まとめ
まだ機能不十分の部分があるものの、SwiftUIを導入することでUI構築の作業効率が上がることを実感できました。また、副作用的にDeployment Targetを上げる必要があったことで、メンテナンスするきっかけにもなったので良いタイミングで導入できたのではないかと感じています。今後も積極的に取り入れてみたいと思います。
エンジニア募集中!
スペースマーケットでは現在アプリエンジニアを積極採用中です。
アプリで社会課題を解決しながら、あたりまえの世界を作っていきたい方がいらっしゃれば、ぜひお話しさせていただきたいです!
この記事が気に入ったらサポートをしてみませんか?