Coder Social home page Coder Social logo

icerockdev / moko-kswift Goto Github PK

View Code? Open in Web Editor NEW
342.0 4.0 21.0 1.54 MB

Swift-friendly api generator for Kotlin/Native frameworks

Home Page: https://moko.icerock.dev

License: Apache License 2.0

Kotlin 100.00%
moko kotlin-native gradle-plugin kotlin-multiplatform kotlin-multiplatform-mobile swift

moko-kswift's Introduction

moko-kswift
GitHub license Download kotlin-version

MOKO KSwift

KSwift it's gradle plugin for generation Swift-friendly API for Kotlin/Native framework.

Kotlin sealed interface/class to Swift enum

sealed classes compare

Kotlin extensions for K/N platform classes

extensions compare

Your own case

KSwift give you API for adding your own generator based on KLib metadata information.

Posts

Table of Contents

Features

  • API for extend logic for own cases - just implement your own ProcessorFeature
  • Reading of all exported klibs - you can generate swift additions to the api of external libraries
  • Kotlin sealed class/interface to Swift enum
  • Kotlin extensions for platform classes to correct extensions instead of additional class with static methods
  • Flexible filtration - select what you want to generate and what not

Requirements

  • Gradle version 6.0+
  • Kotlin 1.6.10

Installation

Plugin

Using legacy plugin application

root build.gradle

buildscript {
    repositories {
        mavenCentral()
        google()
        gradlePluginPortal()
    }
    dependencies {
        classpath("dev.icerock.moko:kswift-gradle-plugin:0.7.0")
    }
}

project where framework compiles build.gradle

plugins {
    id("dev.icerock.moko.kswift")
}

Using the plugins DSL

settings.gradle

pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
        mavenCentral()
    }
}

project where framework compiles build.gradle

plugins {
    id("dev.icerock.moko.kswift") version "0.7.0"
}

Runtime library

root build.gradle

allprojects {
    repositories {
        mavenCentral()
    }
}

project build.gradle

dependencies {
    commonMainApi("dev.icerock.moko:kswift-runtime:0.7.0") // if you want use annotations
}

Usage

Xcode configuration

The Swift code generated from this plugin is not automatically included in the shared framework you might have.

You have 2 options to use it in your iOS project:

  • Xcode direct file integration
  • CocoaPods integration

Xcode direct file integration

You can directly import the generated file in your Xcode project like it's a file you have written on your own.

To do so:

  • open the Xcode project
  • right click on "iosApp"
  • choose "Add files to iOSApp"
  • add the file from the generated folder (you might need to read the FAQ to know where the generated folder is)
  • you are now good to go!

CocoaPods integration

After you have added the moko-kswift plugin to your shared module and synced your project, a new Gradle task should appear with name kSwiftXXXXXPodspec where XXXXX is the name of your shared module (so your task might be named kSwiftsharedPodspec).

  • Run the task doing ./gradlew kSwiftsharedPodspec from the root of your project. This will generate a new podspec file, XXXXXSwift.podspec, where XXXXX is still the name of your shared module (so e.g. sharedSwift.podspec)

  • Now edit the Podfile inside the iOS project adding this line pod 'sharedSwift', :path => '../shared' just after the one already there for the already available shared module pod 'shared', :path => '../shared'

  • Now run pod install from the iosApp folder so the new framework is linked to your project.

  • Whenever you need a Swift file generated from moko-kswift just import the generated module (e.g. import sharedSwift) and you are good to go!

Sealed classes/interfaces to Swift enum

Enable feature in project build.gradle:

kotlin:

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature)
}

groovy:

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature.factory)
}

That's all - after this setup all sealed classes and sealed interfaces will be parsed by plugin and plugin will generate Swift enums for this classes.

For example if you have in your kotlin code:

sealed interface UIState<out T> {
    object Loading : UIState<Nothing>
    object Empty : UIState<Nothing>
    data class Data<T>(val value: T) : UIState<T>
    data class Error(val throwable: Throwable) : UIState<Nothing>
}

Then plugin will generate source code:

/**
 * selector: ClassContext/moko-kswift.sample:mpp-library-pods/com/icerockdev/library/UIState */
public enum UIStateKs<T : AnyObject> {

  case loading
  case empty
  case data(UIStateData<T>)
  case error(UIStateError)

  public init(_ obj: UIState) {
    if obj is shared.UIStateLoading {
      self = .loading
    } else if obj is shared.UIStateEmpty {
      self = .empty
    } else if let obj = obj as? shared.UIStateData<T> {
      self = .data(obj)
    } else if let obj = obj as? shared.UIStateError {
      self = .error(obj)
    } else {
      fatalError("UIStateKs not syncronized with UIState class")
    }
  }

}

For each generated entry in comment generated selector - value of this selector can be used for filter. By default all entries generated. But if generated code invalid (please report issue in this case) you can disable generation of this particular entry:

kotlin:

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature) {
        filter = excludeFilter("ClassContext/moko-kswift.sample:mpp-library-pods/com/icerockdev/library/UIState")
    }
}

groovy:

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature.factory) {
        it.filter = it.excludeFilter("ClassContext/moko-kswift.sample:mpp-library-pods/com/icerockdev/library/UIState")
    }
}

As alternative you can use includeFilter to explicit setup each required for generation entries:

kotlin:

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature) {
        filter = includeFilter("ClassContext/moko-kswift.sample:mpp-library-pods/com/icerockdev/library/UIState")
    }
}

groovy:

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature.factory) {
        it.filter = it.includeFilter("ClassContext/moko-kswift.sample:mpp-library-pods/com/icerockdev/library/UIState")
    }
}

Extensions from platform classes

Enable feature in project build.gradle:

kotlin:

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.PlatformExtensionFunctionsFeature)
}

groovy:

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.PlatformExtensionFunctionsFeature.factory)
}

That's all - after this setup all extension functions for classes from platform.* package will be correct swift code.

For example if you have in your kotlin code:

class CFlow<T>(private val stateFlow: StateFlow<T>) : StateFlow<T> by stateFlow

fun UILabel.bindText(coroutineScope: CoroutineScope, flow: CFlow<String>) {
    val label = this
    coroutineScope.launch {
        label.text = flow.value
        flow.collect { label.text = it }
    }
}

Then plugin will generate source code:

public extension UIKit.UILabel {
  /**
   * selector: PackageFunctionContext/moko-kswift.sample:mpp-library/com.icerockdev.library/Class(name=platform/UIKit/UILabel)/bindText/coroutineScope:Class(name=kotlinx/coroutines/CoroutineScope),flow:Class(name=com/icerockdev/library/CFlow)<Class(name=kotlin/String)> */
  public func bindText(coroutineScope: CoroutineScope, flow: CFlow<NSString>) {
    return UILabelExtKt.bindText(self, coroutineScope: coroutineScope, flow: flow)
  }
}

Selector from comment can be used for filters as in first example.

Generation of Swift copy method for data classes

Enable feature in project build.gradle:

kotlin:

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.DataClassCopyFeature)
}

groovy:

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.DataClassCopyFeature.factory)
}

With this feature for data class like this:

