5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UIHostingController の sizingOptions の使い方

Last updated at Posted at 2024-06-29

UIHostingControllerを使う際、SwiftUI側のViewのサイズ変更に合わせて、UIKit側のViewのサイズを調整しなければならないケースがあると思います。このような場合に利用できる仕組みとしてiOS 16から sizingOptions が導入されています。

sizingOptionsに設定できる値は UIHostingControllerSizingOptions というOptionSetです。

  • []: 空の場合サイズ調整をしない。デフォルト値。
  • .preferredContentSize: SwiftUIのViewサイズに合わせて UIHostingController の preferredContentSize を更新する
  • .intrinsicContentSize: SwiftUIのViewサイズに合わせて UIHostingController の view の intrinsicContentSize を更新する

wwdc2022_10072.png
WWDC2022 - Use SwiftUI with UIKit より

preferredContentSize と intrinsicContentSize とは

preferredContentSizeUIViewController が持つプロパティで、presentされたときのビューのサイズを規定するものです。主にpopoverとして表示される際に利用されます。

intrinsicContentSizeUIView が持つプロパティで、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が自動的に調整されます。

Simulator Screen Recording - iPhone 15 Pro - 2024-06-29 at 18.29.21.gif

sizingOptions = .intrinsicContentSize の利用シーン

UIHostingController を別のビューコントローラーの子として埋め込む場合に利用します。

例として、上記と同じSwiftUIのViewを利用して、今度はUIKitのビュー階層の一部として UIHostingController の view を配置します。

baseView に16ptの余白をつけて UIHostingControllerview を配置しています。

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 のサイズが調整されています。

Simulator Screen Recording - iPhone 15 Pro - 2024-06-29 at 18.42.22.gif

注意点

sizingOptions を指定するとSwiftUIのView更新のタイミングでUIKit側のレイアウト更新処理が走るためにパフォーマンスに影響する可能性があります。これが問題になる場合は sizingOptions を指定せずに自前でサイズ更新の制御を行うのが良いでしょう。

参考資料

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?