Friday, April 3, 2015

Translating your Objective-C project to Swift

transmision

If you've got an existing app written in Objective-C, migrating it into Swift is an excellent exercise for learning Swift, experimenting with Swift, and deciding whether to adopt Swift on a full-time basis. I've performed this migration with several real apps, so here are some tips culled from my actual experience.

Hybrid targets

You're surely not going to translate all your code into Swift at once; you're much more likely to translate one class at a time. As soon as you add a Swift file to your Objective-C app target, you've got a hybrid target: some classes are written in Swift, while other other classes are written in Objective-C. Thus, declarations in each language will need to be visible to the other language. Before you start, it's a good idea to understand how this visibility works.

Recall that an Objective-C class declaration is conventionally spread over two files: a header file (.h) containing the @interface section, and a code file (.m) containing the @implementation section. If a .m file needs to know about a class, it imports the corresponding .h file.

Visibility of Swift and Objective-C to one another depends upon this convention: it works through .h files. There are two directions of visibility, and they must be considered separately.

How Swift sees Objective-C

When you add a Swift file to an Objective-C target, or an Objective-C file to a Swift target, Xcode offers to create a bridging header. This is a .h file in the project. Its default name is derived from the target name, but it is arbitrary and can be changed, provided you change the target's Objective-C Bridging Header build setting to match.

An Objective-C .h file will be visible to Swift, provided you #import it in this bridging header.

How Objective-C sees Swift

If you have a bridging header, then when you build your target, the appropriate top-level declarations of all your Swift files are automatically translated into Objective-C and are used to construct a hidden bridging header inside the Intermediates build folder for this target, deep inside your DerivedData folder. The easiest way to see this is with the following Terminal command:

[crayon-551f19f54939c059301342/]

This will reveal the name of the hidden bridging header. Alternatively, examine (or change) the target's Product Module Name build setting; the hidden bridging header's name is derived from this.

Your Objective-C files will be able to see your Swift declarations, provided you #import this hidden bridging header into each Objective-C file that needs to see them.

Exactly where at the top of a .m file you #import the hidden bridging header can make a difference. The usual sign of trouble is that you get an "Unknown type name" compile error, where the unknown type is a class declared in Objective-C. The solution is to #import the .h file containing the declaration for the unknown type in your Objective-C files as well, before you #import the hidden bridging header. Having to do this can be an annoyance, especially if the Objective-C file in question has no need to know about this class, but it resolves the issue and allows compilation to proceed.

A step-by-step approach

Before making any changes, start a new git branch. Now translate your classes from Objective-C into Swift, one at a time. I take a systematic step-by-step approach, like this:

  1. Pick a .m file to be translated into Swift. Objective-C cannot subclass a Swift class, so if you have defined both a class and its subclass in Objective-C, start with the subclass. Leave the app delegate class for last.

  2. Remove that .m file from the target. To do so, select the .m file and use the File inspector.

  3. In every Objective-C file that #imports the corresponding .h file, remove that #import statement and import in its place the hidden bridging header. (If you're already importing the hidden bridging header in this file, you don't need to import it again.)

  4. If you were importing the corresponding .h file in the bridging header, remove the #import statement.

  5. Create the .swift file for this class. Make sure it is added to the target.

  6. In the .swift file, declare the class and provide stub declarations for all members that were being made public in the .h file. If this class needs to adopt Cocoa protocols, adopt them; you may have to provide stub declarations of required protocol methods as well. If this file needs to refer to any other classes that your target still declares in Objective-C, import their .h files in the bridging header.

  7. The project should now compile! It doesn't work, of course, because you have not written any real code in the .swift file. But who cares about that? Time for a beer!

  8. Now fill out the code in the .swift file. My technique is to translate more or less line-by-line from the original Objective-C code, even though the outcome is not particularly idiomatic (Swifty).

  9. When the code for this .m file is completely translated into Swift, build and run and test. If the runtime complains (probably accompanied by crashing) that it can't find this class, find all references to it in the nib editor and reenter the class's name in the Identity inspector (and press Tab to set the change). Save and try again.

  10. On to the next .m file! Repeat all of the above steps.

  11. When all of the other files have been translated, translate the app delegate class. At this point, if there are no Objective-C files left in the target, you can delete the main.m file (replacing it with a @UIApplicationMain attribute in the app delegate class declaration) and the .pch (precompiled header) file.