data class DataClass(
    val stringValue: String,
    val optionalStringValue: String?,
    val intValue: Int,
    val optionalIntValue: Int?,
    val booleanValue: Boolean,
    val optionalBooleanValue: Boolean?
)

Will be generated swift code that can be used like this:

let newObj = dataClass.copy(
    stringValue: {"aNewValue"}, 
    intValue: {1}, 
    booleanValue: {true}
)

Implementation of own generator

First create buildSrc, if you don't. build.gradle will contains:

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.6.10"
}

repositories {
    mavenCentral()
    google()
    gradlePluginPortal()

    maven("https://jitpack.io")
}

dependencies {
    implementation("com.android.tools.build:gradle:7.0.0")
    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21")
    implementation("dev.icerock.moko:kswift-gradle-plugin:0.7.0")
}

Then in buildSrc/src/main/kotlin create MyKSwiftGenerator:

import dev.icerock.moko.kswift.plugin.context.ClassContext
import dev.icerock.moko.kswift.plugin.feature.ProcessorContext
import dev.icerock.moko.kswift.plugin.feature.ProcessorFeature
import dev.icerock.moko.kswift.plugin.feature.BaseConfig
import dev.icerock.moko.kswift.plugin.feature.Filter
import io.outfoxx.swiftpoet.DeclaredTypeName
import io.outfoxx.swiftpoet.ExtensionSpec
import io.outfoxx.swiftpoet.FileSpec
import kotlin.reflect.KClass


class MyKSwiftGenerator(
    override val featureContext: KClass<ClassContext>,
    override val filter: Filter<ClassContext>
) : ProcessorFeature<ClassContext>() {
    override fun doProcess(featureContext: ClassContext, processorContext: ProcessorContext) {
        val fileSpec: FileSpec.Builder = processorContext.fileSpecBuilder
        val frameworkName: String = processorContext.framework.baseName

        val classSimpleName = featureContext.clazz.name.substringAfterLast('/')

        fileSpec.addExtension(
            ExtensionSpec
                .builder(
                    DeclaredTypeName.typeName("$frameworkName.$classSimpleName")
                )
                .build()
        )
    }

    class Config : BaseConfig<ClassContext> {
        override var filter: Filter<ClassContext> = Filter.Exclude(emptySet())
    }

    companion object : Factory<ClassContext, MyKSwiftGenerator, Config> {
        override fun create(block: Config.() -> Unit): MyKSwiftGenerator {
            val config = Config().apply(block)
            return MyKSwiftGenerator(featureContext, config.filter)
        }

        override val featureContext: KClass<ClassContext> = ClassContext::class

        @JvmStatic
        override val factory = Companion
    }
}

in this example will be generated swift extension for each class in kotlin module. You can select required Context to got required info from klib metadata.

last step - enable feature in gradle:

kotlin:

kswift {
    install(MyKSwiftGenerator)
}

groovy:

kswift {
    install(MyKSwiftGenerator.factory)
}

Set iOS deployment target for podspec

kotlin:

kswift {
    iosDeploymentTarget.set("11.0")
}

groovy:

kswift {
    iosDeploymentTarget = "11.0"
}

Samples

FAQ

Where destination directory for all generated sources?

Swift source code generates in same directory where compiles Kotlin/Native framework. In common case it directory build/bin/{iosArm64 || iosX64}/{debugFramework || releaseFramework}/{frameworkName}Swift.

Kotlin/Native cocoapods plugin (and also mobile-multiplatform cocoapods plugin by IceRock) will move this sources into fixed directory - build/cocoapods/framework/{frameworkName}Swift.

How to exclude generation of entries from some libraries?

kswift {
    excludeLibrary("{libraryName}")
}

How to generate entries only from specific libraries?

kswift {
    includeLibrary("{libraryName1}")
    includeLibrary("{libraryName2}")
}

Samples

More examples can be found in the sample directory.

Set Up Locally

Clone project and just open it. Gradle plugin attached to sample by gradle composite build, so you will see changes at each gradle build.

# clone repo
git clone [email protected]:icerockdev/moko-kswift.git
cd moko-kswift 
# generate podspec files for cocopods intergration. with integration will be generated swift files for cocoapod
./gradlew kSwiftmpp_library_podsPodspec
./gradlew kSwiftMultiPlatformLibraryPodspec
# go to ios dir
cd sample/ios-app
# install pods
pod install
# now we can open xcworkspace and build ios project
open ios-app.xcworkspace
# or run xcodebuild
xcodebuild -scheme ios-app -workspace ios-app.xcworkspace test -destination "platform=iOS Simulator,name=iPhone 12 mini"
xcodebuild -scheme pods-test -workspace ios-app.xcworkspace test -destination "platform=iOS Simulator,name=iPhone 12 mini"

Contributing

All development (both new features and bug fixes) is performed in develop branch. This way master sources always contain sources of the most recently released version. Please send PRs with bug fixes to develop branch. Fixes to documentation in markdown files are an exception to this rule. They are updated directly in master.

The develop branch is pushed to master during release.

More detailed guide for contributers see in contributing guide.

License

Copyright 2021 IceRock MAG Inc

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

moko-kswift's People

Contributors

alex009 avatar atticus183 avatar cl3m avatar ema987 avatar kazumanagano avatar nathan-livefront avatar shyngyssaktagan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

moko-kswift's Issues

How do you prevent the library from being generated?

I have some libraries that shouldn't be generated, I've tried using the following:

excludeLibrary("com.russhwolf:multiplatform-settings-no-arg")
excludeLibrary("com.russhwolf:multiplatform-settings-serialization")
excludeLibrary("com.russhwolf:multiplatform-settings-coroutines")
excludeLibrary("com.russhwolf:multiplatform-settings-test")

or like this:

excludeLibrary("multiplatform-settings-no-arg")
excludeLibrary("multiplatform-settings-serialization")
excludeLibrary("multiplatform-settings-coroutines")
excludeLibrary("multiplatform-settings-test")

but it always generates, how do you prevent it from generating?

It is possible to exclude all by default?

I want to exclude all classes minus the ones included via annotations or via gradle configuration.

I do not find a way to do it. Is it possible with the current implementation?

Thanks a lot.

Using with regular framework

๐Ÿ‘‹

I'm using :embedAndSignAppleFrameworkForXcode to build a regular framework instead of using Cocoapods.

I can see the generated .swift sources available in build/xcode-framework/$(CONFIGURATION)/$(SDK_NAME) but I can't reference them from the iOS project. Is there anything manual process that needs to be done here?

Only use Kswift when building for iOS

We run some tests on CI on the shared code, so it doesn't need the Kswift files at all. Is there a way in Gradle to only install the Kswift plugin and run in if a condition (eg a flag or env variable) is set to true?

No way to specify spec.ios.deployment_target for generated spec file

Because there's no way to specify the iOS deployment target, the build defaults to the lowest support iOS version (I believe iOS 4.3) for the sharedSwift project. This results in the build failing with:

<unknown>:0: error: Swift requires a minimum deployment target of iOS 7
Command CompileSwiftSources failed with a nonzero exit code

This can be worked around by forcing the target in the projects Podfile, but this is obviously not ideal.

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
    end
  end
