...

Building an SDK with Swift & Objective-C: A Developer's Guide

April 20, 2023

By Denis Bogomolov

    ...

    A large enterprise came to us with the objective of writing an iOS SDK for Swift and Objective-C developers. Converting Objective-C to Swift and vice-versa can be a grueling task, but not for our expert iOS team.

    Our Senior iOS Developer, Denis Bogomolov, takes us through the technical challenges of creating interoperable frameworks for Swift and ObjC - highlighting some of the complex problems we overcame. To make the most of this post, you will need an understanding of;

    • Swift and/or Objective C programming languages.
    • Xcode IDE
    • Data types such as integers, strings, arrays and objects.
    • Common programming constructs e.g. functions, classes and methods.
    • Protocols and their role in defining interface contracts between components of an application.

    Swift VS Objective-C

    Released in 2014, Apple claims that Swift is 2.6 times faster than Objective-C while being easier and safer to develop with. On the other hand, Objective C is nearly 40 years old and Swift applications use up to 70% less code. This begs the question; why would we need to do anything with ObjC these days? Well, despite Apple’s intention to replace Objective-C, there's still a substantial amount of developers keeping the language alive and tons of core iOS and MacOS programs written in Objective-C (that won't be going anywhere anytime soon). In addition, Objective-C boasts excellent interoperability with C and C++. 

    Writing an SDK in Swift, ObjC, or a Mix of Both: Client Requirements and Our Approach

    On the face of things, our client's objectives were quite straightforward:

    1. Write an SDK that can be consumed by Objective-C and Swift apps

    2. Make the public API feel native to Objective-C and Swift developers

    The client trusted our expertise in coming up with the best course of action. We know it's becoming harder to find skilled developers that are proficient in ObjC. To avoid making it difficult for our client to support the project in the future, we consciously decided to write as little ObjC as possible while adhering to their objectives. Although writing a project in a mix of Objective-C and Swift is the worst of both worlds, because it limits the full utilization of Swift's advanced features, the public API wasn't too complex. This meant we could write 95% in Swift with edge cases written in ObjC.

    Special Mention to Xcode’s Generated Interface

    Before we go into details, we'd like to say a huge "thank you" to this square-chain guy and his friend, Generated Interface.

    Basically, Xcode would generate Swift interfaces from  ObjC files, and an ObjC header file from Swift files, so you can look at how one language imports declarations from another. This helped us understand how different attributes affect public APIs.

    How to Import ObjC into Swift and Swift into ObjC,

    Let's start with the basics:

    Importing ObjC into Swift

    There are no bridging headers when dealing with frameworks. Instead, in the umbrella header, you need to import all of the ObjC headers you want to expose to Swift. Once this is done, the content of these headers is automatically available in any Swift file. No imports needed.

    //! Project version number for TotallyUnique.
    FOUNDATION_EXPORT double TotallyUniqueVersionNumber;
    
    //! Project version string for TotallyUnique.
    FOUNDATION_EXPORT const unsigned char TotallyUniqueVersionString[];
    
    // In this header, you should import all the public headers of your framework using statements like #import <TotallyUnique/PublicHeader.h>
    #import <TotallyUnique/TUQInterfaceStyle.h>
    #import <TotallyUnique/TUQOptionKey.h>
    

    Importing Swift into ObjC

    In ObjC implementation `.m` files, you can import the Xcode-generated `-Swift.h` header to use your Swift code.

    #import <TotallyUnique/TotallyUnique-Swift.h>
    

    You may have already noticed our first big issue. To expose an ObjC declaration to Swift or Swift declaration to ObjC, you need to mark this declaration as public, in other words, make it part of the public API.

    Macros

    When bridging Objective-C and Swift, there are a number of macros to aid interoperability. A few of these include:

    • NS_ASSUME_NONNULL_BEGIN and NS_ASSUME_NONNULL_END
    • NS_SWIFT_NAME
    • NS_REFINED_FOR_SWIFT
    • NS_TYPED_ENUM
    • NS_ENUM and NS_OPTIONS
    • NS_DESIGNATED_INITIALIZER

    The usage of macros would depend on the specific challenges of your project. Here we explain how we used some of them while writing this SDK.

    NS_TYPED_ENUM

    Personally, RawRepresentable enums are one of my most used features in Swift, but they are incompatible with ObjC, with the exception of integer-based enums. To get around this you can employ the aforementioned `NS_TYPED_ENUM`  macro. You declare a new typedef for your constants, then add the macro, and you can even customize the type name in Swift using 'NS_SWIFT_NAME'.

    //! Project version number for TotallyUnique.
    FOUNDATION_EXPORT double TotallyUniqueVersionNumber;
    
    //! Project version string for TotallyUnique.
    FOUNDATION_EXPORT const unsigned char TotallyUniqueVersionString[];
    
    // In this header, you should import all the public headers of your framework using statements like #import <TotallyUnique/PublicHeader.h>
    #import <TotallyUnique/TUQInterfaceStyle.h>
    #import <TotallyUnique/TUQOptionKey.h>typedef NSString * TUQInterfaceStyle NS_TYPED_ENUM NS_SWIFT_NAME(InterfaceStyle);
    /// Interface style based on user preferences.
    FOUNDATION_EXPORT TUQInterfaceStyle const TUQInterfaceStyleAuto;
    /// Dark interface.
    FOUNDATION_EXPORT TUQInterfaceStyle const TUQInterfaceStyleDark;
    /// Light interface.
    FOUNDATION_EXPORT TUQInterfaceStyle const TUQInterfaceStyleLight;
    

    It will produce this for you. It's close to what I would write myself.

    public struct InterfaceStyle : Hashable, Equatable, RawRepresentable {
        public init(rawValue: String)
    }
    extension InterfaceStyle {
        /// Interface style based on user preferences.
        public static let auto: InterfaceStyle
        /// Dark interface.
        public static let dark: InterfaceStyle
        /// Light interface.
        public static let light: InterfaceStyle
    }
    

    In the first block of code, you can see we have created a new type representing different interface styles called "TUQInterfaceStyle". We then used the NS_TYPED_ENUM macro which generates a struct that uses the new type and makes it conform to Hashable, Equatable, and RawRepresentable protocols. This alleviates the problem of incompatible enums in Objective-C.

    NS_SWIFT_NAME

    We've just seen this in the previous point. At a glance, this macro simply overrides a type name that can be used in Swift, but it can do more than that. For example, it can nest one type within another and transform C-style functions into class functions or properties.

    typedef NSString * TUQInterfaceStyle NS_TYPED_ENUM NS_SWIFT_NAME(InterfaceStyle);
    FOUNDATION_EXPORT BOOL TUQInterfaceStyleIsSafeForEyes(TUQInterfaceStyle style)
        NS_SWIFT_NAME(getter:InterfaceStyle.isSafeForEyes(self:));
    FOUNDATION_EXPORT TUQInterfaceStyle TUQInterfaceStyleFromInt(NSInteger style)
        NS_SWIFT_NAME(InterfaceStyle.init(from:));
    

    public struct InterfaceStyle : Hashable, Equatable, RawRepresentable {
        public init(rawValue: String)
    }
    extension InterfaceStyle {
        public var isSafeForEyes: Bool { get }
        public /*not inherited*/ init(from style: Int)
    }
    

    Be careful! It's easy to get into a chicken-egg situation:

    @class Option;
    typedef NSString * TUQOptionKey NS_TYPED_ENUM 
    NS_SWIFT_NAME(Option.Key);
    FOUNDATION_EXPORT TUQOptionKey const TUQOptionKeyScale;
    
    @objc(TUQOption)
    public class Option: NSObject {
        @objc
        public var key: Option.Key
        @objc(initWithKey:)
        public init(key: Option.Key) {
            self.key = key
            super.init()
        }
    }
    

    This code would compile, and even produce expected preview headers, but declarations would not be available when you try to consume the framework.

    Fun Fact: The NS prefix is a nod to NeXTSTEP - an operating system developed by NeXT in the late 1980s. NeXT was founded by Steve Jobs after leaving Apple, and the company developed innovative hardware and software solutions. NeXTSTEP had a significant impact on computing history, including influencing Objective-C and Cocoa frameworks, and serving as the platform for the creation of the first web browser by Tim Berners-Lee. Apple acquired NeXT in 1996 and incorporated much of NeXTSTEP into Mac OS X, including existing class names. Take a look at Steve Jobs unveiling the $6500, 17" display NeXT computer in 1988. Steve Jobs Unveils the NeXT Computer - October 12, 1988

    @objc

    You may have noticed one more relevant attribute. 

    @objc(TUQOption)
    public class Option: NSObject {
        @objc
        public var key: Option.Key
        @objc(initSomethingSomethingKey:)
        public init(key: Option.Key) {
            self.key = key
            super.init()
        }
    }
    
    SWIFT_CLASS_NAMED("Option")
    @interface TUQOption : NSObject
    @property (nonatomic) TUQOptionKey _Nonnull key;
    - (nonnull instancetype)initSomethingSomethingKey:(TUQOptionKey _Nonnull)key ObjC_DESIGNATED_INITIALIZER;
    - (nonnull instancetype)init SWIFT_UNAVAILABLE;
    + (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable");
    @end
     
    

    According to the documentation, the generated `-Swift.h` header should contain all declarations marked as `public` or `open`. We found this behavior to be inconsistent, so we agreed to explicitly mark all declarations that should be available in ObjC with `@objc`. Additionally, you can provide a new ObjC-name in parenthesis similar to `NS_SWIFT_NAME`. 

    You can add an `@objc` attribute to an internal declaration. This way, it would not be exposed in the public header but will be available in ObjC runtime for magic like: `valueForKey`, `selectorWithName` etc. Beware! Adding @objc to internal declarations can have performance and binary implications so it's important to understand the reasons why you would do this. Exposing declarations to Objective-C increases the size of your binary, potentially leading to other issues such as; longer download and installation times for users, increased memory/resource consumption on a user's device and more potential for errors and conflicts as larger binaries can increase the risk of bugs. This is why, in general, you should only mark declarations with @objc if you know that they will be used from Objective-C code, or if you want to ensure that they can be accessed from other Swift modules that rely on Objective-C APIs.

    These 3 macros/attributes covered almost all of our needs. Ultimately, the examples above made it much easier for Swift and Objective-C developers to work with interface styles such as dark mode and light mode.

    Technical problems: Default values in functions, Mutability Mappings and more.

    Let's take a look at how we overcame some of the trickier problems we faced:

    Default values in functions

    Unlike Swift, there isn't a direct way of setting default parameter values in Objective-C. In Swift, you can declare a default value in the function definition. This means that if the caller doesn’t provide a value, the default value is used automatically. To achieve this in Objective-C, we need to write such functions twice: once with the default value set inside the function code and once without the default value. 

    @objc
    public func withValue(_ number: Int = 9) { }
    
    - (void)withValue:(NSInteger)number;
    

    This is also an example of why writing in Swift uses less code than Objective-C.

    Passing primitives around

    It's common for ObjC to pass an array of primitives or C-structs as a pointer to array and number of items. While in Swift you would expect to pass just an array of primitives, when translated into ObjC the compiler automatically wraps such primitives in `NSValue`. As a result, we need to declare two functions: one Swift style and one ObjC style.

    @objc
    public func withValue(_ locations: [CLLocationCoordinate2D]) { }
    @objc
    public func withValue(_ locations: UnsafePointer<CLLocationCoordinate2D>, count: Int) { }
    
    - (void)withValue:(NSArray<NSValue *> * _Nonnull)locations;
    - (void)withValue:(CLLocationCoordinate2D const * _Nonnull)locations 
    count:(NSInteger)count;
    

    In ObjC code you could use `NS_REFINED_FOR_SWIFT` to provide a different function or `NS_SWIFT_UNAVAILABLE` to hide one of them.

    Swift has no such tools. 

    Mutability mappings

    Let's say you are writing a function that takes `URLRequest`, modifies it and returns it.

    In ObjC you would just pass in `NSMutableURLRequest` but in Swift, `URLRequest` is a struct. 

    @obj
    cprotocol URLRequestTransformer: NSObjectProtocol {
        func modify(_ request: NSMutableURLRequest)
        func modify(_ request: URLRequest) -> URLRequest
        func modify(_ request: inout URLRequest)
    }
    

    The first variant is foreign in Swift.

    The second is foreign in ObjC.

    The third cannot be represented in ObjC at all.

    Building Cross-Language Swift & Objective-C SDKs

    In all our years of iOS development, we've found ourselves in many precarious situations when there's a correct, intended, Apple way to do something, and then there is what you need to do.

    This is a case in point. Apple does a lot to help you use old ObjC code in Swift, but doing the inverse poses a myriad of issues.

    Was it worth it? Definitely! We're not scared of tackling the hairier software challenges when it comes to making our clients' lives easier. These examples are just a smidgen of the work that went into creating the SDK. 

    This project expands the market of our client's enterprise software to over a billion iOS users. Leveraging our team's intricate understanding of the two main languages that power iOS, the software now has functional parity across Web, Android, and iOS SDKs. We're incredibly proud of our work on this project because it opens access to a more diverse and skilled developer community while future-proofing our client's software.


    Denis Bogomolov (project lead) is a Senior iOS developer at Janea Systems. He believes it is important not just to deliver a solution, but to deliver the correct solution with clear justification. Denis has over a decade of experience in software development, delivering projects for companies such as Microsoft, MetLife, Showmax, Nedbank, and Vodafone. Find him on Github and Linkedin.

    Learn more about how Janea Systems solves some of the most complex technological software engineering and product development challenges. Speak to an expert here.

    Related Blogs

    Let's talk about your project

    113 Cherry Street #11630

    Seattle, WA 98104

    Janea Systems © 2024

    • Memurai

    • Privacy Policy

    • Cookies

    Let's talk about your project

    Ready to discuss your software engineering needs with our team of experts?

    113 Cherry Street #11630

    Seattle, WA 98104

    Janea Systems © 2024

    • Memurai

    • Privacy Policy

    • Cookies

    Show Cookie Preferences