When I embed a SwiftUI view inside a CMP composabl...
# compose-ios
c
When I embed a SwiftUI view inside a CMP composable, I cannot get the state changes from the SwiftUI side to propagate. This is probably me doing something wrong. Code in thread.
👀 1
First, I have a "container" written using CMP:
Copy code
// ProgressContainerIos.kt
fun ProgressContainer(
    progressState: ProgressState,
    createUiViewController: () -> UIViewController
) UIViewController = ComposeUIViewController {
    ProgressContainer(
        progressState = progressState,
        modifier = Modifier.fillMaxSize()
    ) {
        UIKitViewController(
            factory = createUiViewController,
            modifier = Modifier.fillMaxSize(),
        )
    }
}
Then I want to embed a SwiftUI view inside ProgressContainer so I do this:
Copy code
//SomeViewUIViewControllerRepresentable.swift
struct SomeViewUIControllerRepresentable: UIViewControllerRepresentable {
    let someState: SomeState

    func makeUIViewController(context: Context) -> UIViewController {
        ProgressContainerIoskt.ProgressContainer(
            progressState: someState.progressState,
            createUiViewController: {
                let swiftUIView = SomeSwiftUIView(someState: someState)
                return UIHostingController(rootView: swiftUIView)
            }
        )
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        // Perhaps I need to do something here for the state to propagate?
    }
}
And finally a SwiftUI view that actually implements the UI
Copy code
struct SomeSwiftUIView: View {
    let someState: SomeState

    var body: some View {
        Text(someState.name)
    }
}
My issue is that when
someState
updates, I don't see my
SomeSwiftUIView
UI change.
I'm guessing this has something to do with the way I pass along the states as
let
constants from the UIVCRepresentable down into the SwiftUI View but I don't know how to fix that
r
You can have a look at my project where I have three ways of doing this, using a Map: https://github.com/rschattauer/compose_multiplatform
c
Interesting. Is this the missing piece then?
r
This is it showing it in three different ways.
c
Hmm I think my code above looks pretty similar to what you have (much simpler though). One difference I see is that you use
@State var polygons: Polygons?
in your SwiftUI View and in my case that is not even required. In my case I have the equivalent of
let polygons: Polygons
because the state updates happen in a KMP ViewModel.
a
to make SwiftUI react on your changes in model, you can use Observable protocol and @ObservedObject property wrapper: https://developer.apple.com/documentation/swiftui/observedobject
c
Sorry I think I did not make my issue clear enough. I'm already using a custom ViewModel written in Kotlin. I'm able to use this ViewModel from SwiftUI and have it react to changes in state. All of this has been working fine. The issue I see is occurring when I embed a SwiftUI view inside a CMP composable.
In my example above, the caller of
SomeViewUIControllerRepresentable
passes in
someState
and in that struct I can see the state value changing. However I don't see the changes inside
SomeViewUIControllerRepresentable
or its callees like
SomeSwiftUIView
a
c
@Andrei Salavei are you saying that the `ObservedObject`/`ObservableObject` mechanism must be specifically if there is a
UIViewControllerRepresentable
in the tree?
This structure works fine in SwiftUI: passing in state as
let
constants to inner SwiftUI views works just fine. The key here is that the state change is happening in the outermost view (using a ViewModel which is implemented as an ObservableObject)
Copy code
struct MyApp: View {
    @StateObject var myViewModel = MyViewModel()
    
    var body: some View {
        One(state: myViewModel.state)
    }
}

struct One: View {
    let state: MyState
    
    var body: some View {
        VStack {
            Text("One")
            Two(state: state)
        }
    }
}

struct Two: View {
    let state: MyState
    
    var body: some View {
        VStack {
            Text("Two")
        }
    }
}
a
Not exactly. In your core sample the
SomeSwiftUIView
does not track changes in the
SomeState
because it does not have attribute
@ObservedObject
. Also,
SomeState
must properly implement
ObservableObject
to make it possible.
What is
MyState
in your sample?
c
Right, in my case the caller of
SomeViewUIControllerRepresentable
is responsible for this state management. This caller is using an ObservableObject/StateObject mechanism to be notified of state changes.
MyState
is a Kotlin data class.
a
Ah, I see your problem
The
updateUIViewController
will be called every time (except the very first call), SwiftUI rebuilds its view with updated
someState
. So you just need to either pass
someState
inside your
ProgressContainerIoskt.ProgressContainer
in the
updateUIViewController
somehow. Or convert
SomeState
to observable object, and let SwiftUI to use its refresh mechanism.
c
Ah! That's what
updateUIViewController
is for (for some reason I've been totally ignoring that method).
Are there examples of how to use that with a composable? I'm not entirely sure what needs to be done in order to • "keep a reference" to the
ProgressContainerIoskt.ProgressContainer
that I currently return from
makeUIViewController
• again pass an updated state to this value in the
updateUIViewController
function
a
> Are there examples of how to use that with a composable Don't remember any. > "keep a reference" to the You don't needed it. it's already getting from the
uiViewController
argument > again pass an updated state to this value in the
updateUIViewController
function Exactly, but it's a bit complicated with SwiftUI as it does not allow to change state from outside. So with current context, the only way I can see it to "convert
SomeState
to observable object, and let SwiftUI to use its refresh mechanism."
👍🏽 1
c
I found a solution for my problem. There are 2 parts 1. How to propagate state if there's a
UIViewControllerRepresentable
in the tree: This is not specific to KMP. And the answer is you cannot use stateless SwiftUI views in this case (you cannot use
let
states; you must replace them with
@Binding var state: MyState
2. How to propagate state from the
updateUIViewController
function to a Composable. For this I took inspiration from this blog post.
Here's pseudocode for part 1 of my problem (the part that's not Compose specific)
Copy code
struct MyApp: View {
    @StateObject var myViewModel = MyViewModel()
    @State private var stateBinding: MyState

    var body: some View {
        One(state: $stateBinding)
            .onChange(of: myViewModel.state) { state in
                self.stateBinding = state
            }
    }
}

struct One: UIViewControllerRepresentable {
    @Binding var state: MyState

    func makeUIViewController(context: Context) -> UIViewController {
        let swiftUIView = VStack {
            Text("One")
            Two(state: $state)
        }
        return UIHostingController(rootView: swiftUIView)
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct Two: View {
    @Binding var state: MyState

    var body: some View {
        VStack {
            Text("Two")
        }
    }
}