Objective-C Grouped String Constants

In an Objective-C project, using C const structs can be a nice way to add logical grouping and type info to your string constants. Here is an example of grouping constants for API keys:

// In your .h file
FOUNDATION_EXPORT const struct APIKey {
    __unsafe_unretained NSString *firstName;
    __unsafe_unretained NSString *lastName;
} APIKey;

// In your .m file
const struct APIKey APIKey = {
    .firstName = @"first_name",
    .lastName = @"last_name",
};

// Usage
NSString *firstNameKey = APIKey.firstName;
NSLog(@"%@", firstNameKey); // "first_name"

If you try to create this struct without using __unsafe_unretained you will get a compiler error stating: ARC forbids Objective-C objects in struct. As a workaround, in this scenario it is ok to use __unsafe_unretained as we are dealing with constant strings that are created at compile time and will never be deallocated.

However, if you have a mixed Swift/Objective-C project things get complicated when you try to access this from Swift:

let firstNameKey = APIKey.firstName;
print(firstNameKey) // Compiler warning

When you try to use firstNameKey Xcode will warn with Expression implicitly coerced from 'Unmanaged<NSString>?' to Any. In addition, if you drill down into the warning there are multiple suggestions (all not ideal):

  • Provide a default value to avoid this warning
  • Force-unwrap the value to avoid this warning
  • Explicitly cast to Any with 'as Any' to silence this warning

A workaround for this:

let firstNameKey = APIKey.firstname.takeUnretainedValue()

As a best practice in Swift, if you receive an unmanaged object from an API, convert it to an ARC memory-managed object. So depending on what is returned from that API, you would use either takeRetainedValue or takeUnretainedValue. So in this case, since the C struct returns an __unsafe_unretained unmanaged string, we need to call takeUnretainedValue which lets Swift convert it to a memory-managed object.

In a large codebase I don't like this as I'd rather not have things like takeUnretainedValue or force-unwraps or as casts scattered everywhere.

NS_STRING_ENUM

One recent alternative for this is to annotate typedef constants with the NS_STRING_ENUM macro that was introduced starting in Xcode 8. You can use this macro to annotate a typedef when you want your Objective-C typed constants to be imported into Swift as a common type:

// In your .h file
typedef NSString *const APIKey NS_STRING_ENUM;
FOUNDATION_EXPORT APIKey APIKeyFirstName;
FOUNDATION_EXPORT APIKey APIKeyLastName;

// In your .m file
APIKey APIKeyFirstName = @"first_name";
APIKey APIKeyLastName = @"last_name";

The Objective-C sample above is declaring the constant in the .h file using the FOUNDATION_EXPORT macro (which gives a bit of extra cross-platform compatibility) but extern would also work just fine for most cases. Make sure to use either of these or you will likely get duplicate symbol linker errors.

Here's how the above Objective-C example gets automatically imported into Swift:

struct APIKey: RawRepresentable {
    typealias RawValue = String

    init(rawValue: RawValue)
    var rawValue: RawValue { get }

    static var firstName: APIKey { get }
    static var lastName: APIKey { get }
}

Here's how to use this on the Swift side:

let firstNameKey = APIKey.firstName.rawValue
print(firstNameKey) // "first_name"

func fooMethod(someAPIKey: String) {
    print(someAPIKey)
}

fooMethod(someAPIKey: APIKey.firstName.rawValue)

No more takeUnretainedValue.

This can be cleaned up further and you can reduce some of the scattered call site rawValue usage from your codebase by declaring your methods to take the type of the constant rather than simply just a string:

func fooMethod(someAPIKey: APIKey) {
    print(someAPIKey.rawValue)
}

fooMethod(someAPIKey: APIKey.firstName) // "first_name"

NS_EXTENSIBLE_STRING_ENUM

You can also use the NS_EXTENSIBLE_STRING_ENUM macro when you need to extend and add additional values to your Objective-C typedef constants in Swift:

extension APIKey {

    static var anExtendedAPIKey: APIKey {
        return APIKey(rawValue: "an_extended_api_key")
    }
}

When using NS_EXTENSIBLE_STRING_ENUM, the imported Swift code will include an additional simplified initializer with an _ that lets you omit the rawValue argument label from the call site:

// init(_ rawValue: String)

APIKey("an_extended_api_key")

Trade Off

The main Objective-C tradeoff is that now the constant names are more verbose (ex: APIKeyFirstName vs APIKey.firstName). It could be looked at as a downgrade to Objective-C (more verbosity) at the expense of cleaner Swift. This may be a tradeoff worth making; especially in codebases where you plan on doing a significant amount of Objective-C -> Swift string constant interop.