Packages
mob
0.6.11
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.2
0.6.1
0.6.0
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.11
0.5.10
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.0
BEAM-on-device mobile framework for Elixir
Current section
Files
Jump to
Current section
Files
ios/MobViewModel.swift
// MobViewModel.swift — Shared state store between BEAM NIFs and SwiftUI.
// NIFs call setRoot() from any thread; the @Published triggers SwiftUI re-render on main.
import SwiftUI
import Combine
@objc public class MobViewModel: NSObject, ObservableObject {
@objc public static let shared = MobViewModel()
@Published public var root: MobNode?
/// Increments on every setRoot call; views use onChange(of: rootVersion) to
/// trigger withAnimation rather than watching root directly (root identity
/// may change even for same-screen re-renders).
@Published public var rootVersion: Int = 0
/// Increments ONLY when a navigation transition is requested.
/// MobRootView uses this (not rootVersion) as the view identity (.id(navVersion))
/// so the whole view is only torn down and rebuilt on screen pushes/pops,
/// not on every state-update re-render (e.g., typing in a text field).
@Published public var navVersion: Int = 0
/// Transition type for the *next* root change. Read by MobRootView before
/// calling withAnimation; not @Published to avoid spurious recompositions.
public var transition: String = "none"
/// Current startup phase message shown while BEAM is initialising.
@Published public var startupPhase: String = "Starting…"
/// Non-nil when a fatal startup error has occurred; the error screen stalls here.
@Published public var startupError: String?
@objc public func setRoot(_ node: MobNode?, transition: String) {
DispatchQueue.main.async {
self.transition = transition
self.root = node
self.rootVersion += 1
if transition != "none" {
self.navVersion += 1
}
}
}
@objc public func setStartupPhase(_ phase: String) {
DispatchQueue.main.async { self.startupPhase = phase }
}
@objc public func setStartupError(_ error: String?) {
DispatchQueue.main.async { self.startupError = error }
}
}
// UIHostingController subclass that intercepts the left-edge swipe gesture
// and forwards it to the BEAM as {:mob, :back}.
// Using UIScreenEdgePanGestureRecognizer rather than a SwiftUI DragGesture
// because it integrates cleanly with scroll views and doesn't require
// threading gesture priority through the view tree.
public class MobHostingController: UIHostingController<MobRootView> {
override public func viewDidLoad() {
super.viewDidLoad()
let edgePan = UIScreenEdgePanGestureRecognizer(
target: self, action: #selector(handleEdgePan(_:)))
edgePan.edges = .left
view.addGestureRecognizer(edgePan)
}
@objc private func handleEdgePan(_ gesture: UIScreenEdgePanGestureRecognizer) {
if gesture.state == .ended {
mob_handle_back()
}
}
}
// Factory: lets ObjC (AppDelegate.m) create the SwiftUI hosting controller
// without knowing about the generic UIHostingController<MobRootView> type.
@objc public class MobUIFactory: NSObject {
@objc public static func makeRootViewController() -> UIViewController {
return MobHostingController(rootView: MobRootView())
}
}