Swift: These took far longer to put together than they should have

At some point, I decided I needed to be able to create strings from string formats and arbitrary arguments. In my Objective-C projects, these play a big role in development. I can better do custom logging, create strings for alerts, etc etc. For whatever reason, it took me a really long time to get this functionality implemented.

In the end, here’s the deceptively simple results that took me weeks to get to. They consist of two functions that apply standard Foundation NSString formatting.

func StringWithFormat(format : String, args: CVarArg...) -> String {
    return NSString(format: format, arguments: getVaList(args))
}

extension String {
    func format(args: CVarArg...) -> String {
        return NSString(format: self, arguments: getVaList(args))
    }
}

With these implemented, you can leverage Foundation format strings from your Swift clients. For example, you might use these new items as follows:

"Hello %@ %@".format("Name", "!!") // string function
StringWithFormat("Hello %@ %@", "Guys", "!!") // standalone function
"Hello %d guys".format(23) // works with numbers
var v = UIView()
"View: %@".format(v) // works with objects

Admittedly, these functions are limited in that you can’t print things that you wouldn’t normally print with Foundation, such as CGRect. If you want to extend to printing to special structs and such, you can implement extensions on a class-by-class basis, returning an encodable element in place of an unencodable one.  The following CGRect extension defers to NSValue, which produces a very nice rect representation.

extension CGRect : CVarArg {
    public func encode() -> [Word] {
        var value = NSValue(CGRect:self)
        return value.encode()
    }
}

Once implemented, this functionality is super handy to have around for the structs built into NSValue (CGPoint, CGVector, CGSize, CGRect, CGAffineTransform, UIEdgeInsets, UIOffset on the iOS side). At the same time,  it’s struct specific so not extensible. You have to build an extension for each type.

In Objective-C, I have a great workaround for this — courtesy of Remy Demarest.  It uses ObjC type encoding to produce a value representation for any struct. This doesn’t work with Swift.

#define VALUE(struct) ({ __typeof__(struct) __struct = struct; [NSValue valueWithBytes:&__struct objCType:@encode(__typeof__(__struct))]; })

So I ended up coming up with a completely different approach, namely to capture the print stream debug output. Swift’s print function can pretty much output anything. It can also take a second argument, which is a destination stream. This enables you to grab the print description of any item as a standard String. Rainer B. had a better solution, which follows.

The following implementation creates a prefix. In this example, I settled on %% because it looks very formatty and I thought it wouldn’t be mistaken for anything else or cause confusion in real format strings. So here’s the prefix operator:

operator prefix %% {}
@prefix func %% <T>(item : T) -> String {
    var result : String = ""
    print(item, &result)
    return result
    return "\(item)" // Thanks Rainer B.
}

And you use it like this:

let rectangle = CGRectMake(9, 9, 8, 8)
let resultString = StringWithFormat("Rect: %@", %%rectangle)

I played around with a few other approaches here, such as a standalone “printable()” function, before settling on (for at least the moment) the %% operator. What I really wanted was a way to sort through all the arguments to the StringWithFormat function to determine which ones were not CVarArg convertible and auto-convert them without the %% workaround. If you have any ideas for doing this, please do let me know!

2 Comments