Adapting Objective-C APIs to Swift with NS_REFINED_FOR_SWIFT

Adapting Objective-C APIs to Swift with NS_REFINED_FOR_SWIFT

If your iOS project uses Objective-C SDKs, you'll find that the compiler does a good job translating those APIs to Swift. Whenever you try to use one of them in Swift, you'll be greeted with a swiftified version of it that has common Objective-C standards translated to Swift. Foundation types like NSData and NSString will be translated to their Swift variants (Data, String), nullables will become optionals, and the name of the method will change to follow Swift's naming conventions. If the method can fail (by having an error pointer), it will even become a throwing function:

- (NSData *)dataForRow:(NSInteger)row error:(NSError **)error;
// becomes:
let data: Data = try data(for: row)

For most cases, this does the trick. While I'm not a fan of how the naming can end up sometimes, there are attributes that you can use to finetune the final result, like NS_SWIFT_NAME(customName) to use a customized name or NS_SWIFT_NOTHROW to disable the error pointer -> throws conversion.

However, what if you want that Objective-C API to become something completely different? One example of a common architectural difference between the languages is the usage of methods versus properties -- most things in Objective-C are methods, while Swift will advise you to use computed properties for things that are computed but yet don't involve actually processing data. The compiler isn't that smart, so by default you'll end up with methods even if they are better defined as something else in Swift:

MyClass.sharedInstance() // would work better in Swift as `MyClass.shared`!

Additionally, the automatic API translation doesn't consider Swift-only features like default arguments and generics in methods, which are really good tools for developing APIs in Swift. Fortunately, the compiler provides you a way to completely customize how Objective-C APIs end up in Swift.

NS_REFINED_FOR_SWIFT

The NS_REFINED_FOR_SWIFT attribute can be added to Objective-C methods to indicate that you want to have full control on how this API is translated to Swift. When added to an Objective-C API, the compiler will still port it, but it will do so in the shape of a special hidden method that you can use to redefine it as something else. Here's an example of a singleton in Objective-C:

@interface SRMyClass : NSObject
+ (instancetype)sharedInstance;
@end

By default this will be converted to a sharedInstance() method in Swift, but in Swift standards, this would look better as a computed property instead.

To customize how it'll be translated, let's add the attribute to the Objective-C definition:

@interface SRMyClass : NSObject
+ (instancetype)sharedInstance NS_REFINED_FOR_SWIFT;
@end

As mentioned, using this attribute won't stop the method from being migrated to Swift -- but it'll be done in a special way. In the case of methods, this special way will be that the method's name will be prefixed by two underscores (__):

let instance = SRMyClass.__sharedInstance()

The reason for this is precisely to indicate that this method shouldn't be used as-is. In fact, if you try to implement this example you'll notice that while you can use it, it will not show in code completion at all. The intention, instead, is for you to abstract this special method into what you actually want this to look like in Swift. In the case of our singleton example, if we want it to become a computed property, we should define that property in our Swift code and implement it by calling the exposed unrefined method:

extension SRMyClass {
    var shared: SRMySingleton {
        return __sharedInstance()
    }
}

Because the original unrefined method doesn't even show up in code completion, you can be sure that the developers will always use the correct swiftified version of it.

My personal favorite use of this attribute is to add default parameters to methods, which is something normally ignored in Objective-C for not being easy to implement, but extremely simple and useful in Swift. To do, we just need to create a version of the method that contains default parameters and internally call the original unrefined one:

public extension SRKeychain {
    func data(
        forDomain domain: String,
        andKey key: String,
        accessGroup: String? = nil,
        accessAttr: String? = nil,
        synchronizable: Bool = false
    ) -> Data? {
        return __data(
            forDomain: domain,
            andKey: key,
            accessGroup: accessGroup,
            accessAttr: accessAttr,
            synchronizable: synchronizable
        )
    }
}

NS_REFINED_FOR_SWIFT is also a great way to enforce type-safety in places where it wouldn't be applicable in Objective-C. In Swift, you can easily abstract unsafe id (Any / AnyObject) Objective-C methods, for example, under generics.

public extension SRPersistance {
    func object<T>(forKey key: String) -> T? {
        return __object(forKey: key) as? T
    }
}

Unfortunately you can't redefine entire types with NS_REFINED_FOR_SWIFT as only methods, properties and initializers are supported, but in my experience, that's enough to give legacy code a good Swift experience.