end

Firstly it would be good to have a setting to be able to specify this. Although, would it make sense for a future version to use the cocoapods plugin configuration if available?

Gradle Sync deletes generated KSwift files

Hi here!
First of all I want to thank the team who made this plugin possible, it's really useful ๐ŸŒŸ

Now, I'm not sure if this is an issue of the plugin or gradle itself (or maybe I'm not setting up the plugin correctly) but whenever I make some updates to the build.gradle of the shared module (where I have set up kswift as well) and click on the Sync now button (or even just restart IntelliJ IDEA and it automatically syncs gradle), it deletes the existing shared/build/cocoapods/framework/sharedSwift folder that won't be generated until I do the following:

Starting point: After gradle sync, the sharedSwift folder is no longer present
Screenshot 2022-12-28 at 9 26 15 AM

Step 1: Go to XCode and run the app until the build fails due to sharedSwift not being available anymore
Screenshot 2022-12-28 at 9 30 09 AM

Step 2: Come back to IntellJ IDEA, open the terminal, ensure that the sharedSwift folder is generated now and then run pod install in its terminal
Screenshot 2022-12-28 at 9 32 11 AM

Step 3: Go back to XCode, and from the popup, select Read from disk to make sure XCode reads the newly generated files
Screenshot 2022-12-28 at 9 32 37 AM

Step 4: Let Step 3 finish its build completely and then the app run/build would finally succeed
Screenshot 2022-12-28 at 9 34 53 AM

This becomes quite tedious while developing ๐Ÿ˜… Therefore I want to ask if there's a way to retain the previously generated files inside the build folder every time we sync gradle (or manually allow the generation of these generated files)?

Flow and StateFlow generics support for Swift

just like mvvm-livedata we need support of coroutines Flow out of box.
for iOS we need class-wrapper to save generic type and we need in iosMain generation of this class-wrapper accessors.
example:

commonMain

class MyViewModel: ViewModel() {
    val login: MutableStateFlow<String> = MutableStateFlow("")
    val isLoginEnabled: Flow<Boolean> = login.map { it.isNotEmpty() }
}

iosMain

class CMutableStateFlowOpt<T>(private val wrapped: MutableStateFlow<T>): MutableStateFlow<T> by wrapped {
    fun observe(block: (T) -> Unit): Closeable { ... }
}

class CMutableStateFlow<T: Any>(wrapped: MutableStateFlow<T>): CMutableStateFlowOpt<T>(wrapped)

class CStateFlowOpt<T>(private val wrapped: StateFlow<T>): StateFlow<T> by wrapped {
    fun observe(block: (T) -> Unit): Closeable { ... }
}

class CStateFlow<T: Any>(wrapped: StateFlow<T>): CStateFlowOpt<T>(wrapped)

class CFlowOpt<T>(private val wrapped: Flow<T>): Flow<T> by wrapped {
    fun observe(block: (T) -> Unit): Closeable { ... }
}

class CFlow<T: Any>(wrapped: Flow<T>): CFlowOpt<T>(wrapped)

iosMain (autogenerated by compiler plugin - need to be developed in this isue)

val MyViewModel.loginWrapped: CMutableStateFlow<String> = CMutableStateFlow(login)
val MyViewModel.isLoginEnabledWrapped: CFlow<Boolean> = CFlow(isLoginEnabled)

as result in swift we will see:

class MyViewModel: ViewModel() {
    let login: MutableStateFlow
    let loginWrapped: CMutableStateFlow<NSString>
    let isLoginEnabled: Flow
    let isLoginEnabledWrapped: CFlow<KotlinBoolean>
}

How to integrate with other Moko libraries

Hi, can someone help me to understand how this type of intergration should look alike, I have done everything written in Setup part, but checking with Sample app there is a completely different structure compared with mine.

Here is my plugins and dependencies list

Screen Shot 2022-05-20 at 17 27 07

And here is Kswift configuration part.

Screen Shot 2022-05-20 at 17 25 21

After that it generates shared.podspec, file

Screen Shot 2022-05-20 at 17 28 32

and pod successfully installs, but I dont get any generated file in my source code. Not for sealed classes and not for any binding extensions.
Can someone help me to understand what I am doing wrong.

I experienced an error generated in KSwift when I updated the kotlin version and kmp-nativecoroutines version

I updated Kotlin Version to 1.7.20 and updated KMP-NativeCoroutines to version 0.13.1.

The error is like this:

e: java.lang.NoSuchMethodError: 'void org.jetbrains.kotlin.ir.util.IrUtilsKt.passTypeArgumentsFrom$default(org.jetbrains.kotlin.ir.expressions.IrFunctionAccessExpression, org.jetbrains.kotlin.ir.declarations.IrTypeParametersContainer, int, int, java.lang.Object)'
        at com.rickclephas.kmp.nativecoroutines.compiler.KmpNativeCoroutinesIrTransformer.callOriginalFunction(KmpNativeCoroutinesIrTransformer.kt:162)
        at com.rickclephas.kmp.nativecoroutines.compiler.KmpNativeCoroutinesIrTransformer.createNativeBody(KmpNativeCoroutinesIrTransformer.kt:111)
        at com.rickclephas.kmp.nativecoroutines.compiler.KmpNativeCoroutinesIrTransformer.visitFunctionNew(KmpNativeCoroutinesIrTransformer.kt:100)
        at org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext.visitFunction(IrElementTransformerVoidWithContext.kt:83)
        at org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid.visitSimpleFunction(IrElementTransformerVoid.kt:72)
        at org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid.visitSimpleFunction(IrElementTransformerVoid.kt:73)
        at org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid.visitSimpleFunction(IrElementTransformerVoid.kt:24)
        at org.jetbrains.kotlin.ir.declarations.IrSimpleFunction.accept(IrSimpleFunction.kt:28)
        at org.jetbrains.kotlin.ir.IrElementBase.transform(IrElementBase.kt:24)
        at org.jetbrains.kotlin.ir.util.TransformKt.transformInPlace(transform.kt:35)
        at org.jetbrains.kotlin.ir.declarations.IrClass.transformChildren(IrClass.kt:56)
        at org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid.visitDeclaration(IrElementTransformerVoid.kt:57)

Is there any solution for this ?

Unable to resolve a Gradle plugin

Hi there!
I've followed an installation guide, added needed repo-s into my project-root build.gradle.kts file, but when I try to Sync the project, I get this:

Caused by: org.gradle.internal.resolve.ModuleVersionNotFoundException: Could not find dev.icerock.moko:kswift-gradle-plugin:0.1.0.
Searched in the following locations:

Sample app compilation issues - Missing xcconfig and no such module

When opening the iOS workspace for the first time, you see the following errors:

image

It would be useful to have better documentation around getting the sample compiling, as this error can easily be fixed by running pod install.

The next issue I hit is:

image

This occurs regardless of running ./gradlew build

Enhancement: Generate extensions over primitive types

Plugin version I've used

plugins {
    id("dev.icerock.moko.kswift") version "0.3.0"
}

Problem

If we define extension function over, e.g. String type in Kotlin, this function will not be translated as an extension in Swift:

