SwiftUI
Ballast can be used from SwiftUI, but it requires a bit of boilerplate to be added to your iOS Swift code. The ViewModel
implementation needed for iOS is IosViewModel
.
Initial Setup (one-time)
Ballast can only be used in iOS with the new Kotlin/Native memory model. Start by making sure your project targets
the new memory model with these instructions. You will also need to make sure you declare an explicit dependency on
kotlinx-coroutines-core
version 1.6.0
or greater, because Ballast is compiled against coroutines 1.5.3, currently.
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
}
}
Next, you'll need to create a Swift file in your iOS project to hold some Swift classes that wraps the Ballast ViewModel and converts its StateFlow into a Combine Publisher. You really don't need to understand what's in this file, you'll only need to create it once. Copy this file from the Ballast KaMPKit repo to your iOS Swift sources to add the necessary boilerplate which connects Kotlin's Flows and Ballast's ViewModels to Swift's Combine framework, so that it can be accessed properly from SwiftUI.
Finally, you will also need to configure your Gradle scripts to export the Ballast dependencies that need to be
used from Swift code. You will need to export ballast-core
, and probably ballast-repository
if you're using that
module. The dependencies you export will also need to be declared as an api
dependency, not implementation
.
kotlin {
ios()
sourceSets {
val commonMain by getting {
dependencies {
api("io.github.copper-leaf:ballast-api:4.2.1")
api("io.github.copper-leaf:ballast-viewmodel:4.2.1")
api("io.github.copper-leaf:ballast-core:4.2.1")
api("io.github.copper-leaf:ballast-repository:4.2.1")
implementation("io.github.copper-leaf:ballast-saved-state:4.2.1")
}
}
}
cocoapods {
framework {
isStatic = false // SwiftUI preview requires dynamic framework
export("io.github.copper-leaf:ballast-api:4.2.1")
export("io.github.copper-leaf:ballast-viewmodel:4.2.1")
export("io.github.copper-leaf:ballast-core:4.2.1")
export("io.github.copper-leaf:ballast-repository:4.2.1")
}
}
}
Using Ballast from SwiftUI
Then, from any SwiftUI View, you can observe one of your IosViewModels
by wrapping it in BallastObservable
. You'll
need to manually connect the BallastObservable
to the SwiftUI View's lifecycle by calling
.activate()
/.deactivate()
on the View's .onAppear { }
/.onDisappear { }
callbacks. One-time initialization should
also be placed in .onAppear()
.
Just like with Jetpack Compose, you should have a separate *Content
View that has no direct knowledge of the Ballast
ViewModel. You'll pass in the observable's vmState
and a callback function for postInput
from the screen that
contains the ViewModel and manages its lifecycle. The *Content
View, then, only needs to be responsible for displaying
its content from the non-null vmState
value, and passing Inputs through postInput
to be processed by the Ballst
ViewModel. Note that Kotlin's Swift name translation will convert the nested class names like
ExampleContract.Inputs.Initialize
to drop the second .
(looking like ExampleContract.InputsInitialize
when created
in Swift), and will also require you to provide labels for the parameters for all Inputs.
import Combine
import SwiftUI
import shared
struct ExampleScreen: View {
@ObservedObject var vm = BallastObservable<
ExampleContract.Inputs,
ExampleContract.Events,
ExampleContract.State>(
viewModelFactory: { ExampleViewModel() }, // create directly or pass it in via DI
eventHandlerFactory: { ExampleEventHandler() } // optional, create directly or pass it in via DI
)
var body: some View {
ExampleContent(
vmState: observableModel.vmState,
postInput: observableModel.postInput
)
.onAppear(perform: {
observableModel.activate()
observableModel.postInput(ExampleContract.InputsInitialize())
})
.onDisappear(perform: {
observableModel.deactivate()
})
}
}
struct ExampleContent: View {
var vmState: ExampleContract.State
var postInput: (ExampleContract.Inputs) -> Void
var body: some View {
// ...
}
}
Since the syntax for appear/disappear will be so common in a Ballast MVI project, the CombineAdapters.swift file
includes a .withViewModel
View extension to reduce the boilerplate a bit
var body: some View {
ExampleContent(
vmState: observableModel.vmState,
postInput: observableModel.postInput
)
.withViewModel(observableModel) {
observableModel.activate()
observableModel.postInput(ExampleContract.InputsInitialize())
}
}