Don't feel obligated to translate all your code into Swift. There may be sections of code that you want to leave in Objective-C, and there's nothing wrong with that. Indeed, some code must be left in Objective-C, because there are parts of the Cocoa API that Swift can't access. For example, you can't form a C function or pointer-to-function in Swift, so you can't call CGPatternCreate or AudioServicesAddSystemSoundCompletion without an Objective-C helper method. And appearanceWhenContainedIn: can't be called from Swift.

On the other hand, code that uses one of the performSelector: methods, which are also not available from Swift, might be left in Objective-C initially, but eventually you'll probably want to work out another approach so that you can replace it with Swift code.

Let's get Swifty

Your app is now wholly or partly rewritten in Swift; congratulations! But if you've followed my advice, it probably isn't very Swifty at this point. It was more important to get your code running in the first place. Now that you've finished, though, you can go back and think about the code, making it more idiomatic. You may well find that things that were clumsy or tricky in Objective-C can be made much neater and clearer in Swift.

Obviously you'll want to adopt Swift native types where you can. Immutable / mutable pairs like NSString and NSMutableString, NSArray and NSMutableArray, and NSDictionary and NSMutableDictionary, can be replaced by Swift's own String, Array, and Dictionary types. And you'll find that you no longer need certain elaborate techniques associated with those types. Array, in particular, has the map, filter, and reduce instance methods; you'll get a lot of mileage out of them.

For example, in one of my apps I have a table view that displays data divided into sections. Under the hood, the data is an array of arrays, where each subarray consists of strings representing the rows of a section. The table is searchable, so now I want to filter that data to eliminate all strings that don't contain the substring the user is entering in the search bar. I want to keep the sections intact, but if removing strings removes all of a section's strings, I want to eliminate that section array entirely. Here's how I did it in Objective-C (sb is the UISearchBar):

[crayon-551f19f5493b2657296950/]

First we form an NSPredicate to filter the array. Then we cycle through our array of arrays, "dealing" each filtered subarray one by one into an empty NSMutableArray; I'm sure you're familiar with that idiom. In Swift, however, we don't need either the NSPredicate or the "dealing" idiom — or the two intermediate arrays! We've got map and filter, and the whole thing happens in a single code statement:

[crayon-551f19f5493bc528298011/]

You'll also find that, where your code was relying on Objective-C's message-sending dynamism, you may be able to do without it, because in Swift a function is a first-class citizen. Here's an example.

In a flashcard app, I have a class Term, representing a Latin word. It declares many properties. Each card displays one term, with its various properties shown in different text fields. If the user taps any of three text fields, I want the interface to change from the term that's currently showing to the next term whose value is different for the particular property that this text field represents. Thus this code is the same for all three text fields; the only difference is which property to consider as we hunt for the next term to be displayed.

In Objective-C, by far the simplest way to express this parallelism was through key-value coding (g is a tap gesture recognizer):

[crayon-551f19f5493c5124456623/]

Now, I can keep using key–value coding in Swift; but this requires that my Term class be descended from NSObject, and it relies on an Objective-C / Cocoa dynamism — translating strings into property names — that's alien to the Swift spirit. It turns out that Swift makes it easy to implement the same dynamism — translating tags into method calls — using a simple array of anonymous functions:

[crayon-551f19f5493cd934410182/]

Conclusion

Swift has many features designed to force your code to be more reliable from the outset. A common mistake in Objective-C is to declare an instance property but forget to set it to an initial value; there's no penalty to sending a message to nil, so you can go for a long time without noticing the problem. Swift forces you to initialize all instance properties. And then, of course, there's Swift's infamous strict typing; it's easy, in Objective-C, to be vague about what sort of thing an array consists of, but a Swift array requires some definite element type. Don't fight these Swift features: be thankful for them! Merely getting your Swift code to compile is a form of auditing; your code is more likely to be correct, simply because of the nature of the Swift language.

If you've been hesitating to experiment with adopting Swift, hesitate no longer. There's never been a better time to start enjoying Swift! Swift 1.2 is available through the Xcode 6.3 beta, and it shows that the Swift language is achieving a solid maturity. Swift is a fun and easy language, and it won't take you long to translate some or all of your Objective-C app into Swift. You may be amazed at how much clearer and simpler your translated code turns out to be.


Editor's note: If you're looking for more on Swift and how it works with the iOS environment you're already familiar with, check out iOS 8 Programming Fundamentals with Swift and Programming iOS 8 by Matt Neuburg.

This post is part of our ongoing exploration into what it means to actually be a software engineer.

Public domain transmission image courtesy of Pixabay.

No comments:

Post a Comment

Related Posts Plugin for WordPress, Blogger...