こんにちは。

 数年前に担当したプロジェクトの立ち上げ当時は、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は不要です。

 このサンプルでは処理系を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で作成できます。

SwiftUIからアラート表示。okボタンでViewControllerを閉じる。

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で組んだ方が良いのは言うまでもありません。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる