UIKitからSwiftUI転換期のiOS開発 〜心理的ハードルと乗り越え方〜
1. はじめに
本記事は、以下のような方に向けて執筆しました。
✓ UIKitでの開発経験が長く、命令的UIの開発に慣れている方
✓ SwiftUIに興味はあるものの、実践に踏み出せていない方
✓ チームでのSwiftUI導入に悩みを抱えている方
✓ 宣言的UIのパラダイムの違いに不安を感じている方
転換期における心理的ハードル
「SwiftUIって確かに良さそうだけど、今更学び直すの大変そう…」
「UIKitで今までうまくいってるし、今から変える必要あるのかな…」
こんな思いを抱えているiOSエンジニアの方は少なくないのではないでしょうか。
私自身、UIKitでの開発に携わってきた一人であり、
SwiftUIの利用になかなか踏み出せずにいました。
チーム全体の課題として捉える
なぜ今SwiftUIなのか
SwiftUIが2019年に発表され、iOSだけでなくAndroidやReact・Flutterなど、モバイル・Web双方で宣言的UIの採用が標準的な流れとなってきています。
新しい機能はSwiftUI優先で追加されていく
例えば、iOS14から導入されたホーム画面にも置けるウィジェット(WidgetKit)はUIKitでの実装は不可能であり、画面要素はすべて SwiftUI で作成する必要があります。
(後述するUIViewRepresentableの利用も許されていません)
公式参考URL:https://developer.apple.com/documentation/widgetkit/swiftui-views
Widgets can’t use UIKit or AppKit views wrapped with UIViewRepresentable or NSViewRepresentable.
実際に経験した困難な状況
ある年の新OS対応の中で、とあるクラスが新OS以上で非推奨となり、
推奨される処理はSwiftUIで実装する必要があると分かりました。
それまでプロジェクト内ではまだSwiftUIを導入しておらず、
自身にもチームにも知見がゼロの状態で実装することになり、
公式ドキュメントなどを参考になんとか実装自体はできたものの、
SwiftUI特有のライフサイクルの記述方法に慣れていなかったこともあり、
バグ発生時に原因特定・解消に手間取りました。
限られた時間の中での慣れない開発だったため苦い思い出となり、
この時はたまたま規模が小さい機能だったのでなんとか対応できましたが、
もっと影響範囲の大きい処理Or機能だったらどうなっていたことか。。
という大きな懸念を感じた出来事でした。
モダンな開発手法への移行は避けられない
上記に挙げた昨今の開発手法の流れから、モダンな開発手法への移行は不可避であり、
いずれは対応を迫られる時が来ると考えられます。
私が実際に経験した出来事を踏まえ、急な対応を迫られて混乱することを避けるために、以下のような段階的な取り組みを行い、より良い状態を目指していきたいと考えています。
- プロジェクト内で小規模な機能から着手
- チーム全体で知見やノウハウの共有
2. 基本的な概念
簡単な画面で比較
一例として、ボタンタップでテキストを更新する簡単な画面で比較してみましょう
// UIKitでの実装(StoryBoardまたはXIB使用)
class SimpleViewController: UIViewController {
@IBOutlet private weak var label: UILabel!
@IBOutlet private weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
label.text = "Hello, UIKit!"
button.setTitle("Tap me!", for: .normal)
}
@IBAction func buttonTapped(_ sender: Any) {
label.text = "Button Tapped!" // 値が変更されたらUIを更新(手続き的UI更新)
}
}
↓↓↓↓↓
// SwiftUIでの実装
struct SimpleView: View {
// @Stateは「この値が変更されたら、関連するViewを再描画して」という指示
@State private var labelText = "Hello, SwiftUI!"
var body: some View {
// VStackの中の要素(この場合TextとButton)の間の垂直方向の間隔を20ポイントに設定
VStack(spacing: 20) {
Text(labelText) // (@Stateの機能によって)textの変更を自動検知して再描画
Button("Tap me!") {
labelText = "Button Tapped!" // ラベルの値変更
}
}
// ビュー全体(VStack)の周囲にデフォルト(16ポイント)の余白を追加
.padding()
}
}
// プレビュー用のコード
struct SimpleView_Previews: PreviewProvider {
static var previews: some View {
SimpleView()
}
}
双方の比較
UIKit | SwiftUI | |
レイアウトについて | Interface Builderで視覚的にレイアウトを構築(XMLファイルが別途必要) | VStackやモディファイア(修飾子)を使って1つのファイルで完結。プレビュー機能でリアルタイムで結果を確認可能に |
UI更新について | ボタンタップなどのイベントを手続き的に実装してUIを更新 | @Stateによる状態管理で自動的にUIが更新される |
UIKitでもコードのみのレイアウトは可能ですが、制約設定が冗長になりやすく、その分コードも長くなります。
ポイントとなる部分
レイアウトについて
Viewやモディファイアの記述方法に最初は戸惑うかもしれませんが、慣れれば直感的に楽しく書けるようになります
プレビュー機能でリアルタイムに結果がわかるので、UIKitの時よりもスムーズにレイアウト調整がしやすくなります
補完機能を活用することで利用可能なViewやモディファイアを探しやすいので、Copilotを活用していきましょう
状態管理について
@Stateの仕組み | @Stateのメリット |
値の変更を監視する | UI更新のための個別の処理が不要 |
UIKitでの「値の変更検知」と「UI更新」を自動化 | 値の変更のロジックに集中できる |
変更があった場合に関連するViewを自動で再描画 | 必要な部分のみが効率的に再描画される |
補足
- 状態管理は最初は難しく感じるかもしれませんが、宣言的UIの重要な基礎となる概念です
- 複数のView更新が必要な場合でも、値の変更だけに注力できるためUIKitよりも自然とコードがシンプルになります
学習のアプローチ
SwiftUIの状態管理は大きく「Local State(ローカル状態)」と「Shared State(共有状態)」の2種類に分類されます。まずは小さな範囲のスコープである「Local State(単一View内の状態管理)」から慣れていきましょう。
3. UIKitとSwiftUIの共存
アプリをSwiftUIに移行する場合、一度に全てを書き換えることは大規模なプロジェクトでは現実的ではないでしょう。
既存のUIKitのコードベースを維持しつつ、徐々にSwiftUIを導入していく段階的な移行を成功させるために、UIKitとSwiftUIを相互に連携させる仕組みが重要になります。この点について見ていきましょう。
// <UIKitからSwiftUIを利用する:画面自体>
// SwiftUI側の画面
struct ProfileView: View {
var body: some View {
VStack {
Text("プロフィール画面")
Image(systemName: "person.circle")
Text("ユーザー名")
}
}
}
// UIKit側での表示
// UIViewControllerのメソッド内などで
let profileView = ProfileView()
let hostingController = UIHostingController(rootView: profileView)
// モーダル表示の場合
present(hostingController, animated: true)
// Push遷移の場合
navigationController?.pushViewController(hostingController, animated: true)
// --------------
// <UIKitからSwiftUIを利用する:画面要素>
// SwiftUI側の要素
struct MySwiftUIView: View {
let message: String
var body: some View {
Text(message)
.font(.title)
}
}
// UIKit側
// UIViewControllerのメソッド内などで
let message = "Hello from UIKit!"
let hostingController = UIHostingController(rootView: MySwiftUIView(message: message))
self.addChild(hostingController)
self.view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
// (省略)Auto Layoutの制約...
// <SwiftUIからUIKitを利用する>
// 既存のUIViewController
class ExistingViewController: UIViewController {
// (省略)既存の実装...
}
// UIViewControllerRepresentableを使ってラップ
struct ExistingViewControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ExistingViewController {
// (省略)SwiftUIとUIVCの間のデータの受け渡しやイベント処理を行う場合はcontextパラメータを利用することで実現可能
// 既存のViewControllerのインスタンスを返す
return ExistingViewController()
}
func updateUIViewController(_ uiViewController: ExistingViewController, context: Context) {
// ViewControllerの更新が必要な場合の処理
}
}
// SwiftUI側での使用
struct ContentView: View {
var body: some View {
ExistingViewControllerRepresentable()
}
}
4. SwiftUIのメリットと注意点
特に伝えたいメリット
プレビューでのUI調整がとにかく快適
- レイアウト構築に慣れてくると、プレビューを見ながらのUI調整は非常に楽しく&やりやすく、もっと早くSwiftUIを使い始めればよかった、、!と感じるほどでした。
人間にとっては読みづらいXML形式のファイルが無くなることで、レイアウト修正時のレビューがしやすくなる
コード量の削減と保守性の向上
- UIKitでは、AutoLayout設定やボタンのアクションを@IBActionで設定するなど、より多くのコードが必要だった。
また、UI更新時はUILabelのtextプロパティを更新するコードを明示的に書く必要があった。 - SwiftUIでは、レイアウトはモディファイアなどで、ボタンのアクションはクロージャで簡潔に記述可能。
状態管理では@Stateを使うことで、UI更新処理を自動化し、コードが簡潔に。- サンプルコードは小さい量なのでメリットが見えにくいかもしれませんが、
大きなプロジェクトの場合、コード量の削減・保守性向上の大きい効果が期待できます。
- サンプルコードは小さい量なのでメリットが見えにくいかもしれませんが、
注意点・制約など
SwiftUIネイティブで実装がしづらい内容、古めのOSだと利用できないViewなどがあります。例をいくつか取り上げます。
- WebView実装:SwiftUIネイティブのWebView実装は現在も提供されておらず、いくつかの制約がある
- SwiftUI単独では実装不可
- 前述のUIViewRepresentableプロトコルを使ってUIKitのWKWebViewをラップする必要がある
- LazyVStack, LazyHStack:iOS14以上で使用可能
- 通常のVStack, HStackと異なり、画面に表示される直前まで子ビューの作成を遅らせるため、
リストのように大量のデータを表示する際に効果的なViewだが、利用できるのはiOS14以上
- 通常のVStack, HStackと異なり、画面に表示される直前まで子ビューの作成を遅らせるため、
今後の期待
Xcode16.3ではデプロイ対象がiOS15〜となったため、古いOSを切ることでSwiftUIへの転換がしやすい環境になっていくことが見込まれます
5. おわりに
極力コンパクトにまとめるために必要最低限の内容に絞りましたが、この記事では到底取り上げきれないくらいできることが沢山あります。
いきなり全てを理解するのはもちろん難しいので、軽い気持ちでまずはシンプルなレイアウト構成・状態管理からトライすることでSwiftUIのメリットを実感してもらえたらと思います。
筆者もまだまだ勉強中の身ですが、まずはプロジェクト全体で協力しながらチーム全体でノウハウをお互い共有し、より良い状態を目指して行きたいです!