SkillAgentSearch skills...

KTViewModelBuilder

A Swift macro for wrapping a Kotlin ViewModel into a SwiftUI ObservableObject with unidirectional/bidirectional Kotlin binding

Install / Use

/learn @frankois944/KTViewModelBuilder
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

KTViewModelBuilder

codecov

A SwiftUI macro to use inside a Apple Kotlin multiplatform project for wrapping a Kotlin ViewModel into a SwiftUI ObservableObject, based on the SKIE library.

The goal of this macro is to increase the iOS development experience by removing the complexity of using a Kotlin ViewModel inside an iOS application written in Swift and also respecting the Lifecycle.

For example, instead of using KotlinInt/KotlinInt?, we're using Int/Int?, it can work with Float/Double/...

Currently supported Kotlin types : dataclass/class, int, double, float, bool, uint, Long (swift int64), please make an issue for more.

You can also bind List but only with type Class and String.

[!NOTE]
The macro can create uniderectional and bidirectionel binding

See the sample for a full and detailed example.

<details> <summary>KMP ViewModel</summary> https://github.com/frankois944/KTViewModelBuilder/blob/983bedf1cec93dc96ffe3d688bdcd1c649540a8e/Sample/shared/src/commonMain/kotlin/com/example/ktviewmodelbuilder/ExampleViewModel.kt#L18-L43 </details> <details> <summary>iOS SwiftUI macro</summary> https://github.com/frankois944/KTViewModelBuilder/blob/983bedf1cec93dc96ffe3d688bdcd1c649540a8e/Sample/iosApp/iosApp/ExampleScreen.swift#L13-L29 </details>

Example

Kotlin ViewModel

public class ExampleViewModel : ViewModel() {

    public var bidirectionalString: MutableStateFlow<String> = MutableStateFlow<String>("SOME INPUT")
    public var bidirectionalBoolean: MutableStateFlow<Boolean> = MutableStateFlow<Boolean>(false)
    public var bidirectionalInt: MutableStateFlow<Int?> = MutableStateFlow<Int?>(42)
    public var bidirectionalLong: MutableStateFlow<Long> = MutableStateFlow<Long>(424242L)

    private val _stringData = MutableStateFlow("Some Data")
    public val stringData: StateFlow<String> = _stringData

    private val _intNullableData = MutableStateFlow<Int?>(null)
    public val intNullableData: StateFlow<Int?> = _intNullableData

    private val _randomValue = MutableStateFlow(0)
    public val randomValue: StateFlow<Int> = _randomValue

    private val _entityData = MutableStateFlow<MyData?>(MyData())
    public val entityData: StateFlow<MyData?> = _entityData

    public fun randomizeValue() {
        _randomValue.value = (0..100).random()
    }
}

SwiftUI ViewModel

The macro generate a SwiftUI ViewModel from the content of the Kotlin ExampleViewModel class.

Important: Some logs are added on DEBUG, there are removed when building on release

@ktViewModelBinding(ofType: ExampleViewModel.self,
                    publishing:
        .init(\.stringData, String.self),
                    .init(\.intNullableData, Int?.self),
                    .init(\.randomValue, Double.self),
                    .init(\.entityData, MyData?.self),
                    .init(\.bidirectionalString, String.self, true),
                    .init(\.bidirectionalInt, Int?.self, true),
                    .init(\.bidirectionalBoolean, Bool.self, true),
                    .init(\.bidirectionalLong, Int64.self, true)
)
class MyMainScreenViewModel: ObservableObject {}
<details> <summary>Generated content</summary>

Important: The debug logs are removed when building on release

class MyMainScreenViewModel : ObservableObject {
    private let viewModelStore = ViewModelStore()

    @Published private(set) var stringData: String

    @Published private(set) var intNullableData: Int?

    @Published private(set) var randomValue: Double

    @Published private(set) var entityData: MyData?

    @Published var bidirectionalString: String {
            didSet {
                instance.bidirectionalString.value = bidirectionalString
            }
        }

    @Published var bidirectionalInt: Int? {
            didSet {
                instance.bidirectionalInt.value = bidirectionalInt != nil ? KotlinInt(integerLiteral: bidirectionalInt!) : nil
            }
        }

    @Published var bidirectionalBoolean: Bool {
            didSet {
                instance.bidirectionalBoolean.value = KotlinBoolean(bool:  bidirectionalBoolean)
            }
        }

    @Published var bidirectionalLong: Int64 {
            didSet {
                instance.bidirectionalLong.value = KotlinLong(value: bidirectionalLong)
            }
        }

