JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.42k stars 1.12k forks source link

Ability to define a UIViewController type #3478

Open GuilhE opened 11 months ago

GuilhE commented 11 months ago

The absence of a specific type for androidx.compose.ui.window.ComposeUIViewControllerand platform.UIKit.UIViewController presents a limitation in using updateUIViewController and, consequently, the associated Composable.

In scenarios where the iOS application requires management of Composable state through an iOS ViewModel, the availability of such types would prove to be advantageous:

struct SampleUIViewController: UIViewControllerRepresentable {

    @Binding var status: String
    let action: () -> Void

    func makeUIViewController(context: Context) -> UIViewController {
        return SharedViewControllers().sampleComposable(status: status, click: action)
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
          //how to update the composable state when binding values changes?
    }
}

struct ComposeScreen: View {

    @StateObject private var viewModel = ViewModels.sampleViewModel()
    @State private var status: String = ""

    var body: some View {
        SampleUIViewController(
            status: $status,
            action: { viewModel.doSomething() }
        )
        .onReceive(viewModel.$state) { new in            
            status = new.label()
        }
        .ignoresSafeArea()
    }
}

I find this use-case quite intriguing, particularly in its potential to attract iOS developers and facilitate incremental adoption. Furthermore, it introduces the exciting possibility of incorporating Composables into a project without the necessity of sharing ViewModels. This offers a more flexible approach compared to a full-fledged Composable + ViewModel sharing strategy.

Currently, the workaround I've discovered involves incorporating a MutableStateFlow within the ComposeUIViewController, which responds to changes in state properties:

object SharedViewControllers {

    private data class ComposeUIViewState(val status: String = "")
    private val state = MutableStateFlow(ComposeUIViewState())

    fun sampleComposable(click: () -> Unit): UIViewController {
        return ComposeUIViewController {
            with(state.collectAsState().value) {
                 Composable(state.status, click)
            }
        }
    }

    fun updateSampleComposable(status: String) {
        state.update { ComposeUIViewState(status = status) }
    }
}

on iosApp side:

struct SampleUIViewController: UIViewControllerRepresentable {

    @Binding var status: String
    let action: () -> Void

    func makeUIViewController(context: Context) -> UIViewController {
        return SharedViewControllers().sampleComposable(click: action)
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        SharedViewControllers().updateSampleComposable(status: status)
    }
}

It works, but it's not very graceful.

elijah-semyonov commented 11 months ago

Thanks for the report. We are currently iterating on API design and real usage examples like this are quite useful.

GuilhE commented 11 months ago

Thanks for the report. We are currently iterating on API design and real usage examples like this are quite useful.

Hello @elijah-semyonov, you can find here a working sample 😉

elijah-semyonov commented 10 months ago

@GuilhE What's currently preventing you from using Kotlin Objective-C object (a ViewModel basically), storing MutableState which could be changed inside update method? In this case your setContent could simply reference this object and this mutable state and execute differently based on its value.

GuilhE commented 10 months ago

@GuilhE What's currently preventing you from using Kotlin Objective-C object (a ViewModel basically), storing MutableState which could be changed inside update method? In this case your setContent could simply reference this object and this mutable state and execute differently based on its value.

I believe that's the work around I'm using, basically I've a Singleton with a Flow/mutableState that the iosApp can call to update the state. The ComposeUIViewController observes this Flow/mutableState and changes accordingly.

elijah-semyonov commented 10 months ago

We are drifting toward a design, where the state is managed outside of ComposeWindow for multiple reasons (scene destruction on removal from window hierarchy, for example). So it feels like a proper way to do and not just workaround.

dima-avdeev-jb commented 10 months ago

Thanks for the article https://proandroiddev.com/compose-multiplatform-managing-ui-state-on-ios-45d37effeda9

dima-avdeev-jb commented 10 months ago

We will discuss it. And try to make better solution for all your needs. But it will take some time.

GuilhE commented 10 months ago

Thanks for your work guys and for letting the community help! 🙏🏼

GuilhE commented 10 months ago

This might come in handy 😇 https://github.com/GuilhE/KMP-ComposeUIViewController

GuilhE commented 10 months ago

@elijah-semyonov @dima-avdeev-jb latest release changelog:

[1.1.0-ALPHA]
- Adds capability to generate .swift files with UIViewControllerRepresentables.
- Adds script to include those generated files into xcodeproj to be accessible in iOS project;

😇

GuilhE commented 2 weeks ago

@elijah-semyonov @dima-avdeev-jb

Hello guys! Just dropping by to share news about my new gradle plugin that simplifies the library setup.

Example:

plugins {
    id("io.github.guilhe.kmp.plugin-composeuiviewcontroller") version "1.1.0"
}

ComposeUiViewController {
    iosAppName = "Gradient"
    targetName = "Gradient"
}

With this, all the code generation and export is handled automatically, no more complex configurations needed! 😊