// StringExt.kt

val String.myExtensionProperty: String get() = "123"
fun String.myExtensionFunction() { ... }

private fun example() {
    val result = "123".myExtensionProperty
    "123".myExtensionFunction()
}

moko-kwift didn't handle this issue, because extension function didn't pass the filter =)

It would be great if moko-kswift will generate extensions in such case.

AtomicFu breaks reading metadata

Not really a kswift issue but thought I worth raise it so it can be noted in kswift's readme to save someone else some time

Basically, if your project uses AtomicFu (I'm using the latest, v0.16.2) then the below exception occurs. Sample app attached below.

Error only shows with --info flag.

library can't be read
java.lang.IllegalStateException: metadata-jvm doesn't have any extensions for module fragment!
	at kotlinx.metadata.jvm.impl.JvmMetadataExtensions.createModuleFragmentExtensions(JvmMetadataExtensions.kt:292)
	at kotlinx.metadata.KmModuleFragment.<init>(nodes.kt:255)
	at kotlinx.metadata.klib.KlibModuleMetadata$Companion.read(KlibModuleMetadata.kt:93)
	at kotlinx.metadata.klib.KlibModuleMetadata$Companion.read$default(KlibModuleMetadata.kt:81)
	at dev.icerock.moko.kswift.plugin.KotlinMetadataLibraryProvider$Companion.readLibraryMetadata(KotlinMetadataLibraryProvider.kt:32)
	at dev.icerock.moko.kswift.plugin.KLibProcessor.processFeatureContext(KLibProcessor.kt:28)
	at dev.icerock.moko.kswift.plugin.PostProcessLinkTask.execute(PostProcessLinkTask.kt:37)
	at dev.icerock.moko.kswift.plugin.PostProcessLinkTask.execute(PostProcessLinkTask.kt:13)
	at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:752)
	at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:725)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$2.run(ExecuteActionsTaskExecuter.java:502)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$3.execute(DefaultBuildOperationRunner.java:75)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$3.execute(DefaultBuildOperationRunner.java:68)
	at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:153)
	at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:68)
	at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:56)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor.lambda$run$1(DefaultBuildOperationExecutor.java:74)
	at org.gradle.internal.operations.UnmanagedBuildOperationWrapper.runWithUnmanagedSupport(UnmanagedBuildOperationWrapper.java:45)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:74)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:487)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:470)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.access$300(ExecuteActionsTaskExecuter.java:106)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$TaskExecution.executeWithPreviousOutputFiles(ExecuteActionsTaskExecuter.java:271)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$TaskExecution.execute(ExecuteActionsTaskExecuter.java:249)
	at org.gradle.internal.execution.steps.ExecuteStep.executeInternal(ExecuteStep.java:89)
	at org.gradle.internal.execution.steps.ExecuteStep.access$000(ExecuteStep.java:40)
	at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:53)
	at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:50)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:200)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:195)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$3.execute(DefaultBuildOperationRunner.java:75)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$3.execute(DefaultBuildOperationRunner.java:68)
	at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:153)
	at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:68)
	at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:62)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor.lambda$call$2(DefaultBuildOperationExecutor.java:79)
	at org.gradle.internal.operations.UnmanagedBuildOperationWrapper.callWithUnmanagedSupport(UnmanagedBuildOperationWrapper.java:54)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:79)
	at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:50)
	at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:40)
	at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:68)
	at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:38)
	at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:50)
	at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:36)
	at org.gradle.internal.execution.steps.CancelExecutionStep.execute(CancelExecutionStep.java:41)
	at org.gradle.internal.execution.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:74)
	at org.gradle.internal.execution.steps.TimeoutStep.execute(TimeoutStep.java:55)
	at org.gradle.internal.execution.steps.CreateOutputsStep.execute(CreateOutputsStep.java:51)
	at org.gradle.internal.execution.steps.CreateOutputsStep.execute(CreateOutputsStep.java:29)
	at org.gradle.internal.execution.steps.CaptureStateAfterExecutionStep.execute(CaptureStateAfterExecutionStep.java:58)
	at org.gradle.internal.execution.steps.CaptureStateAfterExecutionStep.execute(CaptureStateAfterExecutionStep.java:39)
	at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:60)
	at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:27)
	at org.gradle.internal.execution.steps.BuildCacheStep.executeWithoutCache(BuildCacheStep.java:180)
	at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:75)
	at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:46)
	at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:40)
	at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:29)
	at org.gradle.internal.execution.steps.RecordOutputsStep.execute(RecordOutputsStep.java:36)
	at org.gradle.internal.execution.steps.RecordOutputsStep.execute(RecordOutputsStep.java:22)
	at org.gradle.internal.execution.steps.SkipUpToDateStep.executeBecause(SkipUpToDateStep.java:105)
	at org.gradle.internal.execution.steps.SkipUpToDateStep.lambda$execute$0(SkipUpToDateStep.java:98)
	at java.base/java.util.Optional.map(Optional.java:265)
	at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:53)
	at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:37)
	at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:85)
	at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:42)
	at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:37)
	at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:27)
	at org.gradle.internal.execution.steps.ResolveCachingStateStep.execute(ResolveCachingStateStep.java:92)
	at org.gradle.internal.execution.steps.ResolveCachingStateStep.execute(ResolveCachingStateStep.java:50)
	at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:114)
	at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:57)
	at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.execute(CaptureStateBeforeExecutionStep.java:73)
	at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.execute(CaptureStateBeforeExecutionStep.java:47)
	at org.gradle.internal.execution.steps.SkipEmptyWorkStep.lambda$execute$2(SkipEmptyWorkStep.java:92)
	at java.base/java.util.Optional.orElseGet(Optional.java:369)
	at org.gradle.internal.execution.steps.SkipEmptyWorkStep.execute(SkipEmptyWorkStep.java:92)
	at org.gradle.internal.execution.steps.SkipEmptyWorkStep.execute(SkipEmptyWorkStep.java:33)
	at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:38)
	at org.gradle.internal.execution.steps.LoadExecutionStateStep.execute(LoadExecutionStateStep.java:43)
	at org.gradle.internal.execution.steps.LoadExecutionStateStep.execute(LoadExecutionStateStep.java:31)
	at org.gradle.internal.execution.steps.AssignWorkspaceStep.lambda$execute$0(AssignWorkspaceStep.java:40)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$TaskExecution$2.withWorkspace(ExecuteActionsTaskExecuter.java:284)
	at org.gradle.internal.execution.steps.AssignWorkspaceStep.execute(AssignWorkspaceStep.java:40)
	at org.gradle.internal.execution.steps.AssignWorkspaceStep.execute(AssignWorkspaceStep.java:30)
	at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:37)
	at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:27)
	at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:44)
	at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:33)
	at org.gradle.internal.execution.impl.DefaultExecutionEngine$1.execute(DefaultExecutionEngine.java:76)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:185)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:174)
	at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:109)
	at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
	at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
	at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
	at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:56)
	at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
	at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
	at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
	at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:200)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:195)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$3.execute(DefaultBuildOperationRunner.java:75)
	at org.gradle.internal.operations.DefaultBuildOperationRunner$3.execute(DefaultBuildOperationRunner.java:68)
	at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:153)
	at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:68)
	at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:62)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor.lambda$call$2(DefaultBuildOperationExecutor.java:79)
	at org.gradle.internal.operations.UnmanagedBuildOperationWrapper.callWithUnmanagedSupport(UnmanagedBuildOperationWrapper.java:54)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:79)
	at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
	at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:74)
	at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:402)
	at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:389)
	at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:382)
	at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:368)
	at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.lambda$run$0(DefaultPlanExecutor.java:127)
	at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:191)
	at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:182)
	at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:124)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
	at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:61)
	at java.base/java.lang.Thread.run(Thread.java:834)

