Skip to main content

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 = flow<Boolean> {
var toggle = false
while (true) {
emit(toggle)
toggle = !toggle
delay(1.seconds)
}
}
}

Then we can use the SKIE-included helpers to interact with the SharedViewModel from SwiftUI. In the sample below, we'll use all of them as an example, not a real-world scenario.

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
}

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

// Observing multiple flows with attached initial values, only requiring a single view closure for content.
Observing(viewModel.counter.withInitialValue(0), viewModel.toggle.withInitialValue(false)) { counter, toggle in
Text("Counter: \(counter), Toggle: \(toggle)")
}
}
}