Hello!
I try to create formatted string in iOS platform module.
In Swift code should be:
NSString(format: "test %s", "test 2")
but in Kotlin/Native i can't use same init method.
public convenience init(format: NSString, _ args: CVarArg...)
was converted to
@kotlinx.cinterop.ObjCMethod public external fun platform.Foundation.NSString.initWithFormat(format: kotlin.String, arguments: platform.posix.va_list? /* = kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVar /* = kotlinx.cinterop.ByteVarOf<kotlin.Byte> */>? */): kotlin.String { /* compiled code */ }
and then call in kotlin was:
NSString().initWithFormat("test %s", "test 2")
what throw exception:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** initialization method -initWithFormat:locale:arguments: cannot be sent to an abstract object of class __NSCFConstantString: Create a concrete instance!'
how create NSString with format in kotlin/native?
init* methods aren't supposed to be used directly in Kotlin/Native.
Initializers declared in Objective-C classes are available as constructors instead, and initializers declared in Objective-C categories -- as create factory methods.
However initWithFormat appears to be variadic, and Kotlin/Native doesn't support variadic Objective-C methods.
By the way, do you have a case that is not covered by pure Kotlin methods and string interpolation? If so, could you please provide more details about it?
i create multiplatform resource classes to use it in viewmodels (in common module).
in common module have expect class:
expect sealed class StringDesc {
class Resource(stringRes: StringResource): StringDesc
class ResourceFormatted(stringRes: StringResource, args: List<Any>): StringDesc {
constructor(stringRes: StringResource, vararg args: Any)
}
class Plural(pluralsRes: PluralsResource, number: Int): StringDesc
class PluralFormatted(pluralsRes: PluralsResource, number: Int, args: List<Any>): StringDesc {
constructor(pluralsRes: PluralsResource, number: Int, vararg args: Any)
}
class Raw(string: String): StringDesc
class Composition(args: List<StringDesc>): StringDesc
}
and for android:
actual sealed class StringDesc {
actual data class Resource actual constructor(val stringRes: StringResource) : StringDesc() {
override fun toString(context: Context): String {
return context.getString(stringRes.resourceId)
}
}
actual data class ResourceFormatted actual constructor(val stringRes: StringResource, val args: List<Any>) : StringDesc() {
override fun toString(context: Context): String {
return context.getString(stringRes.resourceId, *(args.toTypedArray()))
}
actual constructor(stringRes: StringResource, vararg args: Any) : this(stringRes, args.toList())
}
actual data class Plural actual constructor(val pluralsRes: PluralsResource, val number: Int) : StringDesc() {
override fun toString(context: Context): String {
return context.resources.getQuantityString(pluralsRes.resourceId, number)
}
}
actual data class PluralFormatted actual constructor(val pluralsRes: PluralsResource, val number: Int, val args: List<Any>) : StringDesc() {
override fun toString(context: Context): String {
return context.resources.getQuantityString(pluralsRes.resourceId, number, *(args.toTypedArray()))
}
actual constructor(pluralsRes: PluralsResource, number: Int, vararg args: Any) : this(pluralsRes, number, args.toList())
}
actual data class Raw actual constructor(val string: String) : StringDesc() {
override fun toString(context: Context): String {
return string
}
}
actual data class Composition actual constructor(val args: List<StringDesc>) : StringDesc() {
override fun toString(context: Context): String {
return StringBuilder().apply {
args.forEach {
append(it.toString(context))
}
}.toString()
}
}
abstract fun toString(context: Context): String
}
but for ios string formatting should be in native swift code for now...and now i get:
actual sealed class StringDesc {
actual data class Resource actual constructor(val stringRes: StringResource) : StringDesc() {
override fun toLocalizedString(formatter: Formatter): String {
return NSBundle.mainBundle().localizedStringForKey(stringRes.resourceId, null, null)
}
}
actual data class ResourceFormatted actual constructor(val stringRes: StringResource, val args: List<Any>) : StringDesc() {
override fun toLocalizedString(formatter: Formatter): String {
val string = NSBundle.mainBundle().localizedStringForKey(stringRes.resourceId, null, null)
return formatter.formatString(string, args.toTypedArray())
}
actual constructor(stringRes: StringResource, vararg args: Any) : this(stringRes, args.toList())
}
actual data class Plural actual constructor(val pluralsRes: PluralsResource, val number: Int) : StringDesc() {
override fun toLocalizedString(formatter: Formatter): String {
return formatter.plural(pluralsRes.resourceId, number)
}
}
actual data class PluralFormatted actual constructor(val pluralsRes: PluralsResource, val number: Int, val args: List<Any>) : StringDesc() {
override fun toLocalizedString(formatter: Formatter): String {
return formatter.formatPlural(pluralsRes.resourceId, number, args.toTypedArray())
}
actual constructor(pluralsRes: PluralsResource, number: Int, vararg args: Any) : this(pluralsRes, number, args.toList())
}
actual data class Raw actual constructor(val string: String) : StringDesc() {
override fun toLocalizedString(formatter: Formatter): String {
return string
}
}
actual data class Composition actual constructor(val args: List<StringDesc>) : StringDesc() {
override fun toLocalizedString(formatter: Formatter): String {
return StringBuilder().apply {
args.forEach {
append(it.toLocalizedString(formatter))
}
}.toString()
}
}
abstract fun toLocalizedString(formatter: Formatter): String
interface Formatter {
fun formatString(string: String, args: Array<out Any>): String
fun plural(key: String, number: Int): String
fun formatPlural(key: String, number: Int, args: Array<out Any>): String
}
}
it's work, but in swift for get string from resource you should pass Formatter implementation, like this:
class StringFormatter: NSObject, EIFStringDescFormatter {
func formatString(string: String, args: EIFStdlibArray) -> String {
var cargs = Array<CVarArg>()
for i in 0...(args.size - 1) {
guard let arg = args.get(index: i) as? CVarArg else {
continue
}
cargs.append(arg)
}
return String(format: string, arguments: cargs)
}
func plural(key: String, number: Int32) -> String {
let localized = NSLocalizedString(key, comment: "")
return String.localizedStringWithFormat(localized, number)
}
func formatPlural(key: String, number: Int32, args: EIFStdlibArray) -> String {
let localized = NSLocalizedString(key, comment: "")
let pluralized = String.localizedStringWithFormat(localized, number)
var cargs = Array<CVarArg>()
for i in 0...(args.size - 1) {
guard let arg = args.get(index: i) as? CVarArg else {
continue
}
cargs.append(arg)
}
return String(format: pluralized, cargs)
}
}
ideally it should be in common-ios module in kotlin. but i can't use String(format: args:) in kotlin.
resourceclasses is simple:
android:
actual class PluralsResource(@PluralsRes val resourceId: Int)
actual class StringResource(@StringRes val resourceId: Int)
ios:
actual class PluralsResource(val resourceId: String)
actual class StringResource(val resourceId: String)
Thank you for the detailed explanation!
Kotlin/Native supports variadic C functions, so as a workaround you can try to use CoreFoundation string formatting, but this appears to be somewhat tricky:
import kotlinx.cinterop.*
import platform.CoreFoundation.*
import platform.darwin.*
import platform.Foundation.*
fun format(format: String, vararg args: Any?): String {
val cfString: CFStringRef = CFBridgingRetain(format)!!.reinterpret()
try {
val encodedArgs = args.map {
if (it is String)
// Prevent from being converted to C string when passing as C variadic argument:
object : NSObject() {
override fun description(): String? = it
}
else it
}
val cfFormattedString = CFStringCreateWithFormat(null, null, cfString, *encodedArgs.toTypedArray())
return CFBridgingRelease(cfFormattedString) as String
} finally {
CFRelease(cfString)
}
}
For example:
fun main(args: Array<String>) {
println(format("%@ %@ %d", "один", "two", 3))
}
good, thx.
it can help in ResourceFormatted case, but for Plural / PluralFormatted should be used String.localizedStringWithFormat (plurals in ios - https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/LocalizingYourApp/LocalizingYourApp.html#//apple_ref/doc/uid/10000171i-CH5-SW10 ).
Yes, you are right.
localizedStringWithFormat can be workarounded by writing a non-variadic wrapper in Objective-C. And you can easily add Objective-C code to your project within a .def file, e.g. formatting.def:
language = Objective-C
---
#import <Foundation/NSString.h>
NSString* NSLocalizedStringWithFormat(NSString* format, int number) {
return [NSString localizedStringWithFormat:format, number];
}
It can be added to your Gradle build using interop artifact. Something like this should work:
konanArtifacts {
interop('formatting')
yourProgram('program') {
libraries {
artifact 'foo'
}
}
}
(see documentation for more details).
After that NSLocalizedStringWithFormat can be imported to your program from formatting package.
Yet another nontrivial workaround :)
@SvyatoslavScherbina, what do you think about creating a special ios-extension library? Maybe it shouldn't be exactly a library, just a set of tips and tricks, I don't know...
I mean, iOs is one of the growth drivers for Kotlin/Native. A lot of developers are writing various adapters and workarounds to support common code on iOS / Android platforms. It would be great to collect everything in one place, so that each developer does not do the same work again :)
Looks like a good idea, maybe on our side we shall support variadic functions eventually, and in the meantime keep docs like this issue an information source. @subprogram if you could consider creating such adapters library, it would be valuable contribution for us.
I'm afraid I have no time for this... If someone create project and write a basis of such library, I'll make a few pull requests.
Thank you for the detailed explanation!
Kotlin/Native supports variadic C functions, so as a workaround you can try to use CoreFoundation string formatting, but this appears to be somewhat tricky:import kotlinx.cinterop.* import platform.CoreFoundation.* import platform.darwin.* import platform.Foundation.* fun format(format: String, vararg args: Any?): String { val cfString: CFStringRef = CFBridgingRetain(format)!!.reinterpret() try { val encodedArgs = args.map { if (it is String) // Prevent from being converted to C string when passing as C variadic argument: object : NSObject() { override fun description(): String? = it } else it } val cfFormattedString = CFStringCreateWithFormat(null, null, cfString, *encodedArgs.toTypedArray()) return CFBridgingRelease(cfFormattedString) as String } finally { CFRelease(cfString) } }For example:
fun main(args: Array<String>) { println(format("%@ %@ %d", "один", "two", 3)) }
@SvyatoslavScherbina how convert to UTF-8? I use your code but return !UTF-8 :so sad:
@lehanphong
Consider using stringFromUtf8OrThrow/stringFromUtf8 or casting to NSString and using one of its encoding methods.
Thank you for the detailed explanation!
Kotlin/Native supports variadic C functions, so as a workaround you can try to use CoreFoundation string formatting, but this appears to be somewhat tricky:import kotlinx.cinterop.* import platform.CoreFoundation.* import platform.darwin.* import platform.Foundation.* fun format(format: String, vararg args: Any?): String { val cfString: CFStringRef = CFBridgingRetain(format)!!.reinterpret() try { val encodedArgs = args.map { if (it is String) // Prevent from being converted to C string when passing as C variadic argument: object : NSObject() { override fun description(): String? = it } else it } val cfFormattedString = CFStringCreateWithFormat(null, null, cfString, *encodedArgs.toTypedArray()) return CFBridgingRelease(cfFormattedString) as String } finally { CFRelease(cfString) } }For example:
fun main(args: Array<String>) { println(format("%@ %@ %d", "один", "two", 3)) }
@SvyatoslavScherbina Your workaround no longer works in Kotlin 1.3.30-eap-125. When exporting the code as a framework I get the following error:
e: /Users/thomas/[...]/Strings.kt: (34, 81): spread operator is not supported for variadic C functions
I'm not sure if this is a bug or if this is supposed to no longer work anymore.
Spread operator is not supposed to work for C variadic functions anymore.
To workaround the original issue starting with 1.3.30, you can add cinterop with the following .def file:
language = Objective-C
---
#import <Foundation/NSString.h>
NSString* format(NSString* format, ...) {
va_list args;
va_start(args, format);
NSString* result = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
return result;
}
Note that Kotlin String passed as variadic argument is still treated as C string, so it still has to be hidden. But the conversion is now based on static type, so upcasting to Any works properly:
val String.va: Any get() = this
fun main() {
println(format("%@ %@ %d", "один".va, "two".va, 3))
}
Starting from 1.3.40, it will be possible to use initWithFormat directly:
NSString.create(format = "%@ %@ %d", args = *arrayOf("один" as NSString, "two" as NSString, 3))
Casting to NSString is required to resolve ambiguity between passing Kotlin string as C string and as NSString (casting to Any works too).
Named arguments are required to resolve ambiguity between initWithFormat: and initWithFormat:locale:.
Alternatively one could use stringWithFormat: class function instead:
NSString.stringWithFormat("%@ %@ %d", "один" as NSString, "two" as NSString, 3)
Most helpful comment
Yet another nontrivial workaround :)
@SvyatoslavScherbina, what do you think about creating a special ios-extension library? Maybe it shouldn't be exactly a library, just a set of tips and tricks, I don't know...
I mean, iOs is one of the growth drivers for Kotlin/Native. A lot of developers are writing various adapters and workarounds to support common code on iOS / Android platforms. It would be great to collect everything in one place, so that each developer does not do the same work again :)