Eliminate Xcode warnings on generated code

It seems 0.5.0 introduced the following Xcode warning:

Forced cast from 'Class.dataclass1' to 'Class.dataclass2' always succeeds

I propose removing the bang operator from the SealedToSwiftEnumFeature.kt file, specifically inside the buildSealedProperty method.

I will happily fork and create the PR myself ๐Ÿ‘๐Ÿผ.

Get Kotlin object from swift enum

When object are created from Swift, there is no way to get the kotlin sealed object.

It would be great if the plugin generate the reverse operation toObject() :

public enum ActionKs {
  case test
  case test2(Action.Test2)

  public init(_ obj: Action) {
    if let obj = obj as? common.Action.Test2 {
      self = .test2(obj)
    } else if obj is common.Action.Test {
      self = .test
    } else {
      fatalError("ActionKs not syncronized with Action class")
    }
  }

  public func toObject() -> Action {
      switch self {
      case .test2(let obj):
          return obj
      case .test:
          return common.Action.Test()
      }
  }
}

Bad interop with swift Arrays

Hello everyone!
I convert my sealed class to swift enum:

sealed class LoadableState<out T> {

    object Initial : LoadableState<Nothing>()

    object Loading : LoadableState<Nothing>()

    class Success<T>(val data: T) : LoadableState<T>()

    class Error private constructor(
        val throwable: Throwable?,
        val message: String?,
    ) : LoadableState<Nothing>() {

        constructor(throwable: Throwable) : this(throwable, throwable.message)

        constructor(message: String?) : this(null, message)
    }
}

kswift converts it to the enum

public enum LoadableStateKs<T : AnyObject> {

  case error(LoadableStateError)
  case initial
  case loading
  case success(LoadableStateSuccess<T>)

  public var sealed: LoadableState<T> {
    switch self {
    case .error(let obj):
      return obj as! MultiPlatformLibrary.LoadableState<T>
    case .initial:
      return MultiPlatformLibrary.LoadableStateInitial() as! MultiPlatformLibrary.LoadableState<T>
    case .loading:
      return MultiPlatformLibrary.LoadableStateLoading() as! MultiPlatformLibrary.LoadableState<T>
    case .success(let obj):
      return obj as MultiPlatformLibrary.LoadableState<T>
    }
  }

  public init(_ obj: LoadableState<T>) {
    if let obj = obj as? MultiPlatformLibrary.LoadableStateError {
      self = .error(obj)
    } else if obj is MultiPlatformLibrary.LoadableStateInitial {
      self = .initial
    } else if obj is MultiPlatformLibrary.LoadableStateLoading {
      self = .loading
    } else if let obj = obj as? MultiPlatformLibrary.LoadableStateSuccess<T> {
      self = .success(obj)
    } else {
      fatalError("LoadableStateKs not synchronized with LoadableState class")
    }
  }

}

Now, in my shared module I use LoadableState<List<CustomObject>> but on the Swift side i canโ€™t use LoadableStateKs<Array<CustomObject>> because LoadableStateKs requires generic argument to be subclass of AnyObject. Maybe I understand this behaviour wrongly, can anyone explain it, please?

Generate extensions over extension methods for Companion object

It seems like no swift code is generated for this function :

fun CostAttributes.Companion.fromDailyEntries(
    today: DailyEntries,
    yesterday: DailyEntries? = null,
    now: String = Clock.System.now().toString()
) = ...

Same things with many other extensions on other companion objects I have

How can I implement SealedToSwiftEnumFeature for xcframework?

So I've created a KMM Library project, not KMM App project. I've follow all the steps in your Github Readme and Medium How to implement Swift-friendly API with Kotlin Multiplatform Mobile. I've created the sealed class
image
And after I generate it into XCFramework ./gradlew assembleXCFramework, the sealed class still convert into this
image

Am I doing it wrong? If you wanted to check out my code, here's the repository https://github.com/Fostahh/MySharedKMMLibrary

Enhancement: Convert sealed classes with deep hierarchy into single Swift enum

Plugin version I've used

plugins {
    id("dev.icerock.moko.kswift") version "0.3.0"
}

Problem

If we define a sealed class with deep hierarchy (when the sealed class has several inheritor sealed classes), moko-kswift will generate different enums for every sealed class.

In Kotlin:

sealed class SealedWithDeepHierarchy {  
  
    abstract val param: String  
  
    sealed class LocalTrigger : SealedWithDeepHierarchy() {  
        object First : LocalTrigger() {  
            override val param: String = "first"  
 		}  
    }  
  
    sealed class RemoteTrigger : SealedWithDeepHierarchy() {  
        class Second(override val param: String) : RemoteTrigger()  
        data class Third(override val param: String) : RemoteTrigger()  
    }  
  
}

// On the Kotlin side this hierarchy splitted into different branches of 'when'-expression
private fun example(s: SealedWithDeepHierarchy) {  
    when (s) {  
        SealedWithDeepHierarchy.LocalTrigger.First -> TODO()  
        is SealedWithDeepHierarchy.RemoteTrigger.Second -> TODO()  
        is SealedWithDeepHierarchy.RemoteTrigger.Third -> TODO()  
    }  
}

moko-kswift will generate bridge code for every sealed class separately:

public enum SealedWithDeepHierarchyLocalTriggerKs {  
  
  case first  
  
  public init(_ obj: SealedWithDeepHierarchy.LocalTrigger) {  
    if obj is HHMobileSdk.SealedWithDeepHierarchy.LocalTriggerFirst {  
      self = .first  
    } else {  
      fatalError("SealedWithDeepHierarchyLocalTriggerKs not syncronized with SealedWithDeepHierarchy.LocalTrigger class")  
    }  
  }  
  
}  
  
public enum SealedWithDeepHierarchyRemoteTriggerKs {  
  
  case second(SealedWithDeepHierarchy.RemoteTriggerSecond)  
  case third(SealedWithDeepHierarchy.RemoteTriggerThird)  
  
  public init(_ obj: SealedWithDeepHierarchy.RemoteTrigger) {  
    if let obj = obj as? HHMobileSdk.SealedWithDeepHierarchy.RemoteTriggerSecond {  
      self = .second(obj)  
    } else if let obj = obj as? HHMobileSdk.SealedWithDeepHierarchy.RemoteTriggerThird {  
      self = .third(obj)  
    } else {  
      fatalError("SealedWithDeepHierarchyRemoteTriggerKs not syncronized with SealedWithDeepHierarchy.RemoteTrigger class")  
    }  
  }  
  
}  
  