    init(_ viewModel: ExampleViewModel) {
        self.viewModelStore.put(key: "ExampleViewModelKey", viewModel: viewModel)
        self.stringData = viewModel.stringData.value
        print("INIT stringData : " + String(describing: viewModel.stringData.value))
        self.intNullableData = viewModel.intNullableData.value?.intValue
        print("INIT intNullableData : " + String(describing: viewModel.intNullableData.value))
        self.randomValue = viewModel.randomValue.value.doubleValue
        print("INIT randomValue : " + String(describing: viewModel.randomValue.value))
        self.entityData = viewModel.entityData.value
        print("INIT entityData : " + String(describing: viewModel.entityData.value))
        self.bidirectionalString = viewModel.bidirectionalString.value
        print("INIT bidirectionalString : " + String(describing: viewModel.bidirectionalString.value))
        self.bidirectionalInt = viewModel.bidirectionalInt.value?.intValue
        print("INIT bidirectionalInt : " + String(describing: viewModel.bidirectionalInt.value))
        self.bidirectionalBoolean = viewModel.bidirectionalBoolean.value.boolValue
        print("INIT bidirectionalBoolean : " + String(describing: viewModel.bidirectionalBoolean.value))
        self.bidirectionalLong = viewModel.bidirectionalLong.value.int64Value
        print("INIT bidirectionalLong : " + String(describing: viewModel.bidirectionalLong.value))
    }

    var instance: ExampleViewModel {
        self.viewModelStore.get(key: "ExampleViewModelKey") as! ExampleViewModel
    }

    func start() async {
        await withTaskGroup(of: (Void).self) {
            $0.addTask { @MainActor [weak self] in
                for await value in self!.instance.stringData where self != nil {
                    if value != self?.stringData {
                        #if DEBUG
                        print("UPDATING TO VIEW stringData : " + String(describing: value))
                        #endif
                        self?.stringData = value
                    }
                }
            }
            $0.addTask { @MainActor [weak self] in
                for await value in self!.instance.intNullableData where self != nil {
                    if value?.intValue != self?.intNullableData {
                        #if DEBUG
                        print("UPDATING TO VIEW intNullableData : " + String(describing: value))
                        #endif
                        self?.intNullableData = value?.intValue
                    }
                }
            }
            $0.addTask { @MainActor [weak self] in
                for await value in self!.instance.randomValue where self != nil {
                    if value.doubleValue != self?.randomValue {
                        #if DEBUG
                        print("UPDATING TO VIEW randomValue : " + String(describing: value))
                        #endif
                        self?.randomValue = value.doubleValue
                    }
                }
            }
            $0.addTask { @MainActor [weak self] in
                for await value in self!.instance.entityData where self != nil {
                    if value != self?.entityData {
                        #if DEBUG
                        print("UPDATING TO VIEW entityData : " + String(describing: value))
                        #endif
                        self?.entityData = value
                    }
                }
            }
            $0.addTask { @MainActor [weak self] in
                for await value in self!.instance.bidirectionalString where self != nil {
                    if value != self?.bidirectionalString {
                        #if DEBUG
                        print("UPDATING TO VIEW bidirectionalString : " + String(describing: value))
                        #endif
                        self?.bidirectionalString = value
                    }
                }
            }
            $0.addTask { @MainActor [weak self] in
                for await value in self!.instance.bidirectionalInt where self != nil {
                    if value?.intValue != self?.bidirectionalInt {
                        #if DEBUG
                        print("UPDATING TO VIEW bidirectionalInt : " + String(describing: value))
                        #endif
                        self?.bidirectionalInt = value?.intValue
                    }
                }
            }
            $0.addTask { @MainActor [weak self] in
                for await value in self!.instance.bidirectionalBoolean where self != nil {
                    if value.boolValue != self?.bidirectionalBoolean {
                        #if DEBUG
                        print("UPDATING TO VIEW bidirectionalBoolean : " + String(describing: value))
                        #endif
                        self?.bidirectionalBoolean = value.boolValue
                    }
                }
            }
            $0.addTask { @MainActor [weak self] in
                for await value in self!.instance.bidirectionalLong where self != nil {
                    if value.int64Value != self?.bidirectionalLong {
                        #if DEBUG
                        print("UPDATING TO VIEW bidirectionalLong : " + String(describing: value))
                        #endif
                        self?.bidirectionalLong = val

Related Skills

View on GitHub
GitHub Stars16
CategoryDevelopment
Updated24d ago
Forks0

Languages

Swift

Security Score

95/100

Audited on Mar 11, 2026

No findings