Using SwiftUI with UIKit
At a glance
| Item | Summary |
|---|---|
| Purpose | Embed SwiftUI views in UIKit controllers and cells, including two-way observable data flow. |
| App architecture | MainMenuViewController routes to three independent examples: child UIHostingController containment, cell-level UIHostingConfiguration, and a diffable UIKit list whose private observable models bind into editable SwiftUI cell content. |
| Main patterns | Framework adapter bridges, Example factory router, Observable model data flow |
| Project style | Ten Swift files; a UIKit navigation shell with three focused SwiftUI integration examples and four reusable SwiftUI cell views. |
Project structure
Source bundle/
├── UseSwiftUIWithUIKit/
│ ├── AppDelegate.swift
│ ├── Examples/
│ │ ├── SwiftUI in Cells/
│ │ │ ├── SwiftUI Views/
│ │ │ │ ├── HeartRateCellView.swift
│ │ │ │ ├── SleepCellView.swift
│ │ │ │ ├── HealthCategoryCellView.swift
│ │ │ │ └── StepCountCellView.swift
│ │ │ └── HostingConfigurationExample.swift
│ │ ├── Data Flow with SwiftUI Cells/
│ │ │ └── CellDataFlowExample.swift
│ │ └── SwiftUI View Controllers/
│ │ └── HostingControllerExample.swift
│ ├── MainMenuViewController.swift
│ └── SceneDelegate.swift
├── Configuration/
│ └── SampleCode.xcconfig
└── UseSwiftUIWithUIKit.xcodeproj/
└── .xcodesamplecode.plist
Structure observations
- The menu is a small factory router; each example is its own composition root rather than one cross-example architecture.
- Controller embedding follows UIKit child-containment rules, while cell embedding uses content configuration with no child controller.
- The data-flow example separates collection identity updates from property updates on each
ObservableObject.
Overall architecture
flowchart LR
MENU["MainMenuViewController"] --> FACTORY["Private MenuItem factories"]
FACTORY --> HOSTVC["HostingControllerViewController"]
FACTORY --> HOSTCELL["HostingConfigurationViewController"]
FACTORY --> FLOW["MedicalConditionsViewController"]
HOSTVC --> UHC["UIHostingController child controllers"]
HOSTCELL --> UICONFIG["UIHostingConfiguration cells"]
FLOW --> STORE["Private DataStore"]
STORE --> SNAPSHOT["Diffable IDs"]
SNAPSHOT --> OBMODEL["MedicalCondition ObservableObject"]
OBMODEL --> UICONFIG
Reference code
UseSwiftUIWithUIKit/MainMenuViewController.swift:11 — example factory registry
private struct MenuItem {
var title: String
var subtitle: String
var viewControllerProvider: () -> UIViewController
static let allExamples = [
MenuItem(title: "SwiftUI View Controllers",
subtitle: "Using UIHostingController",
viewControllerProvider: { HostingControllerViewController() }),
MenuItem(title: "SwiftUI in Cells",
subtitle: "Using UIHostingConfiguration",
viewControllerProvider: { HostingConfigurationViewController() })
]
}Interpretation
The sample compares integration boundaries, not competing app architectures. A hosting controller adapts SwiftUI into the UIKit controller hierarchy; a hosting configuration adapts SwiftUI into cell content; observable objects allow the two frameworks to share state while the UIKit data store retains collection ownership.
Ownership and state
classDiagram
MainMenuViewController *-- UICollectionView : private
MenuItem *-- ViewControllerFactory : closure
HostingControllerViewController *-- HeartHealthAlertArray : private models
HostingControllerViewController *-- UIHostingControllerArray : private children
MedicalConditionsViewController *-- DataStore : private
MedicalConditionsViewController *-- UICollectionView : private
MedicalConditionsViewController *-- DiffableDataSource : private
DataStore *-- MedicalConditionArray : private
MedicalConditionView o-- MedicalCondition : ObservedObject
Ownership evidence
UseSwiftUIWithUIKit/Examples/Data Flow with SwiftUI Cells/CellDataFlowExample.swift:140 — UIKit list and store ownership
class MedicalConditionsViewController: UIViewController {
private var dataStore = DataStore()
private var collectionView: UICollectionView!
private var dataSource:
UICollectionViewDiffableDataSource<Section, MedicalCondition.ID>!
override func loadView() {
setUpCollectionView()
view = collectionView
}
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
MainMenuViewController |
Menu collection view | Creates and privately retains the UIKit list | It invokes the selected item’s controller factory and pushes the result. |
HostingControllerViewController |
Alert models and hosting-controller array | Creates private observable models and child controllers | It performs containment and updates surrounding UIKit summary UI. |
HostingConfigurationViewController |
Static example data and cell registrations | Privately retains collection/cell configuration state | It chooses which SwiftUI view configures each cell type. |
MedicalConditionsViewController |
Data store, collection view, diffable source | Creates and privately retains all list collaborators | It mutates collection membership and applies stable-ID snapshots. |
Private DataStore |
Ordered/dictionary medical-condition collection | Owns both indexes | It alone inserts, deletes, sorts, and shuffles the collection. |
MedicalConditionView |
Observed condition reference | Non-owning semantic observation of a store-owned model | The binding writes the model’s published text. |
Composition denotes source-visible construction and retained value state. Aggregation denotes a stored or injected collaborator whose exclusive lifetime the source does not prove.
Class and protocol design
UseSwiftUIWithUIKit/Examples/SwiftUI View Controllers/HostingControllerExample.swift:136 — SwiftUI child-controller adapter
private func createHostingControllers() {
for alert in alerts {
let alertView = HeartHealthAlertView(alert: alert)
let hostingController = UIHostingController(rootView: alertView)
hostingController.sizingOptions = .intrinsicContentSize
hostingControllers.append(hostingController)
}
}
private func setUpViews() {
hostingControllers.forEach { addChild($0) }
// Add each hosted view to the UIKit hierarchy.
hostingControllers.forEach { $0.didMove(toParent: self) }
}| Type | Responsibility | Depends on or conforms to |
|---|---|---|
MainMenuViewController / MenuItem |
Display and instantiate independent examples | UIKit collection callbacks plus closure factories |
HostingControllerViewController |
Owns SwiftUI child-controller containment | UIViewController, UIHostingController |
HostingConfigurationViewController |
Builds cells whose content is SwiftUI | UIViewController, UIHostingConfiguration |
MedicalConditionsViewController |
Coordinates UIKit collection membership with SwiftUI-bound cell models | Diffable UIKit list and hosting configurations |
HeartHealthAlert / MedicalCondition |
Publish mutable state observed by SwiftUI | ObservableObject; MedicalCondition also Identifiable |
| SwiftUI cell/view structs | Render framework-neutral value or observable model state | View |
View, ObservableObject, Identifiable, and UIKit data-source/delegate roles are framework contracts. The sample defines no local protocol abstraction; its reusable boundaries are adapters, observable models, and factory closures.
Access control
| Symbol | Access | Verified effect | Design reason |
|---|---|---|---|
MenuItem |
top-level private |
Visible only within MainMenuViewController.swift |
The route registry is an implementation detail of the menu file. |
HeartHealthAlert, SwiftUI alert view |
top-level private |
Confined to the hosting-controller example file | Prevents one demonstration’s teaching model from becoming a cross-example API. |
MedicalCondition, DataStore, Section, MedicalConditionView |
top-level private |
Confined to the data-flow example file | The file is the feature boundary; only its UIKit entry controller must be routable. |
| Entry view-controller types | implicit internal |
The menu can construct them across files | They are app routes, not external library symbols. |
@MainActor model annotations |
actor isolation, not access control | Serialize observable UI model access on the main actor | UIKit/SwiftUI state mutation shares one UI concurrency domain. |
Reference code
UseSwiftUIWithUIKit/Examples/Data Flow with SwiftUI Cells/CellDataFlowExample.swift:19 — file-private actor-isolated model
@MainActor
private class MedicalCondition: Identifiable, ObservableObject {
let id: UUID
@Published var text: String
}
@MainActor
private class DataStore {
private var data: (
conditions: [MedicalCondition],
conditionsByID: [MedicalCondition.ID: MedicalCondition]) = ([], [:])
}Here, top-level private makes each example file a hard implementation boundary. @MainActor adds concurrency isolation but does not widen or narrow visibility. The sample exposes no public, open, or fileprivate surface.
Logic ownership and placement
| Logic | Owning type or file | Why it lives there |
|---|---|---|
| Example registration and navigation | MainMenuViewController / private MenuItem |
The menu knows available examples and constructs their public-in-module entry controllers. |
| Child-controller containment | HostingControllerViewController |
UIKit parent/child lifecycle belongs to the containing controller. |
| Cell SwiftUI composition | Cell registrations in hosting examples | The registration owns the UIKit-to-SwiftUI content adapter. |
| Collection membership and stable identity | Private data-flow DataStore and UIKit controller |
Store changes produce diffable snapshots keyed by model IDs. |
| Per-item text mutation | MedicalCondition plus MedicalConditionView binding |
Observable object changes refresh only interested SwiftUI content. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| Framework adapter bridges | UseSwiftUIWithUIKit/Examples/SwiftUI View Controllers/HostingControllerExample.swift:140 |
Adapts SwiftUI content to controller containment; hosting configurations adapt it to cells. |
| Example factory router | UseSwiftUIWithUIKit/MainMenuViewController.swift:14 |
Registers independent demonstrations without a switch over controller types. |
| Observable model data flow | UseSwiftUIWithUIKit/Examples/Data Flow with SwiftUI Cells/CellDataFlowExample.swift:20 |
Shares per-item mutable state across UIKit-owned lists and SwiftUI views. |
Naming conventions
- Example entry controllers name the bridge (
HostingController,HostingConfiguration,MedicalConditions). - Private models use domain names (
HeartHealthAlert,MedicalCondition,DataStore) rather than framework mechanics. - Adapter/view names state content (
HeartRateCellView,MedicalConditionView). - One-file examples intentionally colocate private models, SwiftUI views, and their UIKit composition root.
Architecture takeaways
- Choose
UIHostingControllerfor controller containment andUIHostingConfigurationfor cell content. - Keep UIKit ownership of collection identity separate from observable mutation of one item’s properties.
- Use file-level private types to stop teaching-example details from becoming accidental module architecture.
Source map
| Source file | Architectural role |
|---|---|
UseSwiftUIWithUIKit/MainMenuViewController.swift |
Private example registry and navigation |
UseSwiftUIWithUIKit/Examples/SwiftUI View Controllers/HostingControllerExample.swift |
Observable alerts and hosting-controller containment |
UseSwiftUIWithUIKit/Examples/SwiftUI in Cells/HostingConfigurationExample.swift |
Value-driven SwiftUI cell configurations |
UseSwiftUIWithUIKit/Examples/Data Flow with SwiftUI Cells/CellDataFlowExample.swift |
Observable data flow, private store, IDs, and hosted editable cells |
UseSwiftUIWithUIKit/Examples/SwiftUI in Cells/SwiftUI Views/HealthCategoryCellView.swift |
Representative reusable SwiftUI cell view |
UseSwiftUIWithUIKit/AppDelegate.swift / UseSwiftUIWithUIKit/SceneDelegate.swift |
Application and scene lifecycle only |