public enum SealedWithDeepHierarchyKs {  
  
  case localTrigger(SealedWithDeepHierarchy.LocalTrigger)  
  case remoteTrigger(SealedWithDeepHierarchy.RemoteTrigger)  
  
  public init(_ obj: SealedWithDeepHierarchy) {  
    if let obj = obj as? HHMobileSdk.SealedWithDeepHierarchy.LocalTrigger {  
      self = .localTrigger(obj)  
    } else if let obj = obj as? HHMobileSdk.SealedWithDeepHierarchy.RemoteTrigger {  
      self = .remoteTrigger(obj)  
    } else {  
      fatalError("SealedWithDeepHierarchyKs not syncronized with SealedWithDeepHierarchy class")  
    }  
  }  
  
}

So in Swift you will use it like this:

func mokoKswiftUsage(c: SealedWithDeepHierarchy) {
        switch SealedWithDeepHierarchyKs(c) {
        
        case let .localTrigger(tr):
            switch SealedWithDeepHierarchyLocalTriggerKs(tr) {
            case .first:
                <#code#>
            }
            
            
        case let .remoteTrigger(tr):
            switch SealedWithDeepHierarchyRemoteTriggerKs(tr) {
            case .second(_):
                <#code#>
            case .third(_):
                <#code#>
            }
        }
    }

It would be great, if in such case moko-kswift will generate single enum for more transparent usage, e.g:

func mokoKswiftUsage(c: SealedWithDeepHierarchy) {
        switch SealedWithDeepHierarchyKs(c) {
        
        case .localTriggerFirst:
            <code>

       case .remoteTriggerSecond(_):
           <code>

       case .remoteTriggerThird(_):
            <code>     
    }

Files for external libraries generate with errors

Hello! We need kswift to generate files for external libraries but it's generating files with these errors:
"Cannot find type 'ClassName' in scope" and "No type named 'ClassName' in module 'shared'"

The generated files are ok but it looks like the libraries are inaccessible in ios/swift for some reason.
We use kswift with default settings in a number of projects and all of the external lib files are generated with this issue

Internal sealed class are exported

Internal sealed class are exported by kswift but won't be available in the library. Swift compilation failed because it can not find types.

internal sealed class Response<T> {
    data class Success<T>(val result: T) : Response<T>()
    data class Failure<T>(val error: String?) : Response<T>()
}

Ks classes were not generated ? Why?

Why LoginViewModelActionKs is not generated ? I am getting

Cannot find 'LoginViewModelActionKs' in scope

I added everything in the gradle . But still no Ks classes generated .

My gradle files:

project gradle

buildscript {
    val compose_version by extra("1.1.0-beta01")
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
        classpath("com.android.tools.build:gradle:7.2.1")
        classpath("dev.icerock.moko:kswift-gradle-plugin:0.5.0")
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

tasks.register("clean", Delete::class) {
    delete(rootProject.buildDir)
}

shared module gradle

plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    id("com.android.library")
    id("dev.icerock.moko.kswift")
}

version = "1.0"
val mokoMvvmVersion = "0.13.0"

kotlin {
    android()
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    cocoapods {
        summary = "Some description for the Shared Module"
        homepage = "Link to the Shared Module homepage"
        ios.deploymentTarget = "14.1"
        podfile = project.file("../iosApp/Podfile")
        framework {
            baseName = "MultiPlatformLibrary"
            export("dev.icerock.moko:mvvm-core:$mokoMvvmVersion")
            export("dev.icerock.moko:mvvm-flow:$mokoMvvmVersion")
        }
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation ("com.badoo.reaktive:reaktive:1.2.2")
                implementation ("com.badoo.reaktive:reaktive-annotations:1.2.2")
                api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1-native-mt")
                api("dev.icerock.moko:mvvm-core:$mokoMvvmVersion")
                api("dev.icerock.moko:mvvm-flow:$mokoMvvmVersion")
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
            }
        }
        val androidMain by getting {
            dependencies {
                api("dev.icerock.moko:mvvm-flow-compose:$mokoMvvmVersion")
            }
        }
        val androidTest by getting
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting
        val iosMain by creating {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
            iosSimulatorArm64Main.dependsOn(this)
        }
        val iosX64Test by getting
        val iosArm64Test by getting
        val iosSimulatorArm64Test by getting
        val iosTest by creating {
            dependsOn(commonTest)
            iosX64Test.dependsOn(this)
            iosArm64Test.dependsOn(this)
            iosSimulatorArm64Test.dependsOn(this)
        }
    }
}

android {
    compileSdk = 32
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    defaultConfig {
        minSdk = 21
        targetSdk = 32
    }
}

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature)
}
dependencies {
    commonMainApi("dev.icerock.moko:kswift-runtime:0.5.0") // if you want use annotations
}

**shared Module Class:**

class LoginViewModel : ViewModel() {
    val login: CMutableStateFlow<String> = MutableStateFlow("").cMutableStateFlow()
    val password: CMutableStateFlow<String> = MutableStateFlow("").cMutableStateFlow()

    private val _isLoading: MutableStateFlow<Boolean> = MutableStateFlow(false)
    val isLoading: CStateFlow<Boolean> = _isLoading.cStateFlow()
    val isButtonEnabled: CStateFlow<Boolean> =
        combine(isLoading, login, password) { isLoading, login, password ->
            isLoading.not() && login.isNotBlank() && password.isNotBlank()
        }.stateIn(viewModelScope, SharingStarted.Eagerly, false).cStateFlow()
    private val _actions = Channel<Action>()
    val actions: CFlow<Action> get() = _actions.receiveAsFlow().cFlow()
    fun onLoginPressed() {
        _isLoading.value = true
        viewModelScope.launch {
            delay(1000)
            _isLoading.value = false
            _actions.send(Action.LoginSuccess)
        }
    }
     @SwiftInclude
    sealed interface Action { // not generated ????
        object LoginSuccess : Action
    }
}

please help me

Initial release

  • parsing of klib and pass it into codegen features
  • ability to add own features outside of plugin
  • filters for codegen - all include by default + exclude list
  • filters for codegen - all exclude by default + include list
  • filter by annotation in code - @KSwiftInclude / @KSwiftExclude
  • filter by library - include/exclude logic for libraries list
  • generator for platform classes extensions (like UILabel.bindText(liveData: LiveData<String>))
  • generator for enum classes from sealed classes
  • runtime library for all targets
  • gradle task for generation podspec of swift additions framework
  • sample with cocoapods gradle plugin
  • sample with mobile multiplatform gradle plugin
  • sample with moko-mvvm
  • readme with samples and guide for new generators

Code generation not working on new kotlinArtifacts DSL Module

Our project does not use cocoapods, we consume KMM modules with XCFramework, the module that put all kotlin modules together for publishing does not have any dependency beside the implementation of kotlinArtifacts DSL.

plugins {
   kotlin("multiplatform")
    id("dev.icerock.moko.kswift") version "0.6.1"
}

kotlin {
    ios()
}

