UIHostingController
を使う際、SwiftUI側のViewのサイズ変更に合わせて、UIKit側のViewのサイズを調整しなければならないケースがあると思います。このような場合に利用できる仕組みとしてiOS 16から sizingOptions
が導入されています。
sizingOptions
に設定できる値は UIHostingControllerSizingOptions
というOptionSetです。
-
[]
: 空の場合サイズ調整をしない。デフォルト値。 -
.preferredContentSize
: SwiftUIのViewサイズに合わせて UIHostingController の preferredContentSize を更新する -
.intrinsicContentSize
: SwiftUIのViewサイズに合わせて UIHostingController の view の intrinsicContentSize を更新する
WWDC2022 - Use SwiftUI with UIKit より
preferredContentSize と intrinsicContentSize とは
preferredContentSize
は UIViewController
が持つプロパティで、presentされたときのビューのサイズを規定するものです。主にpopoverとして表示される際に利用されます。
intrinsicContentSize
は UIView
が持つプロパティで、Auto Layoutでビューがレイアウトされる際に、そのビューの本来のサイズとして利用される値です。例えば UILabel
では設定されているテキストにぴったり合うサイズになります。
それでは sizingOptions
の .preferredContentSize
と .intrinsicContentSize
は具体的にどのようなときに使えばいいのでしょうか。それを以下で解説します。
sizingOptions = .preferredContentSize の利用シーン
UIHostingController
をビューコントローラーとして直接 present する場合に利用します。
サンプルとして、ボタンを押すと中身のテキストが変わってサイズが変わるSwiftUIのViewを用意します。
struct PopoverContent: View {
@State var text: String = "Hello, World!"
private let texts = [
"Hello, World!",
"Hello, World!\nHello, Hello, World!",
]
var body: some View {
Button {
text = texts.first(where: { $0 != text }) ?? ""
} label: {
Text(text)
}
.padding(16)
}
}
続いて、ビューコントローラーに配置したボタンを押すと、UIHostingController
を使って、上記のビューをpopoverとして表示するようにします。
final class ViewController: UIViewController {
private let button = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
button.setTitle("present", for: .normal)
button.addAction(.init {_ in
self.presentPopover()
}, for: .touchUpInside)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
private func presentPopover() {
let hostingController = UIHostingController(rootView: PopoverContent())
hostingController.sizingOptions = .preferredContentSize
hostingController.modalPresentationStyle = .popover
if let popover = hostingController.popoverPresentationController {
popover.permittedArrowDirections = .down
popover.sourceView = button
popover.delegate = self
}
present(hostingController, animated: true)
}
}
extension ViewController: UIPopoverPresentationControllerDelegate {
// iPhoneでpopoverを表示するのに必要な実装
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
.none
}
}
実行すると、以下のようにテキストに合わせたサイズにpopoverが自動的に調整されます。
sizingOptions = .intrinsicContentSize の利用シーン
UIHostingController
を別のビューコントローラーの子として埋め込む場合に利用します。
例として、上記と同じSwiftUIのViewを利用して、今度はUIKitのビュー階層の一部として UIHostingController
の view を配置します。
baseView
に16ptの余白をつけて UIHostingController
の view
を配置しています。
final class ViewController: UIViewController {
private let baseView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
baseView.backgroundColor = .lightGray
view.addSubview(baseView)
baseView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
baseView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
baseView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
layoutContent()
}
private func layoutContent() {
let hostingController = UIHostingController(rootView: PopoverContent())
hostingController.sizingOptions = .intrinsicContentSize
addChild(hostingController)
baseView.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
let padding: CGFloat = 16
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: baseView.leadingAnchor, constant: padding),
hostingController.view.topAnchor.constraint(equalTo: baseView.topAnchor, constant: padding),
hostingController.view.trailingAnchor.constraint(equalTo: baseView.trailingAnchor, constant: -padding),
hostingController.view.bottomAnchor.constraint(equalTo: baseView.bottomAnchor, constant: -padding),
])
}
}
SwiftUIのビューサイズに合わせて、16ptの余白を保ちながら親ビューである baseView
のサイズが調整されています。
注意点
sizingOptions
を指定するとSwiftUIのView更新のタイミングでUIKit側のレイアウト更新処理が走るためにパフォーマンスに影響する可能性があります。これが問題になる場合は sizingOptions
を指定せずに自前でサイズ更新の制御を行うのが良いでしょう。
参考資料