DrewCarlson / mobius.kt

Kotlin Multiplatform framework for managing state evolution and side-effects
https://drewcarlson.github.io/mobius.kt/
Apache License 2.0
65 stars 3 forks source link

Sample project #13

Closed ffgiraldez closed 8 months ago

ffgiraldez commented 3 years ago

Hi, do you know some open source projects that uses this lib? I'm interested how to manage the kotlin native with swift UI

DrewCarlson commented 2 years ago

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.

DrewCarlson commented 2 years ago

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) ```swift import SwiftUI import class UIConnectable : Connectable { private let modelBinding: Binding private let consumerBinding: Binding init(modelBinding: Binding, consumerBinding: Binding) { self.modelBinding = modelBinding self.consumerBinding = consumerBinding } class SimpleConnection : Connection { private let modelBinding: Binding private let consumerBinding: Binding init(modelBinding: Binding, consumerBinding: Binding) { 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) ```swift import Foundation import SwiftUI import extension View { public func bindController( loopController: MobiusLoopController, modelBinding: Binding, consumerBinding: Binding ) -> some View { onAppear { if !loopController.isRunning { if consumerBinding.wrappedValue == nil { try! loopController.connect(view: UIConnectable( 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( 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)
    }
}
oco-adam commented 1 year ago

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?

davidscheutz commented 1 year ago

@oco-adam It should work without the AnyView :v:

isfaaghyth commented 1 year ago

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

DrewCarlson commented 8 months ago

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.