// New format https://kotlinlang.org/docs/multiplatform-native-artifacts.html#xcframeworks
kotlinArtifacts {
    Native.XCFramework("kmm") {
        targets(iosSimulatorArm64, iosArm64, iosX64)
        setModules(
            projects.X,
            projects.Y,
            projects.Z,
        )
    }
}

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature)
}

When running :kmm:assembleKmmXCFramework no Swift files are generated from the sealed class from modules X Y Z

Kotlin 1.7.0 Compatibility issue

Hi,

After upgrading to Kotlin 1.7.0, I have started getting to following build error:

The following build commands failed:
PhaseScriptExecution [CP-User]\ Build\ shared /.../build/ios/Pods.build/Debug-iphonesimulator/shared.build/Script-179B9B8F4A300C109F0453B82420AE1B.sh 
(in target 'shared' from project 'Pods')

It works as expected when I downgrade it, or remove moko-kswift from the project.

Resolve mangled names automatically

Kotlin/Native use mangling of conflicted names. For example with code:

interface A {
    fun test(value: Int)
}

interface B {
    fun test(value: String)
}

we got after compilation:

protocol A {
    func test(value: Int)
}

protocol B {
    func test(value_: String)
}

same will be with same classnames some.package.A, some.other.package.A will be A and A_.

now generated Swift code can't detect what name will be mangled. but we can use @KSwiftOverrideName annotation to give new name manually.

maybe we can resolve mangling automatically?

this code:

echo "import shared\n:type lookup shared" | \
	xcrun --sdk macosx swift -F../shared/build/cocoapods/framework/ | \
	tail -n+2 >| ./Sources/Shared/Shared.swift

generates swift api for compiled kotlin framework. and maybe we can find new name of kotlin definition using this generated swift code and swift-ast

Duplicate module names overwrite

We have a gradle module structure like:

featX:ui
featX:data
featY:ui
featY:data

And an umbrealla module consuming all other modules for iOS. Currently the task generates swift files having the module name and not the path of the module.
This leads to a single ui.swift of the last module in the compilation overwriting all other ui.swift modules.

It would be good if that would not happen and the whole module path would be used for the swift name, for example:
featY_ui.swift, featX_ui.swift

I'm using 0.3.0

Xcode warnings on generated code: forced cast always succeeds

I believe I'm experiencing the same issue as #46, even though this is supposed to be fixed in 0.6.0.

I'm seeing 125 Xcode warnings inside my generated Swift file for "forced cast from '..' to '..' always succeeds, did you mean to use 'as'?". These all occur inside the sealed Swift property relating to Kotlin sealed classes, where the Swift enum case always has 1 parameter and the Kotlin sealed class does not use generics at all.

I believe this should fall into the logic mentioned in #46:

If the enum case has a parameter, and the return type is not generic, it does not need to be force-cast.

My environment:

  • KSwift version 0.6.0
  • Xcode 13.4.1 (13F100)
  • iOS project targeting iOS 14.1+
  • Manually integrating the generated Swift files rather than using Cocoapods

Support all Apple Kotlin/Native targets

i not test kswift with not ios apple targets, not know what will work what not...i think it should be separated issue for support all of apple targets - we should implement samples for testing at all, and deployment_target control

Originally posted by @Alex009 in #13 (comment)

It's seems, that current library don't work with gradle 7.5 and with other dependency

Execution failed for task ':verifyReleaseResources'.
> Could not resolve all files for configuration ':releaseRuntimeClasspath'.
   > Failed to transform napier-release.aar (io.github.aakira:napier-android:2.6.1) to match attributes {artifactType=android-compiled-dependencies-resources, org.gradle.category=library, org.gradle.status=release, org.gradle.usage=java-runtime, org.jetbrains.kotlin.platform.type=androidJvm}.
      > Could not isolate parameters com.android.build.gradle.internal.dependency.AarResourcesCompilerTransform$Parameters_Decorated@fe534de of artifact transform AarResourcesCompilerTransform
         > Could not isolate value com.android.build.gradle.internal.dependency.AarResourcesCompilerTransform$Parameters_Decorated@fe534de of type AarResourcesCompilerTransform.Parameters

something wrong with io.github.aakira:napier-android:2.6.1
But I am not sure

Filter with mask/wildcard

Can we filter with mask? like this

kswift {
  install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature) {
    filter = includeFilter("ClassContext/GithubSearchKMM:shared/com/hoc081098/github_search_kmm/**")
  }
}

No signature of method: kswift() is applicable for argument types

Hi!
I tried to follow installation guide.
I've added to my settings.gradle

    gradlePluginPortal()
    maven { url "https://jitpack.io" }

and to my build.gradle

buildscript {
    repositories {
        gradlePluginPortal()
        maven { url "https://jitpack.io" }
    }
}

plugins {
    id 'dev.icerock.moko.kswift' version '0.2.0'
}

kswift {
    install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature) 
}

And after that gradle throws me an error, pointing to line with 'intsall(':

> No signature of method: build_4x81nw95mougfd7sujytehczz.kswift() is applicable for argument types: (build_4x81nw95mougfd7sujytehczz$_run_closure3) values: [build_4x81nw95mougfd7sujytehczz$_run_closure3@381aa491]
  Possible solutions: notify(), wait(), wait(long), print(java.lang.Object), with(groovy.lang.Closure), split(groovy.lang.Closure)

I am using Gradle 6.8.2, Kotlin 1.5.21. Same error if I switch to gradle 7.0.2.

I'm really out of ideas what happening. Maybe you can help?

Not nested sealed subclasses

Subclasses of sealed class declared outside of their parent body end up breaking Swift enums generation.
This is similar to #43 , I'm opening a new issue to point out the bug is not related to sealed interfaces but to the nesting of the subclasses.

I took the NonGenericSealedClass in the example and moved out the subclasses to have the following:

sealed class ExternalNonGenericSealedClass

object ExternalNonGenericWithoutProperty : ExternalNonGenericSealedClass()

data class ExternalNonGenericWithProperty(val value: String) : ExternalNonGenericSealedClass()

Generated Swift code is this one:

public enum ExternalNonGenericSealedClassKs {

  case com/icerockdev/library/ExternalNonGenericWithoutProperty
  case com/icerockdev/library/ExternalNonGenericWithProperty(ExternalNonGenericWithProperty)

  public var sealed: ExternalNonGenericSealedClass {
    switch self {
    case .com/icerockdev/library/ExternalNonGenericWithoutProperty:
      return MultiPlatformLibrary.ExternalNonGenericWithoutProperty() as MultiPlatformLibrary.ExternalNonGenericSealedClass
    case .com/icerockdev/library/ExternalNonGenericWithProperty(let obj):
      return obj as! MultiPlatformLibrary.ExternalNonGenericSealedClass
    }
  }

  public init(_ obj: ExternalNonGenericSealedClass) {
    if obj is MultiPlatformLibrary.ExternalNonGenericWithoutProperty {
      self = .com/icerockdev/library/ExternalNonGenericWithoutProperty
    } else if let obj = obj as? MultiPlatformLibrary.ExternalNonGenericWithProperty {
      self = .com/icerockdev/library/ExternalNonGenericWithProperty(obj)
    } else {
      fatalError("ExternalNonGenericSealedClassKs not synchronized with ExternalNonGenericSealedClass class")
    }
  }

}

