UIKitMiddleTechnical

Как интегрировать SwiftUI-представления в UIKit-приложение с помощью UIHostingController?

UIHostingController — подкласс UIViewController, принимающий SwiftUI-представление; добавляется как дочерний контроллер через addChild/addSubview/didMove(toParent:) и обновляется через rootView.

UIHostingController: встраивание SwiftUI в UIKit

UIHostingController — это подкласс UIViewController, который принимает SwiftUI-представление (View) и отображает его в UIKit-иерархии. Это официальный мост между двумя UI-фреймворками.

Базовое использование

Создайте UIHostingController, передав SwiftUI-представление в инициализатор, и добавьте его как дочерний контроллер.

import SwiftUI
import UIKit

// SwiftUI-компонент
struct BadgeView: View {
    let count: Int
    var body: some View {
        Text("\(count)")
            .padding(8)
            .background(Color.red)
            .foregroundColor(.white)
            .clipShape(Circle())
    }
}

// UIKit-контроллер
class ProfileViewController: UIViewController {

    private var hostingController: UIHostingController<BadgeView>?

    override func viewDidLoad() {
        super.viewDidLoad()
        embedBadge()
    }

    private func embedBadge() {
        let swiftUIView = BadgeView(count: 5)
        let hosting = UIHostingController(rootView: swiftUIView)

        // Стандартный паттерн встраивания дочернего VC
        addChild(hosting)
        view.addSubview(hosting.view)
        hosting.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hosting.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
            hosting.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            hosting.view.widthAnchor.constraint(equalToConstant: 40),
            hosting.view.heightAnchor.constraint(equalToConstant: 40)
        ])
        hosting.didMove(toParent: self)
        hostingController = hosting
    }

    // Обновление SwiftUI-представления
    func updateBadge(count: Int) {
        hostingController?.rootView = BadgeView(count: count)
    }
}

Встраивание в UITableViewCell / UICollectionViewCell

Для использования SwiftUI внутри ячеек создайте UIHostingController один раз и обновляйте его rootView при переиспользовании ячейки. Избегайте создания нового контроллера в каждом cellForRowAt.

class SwiftUITableViewCell: UITableViewCell {
    private var hostingController: UIHostingController<AnyView>?

    func configure<Content: View>(with content: Content, parent: UIViewController) {
        if hostingController == nil {
            let hosting = UIHostingController(rootView: AnyView(content))
            hosting.view.backgroundColor = .clear
            parent.addChild(hosting)
            contentView.addSubview(hosting.view)
            hosting.view.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                hosting.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
                hosting.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
                hosting.view.topAnchor.constraint(equalTo: contentView.topAnchor),
                hosting.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
            ])
            hosting.didMove(toParent: parent)
            hostingController = hosting
        } else {
            hostingController?.rootView = AnyView(content)
        }
    }
}

Передача данных из UIKit в SwiftUI

Используйте @ObservableObject / ObservableObject или @State-привязанные значения через Binding. Обновление hostingController.rootView вызывает ре-рендер SwiftUI.

class CounterViewModel: ObservableObject {
    @Published var count: Int = 0
}

struct CounterView: View {
    @ObservedObject var vm: CounterViewModel
    var body: some View {
        Text("Count: \(vm.count)")
    }
}

// В UIKit:
let vm = CounterViewModel()
let hosting = UIHostingController(rootView: CounterView(vm: vm))
// Позже:
vm.count += 1  // SwiftUI обновится автоматически

Подводные камни

  • Забыть вызвать addChild(_:) и didMove(toParent:) — без этого UIKit не управляет жизненным циклом вложенного контроллера, что ведёт к утечкам и некорректной работе viewWillAppear.
  • Создавать новый UIHostingController при каждой переиспользовании ячейки в таблице — это дорого. Обновляйте только rootView.
  • Не сбрасывать view.backgroundColor = .clear — по умолчанию фон чёрный/белый, что портит дизайн при встраивании.
  • Ожидать, что UIHostingController корректно вычислит intrinsicContentSize без дополнительной настройки — в сложных случаях нужен вручную заданный sizingOptions (iOS 16+: preferredContentSize / UIHostingControllerSizingOptions).
  • Передавать ссылочные типы напрямую без @ObservableObject — SwiftUI не знает об изменениях и не перерисовывается.
  • Использовать AnyView в горячем пути — это отключает type-erasure оптимизации SwiftUI и замедляет diffing.
  • Не учитывать Safe Area — UIHostingController по умолчанию учитывает её; внутри ячеек это нежелательно, установите hosting.view.insetsLayoutMarginsFromSafeArea = false.

Common mistakes

  • Сводить «интегрировать SwiftUI-представления в UIKit-приложение с помощью UIHostingController» к синтаксису и не объяснять reuse pool.
  • Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии uikit-29.
  • Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.

What the interviewer is testing

  • Формулирует точную модель для «интегрировать SwiftUI-представления в UIKit-приложение с помощью UIHostingController» и подтверждает ее корректным примером.
  • Умеет связать ответ с иерархия view controller, тестированием и отладкой на устройстве.
  • Называет ограничения подхода uikit-29, включая производительность, память и сопровождение.

Sources

Related topics