- バックエンド
- PdM
- フロントエンドエンジニア
- Other occupations (23)
- Development
- Business
SwiftUIメインの開発者がUIKitの画面を実装するときに役立った3つの工夫
Photo by Igor Savelev on Unsplash
こんにちは、ウォンテッドリーでiOSエンジニアをしている原田祐也 (@yuya_h_x) です。この記事はWantedly夏のアドベントカレンダーの19日目の記事です。
目次
はじめに
SwiftUIとUIKitの違い
UIKit採用画面の構成
1. PreviewマクロでUIの状態を即時確認できるようにする
2. UIの制約の付け方を最小限の例から手を動かして覚える
3. RxSwiftは「やりたいことベース」で必要な範囲だけ理解する
おわりに
はじめに
SwiftUIを中心にiPhoneアプリ開発をしていると、UIKitの実装に触れる機会は減りがちです。しかし、実務では既存の画面構成やライブラリとの互換性、サポートOSの制約などから、UIKitで実装された画面を保守・改修する必要がある場面が依然として存在します。
私自身、ここ数年はSwiftUIベースの開発が中心でしたが、ある施策で既存のUIKitベースの画面に新機能を追加する機会がありました。その際、RxSwift(リアクティブプログラミングライブラリ)やSnapKit(制約を簡潔に記述できるライブラリ)といった技術にも初めて触れました。この記事では、SwiftUIに慣れた私がUIKitの実装に取り組む中で、特に有効だった3つの工夫を紹介します。
これから初めて、あるいは久しぶりにUIKitに取り組むSwiftUI開発者の方々の参考になれば幸いです。
SwiftUIとUIKitの違い
まず大前提として、SwiftUIとUIKitではUIの構築アプローチが大きく異なります。SwiftUIは宣言的な記述スタイルで、状態とUIを同期させる設計が特徴です。一方、UIKitは命令的で、UIコンポーネントを手動で生成・更新し、状態変化を明示的に反映する必要があります。
たとえば、SwiftUIであれば .padding() や VStack を使って柔軟にレイアウトが書けますが、UIKitでは UIStackView や NSLayoutConstraint を用いて逐次的に構成する必要があります。この考え方の違いを把握せずに進めると、実装時に混乱しやすくなります。最初に開発スタイルの違いを整理しておくことは、SwiftUIに慣れた開発者にとってUIKitを理解する第一歩になります。
UIKit採用画面の構成
iPhoneアプリの開発では、現在SwiftUIを中心に画面開発を進めています。しかし、すべての画面がSwiftUIに移行済みというわけではなく、既存の構成や使用しているライブラリとの互換性といった理由から、UIKitを採用している画面もいくつか残っています。
今回担当したのは、そうしたUIKitベースで構築されている画面の1つで、Kotlin Multiplatform(KMP)で定義されたReactorのステートをRxSwiftで監視し、そのステートに応じてUIKitのViewを更新するというアーキテクチャになっていました。UIの制約はSnapKitを使ってすべてコードで記述しており、Storyboardは使用していません。
モバイルアプリのアーキテクチャ
このように、SwiftUIがメインになりつつある環境の中でも、既存の技術スタックや設計上の理由からUIKitを使うケースは依然として存在します。次のセクションでは、そんなUIKitの構成を前提とした画面を実装する際に、SwiftUIメインで開発を行っている私自身がどのように取り組み、工夫をしたかを3つの観点から紹介します。
1. PreviewマクロでUIの状態を即時確認できるようにする
UIKitでUIを確認する従来の方法は、ビルド → シミュレーター起動 → 対象画面へ遷移、という手順が必要でした。小さなレイアウト変更や状態によってUIが変化する場合、これを毎回行うのは非効率です。そこで今回は、iOS 17以降で使える Preview マクロと、then (クロージャ内でプロパティを設定できるライブラリ)を活用し、プレビュー上で状態ごとのUIを即時確認できるようにしました。
以下は、StatusLabelViewという仮のUIを作成し、状態によってラベルの表示・非表示を切り替えるコード例です。
final class StatusLabelView: UIView {
enum Status {
case visible
case hidden
}
private let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
label.text = "テストラベル"
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// UIを更新するメソッド
func update(to status: Status) {
label.isHidden = status == .hidden
}
}
@available(iOS 17, *)
#Preview("Label Visible") {
StatusLabelView().then {
$0.update(to: .visible)
}
}
@available(iOS 17, *)
#Preview("Label Hidden") {
StatusLabelView().then {
$0.update(to: .hidden)
}
}
状態を変更するメソッドを切り出しておくことで、Preview上でも柔軟にUIを確認できます。このように状態単位でPreviewを定義することで、UIKitの実装でもSwiftUIと似た開発体験が得られ、視覚的かつ効率的なUI検証が可能になります。
2. UIの制約の付け方を最小限の例から手を動かして覚える
UIKitでは、UIのレイアウトを定義するためにAuto Layoutを使いますが、コードで制約を記述するのは慣れていないと難しく感じることがあります。今回の実装では、SnapKitというDSLを使って制約を記述しました。SnapKitはAuto Layoutを簡潔に書けるため、コードベースでのUI構築において非常に便利です。
もともとNSLayoutAnchorで制約を書く経験はありましたが、私にとってSnapKitを使うのは初めてだったため、最初から全てを把握しようとするのは学習コストが高く非効率だと考えました。そこで私は、「UIに必要な最小限の制約だけ」に絞り、ドキュメントや技術記事を参照しながら実際に手を動かして覚えていく方針を取りました。
以下は、SnapKitを使ってレイアウト制約を定義する基本的な例です。
someView.snp.makeConstraints {
$0.top.equalToSuperview().inset(16)
$0.leading.trailing.equalToSuperview().inset(24)
$0.height.equalTo(48)
}
こうした基本的な構文(inset、equalToSuperview, equalToなど)から始めて、必要に応じて他の制約の付け方を調べるスタイルにしたことで、短期間でも着実に使いこなせるようになりました。「一通り学んでから書く」ではなく、「書きながら覚える」方が、SnapKitに限らず他の新しいライブラリにも応用できるアプローチです。
3. RxSwiftは「やりたいことベース」で必要な範囲だけ理解する
今回の実装では、KMPで定義されたReactorのstateをRxSwiftで購読し、状態の変化に応じてUIKitのViewを更新する構成でした。ボタンの活性状態やラベルのテキスト、特定条件でのUI表示など、特定のプロパティが変化したときだけViewを更新したいというのが主な目的です。状態が変わるたびに毎回UIを再描画するとパフォーマンスに影響が出るため、プロパティの変化を監視しつつ、不要なレンダリングを避ける工夫が必要でした。
私はRxSwiftを触るのが初めてだったため、最初からすべてを理解しようとせず、「今やりたいこと」に絞って調べる方針を取りました。そして基本的なオペレーターを使いながら、実際に動かして挙動を確認することで理解を深めました。
以下は、RxSwiftの基本的なオペレーターの使い方を示したサンプルコードです。reactor.stateの中から特定の条件に合う値を取り出してUIを更新しています。
reactor.state
.filter {
$0.user != nil && $0.notification?.isUnread == true
}
.compactMap { $0.notification?.type }
.distinctUntilChanged()
.subscribe(onNext: { [weak self] notificationType in
self?.showBanner(for: notificationType)
})
.disposed(by: disposeBag)
- filter: 条件に合う値だけ通す
- compactMap: nilを除外しつつ値を変換する
- distinctUntilChanged: 値が変わったときだけ通知する
特にdistinctUntilChangedは、不要なViewの再描画を防ぐために活用しました。
こうした基本的なオペレーターに絞り、既存のコードや技術ブログから具体例を確認 → 実装 → 挙動確認、というサイクルを何度か繰り返すことで、自然と理解が深まりました。目的を明確にしたうえで、必要な概念だけを段階的に学ぶことで、短期間でも十分に業務で活用できるレベルに到達できます。
おわりに
SwiftUI中心で開発していると、UIKitに対して「今さら学ぶのは非効率では?」と感じることもあるかもしれません。しかし、現場ではUIKitベースの画面を扱う機会はまだまだ多く、対応できるスキルがあると開発の幅が広がります。
この記事で紹介した3つの工夫は、いずれも時間が限られている中で、いかに効率的にキャッチアップできるかにフォーカスした内容です。すべてを完璧に理解する必要はなく、必要な部分に的を絞って実装と学習を両立することで、SwiftUI開発者でもUIKitの実装に対応できる手応えを得られました。
同じような状況にいる方にとって、少しでも課題解決の助けになれば幸いです。