Skip to content

Commit

Permalink
Merge pull request #93 from icerockdev/develop
Browse files Browse the repository at this point in the history
Release 0.7.0
  • Loading branch information
Alex009 authored Jan 22, 2024
2 parents 511bb27 + ca08184 commit cccacec
Show file tree
Hide file tree
Showing 13 changed files with 418 additions and 54 deletions.
47 changes: 43 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ buildscript {
gradlePluginPortal()
}
dependencies {
classpath("dev.icerock.moko:kswift-gradle-plugin:0.6.1")
classpath("dev.icerock.moko:kswift-gradle-plugin:0.7.0")
}
}
```
Expand Down Expand Up @@ -95,7 +95,7 @@ project where framework compiles `build.gradle`

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

Expand All @@ -115,7 +115,7 @@ project `build.gradle`

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

Expand Down Expand Up @@ -308,6 +308,45 @@ public extension UIKit.UILabel {

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:
```kotlin
kswift {
install(dev.icerock.moko.kswift.plugin.feature.DataClassCopyFeature)
}
```

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

With this feature for data class like this:
```kotlin
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:
```swift
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:
Expand All @@ -328,7 +367,7 @@ repositories {
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.2.0")
implementation("dev.icerock.moko:kswift-gradle-plugin:0.7.0")
}
```

Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ androidLifecycleVersion = "2.1.0"
androidCoreTestingVersion = "2.1.0"
coroutinesVersion = "1.6.0-native-mt"
mokoTestVersion = "0.4.0"
mokoKSwiftVersion = "0.6.1"
mokoKSwiftVersion = "0.7.0"

mokoMvvmVersion = "0.11.0"
mokoResourcesVersion = "0.16.2"

kotlinxMetadataKLibVersion = "0.0.1"

kotlinPoetVersion = "1.6.0"
swiftPoetVersion = "1.3.1"
swiftPoetVersion = "1.5.0"

[libraries]
appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import io.outfoxx.swiftpoet.parameterizedBy
import kotlinx.metadata.ClassName
import kotlinx.metadata.Flag
import kotlinx.metadata.KmClass
import kotlinx.metadata.KmConstructor

fun KmClass.isDataClass(): Boolean {
return Flag.Class.IS_DATA(flags)
}

fun KmClass.getPrimaryConstructor(): KmConstructor = constructors.getPrimaryConstructor()

