Skip to content

Conversation

@JessalynWang
Copy link
Contributor

@JessalynWang JessalynWang commented Nov 25, 2025

Add a recipe that demonstrates some APIs that can be used to:

  1. Launch presenters from Swift
  2. Render SwiftUI views from a presenter's model StateFlow
  3. Integrate a presenter backstack with SwiftUI NavigationStack

With this change the recipe app in iOS has a different landing screen where users are able to choose the CMP-rendered recipes or the SwiftUI one

swiftui_recipe.mp4

See #154

@JessalynWang JessalynWang force-pushed the jesslwan/native-swift-presenter-launch branch from 9589dbf to e7dd43d Compare November 25, 2025 07:33
@JessalynWang JessalynWang marked this pull request as ready for review November 25, 2025 15:24
…nd how we can render the models using SwiftUI rather than renderers.

See #154
@JessalynWang JessalynWang force-pushed the jesslwan/native-swift-presenter-launch branch from e7dd43d to 718ec3e Compare November 25, 2025 15:40
import SwiftUI

/// A protocol for view models that create their own SwiftUI view representation.
protocol SelfRenderingViewModel {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I originally implemented this stuff, the original idea was to make a Renderer registry, which is probably more similar to how things work in KMP. A registry requires a registration step though, and I don't like registration steps (in Kotlin, the DI framework handles this more automatically). But really a registry wasn't required. In Swift, you can extend any object and make it conform to a protocol, so I made SelfRenderingViewModel. Any Kotlin view model can be extended in Swift to conform to SelfRenderingViewModel and make its own Renderer.

Some minor tweaks I would make moving forward:

  • Get rid of the registry (actually, I don't think you are using the registry in this change, so that is good)
  • Use SelfRenderingViewModel everywhere a ViewModel is expected. In other words, use any SelfRenderingViewModel instead of Any.
  • Maybe rename SelfRenderingViewModel because all view models should be self-rendering, so maybe name it PlatformViewModel or ViewModelProtocol or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also felt like it was unnecessary so I omitted the registry and factory in this change and tried to keep it simple. I'll do some more refactor here

/// Note that `PresenterView` should not be used often. `Presenters` are hierarchical, with parent `Presenters` containing the models of their children.
/// The view for a parent `Presenter` model should also present the model of its children, so `PresenterView` is only be needed for the root
/// parent `Presenter`.
struct PresenterView<Model>: View {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per my comment below on SelfRenderingViewModel, I think you should change <Model> to <Model: SelfRenderingViewModel>.

Copy link
Contributor Author

@JessalynWang JessalynWang Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do!

Re-evaluating for the same reasons as other comments

/// This view is a lightweight wrapper that delegates view creation to the `Models` themselves. `Models` must conform
/// to `SelfRenderingViewModel` and provide their view in `makeViewRenderer()`. This is a simple API that
/// enables view creation for models regardless of type. If needed this implementation can be changed to follow a factory pattern.
struct PresenterModelView<Model>: View {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per my comment below on SelfRenderingViewModel, I think you should change <Model> to <Model: SelfRenderingViewModel>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to put a type constraint on this because some parents that render children models might be agnostic to what that model actually is. For example, in SwiftUiHomePresenterView we render the backstack, which can contain a multitude of different model types. Thus we only know that our models are of type BaseModel.

I suppose we could extend BaseModel to conform to SelfRenderingViewModel and always render some default UI, but I'm not sure that's more preferable to crashing intentionally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussing this a bit more, going to enforce the constraint with this API and push the conformance requirement to "features" to make it explicit

Comment on lines 25 to 29
if let selfRenderingModel = model as? (any SelfRenderingViewModel) {
return AnyView(selfRenderingModel.makeViewRenderer())
}

fatalError("Could not find view builder for \(type). Make \(type) conform to `SelfRenderingViewModel` protocol.")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By changing <Model> to <Model: SelfRenderingViewModel>, you won't need a fatalError here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd still like to do this


var body: some View {
if let viewModel = viewModelObserver.viewModel {
PresenterModelView(model: viewModel)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, without a registry, you really don't need PresenterModelView at all. Just call viewModel.makeViewRenderer() here.

/// The view for a parent `Presenter` model should also present the model of its children, so `PresenterView` is only be needed for the root
/// parent `Presenter`.
struct PresenterView<Model>: View {
struct PresenterView<Model: BaseModel>: View {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I run into issues with existential types if I try to add a constraint on PresenterViewModel, so I used the similar pattern to SwiftUiHomePresenterView. It doesn't push out responsibility to the leaf nodes as much as I would like, open to other ideas.

At the same time, this class should not be used often and if it's failing it should be pretty obvious to the user, since it's the entry point of their view hierarchy

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I would prefer to use fatalError(...) in the BaseModel extension then to crash in all build types?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants