mobius.kt icon indicating copy to clipboard operation
mobius.kt copied to clipboard

Sample project

Open ffgiraldez opened this issue 3 years ago • 3 comments

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

ffgiraldez avatar Oct 08 '21 09:10 ffgiraldez

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 avatar Oct 09 '21 21:10 DrewCarlson

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)
    }
}

DrewCarlson avatar Jan 13 '22 06:01 DrewCarlson

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 avatar Nov 03 '22 19:11 oco-adam

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

davidscheutz avatar Jan 31 '23 22:01 davidscheutz

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

isfaaghyth avatar Jul 09 '23 16:07 isfaaghyth

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.

DrewCarlson avatar Feb 03 '24 04:02 DrewCarlson