こんにちは。
数年前に担当したプロジェクトの立ち上げ当時は、SwiftUIが非対応のOSバージョンだったこともあり、UIKit(Storyboard, xib)を用いて iOSアプリの画面を構築してきましたが、とうとうXcode 16で、 IBDesignable、IBInspectable を利用したプレビューができなくなりました。
開発がとても不便。
参考記事: https://forums.developer.apple.com/forums/thread/768087
それにしても、UIもどんどん複雑なデザインが要求され、いちいちStoryboard で IBOutlet と紐付けて、制約をつけて… などをやるには時間が足りなくなってきました。
・開発中にプレビューできない
・AutoLayoutの制約がとても面倒
・紐付けがあるせいで、簡単にxibファイルなど複製できない
・Merge Requestを出されてもレビューしづらい
“From the deepest desires often come the deadliest hate.”
「最も強い憎しみは大抵最も深い欲望から生まれる」…
Storyboardに対する憎しみは募るばかりですが、現在稼働しているコードを大幅に変えるほどの工数を得られるわけもなし…というわけで、
UIViewController、UITableView、UICollectionViewCell、UIStackViewなどの「枠組み」はそのままに、
その中に追加する「UIView」の部分を代わりに「SwiftUI」で追加していく
ことにしました。
(UIViewControllerを置き換えるのはそれこそ骨ごと入れ替える大手術になるので)
※新規の画面(UIViewController)は、Storyboardなど使わないで済むようにします。
環境
- Swift 5.9.2
- Xcode 16.0
- iOS Development Target 16.0
- macOS Sonoma 14.7.1
UIViewControllerにSwiftUIのViewを載せる
枠組みとして、新規の UIViewController.swift にSwiftUIのViewを載せます。
UIViewControllerのStoryboardは不要です。


Storyboardやxibは不要。
このサンプルでは処理系をTCAで実装しましたが、SwiftUIからViewControllerへ通知できるなら、他のアーキテクチャでも問題ありません。
class NextViewController: UIHostingController<NextView> {
init(caption: String, date: Date) {
super.init(rootView: NextView(
store: Store(initialState: NextFeature.State()) {
NextFeature()
},
caption: caption,
date: date
))
}
required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}要は、UIHostingController<SwiftUIのView>で派生したクラスのViewControllerを作成し、super.init(rootView)でSwiftUIのViewを作成すれば、Viewの部分はSwiftUIで作成できます。

内容自体は特殊な点はないです

UITableViewCell, UICollectionViewCellに載せる

UITableViewCellを再利用する箇所で、デフォルトのCellを作成し、cell.contentConfigurationにUIHostingConfiguration(content: { SwiftUIのView })を代入する処理に置き換えます。
(UICollectionViewCell にも cell.contentConfiguration はあるため、同様に実装可能)
self.dataSource = UITableViewDiffableDataSource<SectionType, SectionItem>(tableView: tableView) {
(tableView: UITableView, indexPath: IndexPath, identifier: SectionItem) -> UITableViewCell? in
switch identifier {
case .item(let data):
let cell = UITableViewCell()
cell.contentConfiguration = UIHostingConfiguration(content: {
TableCellView(name: data.name, caption: data.caption)
})
.margins(.all, 0)
return cell
}
}
内容自体は特殊な点はないです。
UITableViewDiffableDataSourceのSectionType、SectionItemも、シンプルなものです。
import Foundation
enum SectionType: Int, CaseIterable {
case list
/// section
var sections: IndexSet {
return IndexSet(integer: self.rawValue)
}
}
enum SectionItem: Hashable {
case item(Content)
}
struct Content: Codable, Hashable {
let name: String
let caption: String
}
UIStackViewに追加する

UIHostingConfigurationは同様ですが、StackViewが子の制約を求めているので、sizeThatFitsを使ってサイズを得てから addArrangedSubview して、制約で高さを合わせたりします。
(今回はSnapKitを用いて制約を追加しました)
このサンプルは横StackViewですが、縦StackViewの場合はheight制約を設定します。
let config = UIHostingConfiguration(content: {
ColorNameView(name: key, color: value)
}).margins(.all, 0)
let view = config.makeContentView()
stackView.addArrangedSubview(view)
// Viewの最適サイズを取得
let size = view.sizeThatFits(
CGSize(
width: CGFloat.greatestFiniteMagnitude,
height: stackView.frame.height
)
)
// width制約を設定する
view.snp.makeConstraints { make in
make.width.equalTo(size.width)
}
内容自体は特殊な点はないです

作成したSwiftUIに、一部のUIViewも使い回したい
この記事の「SwiftUIからUIKitのViewを使用したい」あたりを参照してください。
しかし、サイズが固定のViewのみにしておきましょう。
可変長のxibを紐付けたSwiftUIを、さらに親のUIKitに取り入れるのは、
レイアウトの制約がうまくいかない可能性が高いため、おすすめしません。
UIKit の UIView と SwiftUI の View を別々に紐付けるか、いっそビュー自体を SwiftUI で作り直すのをおすすめします。xibより短時間で再現できるでしょう。
“The true hero is one who conquers his own anger and hatred.”
「真の英雄とは、自分自身の怒りと憎しみを克服した人だ」…
そもそも憎しみの元に突っ込む必要もなく、回り道する新しい手段があるのだ。
・ビルドする前にレイアウトの結果をプレビューできる
・固定資産とも呼べるほどのUIViewControllerの大改築を避けられる
・(細かい動作はカスタムが必要だが)比較的短時間で画面構築ができる
・Swiftファイルなので、複製使い回し&レビューもしやすい。
以上の点で、Semi-SwiftUIな構築はかなりお勧めです。
(また、近い未来に新たなUIの概念が生まれてもいいように、いまのうちにViewの層からは要件仕様のロジックは分けておきましょう)
新規のプロジェクトは、最初からSwiftUIで組んだ方が良いのは言うまでもありません。








