Current section

Files

Jump to
mob_dev lib mob_dev plugin ios_bootstrap.ex
Raw

lib/mob_dev/plugin/ios_bootstrap.ex

defmodule MobDev.Plugin.IOSBootstrap do
@moduledoc """
Code-generates the iOS plugin bootstrap Swift source from the activated
plugins' `:ui_components` declarations.
The generated file defines one C-callable function, `mob_register_plugins`,
that registers a factory closure with `MobNativeViewRegistry.shared` for
each component the host should expose. The host's `AppDelegate` calls
`mob_register_plugins()` once, before `mob_init_ui()`, and from that point
every component the BEAM mounts under a plugin's `view_module` key resolves
to the plugin's SwiftUI view.
Plain function, no state — call `swift_source/1` with the same shape
`MobDev.Plugin.activated/0` returns (`[{plugin_dir, manifest}]`) and you
get back the Swift source string ready to write to disk.
Pre-codegen, each plugin shipped its own `@objc class MobXxxPlugin { @objc
class func mob_register() }` and the host hand-wrote one
`[MobXxxPlugin mob_register];` line per plugin into AppDelegate.m. That's
the smoke-test pattern from the 2026-05-28 iOS plugin bring-up. This
module replaces both halves: plugins drop the @objc wrapper and just
ship the SwiftUI struct, and the host always calls a single generated
entry point regardless of how many plugins it has activated.
The mapping from a `ui_components` entry to a `register` call comes from
two manifest fields:
* `ui_components.ios.view_module` — the registry key (string match against
`MobNode.nativeViewModule`); the BEAM derives this from
`Mob.Component.module_name/1` so it must agree with the manifest.
* `ui_components.ios.swift_struct` — the Swift struct name to instantiate
(`StructName(props: props)`). Two plugins are free to register
different `view_module` keys that resolve to the same struct, and the
struct name need not be derivable from the registry key.
A component without `:swift_struct` is silently skipped (the
`MobDev.Plugin.Validator.validate_plugin/3` step is where authors get told
to add the field); the validator's job is to surface this before build
time. The generator stays purely transform-shaped so the build pipeline
never has to think about missing manifest fields.
"""
@doc """
Returns the full Swift source for the bootstrap file.
`plugins` is the activated-plugin list — `[{plugin_dir, manifest}]`
same shape `MobDev.Plugin.activated/0` returns. Tier-0 (nil-manifest)
plugins contribute nothing. Components missing `:swift_struct` are
silently dropped: the validator is what nags about that.
The output is stable: components are emitted in the order
`MobDev.Plugin.Merge.ui_components/1` lists them (i.e. activation order,
then declaration order within a manifest). Stable output keeps the build
cache hit-rate high.
"""
@spec swift_source([MobDev.Plugin.Merge.plugin()]) :: String.t()
def swift_source(plugins) do
components = MobDev.Plugin.Merge.ui_components(plugins)
body = components |> Enum.flat_map(&register_lines/1) |> Enum.join("\n")
header() <>
"@_cdecl(\"mob_register_plugins\")\n" <>
"public func mob_register_plugins() {\n" <> body <> "\n}\n"
end
defp header do
"""
// Auto-generated by MobDev.Plugin.IOSBootstrap.
//
// Registers the activated plugins' SwiftUI views with
// MobNativeViewRegistry.shared, one entry per ui_components manifest
// declaration. AppDelegate.m calls mob_register_plugins() once before
// mob_init_ui() so the registry is populated by the time the BEAM
// mounts any plugin-contributed node.
//
// Regenerated on every `mix mob.deploy --native`. Do not edit by hand.
import Foundation
import SwiftUI
"""
end
# One component maps to zero or one register lines: a missing :swift_struct
# means the component can't be registered, so we drop it. The validator
# surfaces this before the build runs.
defp register_lines(component) do
with view_module when is_binary(view_module) <- get_in(component, [:ios, :view_module]),
swift_struct when is_binary(swift_struct) <- get_in(component, [:ios, :swift_struct]) do
[
" MobNativeViewRegistry.shared.register(\"#{view_module}\") { props, _send in",
" AnyView(#{swift_struct}(props: props))",
" }"
]
else
_ -> []
end
end
end