Living in an Objective-C World: Porting an Objective-C Codebase to Swift
Special thanks: Many thanks to Jeff Rames who was gracious enough to provide a preliminary review of this post and provided some really helpful feedback.
It's no secret that Swift is the future of iOS development. However, there is much legacy code still written in Objective-C, including Cocoa Foundation itself. Understanding Objective-C and its interoperability with Swift is essential to being an effective, well-rounded Cocoa developer. To that end, porting an Objective-C codebase to Swift is a useful exercise for developers whose first language is Swift.
This post highlights some learnings made when I migrated Ash Furrow's C-41, an open-source, ReactiveCocoa app, to Swift. Some lessons learned include:
The difference between static libraries and dynamic frameworks, and making these dependencies visible to Swift
Swift's interactions with the Objective-C runtime including dynamic dispatch, key-value observing and key-value coding
Working with Objective-C enums and preprocessor macros
Leveraging the power of Swift extensions to progressively migrate and troubleshoot Objective-C code
Here are some resources I found helpful along the way:
Apple Guide: Migrating Your Objective-C Code to Swift
On Objective-C features and syntax: I'm a big fan of Paul Hudson's materials, and Objective-C for Swift developers is a concise, to-the-point compilation of all kinds of Objective-C knowledge. Highly recommended.
On (not) using preprocessor macros in Swift: From Objective-C to Swift: Use Complex #define Macros in Swift and MVVM, Swift and ReactiveCocoa - It's all good!
On creating custom operators in Swift: Infix Operators
On weird thing about Objective-C Blocks - A Swift Reaction
On setting up unit tests in a mixed Swift/Objective-C project
Finding a Project
After some perusing and deliberation, I settled on Ash Furrow's C-41. Ash describes C-41 as "an application to help people develop film at home by providing a series of 'recipes' for photographers to use. The app demonstrates how to use Core Data, unit tests, ReactiveCocoa, and Model-View-ViewModel."
In total, the app consists of five view controller classes, five viewmodels (one for each view), and a couple of supporting classes for model persistence and testing. The app is a small-enough Objective-C codebase that felt manageable, while providing the chance to study not only Objective-C fundamentals but examples of ReactiveCocoa as written by a skilled iOS developer.
In its migration guide, Apple provides the following advice:
"Because you can’t subclass Swift classes in Objective-C, it’s best to choose an Objective-C class in your app that doesn’t have any subclasses."
Replace each corresponding pair of .m and .h files with a Swift class
Remove the original .m and .h implementations from your app target, but do keep them in your project for troubleshooting your Swift implementation.
When I began this project, the C-41 codebase included a few third-party libraries (via Cocoapods). A number of these libraries - including ReactiveCocoa, OCMock, Specta and Expecta - have long since been updated. While I could have started the migration by upgrading the dependencies first, my approach was to migrate the existing Objective-C codebase without introducing any additional variability - and room for error - to the results of the migration.
Apple notes that in a production environment, you may want to ensure existing Objective-C code is up-to-date with modern standards (such as the use of nullability annotations and generics) prior to migration. However, my goals were more educational in nature and so I kept my focus solely on migrating the app code.
A Side Note on MVVM
While not the main topic of this post, it may be worth quickly introducing MVVM and ReactiveCocoa for those who may be a little unfamiliar.
Model-View-ViewModel, abbreviated MVVM, is an architectural paradigm in which the view and model layers are isolated from each other and communicate indirectly solely through a "viewmodel." The viewmodel, in turn, is responsible for applying business logic to the model to facilitate the view's presentation of model data.
For example, applying a DateFormatter to a Date variable is the responsibility of the viewmodel. The viewmodel also translates interactions with the view into model updates as necessary (for example, incrementing a numberOfLikes variable every time the user taps the "like" button. I would encourage anyone new to this paradigm to read objc.io's excellent primer on MVVM. As noted in the article, MVVM typically works best with reactive bindings to glue the view, model and viewmodel code together (for example RxSwift, a popular reactive framework).
I thought a UITableViewCell subclass would be a good place to start small and simple. The ASHTextFieldCell subclass was a great candidate because it comprises only one property - a single text field. I proceeded to port this class as follows:
1. Deselect the implementation .m file from the app target membership. (Don't delete it! At least not yet. As mentioned earlier, save the original Objective-C files for troubleshooting.)
2. Add the .swift file that replaces the Objective-C class (in my case, ASHTextFieldCell.swift). The first time you add a .swift class to an Objective-C project, you will be prompted to add a bridging header. Say yes!
3. This cell subclass has only one property - textField, declared as (nonatomic, weak) in the Objective-C implementation file.
4. Behind the scenes, Xcode automatically generates a Swift bridging header to enable visibility of your Swift code from Objective-C. This file will take on the name of the module in which the Swift code is contained suffixed with "-Swift.h". In my specific case, because C-41 is hyphenated, its Swift bridging header was automatically generated as C_41-Swift.h (the compiler replaced the hyphen with an underscore).
Import this Swift bridging header into the Objective-C implementation (.m) file(s) that previously imported ASHTextFieldCell.h.
4. If working with a storyboard, you might have to explicitly disconnect your outlets from the Objective-C file and reconnect them in your Swift file. You may also need to double-check that the module specifier shows the correct/current module, otherwise all kinds of compile- or run-time weirdness might occurr. At least in my case, it helped to place my cursor in the "Module" text field and press "Enter" to get the storyboard to recognize the correct file.
5. Build and run the project to confirm everything works.
Eureka! Easy enough, right? The same general approach is applied to porting more complex classes with some caveats around handling dependencies and inter-operating with the Objective-C runtime. Rather than highlight the minutiae, I'll highlight some key learnings from one of the more interesting classes as a case study.
Side Note: What's the deal with non-atomic? Tagging a property with the nonatomic attribute means that writing to this property happens piecemeal or "non-atomically." This means that while the property is being written to, an accessing thread is not guaranteed a sensible, non-garbage value. This behavior is acceptable for storyboard outlets because outlets are not intended to be accessed anywhere from other than the main thread.
It's also worth noting that unless explicitly overridden, Objective-C properties are by default readwrite, atomic, and strong. By default, Swift will also assume that Objective-C properties are implicitly unwrapped optionals unless overridden with explicit nullability annotations.
I would recommend Paul Hudson's excellent book, Objective-C for Swift Developers, as a great resource for understanding annotations and other nuances of the Objective-C language.
Case Study: Porting ASHTimerViewModel
In addition to the general approach outlined in the previous section, more complex classes warranted some additional steps to handle dependencies and other nuances of interoperating with Objective-C and the Objective-C runtime.
Making Objective-C Dependencies Visible to Swift
Third-party libraries: Apple added support for third-party dynamic framework support in iOS 8. I would encourage you to read this excellent primer by the Cocoapods team on static libraries and dynamic frameworks.
Like virtually all other classes within the project, the ASHTimerViewModel class is highly dependent on the ReactiveCocoa library and its sibling library ReactiveViewModel. ReactiveViewModel is designed to build Cocoa applications using MVVM in combination with ReactiveCocoa. However, because C-41's codebase was last updated prior to iOS 8, all cocoapods were integrated as static libraries and not dynamic frameworks.
If our pods project was integrating with the use_frameworks! directive in our podfile, we could simply import Pod_name in our Swift file and we'd be all set. However, because the pods were already integrated as static libraries, we add the pod's header file to our bridging header instead. This was a useful exercise in understanding some of the differences between how static and dynamic libraries are built and incorporated into an Xcode project.
In addition, this particular viewmodel relies on Apple's AudioToolbox framework to trigger vibrational feedback at the end of the countdown for a particular recipe. Because Audio Toolbox is packaged as a system framework, we get to import that directly in our .Swift file with a regular import statement:
Internal dependencies: As we can observe from its original Objective-C implementation, the ASHTimerViewModel class also needs to know about the ASHStep and ASHRecipe CoreData model classes, and we import these respective classes' header files as well.
Progressive Migration via Swift Extensions
For more complex classes, I sometimes found that progressively swapping the Objective-C for the Swift implementation was a useful approach. To do this, we leverage the power of Swift extensions. Instead of replacing the .m and .h files in one go, we create an extension of the existing Objective-C class. Then, method-by-method, we comment out an Objective-C method and replace it with its equivalent in the Swift extension. Especially when dealing with some fairly intricate functionality (such as ReactiveCocoa), this is a great way to replace an Objective-C class piecemeal, and makes troubleshooting the migration a little more manageable.
When all methods are transposed to Swift, the .m file can be removed from the target's membership, and extension ClassName can become class ClassName in the Swift file. This is also the point at which you would add any instance variables to the ported class (since we're unable to add stored properties to a Swift extension).
Making Objective-C enums Swift-friendly
By default, Objective-C enums are not visible to Swift. However, we can wrap the enum in the NS_ENUM macro to make Objective-C enums Swift compatible!
The Swift compiler will intelligently rename each enum case by removing the maximum number of common characters that prepend each case name and lowercasing it (in alignment with the Swift 3 API guidelines). So the enum case declared as ASHRecipeFilmTypeColourNegative in Objective-C will be visible to Swift as ASHRecipeFilmType.colourNegative.
At first pass, I thought this was a slam dunk Objective-C-to-Swift translation. Spoiler alert - it wasn't!
Upon running the app, it appeared that the properties that were supposed to be updated with the countdown - time remaining, current step, next step - were not getting updated. My reactive bindings, when ported to Swift, weren't working! Argh!
Some hair-pulling later, I discovered that Swift properties should be tagged with the dynamic keyword in order to be visible to the Objective-C runtime or they may silently fail.
... if you have a method that is never overridden, Swift will notice this and will use direct dispatch if it can … If a property is observed with KVO, and the property is upgraded to direct dispatch, the code will still compile, but the dynamically generated KVO method will not be triggered."
-- Raiz Labs, "Method Dispatch in Swift
It turns out that, at least as of v2.1.8, ReactiveCocoa's binding mechanism relied on Key-Value Observing under the hood. As pointed out in Raiz Labs' excellent article, subclassing NSObject in Swift does not guarantee that the the subclass's getter and setter methods will be dispatched dynamically via the Objective-C runtime. The key here is to explicitly tag properties that require dynamic dispatch as dynamic:
Objective-C Preprocessor Macros
Objective-C macros are not visible to Swift, so some creativity is necessary to address this while keeping the overall syntax fairly comparable. Used in the ASHTimerViewModel class (and sprinkled throughout the codebase) is a RAC preprocessor macro which binds an NSObject to a signal. After racking (RACing? 😁) my brain for a bit, I stumbled onto a handy-dandy replacement in an online thread from a couple of years ago. This (very gnarly looking macro)…
is translated to this Swift struct:
At the call site, using this struct has the advantage of closely resembling the RAC macro as called from Objective-C. Note how we also use a custom infix operator (~>) in Swift to make the binding of reactive signals to properties a little more elegant (since the equals operator is not available to us for this purpose).
Key-Value Coding (and how #keyPath is your friend!)
Under the hood, the RAC helper struct uses an existing ReactiveCocoa method - RACSignal.setKeyPath(_:on: nilValue:) - to bind a given key path to a signal. Because the keypath parameter in the initializer is stringly-typed, we use the Swift 3 #keyPath expression to perform a static type check at the call site. This is adds some much-needed compile-time safety as we can be sure that our key paths are valid prior to running our app!
Summary and Areas for Further Exploration
I deliberately ported approximately half of the existing classes from Objective-C to Swift and learned a lot about Objective-C and the current state of Swift interoperability while doing so. While I could have ported all classes, I decided that leaving it as a hybrid Objective-C/Swift project will be a good way to have a playground on hand to explore other areas of this topic in the future. A couple areas of interest that I haven't yet gotten the chance to explore come to mind:
Optionality and Nullability Annotations
By default, the Swift compiler will interpret all Objective-C properties as implicitly unwrapped optionals. So the Objective-C method signature -(instancetype) initWithName: (NSString*) name will be visible to Swift as init! (name: String!). We can add nullability annotations to properties and methods in the Objective-C header file in order to explicitly tell Swift where null is a possible parameter or return value. Modernizing C-41s Objective-C codebase to make use of this feature would be a fun little project.
To keep things focused, I didn't get around to implementing a hybrid Objective-C/Swift test target, but this would also be a nice value-add to the project.
studying the OBJECTIVE-C RUNTIME REFERENCE
We often talk about the Objective-C runtime in an abstract sense, but getting a deeper handle on its core concepts and APIs would be a very useful way to further one's understanding of Objective-C's interoperability with Swift.
As noted in Apple's documentation, the Objective-C runtime "is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps." Apple further notes: "You typically don't need to use the Objective-C runtime library directly when programming in Objective-C. This API is useful primarily for developing bridge layers between Objective-C and other languages, or for low-level debugging."