We have some code today that takes an NSArray and passes it as a argument list to -[NSString initWithFormat:arguments] and we\'re trying to get this to work with ARC. Here\'s th
I tried mcfedr
's code. Somehow, my Xcode 11 treated CVarArgType
as undeclared type, so I investigated into this for a while.
I didn't not understand the closure part of his/her code. And, I just simplified to hard casted each element to CVarArg
using as!
operator.
func format(key: String, args: [Any]) -> String {
return String(format: key, arguments: args.map { ($0 as! CVarArg) })
}
let doubleValue: Double = 1.25
let floatValue: Float = 2.75
let intValue: Int = 3
let numberValue: NSNumber = 4.5 as NSNumber
let hello: String = "Hello"
let world: NSString = "World" as NSString
print(format(key: "double: %f, float: %f, int: %d, number: %@, %@, %@", args: [doubleValue, floatValue, intValue, numberValue, hello, world]))
// double: 1.250000, float: 2.750000, int: 3, number: 4.5, Hello, World
It seems it's working fine under swift 5.1, but there may be some pitfalls.
I write solution use NSInvocation and signatures.
Answer create in this. Also I write detailed description how it work but only on Russian ((
Maybe it help for someone.
This only works for arrays with a single element
The answer by chrisco was working well, until I went to compile with 64-bit architecture. This caused an error:
EXC_BAD_ADDRESS type EXC_I386_GPFLT
The solution was to use a slightly different approach for passing the argument list to the method:
+ (id)stringWithFormat:(NSString *)format array:(NSArray*) arguments;
{
__unsafe_unretained id * argList = (__unsafe_unretained id *) calloc(1UL, sizeof(id) * arguments.count);
for (NSInteger i = 0; i < arguments.count; i++) {
argList[i] = arguments[i];
}
NSString* result = [[NSString alloc] initWithFormat:format, *argList] ;// arguments:(void *) argList];
free (argList);
return result;
}
Expanding on @mcfedr's answer, this Swift 3 helper does the job:
import Foundation
@objc (FTStringFormat) public class StringFormat: NSObject {
@objc public class func format(key: String, args: [AnyObject]) -> String {
let locArgs: [CVarArg] = args.flatMap({ (arg: AnyObject) -> CVarArg? in
if let arg = arg as? NSNumber {
return arg.intValue
}
if let arg = arg as? CustomStringConvertible {
return arg.description
}
return nil
});
return String(format: key, arguments: locArgs)
}
}
Calling from Objective-C:
[FTStringFormat formatWithKey:@"name: %@ age: %d" args:@[@"John", @(42)]]
For the %@
format specifier we're using Swift's CustomStringConvertible
protocol in order to call description
on all of the array members.
Supporting all number format specifiers like %d
and %f
is not really possible because the NSNumber
object doesn't reveal if it's an integer or float. So we could only support one or the other. Here we use intValue
, so %d
is supported but %f
and %g
are not.
The only thing you need to do is remove the autorelease.
You're malloc'ing and free'ing yourself - ARC doesn't care about that.
Cannot find a way to do this obj-c but a swift helper class finally got this working (my whole project is obj-c except this class)
@objc class StringFormat: NSObject {
class func format(key: String, args: [AnyObject]) -> String {
let locArgs: [CVarArgType] = args.map({ (arg: AnyObject) -> CVarArgType in
if let iArg = (arg is NSNumber ? arg.intValue : nil) {
return iArg
}
return arg as! CVarArgType
});
return String(format: key, arguments: locArgs)
}
}
There is some magic going on, to do with how [CVarArgType]
doesn't behave like a normal array - but this works in the flexible cross architecture way you expect it to.