6 minute read
Swift is gaining popularity among iOS developers, which is of no surprise. It's strictly typed, which means you can prove the correctness of your program at compile time, given that your typesystem describes the domain well. It's a modern language offering syntax constructs encouraging developers to write better architecture using fewer lines of code, making it expressive. It's more fun to work with, and all the new Cocoa projects are being written in Swift.
At Shopify, we want to adopt Swift where it makes sense, while understanding that many existing projects have an extensive codebase (some of them written years ago) in Objective-C (OBJC) that are still actively supported. It's tempting to write new code in Swift, but we can't migrate all the existing OBJC codebase quickly. And sometimes it just isn't worth the effort.
Can we maintain a hybrid codebase? If we start adding new code written in Swift and keep the existing code in OBJC, would it be possible to integrate them together and call OBJC-based components from Swift and vice-versa? Will we have to restrain from using certain Swift or OBJC features in order to be able to use a hybrid codebase?
Turns out a hybrid codebase is totally possible, and in this blog post I’ll share the tricks I used to design it.
I'm only looking at the use case of writing all new code in Swift within a hybrid codebase. You can create OBJC design with Swift interoperability in mind (and there are tools that make it easier like NS_SWIFT_NAME) but I'm not going to cover that use case. The only thing I’ll mention is nullability annotation ( `nullable` and `nonnull` attributes). Annotating OBJC APIs with correct optionality is very important because if incorrect it would drop one of the main Swift advantages, optional type safety. In OBJC, every object variable is a pointer and can theoretically be null. Swift makes a clear distinction between variables that can be null and those that are guaranteed never to be null at compile-time. That’s why when bridging OBJC code to Swift, we annotate pointers with additional semantic information (is this pointer nullable or not). If this information is missing or incorrect, the Swift code accessing OBJC provided pointers may crash at runtime.
I have a story of when this happened. In WebKit, some properties were imported into Swift as non-optional but their semantics were optional. This is most likely due to the automatic insertion of NS_ASSUME_NONNULL_BEGIN. Examples of such properties are -[WKNavigationAction sourceFrame] and -[WKFrameInfo request].
Here's the source code:
When trying to access these properties in Swift there's no way check if it's nil and in certain cases the application crashes. I worked around this mistake in the WebKit APIs by creating my own category on those classes with the following code:
and then I used optionalSourceFrame in Swift.
When writing Swift code, some language constructs can be seen in the OBJC runtime automatically and with Swift 3.1 that support is provided automatically.
Swift classes that are inherited from OBJC classes are bridged automatically. That means any class inherited from, for example, UIViewController is automatically seen in the OBJC runtime. If you're creating a class that doesn’t inherit from anything, then make it an NSObject subclass, as you would in OBJC. Protocols annotated with @objc are automatically seen in the OBJC runtime. You can't put anything non-OBJC-friendly in there like generic functions because Swift won't compile it. Same @objc rule applies to enums with even stricter requirements as it has to be an Int raw value.
While developing Swift APIs that are seen in the OBJC runtime, one caveat that caused me frustration was how Swift symbols must be public to import them in <Module>-Swift.h. It’s inconvenient because sometimes you want to combine pieces of code that interact between Swift and OBJC internally but aren’t exposed publicly. Unfortunately, it's annoying requirements like these that we have to be mindful of while attempting a hybrid codebase.
Granular Bridging Control
@objc attribute is very powerful. It doesn’t only instruct compilers to generate corresponding OBJC runtime objects, but it can also specify the class names and methods names of those OBJC runtime objects.
For example, the following declaration in Swift:
will generate this presence in the OBJC runtime:
You can also specify class, protocol, enum, and even case names using this attribute:
This may be useful as API naming guidelines for OBJC and Swift differ and it may not be feasible to come up with a name that perfectly aligns with both at the same time.
Extending Object’s Presence
Modern Swift features like enums and structs are not seen in the OBJC runtime. However, this is not a show-stopper and if you see a good architectural approach for using these features you should go ahead with it. It’s possible to use Swift features with the help of some bridging magic. You can simply wrap the instance of Swift-only type into a class inherited from NSObject. This class will be seen in the OBJC runtime, but the unwrapped, Swift-only typed property will not. To make it work, you need to make OBJC-visible properties on the NSObject subclass wrapper that would then proxy into an instance of Swift-only type:
then in OBJC file you'll be able to work with UsefulStruct like this:
Power of Extensions
One of the most powerful features of Swift extensions to NSObject subclasses (e.g. classes seen in OBJC) is that they are added to the OBJC runtime as categories (provided that you add methods also visible in OBJC). This means that the methods you add in certain class's Swift extension will be visible on that class in the OBJC code. This allows you to mix-n-match code. I'll explain what I mean by that.
Suppose you have some types, generics, or Swift-only protocols that you want to use in one of your OBJC-classes. You’ll create a new Swift file with extension to your OBJC class and add a simple method that interacts with these Swift-only things. It’s important to make this method simple and visible from OBJC so you'll be able to call it from other parts of your OBJC class. For example:
There's one more trick I wanted to note. We created the bridging NSObject subclasses that were wrapping Swift only values with proxy methods. However, if the nature of the work you're doing is mostly adding new Swift code, you may not even need those proxy methods. If you need to store the value contained in a stored property you need to create the NSObject inherited wrapper for that value. However, if the value is only going to be used in the new code, consider adding convenience unwrapper for that Swift only value in a Swift extension. For example:
When I joined the Mobile Store Builder project here at Shopify, most of its core was written in OBJC and for good reason: it’s a powerful template-based dynamic platform that can instantiate complex view hierarchies from given configurations. Rewriting the existing code was out of the question and by developing these patterns, I created a way to develop a hybrid codebase, type safe models written in Swift and dynamic UI inflation written in OBJC.