Expected code to be generated:

public enum ExternalNonGenericSealedClassKs {

  case externalNonGenericWithoutProperty
  case externalNonGenericWithProperty(ExternalNonGenericWithProperty)

  public var sealed: ExternalNonGenericSealedClass {
    switch self {
    case .externalNonGenericWithoutProperty:
      return MultiPlatformLibrary.ExternalNonGenericWithoutProperty() as MultiPlatformLibrary.ExternalNonGenericSealedClass
    case .externalNonGenericWithProperty(let obj):
      return obj as! MultiPlatformLibrary.ExternalNonGenericSealedClass
    }
  }

  public init(_ obj: ExternalNonGenericSealedClass) {
    if obj is MultiPlatformLibrary.ExternalNonGenericWithoutProperty {
      self = .externalNonGenericWithoutProperty
    } else if let obj = obj as? MultiPlatformLibrary.ExternalNonGenericWithProperty {
      self = .externalNonGenericWithProperty(obj)
    } else {
      fatalError("ExternalNonGenericSealedClassKs not synchronized with ExternalNonGenericSealedClass class")
    }
  }

}

Generated Swift file should use `KotlinThrowable` instead of `Throwable`

Hey, first of all, thanks for this library and the others useful libraries. As a KMM passionated developer, I am very happy to see that you and your team at icerockdev are pushing KMM to the limit ๐Ÿ˜„ ๐Ÿ‘

While I am working on the Swift bridging with moko-kswift, I found out that the generated file use "regular" Throwable instead of KotlinThrowable which result in compiling error at the Client side.

Screen Shot 2021-09-24 at 13 38 03

This is the generated .h content from the Kotlin Native side.

This looks like a bug to me, or I was wondering whether I am missing anything here?

Provide Swift friendly alternative to data class copy methods

Similar to #8, but requires a different solution There is no good Swift way to call a copy methods on a Kotlin data class because the default parameter values are not provided, but it isn't possible to actually have parameters default to values based on the object so it is not possible to write an exact replica of the copy function. It isn't important that what is generated be the same syntactically. It is more important to get the same functionality, e.g to make a copy of the data class changing only part of the object without having to specify every property.

Here is a SO post that talks about how to write such a method in Dart, which I know is not Swift, but the same concepts and limitations apply: https://stackoverflow.com/questions/68009392/dart-custom-copywith-method-with-nullable-properties

The best alternative is probably the one that takes nullable parameters that default to nil and if it is nil the property is unchanged. The wrinkle is for properties that were nullable, in which case you add a boolean parameter for each such property that says whether to treat null as null or unchanged.

SealedToSwiftEnumFeature: Any is not a class type

Consider the following sealed class.

sealed class Status<T : Any> {
    data class Success<T : Any>(val data: T) : Status<T>()
    data class Failure<T : Any>(val exception: Exception) : Status<T>()
}

SealedToSwiftEnumFeature converts this to:

public enum StatusKs<T : Any> {

  case success(StatusSuccess<T>)
  case failure(StatusFailure<T>)

  public init(_ obj: Status<T>) {
    if let obj = obj as? shared.StatusSuccess<T> {
      self = .success(obj)
    } else if let obj = obj as? shared.StatusFailure<T> {
      self = .failure(obj)
    } else {
      fatalError("StatusKs not syncronized with Status class")
    }
  }
}

However, as Any allows structs etc the code won't compile.

image

Instead, converting Any to AnyObject fixes the above compilation issue.

The Kotlin conversion of Status is as follows:

open class Status<T> : KotlinBase where T : AnyObject {

    public init()
}

public class StatusFailure<T> : Status<T> where T : AnyObject {

    public init(exception: KotlinException)

    
    open func component1() -> KotlinException

    open func doCopy(exception: KotlinException) -> StatusFailure<T>

    open func isEqual(_ other: Any?) -> Bool

    open func hash() -> UInt

    open func description() -> String

    open var exception: KotlinException { get }
}

public class StatusSuccess<T> : Status<T> where T : AnyObject {

    public init(data: T)

    
    open func component1() -> T

    open func doCopy(data: T) -> StatusSuccess<T>

    open func isEqual(_ other: Any?) -> Bool

    open func hash() -> UInt

    open func description() -> String

    open var data: T { get }
}

Interfaces's extensions not generated

Extensions not generated for interfaces and interface's implementations.
https://github.com/Merseyside/mersey-kmp-time/blob/master/time/src/commonMain/kotlin/com/merseyside/merseyLib/time/ext/ZonedTimeUnitExt.kt (generates as expected in extensions swift block because ZonedTimeUnit doesn't extend any interface)
https://github.com/Merseyside/mersey-kmp-time/blob/master/time/src/commonMain/kotlin/com/merseyside/merseyLib/time/ext/TimeUnitExt.kt (generates separately with class name of kotlin file contains extensions)

Example of usage: https://github.com/Merseyside/mersey-kmp-time/blob/master/ios-app-swiftui/ios-app-swiftui/TimeTest.swift

Redundand libs are processed and generates code

In moko-kswift 0.6.0 changed libs processing logic - later we generates code only for libs that added to export. but in 0.6.0 we start generates code for all libs, not only exported. and this generated code is invalid by default because naming. Need to rollback logic

sealed interface structure bug

Bug

sealed interface FeedListUnit

data class AddPostUnit(
    val onClick: () -> Unit
): FeedListUnit

data class FeedUnit(
    val dateTimeString: StringDesc,
    val postText: String,
    val likesCount: Int,
    val dislikeCount: Int,
    val feedReaction: FeedReaction,
    val likeClick: () -> Unit,
    val dislikeClick: () -> Unit
): FeedListUnit

compile to

public enum FeedListUnitKs {

  case ru/app/mobile/shared/feed/presentation/feed/AddPostUnit(AddPostUnit)
  case ru/app/mobile/shared/feed/presentation/feed/FeedUnit(FeedUnit)

  public init(_ obj: FeedListUnit) {
    if let obj = obj as? shared.AddPostUnit {
      self = .ru/app/mobile/shared/feed/presentation/feed/AddPostUnit(obj)
    } else if let obj = obj as? shared.FeedUnit {
      self = .ru/app/mobile/shared/feed/presentation/feed/FeedUnit(obj)
    } else {
      fatalError("FeedListUnitKs not syncronized with FeedListUnit class")
    }
  }
}

`No extensions handle the extension type: kotlinx.metadata.klib.KlibModuleFragmentExtensionVisitor`

I tried to integrate this plugin by adding id("dev.icerock.moko.kswift") version "0.6.0" to my plugins{} section and kswift { install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature) } as config. Unfortunately, the following error shows up when I try to build my KMM project using ./gradlew shared:build:

> Task :shared:linkDebugFrameworkIosArm64 FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':shared:linkDebugFrameworkIosArm64'.
> No extensions handle the extension type: kotlinx.metadata.klib.KlibModuleFragmentExtensionVisitor

I am using Kotlin 1.7.10.

Do you have any idea how to solve this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.