mobius.kt
mobius.kt copied to clipboard
Sample project
Hi, do you know some open source projects that uses this lib? I'm interested how to manage the kotlin native with swift UI
There are Swift projects using it but none with SwiftUI and they are not ideal for learning. There was a sample but it has been removed, the iOS project and readme notes may be helpful in getting started.
I hope to add a full example app soon with a SwiftUI demo. Until then, here are some useful bits for connecting a MobiusLoop to a SwiftUI View:
First add UIConnectable
to bind a MobiusLoop.Controller
to your View so you can receive model changes and a Consumer<Event>
to send events with:
UIConnectable.swift (click to expand)
import SwiftUI
import <KN/Framework Name>
class UIConnectable<T> : Connectable {
private let modelBinding: Binding<T>
private let consumerBinding: Binding<Consumer?>
init(modelBinding: Binding<T>, consumerBinding: Binding<Consumer?>) {
self.modelBinding = modelBinding
self.consumerBinding = consumerBinding
}
class SimpleConnection : Connection {
private let modelBinding: Binding<T>
private let consumerBinding: Binding<Consumer?>
init(modelBinding: Binding<T>, consumerBinding: Binding<Consumer?>) {
self.modelBinding = modelBinding
self.consumerBinding = consumerBinding
}
func accept(value: Any?) {
guard let model: T = value as? T else { return }
modelBinding.wrappedValue = model
}
func dispose() {
consumerBinding.wrappedValue = nil
}
}
func connect(output: Consumer) throws -> Connection {
let connection = SimpleConnection(modelBinding: modelBinding, consumerBinding: consumerBinding)
consumerBinding.wrappedValue = output
return connection
}
}
Then add this View.bindController<M>(..)
extension to connect a View to the MobiusLoop.Controller
and manage the lifecycle:
LoopControllerBinder.swift (click to expand)
import Foundation
import SwiftUI
import <KN/Framework Name>
extension View {
public func bindController<M>(
loopController: MobiusLoopController,
modelBinding: Binding<M>,
consumerBinding: Binding<Consumer?>
) -> some View {
onAppear {
if !loopController.isRunning {
if consumerBinding.wrappedValue == nil {
try! loopController.connect(view: UIConnectable<M>(
modelBinding: modelBinding,
consumerBinding: consumerBinding
))
}
try! loopController.start()
}
}
.onDisappear {
if loopController.isRunning {
try! loopController.stop()
}
try! loopController.disconnect()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { output in
if loopController.isRunning {
try! loopController.stop()
}
try! loopController.disconnect()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { output in
if !loopController.isRunning {
if consumerBinding.wrappedValue == nil {
try! loopController.connect(view: UIConnectable<M>(
modelBinding: modelBinding,
consumerBinding: consumerBinding
))
}
try! loopController.start()
}
}
}
}
Put everything together in a View like this:
import SwiftUI
import <KN/Framework Name>
struct ExampleScreen: View {
@State private var model: ExampleModel = ExampleModel.companion.DEFAULT
@State private var eventConsumer: Consumer? = nil
private let loopController: MobiusLoopController
init() {
let handler = ExampleHandler.shared.create()
let loopFactory = Mobius.shared.loop(update: ExampleUpdate.shared, effectHandler: handler)
.logger(logger: SimpleLogger<AnyObject, AnyObject, AnyObject>(tag: "Example"))
loopController = Mobius.shared.controller(
loopFactory: loopFactory,
defaultModel: ExampleModel.companion.DEFAULT,
modelRunner: DispatchQueueWorkRunner(dispatchQueue: DispatchQueue.main))
}
var body: some View {
AnyView(VStack {
// Your SwiftUI code here ...
// read `model` fields as expected and UI will update when the model changes
// use eventConsumer?.accept(ExampleEvent.EventName()) to dispatch events into the running loop
}.bindController(
loopController: loopController,
modelBinding: $model,
consumerBinding: $eventConsumer)
}
}
Hi @DrewCarlson, do SwiftUI views need to be wrapped in AnyView to use this library with SwiftUI, or it is possible to integrate into SwiftUI without wrapping the view in AnyView?
@oco-adam It should work without the AnyView
:v:
Hi folks! I just created a sample repo using this mobius.kt
with KMM and Compose Multiplatform. If you're interested to explore, please check this PR link:
https://github.com/isfaaghyth/kmm-compose/pull/1
https://github.com/DrewCarlson/pokedex-mobiuskt
Here is a sample project which includes a SwiftUI and Compose Multiplatform implementation. It still requires some cleanup and a detailed readme, but the important parts are there.
Specifically on the topic of SwiftUI bindings, the project uses a simpler utility than the previous example. The usage looks like:
struct PokedexScreen: View {
@EnvironmentObject var navigation: SwiftUINavigation
@State private var model: PokedexModel = PokedexModel.companion.create()
@State private var eventConsumer: ((PokedexEvent) -> Void)? = nil
var body: some View {
VStack {
// ...
}
.navigationTitle("Pokédex")
.navigationBarTitleDisplayMode(.automatic)
.bindLoop(
initFunc: PokedexInit(),
modelBinding: $model,
consumerBinding: $eventConsumer,
loopFactory: FlowMobius.shared.loop(
update: PokedexUpdate(),
effectHandler: Dependencies.shared.getPokedexHandler()
)
.logger(logger: mobiusLogger(tag: "Pokedex Screen"))
)
}
}
The implementation is here and the full usage example is here.
I'll close this issue and update the docs with links to the sample project. If your use-case is not covered or have other suggestions, please open issues on pokedex-mobiuskt.