Has there been some changes around Flow support in...
# swift-export
j
Has there been some changes around Flow support in recent dev versions? I had been using
2.4.0-dev-7255
successfully but getting following in 8268 for example
Copy code
Could not find or use auto-linked library 'KotlinCoroutineSupport': library 'KotlinCoroutineSupport' not found

Undefined symbol: (extension in Shared):ExportedKotlinPackages.dev.johnoreilly.chip8.Emulator.screen.getter : any KotlinCoroutineSupport.KotlinTypedStateFlow<Self.Swift.AsyncSequence.Element == (extension in Shared):ExportedKotlinPackages.dev.johnoreilly.chip8.Screen>

Undefined symbol: nominal type descriptor for KotlinCoroutineSupport.KotlinTypedFlowIterator

Undefined symbol: protocol conformance descriptor for KotlinCoroutineSupport.KotlinTypedFlowIterator<A> : Swift.AsyncIteratorProtocol in KotlinCoroutineSupport
a
Does that error persist after
gradlew clean
and cleaning the derived data of xcode?
j
yeah, seems to be..... change in following branch fwiw https://github.com/joreilly/chip-8/tree/dep_updates
a
I will take a look today, thanks 🙂
With this patch freshly downloaded project is build and run
We had to update the resulted API and hide the AsyncSequence conformance behind a call, because of correctness - cold Flow was never designed to be iterable out of the box.
j
Thanks, will try that shortly. Is that plan for how StateFlow will be observed in Swift?
It will I think be very common scenario
a
For now we have decided to have a single approach for any Flow - any flow should be iterated through
.asAsyncSequence
. We can do a shorthand for
SharedFlow
and all who inherit from it - and provide AsyncSequence conformance out of the box. But for that we would love to hear user feedback - if there is enough requests than we will break consistency and we will provide a difference in behaviours. but by default we think that consistency in the resulted API and usages of that API is more important.
👍 1
j
In at least some of the samples I have I'll be looking to replace use of SKIE's
Observing
(https://skie.touchlab.co/features/flows-in-swiftui#observing-swiftui-view) ......but can easily enough replace that with for await etc (as @russhwolf for example did here https://github.com/russhwolf/To-Do/commit/0f6e543bb66e4acd359859dcce18d0320de582b0)
a
I guess our current approach doesn't break that pattern? One can still write
for try await toDos in repository.getList().asAsyncSequence() {
?
j
Yeah, exactly
kodee happy 1
d
Is the
toStruct()
call a concept required to get SwiftUI to work/be performant? I'd like to not have to recreate my models in Swift but unsure if that would introduce other problems... I assume we can't get automatic conversion of data classes to structs with swift export as it might not be desirable in all cases? Perhaps behind a flag?
d
@dorche compiling Kotlin class to Swift struct is a known user request, but as I know It's out of dev scope for now
d
What's the behaviour without it? Does it simply not compile at all?
a
> Is the
toStruct()
call a concept required to get SwiftUI to work I'm not sure I understand what do you mean. 1. "to work" part - please consult this repo as an example of SwiftUI working with ref types. Or this. Or the two that were mentioned in this thread before - all of them uses Swift Export with SwiftUI. 2. "be performant" part - Can you please elaborate, why usage of reference type would affect performance of SwiftUI?
d
Your
swift-export-sample
makes it a bit clearer,
UsersDetailView
directly uses
Shared.User
which is closer to what most people will want to do. re the "be performant" part - I am not sure how SwiftUI looks under the hood and whether reference types are a problem, this was purely a guess - if it works similarly to Compose, it's very rare that you'd want referential equality checks, which would explain the usage of structs in pure Swift code. Also (this should probably be a separate thread) none of these samples show the most common use case - the ViewModel having a
sealed class/interface
that models the whole screen state, i.e. Loading, Error, Success with data. I think if we had such a sample available it would make things a bit more obvious and also I think it would reveal the need for something similar to SKIE's
Observing
like John has mentioned.
a
> I am not sure how SwiftUI looks under the hood and whether reference types are a problem, this was purely a guess I would be curious to see some experiments from the community to demonstrate the performance differences between those two 😉 But kotlin as a language has no concept that can be correctly mapped onto swift structs yet. Please follow https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0453-better-immutability-value-classes-motivation.md for further reading. > none of these samples show ... use case - the ViewModel having a
sealed class/interface
that models the whole screen state That is correct - exhaustive sealed hierarchies are not fully supported yet. Feel free to follow https://youtrack.jetbrains.com/issue/KT-80000 for the progress.
r
Back to the original question here: is there documentation on why
AsyncSequence
conformance is not possible for flow
Flow
but is possible for
SharedFlow
et al? I'm curious to understand the correctness issue more.
a
Sadly, there is no documentation on that. Basically, such a translation breaks structured concurrency in a non obvious way Imagine an input
Copy code
public val myPublicFlow = flow {
    try {
        emit('x')
    } finally {
        withContext(NonCancellable) {
            delay(100.milliseconds)
            println("Flow finished cancelling")
        }
    }
}
and a usage
Copy code
public fun activateKotlin() {
    val job = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
        try {
            myPublicFlow.collect {
                awaitCancellation()
            }
        } catch (_: Throwable) {
        }
        println("Job is cancelled")
    }

    job.cancel()
}
This will produce
Copy code
Flow finished cancelling
Job is cancelled
If we call that input from Swift
Copy code
@MainActor
func activate() async {
    let t = Task {
        for await _ in myPublicFlow {
            while (Task.isCancelled != false) {} // immitates awaitCancellation
        }
        print("Job is cancelled")
    }
    t.cancel()
}
We will get
Copy code
Job is cancelled
Flow finished cancelling
This is not OK in general case. Behaviour becomes identical if we change the input with the following addition:
Copy code
.shareIn(
        GlobalScope,
        started = Lazily, // doesn't matter
    )
There are couple of more puzzlers that were demonstrated to us by the coroutines library team, but the main idea is visible - it's not OK to silently create a global dispatcher, that's not a trivial operation and the presence of such an operation should be visible to the end user.
asAsyncSequence
is a place for us to customise that operation in the future and to highlight to the user that there is nothing trivial going on here.