Flows in SwiftUI (preview)
SKIE has a preview support for observing Flows in SwiftUI views. When enabled, SKIE will include two APIs for observing Flows in SwiftUI. Each has its own uses, and we'd like your feedback on both.
The first one is a view modifier collect
,
which you can use to collect a flow inside a view.
Either into a @State
property directly using a SwiftUI Binding
,
or through a provided async
closure.
The other one is a SwiftUI view Observing
,
which you can use to collect one or multiple flows and display a view based on the latest values.
This is similar to the builtin SwiftUI view ForEach
.
Where ForEach
takes a synchronous sequence (e.g. an array),
Observing
takes an asynchronous sequence (e.g. a Flow, a SharedFlow, or a StateFlow).
Both of these can be enabled by adding the following to your build.gradle.kts
:
skie {
features {
enableSwiftUIObservingPreview = true
}
}
Let's consider the following Kotlin view model which we'll want to interact with from SwiftUI.
class SharedViewModel {
val counter = flow<Int> {
var counter = 0
while (true) {
emit(counter++)
delay(1.seconds)
}
}
val toggle = MutableStateFlow<Boolean>(false)
}
Then we can use the SKIE-included helpers to interact with the SharedViewModel
from SwiftUI.
In the samples below, we'll use all of them as an example, not a real-world scenario.
Observing
SwiftUI view
For most scenarios, you'll want to use the Observing
SwiftUI view.
It takes one or more Flows and a closure to create the view based on the latest values.
That means you don't need a @State
property to store the latest value.
The Observing
view has two variants.
One that requires each passed flow to be a StateFlow or have an initial value provided.
It's the simplest way to observe one or more StateFlows.
In the example below, we're observing both the counter
Flow and toggle
StateFlow.
Notice that we need to provide an initial value for the counter
Flow using the withInitialValue
method.
struct ExampleView: View {
let viewModel = SharedViewModel()
var body: some View {
// Observing multiple flows with attached initial values, only requiring a single view closure for content.
Observing(viewModel.counter.withInitialValue(0), viewModel.toggle) { counter, toggle in
Text("Counter: \(counter), Toggle: \(toggle)")
}
}
}
The other variant requires an initial content view closure, but can observe any type of Flow
.
It's useful when it doesn't make sense to provide an initial value for the flow.
In the example below we're observing both the counter
Flow and toggle
StateFlow.
Until counter
produces a value, we'll show a ProgressView
,
and then we'll replace it with a Text
view showing the latest values.
struct ExampleView: View {
let viewModel = SharedViewModel()
var body: some View {
// Observing multiple flows with a "initial content" view closure.
Observing(viewModel.counter, viewModel.toggle) {
ProgressView("Waiting for counters to flows to produce a first value")
} content: { counter, toggle in
Text("Counter: \(counter), Toggle: \(toggle)")
}
}
}
collect
view modifier
When you need more control over how a Flow is collected,
you can use the collect
view modifier.
It allows collecting any Flow into a @State
property using a SwiftUI Binding
,
or through a provided async
closure.
The async closure
is useful when you need to do extra work for each value emitted by the Flow.
struct ExampleView: View {
let viewModel = SharedViewModel()
@State
var boundCounter: KotlinInt = 0
@State
var manuallyUpdatedCounter: Int = 0
var body: some View {
// Collecting a flow into a SwiftUI @State property using a Binding, which requires the property to be of the same type.
Text("Bound counter using Binding: \(boundCounter)")
.collect(flow: viewModel.counter, into: $counter)
Text("Manually updated counter: \(manuallyUpdatedCounter)")
.collect(flow: viewModel.counter) { latestValue in
manuallyUpdatedCounter = latestValue.intValue
}
}
}