After enough SwiftUI screens, I noticed I was writing the same scaffolding
over and over: a UIHostingController subclass, a model that owns the
dependencies and constructs the view-model, a weak reference back to the
view controller for navigation, the same wiring on every single screen. The
per-screen content was small; the boilerplate around it was not.
SwiftUIEx is a tiny library that absorbs that boilerplate into three types:
ViewModel — a plain @Observable class with properties and
closures. Preview-friendly. Knows nothing about dependencies, navigation,
or anything outside the view.HostingScreenModel — owns the screen’s dependencies, builds the
view-model, and holds a weak reference to the UIViewController so
callbacks can push/present/dismiss without dragging UIKit imports into
the view layer.HostingScreen<Model, View> — a generic UIHostingController
subclass that connects the two. Each screen subclasses it with concrete
generic parameters, and that’s basically the whole controller.@Observable @MainActor
final class BookNoteListScreenModel: HostingScreenModel {
weak var viewController: UIViewController?
lazy var viewModel = BookNoteListViewModel(
selectNote: { [weak self] note in
self?.handleSelect(note)
}
)
}
final class BookNoteListScreen:
HostingScreen<BookNoteListScreenModel, BookNoteListView> {}
That’s the entire screen-level plumbing. What’s left for each new screen is the actual model, view-model, and view content… the per-screen stuff.
It commits hard to @Observable. No Combine, no @State per property, no
@Bindable gymnastics — just a shared observable view-model that the view
reads from and the model writes to. Navigation stays in app code, not in
the framework, so you can structure it however you want.