· 6 min read Posted by Gustavo Fão Valvassori
Consuming SKIE Flows in SwiftUI
One of SKIE’s most useful features is the ability to consume Kotlin Flows in Swift. However, when you want to consume it in a SwiftUI Component, you need to initiate a task, collect the flow, and assign it to a state. Even though this is a simple process, repeating it on every screen quickly adds a lot of boilerplate, making the UI layer harder to maintain.
To make life easier for developers, SKIE offers an optional feature that reduces boilerplate. In this article, we will show you how to enable and use it.
The Project
Before we chat about the SKIE APIs, we will build a quick sample app. To keep things simple, it will be just one screen showing two things:
- A square showing a random color;
- A button to change the color to another random value;
As we want to demo the SwiftUI integration, it will be a SwiftUI View:
struct ColorSample: View {
let color: Color
let onRandomize: () -> Void
var body: some View {
VStack(spacing: 32) {
RoundedRectangle(cornerRadius: 16)
.fill(color)
.frame(width: 200, height: 200)
Button("Random color") {
onRandomize()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.padding()
}
}
Then, to control the state, we will create a simple “view model” in KMP with a StateFlow holding the color. This view model will be the same on all our samples and will not change after the initial implementation.
class RandomColorViewModel {
private val _color = MutableStateFlow(UIColor.grayColor)
val color: StateFlow<UIColor> = _color.asStateFlow()
fun randomize() {
_color.value = UIColor(
red = Random.nextDouble(),
green = Random.nextDouble(),
blue = Random.nextDouble(),
alpha = 1.0,
)
}
}
Here is a quick look at how it shows to the user:
Enabling the extensions
SKIE lets you enable features like the ones discussed in this post in your Gradle configuration.
Inside the skie {} block in your Gradle file, you can choose which features are enabled and disabled.
To access the SwiftUI APIs, you can enable the configuration as follows:
skie {
features {
enableSwiftUIObservingPreview = true
}
}
Collecting the states
To collect the states, you have three main options:
- Collect the flow inside an
asyncblock - Use the
collectmodifier - Use the
Observingview wrapper
Async Collect
If you already used SKIE, you probably know about the first option.
When you compile your KMP Framework with SKIE, it automatically converts your flows into a Swift AsyncSequence.
To hook that with SwiftUI, you can simply use it inside a SwiftUI .task {} modifier.
struct ContentView: View {
let viewModel: RandomColorViewModel
@State private var color = Color.gray
var body: some View {
ColorSample(color: color, onRandomize: viewModel.randomize)
.task {
do {
for try await item in viewModel.color {
self.color = Color(item)
}
} catch {}
}
}
}
collect modifier
The second layer of abstraction for StateFlows, provided by SKIE, is the collect modifier.
This modifier takes a flow and calls a callback each time the value changes, abstracting the code from the Async collection.
You can also apply it to the initial sample to simplify it.
// SKIE extension signature
extension View {
public func collect<Flow : SkieSwiftFlowProtocol>(
flow: Flow,
perform: @escaping (Flow.Element) async -> Void
) -> some View { /** ... **/ }
}
// Updated Sample
struct ContentViewCollect: View {
let viewModel: RandomColorViewModel
@State private var color = Color.gray
var body: some View {
ColorSample(color: color, onRandomize: viewModel.randomize)
.collect(flow: viewModel.color) { color in
self.color = Color(color)
}
}
}
To make this even easier for devs, we also provide a variant function that takes a state binding and assigns the value directly.
// SKIE extension signature
extension View {
public func collect<Flow : SharedLogic.SkieSwiftFlowProtocol, U>(
flow: Flow,
into binding: SwiftUI.Binding<U>,
transform: @escaping (Flow.Element) async -> U?
) -> some SwiftUI.View {
}
// Updated Sample
struct ContentViewCollect: View {
let viewModel: RandomColorViewModel
@State private var color = Color.gray
var body: some View {
ColorSample(color: color, onRandomize: viewModel.randomize)
.collect(flow: viewModel.color, into: $color) { color in
Color(color)
}
}
}
The collect(flow: into:) method allows you to avoid the lambda entirely, as long as the bind that you provide has the same signature as the value you are collecting.
If the types differ (like in the sample above), you still need a lambda to convert them (from UIKit.UIColor to SwiftUI.Color here).
The Observing wrapper
The last API you need to know is the Observing View wrapper.
That’s a normal SwiftUI View that takes many Flows (or StateFlows) and just gives you the values as arguments.
This View uses the same ideas presented earlier, but removes all state management from your side.
// SKIE View Signature
extension Observing where InitialContent == EmptyView {
public init<Flow1 : SkieSwiftFlowWithInitialValue>(
_ flow1: Flow1,
@ViewBuilder content: @escaping (Flow1.Element) -> Content
) where Flow1 : SkieSwiftFlowWithInitialValue, Values == (Flow1.Element) { /** ... **/ }
}
// Updated Sample
struct ContentViewObserving: View {
let viewModel: RandomColorViewModel
var body: some View {
Observing(viewModel.color) { color in
ColorSample(color: Color(color), onRandomize: viewModel.randomize)
}
}
}
Bonus: Making it animated
One interesting SwiftUI feature is that you can animate state changes by updating state inside the withAnimation {} block.
To make sure you don’t need to find a workaround manually, all SKIE APIs contain an animation: parameter that allows you to define how you want to animate it.
By default, no animations are applied, but setting a value for the parameter enables them.
If you want the “default” animation configuration SwiftUI already provides, use animation: .default.
However, you can use any other animation configuration to fine-tune it to your needs.
With the animation, the Observing sample becomes like this:
struct ContentViewObserving: View {
let viewModel: RandomColorViewModel
var body: some View {
// Including the default animation
Observing(viewModel.color, animation: .default) { color in
ColorSample(color: Color(color), onRandomize: viewModel.randomize)
}
}
}
Conclusion
The SwiftUI APIs provided by SKIE can help you abstract a lot of complexity from your app. The decision of which level of abstraction or API to use depends on what you need for your project.
Each step in this article builds on the last. We make it as flexible as possible, so you can adapt as much as you need.
For more information, see the SKIE Official docs for Flows in SwiftUI. The code we’ve built and demonstrated in this post is available in the faogustavo/SkieSwiftUIDemo repo on GitHub.