13

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 the code were using

NSString* format = @"Item %s and Item %s"; // Retrieved elsewhere
NSArray* args = [NSArray arrayWithObjects:@"1", @"2", nil]; // Retrieved elsewhere

// http://cocoawithlove.com/2009/05/variable-argument-lists-in-cocoa.html
char* argsList = (char*) malloc(sizeof(NSString*) * args.count);
[args getObjects:(id*) argsList];
NSString* message = [[[NSString alloc] initWithFormat:format arguments:argsList] autorelease];
free(argsList);

Any recommendations on how to make this ARC compliant? Or we're even open to a better way of doing it.

Gary Rudolph
  • 1,062
  • 9
  • 21
  • 2
    http://stackoverflow.com/questions/8211996/fake-va-list-in-arc – Mattias Wadman Nov 25 '11 at 19:49
  • I like that answer with NSMutableData, much "cleaner" (for a not very clean concept). Annoying how I couldn't find it with search. – Gary Rudolph Nov 25 '11 at 20:31
  • possible duplicate of [How to create a NSString from a format string like @"xxx=%@, yyy=%@" and a NSArray of objects?](http://stackoverflow.com/questions/1058736/how-to-create-a-nsstring-from-a-format-string-like-xxx-yyy-and-a-nsarr) – Chuck Jan 14 '14 at 22:16

6 Answers6

4

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;
}
Brett
  • 769
  • 6
  • 16
  • 1
    this one crash on iOS7 – Marcin Feb 12 '14 at 01:10
  • crash on [[NSString alloc] initWithFormat:format, *argList], nothing specific. – Marcin Feb 18 '14 at 17:44
  • Marcin, without any debug output it is hard to say what is going on. But I might suggest that you are using something other than id to store the pointers (char or int perhaps?) – Brett Mar 03 '14 at 13:45
  • well... I did exact copy of code pasted in this answer. Crash every time with current Xcode5. – Marcin Mar 03 '14 at 20:57
  • but what parameters you are calling it with? I imagine your format or array arguments are incorrect. – Brett Mar 05 '14 at 15:53
  • 1
    [ATLSAppDelegate stringWithFormat:@"test %@ - %@" array:@[@"hi", @"bye"]] Results in a EXC_BAD_ACCESS error. – Isaac Paul Mar 11 '14 at 16:19
  • 2
    I actually works for me, but just inserts the first parameter. Any fix? – Fabrizio Bartolomucci May 19 '14 at 20:52
  • 1
    I get EXC_BAD_ACCESS if my format string contains more than one item to be reformatted. – jowie Mar 26 '15 at 12:48
  • This solution only works for a single element. Please consult http://stackoverflow.com/questions/1058736/how-to-create-a-nsstring-from-a-format-string-like-xxx-yyy-and-a-nsarr?lq=1 – Brett Mar 26 '15 at 16:51
1

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.

mcfedr
  • 7,845
  • 3
  • 31
  • 27
1

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.

Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213
0

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.

Community
  • 1
  • 1
ajjnix
  • 462
  • 3
  • 17
0

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.

Kaz Yoshikawa
  • 1,577
  • 1
  • 18
  • 26
0

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.

tarmes
  • 15,366
  • 10
  • 53
  • 87