fun KmClass.buildTypeVariableNames(
kotlinFrameworkName: String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.kswift.plugin

import kotlinx.metadata.Flag
import kotlinx.metadata.KmConstructor

fun KmConstructor.isPrimary(): Boolean = Flag.Constructor.IS_SECONDARY(flags).not()

fun List<KmConstructor>.getPrimaryConstructor(): KmConstructor = single { constructor -> constructor.isPrimary() }
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
package dev.icerock.moko.kswift.plugin

import io.outfoxx.swiftpoet.ANY_OBJECT
import io.outfoxx.swiftpoet.ARRAY
import io.outfoxx.swiftpoet.BOOL
import io.outfoxx.swiftpoet.DICTIONARY
import io.outfoxx.swiftpoet.DeclaredTypeName
import io.outfoxx.swiftpoet.FunctionTypeName
import io.outfoxx.swiftpoet.INT32
import io.outfoxx.swiftpoet.ParameterSpec
import io.outfoxx.swiftpoet.SET
import io.outfoxx.swiftpoet.STRING
import io.outfoxx.swiftpoet.TypeName
import io.outfoxx.swiftpoet.TypeVariableName
import io.outfoxx.swiftpoet.UINT64
import io.outfoxx.swiftpoet.VOID
import io.outfoxx.swiftpoet.parameterizedBy
import kotlinx.metadata.ClassName
import kotlinx.metadata.Flag
import kotlinx.metadata.KmClassifier
import kotlinx.metadata.KmType
import kotlinx.metadata.KmTypeProjection

@Suppress("ReturnCount")
fun KmType.toTypeName(
Expand All @@ -35,12 +41,12 @@ fun KmType.toTypeName(
} else throw IllegalArgumentException("can't read type parameter $this without type variables list")
}
is KmClassifier.TypeAlias -> {
classifier.name.kotlinTypeNameToSwift(moduleName, isUsedInGenerics)
classifier.name.kotlinTypeNameToSwift(moduleName, isUsedInGenerics, arguments)
?: throw IllegalArgumentException("can't read type alias $this")
}
is KmClassifier.Class -> {
val name: TypeName? =
classifier.name.kotlinTypeNameToSwift(moduleName, isUsedInGenerics)
classifier.name.kotlinTypeNameToSwift(moduleName, isUsedInGenerics, arguments)
return name ?: kotlinTypeToTypeName(
moduleName,
classifier.name,
Expand All @@ -51,14 +57,23 @@ fun KmType.toTypeName(
}
}

fun String.kotlinTypeNameToSwift(moduleName: String, isUsedInGenerics: Boolean): TypeName? {
@Suppress("LongMethod", "ComplexMethod")
fun String.kotlinTypeNameToSwift(
moduleName: String,
isUsedInGenerics: Boolean,
arguments: MutableList<KmTypeProjection>
): TypeName? {
return when (this) {
"kotlin/String" -> if (isUsedInGenerics) {
DeclaredTypeName(moduleName = "Foundation", simpleName = "NSString")
} else {
STRING
}
"kotlin/Int" -> DeclaredTypeName(moduleName = "Foundation", simpleName = "NSNumber")
"kotlin/Int" -> if (isUsedInGenerics) {
DeclaredTypeName(moduleName = moduleName, simpleName = "KotlinInt")
} else {
INT32
}
"kotlin/Boolean" -> if (isUsedInGenerics) {
DeclaredTypeName(moduleName = moduleName, simpleName = "KotlinBoolean")
} else {
Expand All @@ -67,6 +82,44 @@ fun String.kotlinTypeNameToSwift(moduleName: String, isUsedInGenerics: Boolean):
"kotlin/ULong" -> UINT64
"kotlin/Unit" -> VOID
"kotlin/Any" -> ANY_OBJECT
"kotlin/collections/List" -> {
arguments.first().type?.run {
DeclaredTypeName.typeName(ARRAY.name).parameterizedBy(
this.toTypeName(
moduleName,
isUsedInGenerics = this.shouldUseKotlinTypeWhenHandlingCollections()
)
)
}
}
"kotlin/collections/Set" -> {
arguments.first().type?.run {
DeclaredTypeName.typeName(SET.name).parameterizedBy(
this.toTypeName(
moduleName,
isUsedInGenerics = this.shouldUseKotlinTypeWhenHandlingCollections()
)
)
}
}
"kotlin/collections/Map" -> {
val firstArgumentType = arguments.first().type
val secondArgumentType = arguments[1].type
if (firstArgumentType != null && secondArgumentType != null) {
DeclaredTypeName.typeName(DICTIONARY.name).parameterizedBy(
firstArgumentType.toTypeName(
moduleName,
isUsedInGenerics = firstArgumentType.shouldUseKotlinTypeWhenHandlingCollections()
),
secondArgumentType.toTypeName(
moduleName,
isUsedInGenerics = secondArgumentType.shouldUseKotlinTypeWhenHandlingCollections()
)
)
} else {
null
}
}
else -> {
if (this.startsWith("platform/")) {
val withoutCompanion: String = this.removeSuffix(".Companion")
Expand Down Expand Up @@ -142,3 +195,13 @@ fun DeclaredTypeName.objcNameToSwift(): DeclaredTypeName {
else -> this
}
}

fun KmType.shouldUseKotlinTypeWhenHandlingOptionalType(): Boolean =
if (classifier.toString().contains("kotlin/String")) {
false
} else {
Flag.Type.IS_NULLABLE(flags)
}

fun KmType.shouldUseKotlinTypeWhenHandlingCollections(): Boolean =
!classifier.toString().contains("kotlin/String")
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.kswift.plugin.feature

import dev.icerock.moko.kswift.plugin.buildTypeVariableNames
import dev.icerock.moko.kswift.plugin.context.ClassContext
import dev.icerock.moko.kswift.plugin.context.kLibClasses
import dev.icerock.moko.kswift.plugin.getDeclaredTypeNameWithGenerics
import dev.icerock.moko.kswift.plugin.getPrimaryConstructor
import dev.icerock.moko.kswift.plugin.isDataClass
import dev.icerock.moko.kswift.plugin.shouldUseKotlinTypeWhenHandlingOptionalType
import dev.icerock.moko.kswift.plugin.toTypeName
import io.outfoxx.swiftpoet.CodeBlock
import io.outfoxx.swiftpoet.ExtensionSpec
import io.outfoxx.swiftpoet.FunctionSpec
import io.outfoxx.swiftpoet.FunctionTypeName
import io.outfoxx.swiftpoet.ParameterSpec
import io.outfoxx.swiftpoet.TypeSpec
import io.outfoxx.swiftpoet.TypeVariableName
import kotlinx.metadata.Flag
import kotlinx.metadata.KmClass
import kotlin.reflect.KClass

/**
* Creates an improved copy method for data classes
*/

class DataClassCopyFeature(
override val featureContext: KClass<ClassContext>,
override val filter: Filter<ClassContext>
) : ProcessorFeature<ClassContext>() {

@Suppress("ReturnCount")
override fun doProcess(featureContext: ClassContext, processorContext: ProcessorContext) {

val kotlinFrameworkName: String = processorContext.framework.baseName
val kmClass: KmClass = featureContext.clazz

if (Flag.IS_PUBLIC(kmClass.flags).not()) return

if (kmClass.isDataClass().not()) return

val typeVariables: List<TypeVariableName> =
kmClass.buildTypeVariableNames(kotlinFrameworkName)

// doesn't support generic data classes right now
if (typeVariables.isNotEmpty()) return

val functionReturnType = kmClass.getDeclaredTypeNameWithGenerics(
kotlinFrameworkName,
featureContext.kLibClasses
)

val constructorParameters = kmClass.getPrimaryConstructor().valueParameters

val functionParameters = constructorParameters.mapNotNull { parameter ->

val parameterType = parameter.type ?: return@mapNotNull null

ParameterSpec.builder(
parameterName = parameter.name,
type = FunctionTypeName.get(
parameters = emptyList(),
returnType = parameterType.toTypeName(
moduleName = kotlinFrameworkName,
isUsedInGenerics = parameterType.shouldUseKotlinTypeWhenHandlingOptionalType()
).run {
if (Flag.Type.IS_NULLABLE(parameterType.flags)) {
makeOptional()
} else {
this
}
}
).makeOptional(),
).defaultValue(
codeBlock = CodeBlock.builder().add("nil").build()
).build()
}
val functionBody = constructorParameters.joinToString(separator = ",") { property ->
property.name.run {
"$this: ($this != nil) ? $this!() : self.$this"
}
}

val copyFunction = FunctionSpec.builder("copy")
.addParameters(functionParameters)
.returns(functionReturnType)
.addCode(
CodeBlock.builder()
.addStatement("return %T($functionBody)", functionReturnType)
.build()
)
.build()

val extensionSpec =
ExtensionSpec.builder(TypeSpec.classBuilder(functionReturnType.name).build())
.addFunction(copyFunction)
.addDoc("selector: ${featureContext.prefixedUniqueId}")
.build()

processorContext.fileSpecBuilder.addExtension(extensionSpec)
}

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

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

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

@JvmStatic
override val factory = Companion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ class SealedToSwiftEnumFeature(
} else {
"as!"
}
} else if (paramType !is ParameterizedTypeName && returnType !is ParameterizedTypeName) {
// If the parameter and return type is not ParameterizedTypeName
// regular cast can be used.
"as"
} else {
// If the parameter and return type have differing generic patterns
// then a force-cast is needed.
Expand Down
Loading

0 comments on commit cccacec

Please sign in to comment.