📜 ⬆️ ⬇️

About transferring a project from Objective-C to Swift

Hello, dear readers.

Among the most burning topics that have been raised on our publishing councils in the last six months, the Swift programming language occupies a special place. With a huge interest in it from Western developers and with a genuine abundance of books on this topic, the language still seems rather raw. Therefore, testing the ground about the demand for a new language, we suggest to get acquainted with the post of the magnificent Matt Neuburg, the author of the book Programming iOS 8: Dive Deep into Views, View Controllers, and Frameworks. The author describes in detail the translation of the application to the new Apple language, convincingly proving: “the eyes are afraid - the hands are doing”, and the hybrid assembly of Objective-C and Swift does not resemble a mixture of French and Nizhny Novgorod.

Enjoy reading and fruitful experiments.

If you already have an application written in Objective-C, try rewriting it in Swift. This is a great opportunity to get acquainted with the Swift language, experiment with Swift and decide for yourself whether you are ready to choose Swift as your main technology. I managed to perform such a migration on several real-world applications, in this article I want to share some observations drawn from this experience.
')
Hybrid assembly

Of course, you don’t have to transfer all your code to Swift at once; much more likely that you will rewrite class after class. As soon as you add the Swift file to the assembly of your application written in Objective-C, this assembly will become a hybrid. Some classes in it will remain in Objective-C, others will be written in Swift. Accordingly, it is necessary to make so that all announcements were visible in a code in both languages. Before embarking on this work, let's understand how the mechanism of such visibility functions.
As you remember, class declaration in Objective-C is usually divided into two parts: the header file (.h), containing the interface section, and the code file (.m), containing the @implementation section. If class information is required in the .m file, it imports this class's .h file.
The mutual visibility of Swift and Objective-C code is based on the following convention: it is provided at the .h file level. There are two directions of visibility, each of which must be considered separately.

How Swift sees Objective-C

When adding a Swift file to an Objective-C assembly or an Objective-C file to the Swift assembly, Xcode will prompt you to create a bridging header. This is the .h file included with the project. By default, its name is made on behalf of the assembly, but in general it is arbitrary and can be changed if you change the “binding header” setting in the Objective-C assembly in the same way.
An Objective-C .h file will be visible to Swift, provided that you import it (#import) into this binding header.

How Objective-C sees Swift

If you have a link header, then when creating your assembly, the corresponding top-level declarations for all your Swift files are automatically translated into Objective-C and used to create a hidden link header in the Intermediates directory of this target assembly, which is deep in the DerivedData folder. This hidden header is most easily viewed with the help of the following command, typed in the terminal window:

$ find ~/Library/Developer/Xcode/DerivedData -name "*Swift.h" 


This will tell you the name of the hidden binding header. Alternatively, try viewing (or changing) the “Product Module Name” setting in your target assembly; The name of the hidden binding header is created based on the product name specified here.

Swift declarations will be visible in your Objective-C files, provided that you import (#import) this hidden binding header into all those Objective-C files where these Swift files should be visible.

The situation may change significantly from where exactly the hidden binding header is imported at the top of the .m file. A common alarm is the appearance of compilation errors “Unknown type name” (Unknown type name), where the unknown type is a class declared in Objective-C. To solve this problem, you need to import the .h file containing the declaration of an unknown type, and into your files on Objective-C, and before importing the hidden binding header. Such work is sometimes annoying, especially if the Objective-C file in question is not necessary to know about this class, but this way we really solve the problem, after which the compilation can be continued.

Step by Step instructions

Before making any changes, create a new branch in git. Now we will translate our classes from Objective-C to Swift, one by one. I will do this using the following algorithm:

  1. Select the .m file to be translated to Swift. The Objective-C language cannot inherit from the Swift class, so you have to define both the subclass and the class itself in Objective-C, starting with the subclass. The delegate class of the application is transferred last.
  2. Remove this .m file from the target assembly. To do this, select the .m file and use the file inspector.
  3. In all Objective-C files that import the corresponding .h file, remove the #import statement, and in its place, import the hidden binding header. (If you are already importing a hidden binding header to this file, you do not need to repeat this procedure.)
  4. If you are importing the corresponding .h file into the binding header, remove the #import statement.
  5. Create a .swift file for this class. Make sure that it is added to the target assembly.
  6. In the .swift file, declare the class and place the stub declarations for all members that were made public in the .h file. If this class must comply with Cocoa protocols, accept them; you may also need to provide stub declarations for all the required methods of this protocol. If this file should refer to any other classes that are still declared in your target assembly in Objective-C, import their .h files into the link header.
  7. Now the project should compile! Of course, it does not work, as there is still no real code in your .swift file. But is this a problem? So, beer for jerk!
  8. Now write the code in our .swift file. I prefer to translate the code from the original Objective-C file line by line, even if the result is not very idiomatic (swift).
  9. When the code of this .m file is fully translated to Swift, collect it, run it and test it. If the runtime begins to swear (especially if it collapses at the same time) that it cannot find this class, search for all references to it in the nib editor and re-enter the class name in the identity inspector (and also press Tab to set the change) . Save everything and try again.
  10. To the next .m file! Repeat all the above steps.
  11. When all the other files are translated, translate the application delegate class. At this stage, if there are no Objective-C files left in the target assembly, you can delete the main.m file (replacing it with the attribute app delegate class declaration), and the .pch file (the precompiled header).


Do not think that you are obliged to translate all the code into Swift. It is possible that some sections will be better left on Objective-C, this is completely normal. In fact, some code must be left on Objective-C, as there are parts in Cocoa API that Swift does not have access to. For example, you cannot write a C function or a function pointer to Swift, so you cannot call CGPatternCreate or AudioServicesAddSystemSoundCompletion without an auxiliary Objective-C method. The appearanceWhenContainedIn: method cannot also be called from Swift.

On the other hand, code that uses one of the performSelector: methods that are also not available on Swift can be immediately left on Objective-C, but sooner or later you will have to come up with some workaround so that these methods can be replaced with Swift code.

Let's experiment with Swift

Congratulations! Now your application is written entirely or partially in Swift. But if you followed all my recommendations, then at this stage the application is not too “swift”. After all, we primarily sought to run our code. Now that everything works, you can go back to the code itself and try to make it more idiomatic. You may find that some tasks that were solved on Objective-C are intricately or awkwardly, much clearer and more pleasantly implemented on Swift

Of course, wherever possible, you should switch to native Swift types. Pairs of immutable / changeable types, for example, NSString and NSMutableString, NSArray and NSMutableArray, as well as NSDictionary and NSMutableDictionary can be replaced with native Swift types: String, Array and Dictionary. You will also find that you no longer need some of the complex techniques that were associated with these types. Thus, an array has instance methods map, filter and reduce; they will be very useful to you.

For example, in one of my applications there was a tabular view where data was divided, divided into sections. At the internal system level, this data is recorded in an array of arrays, where each subarray consists of rows representing table rows in a particular section. In the table, you can perform a search, and now I want to find and delete those lines in which there is no substring entered by the user in the search field. I don’t intend to touch the sections themselves, but if, when deleting lines, a section is completely empty, I want to delete the entire array of this section as well. Here’s how it was done in Objective-C (sb is UISearchBar):

  NSPredicate* p = [NSPredicate predicateWithBlock: ^BOOL(id obj, NSDictionary *d) { NSString* s = obj; NSStringCompareOptions options = NSCaseInsensitiveSearch; return ([s rangeOfString:sb.text options:options].location != NSNotFound); }]; NSMutableArray* filteredData = [NSMutableArray new]; for (NSArray* arr in self.sectionData) { NSArray* filteredArr = [arr filteredArrayUsingPredicate:p]; if (filteredArr.count) [filteredData addObject: filteredArr]; } self.filteredSectionData = filteredData; 


First we form NSPredicate to filter the array. Then loop through our array of arrays, “distributing” the components of each filtered subarray into an empty NSMutableArray, one after the other; I am sure you are familiar with this idiom. In Swift, we do not need either NSPredicate or “distribution” idioms - as well as two intermediate arrays! We have a map and a filter; all the work fits into a single statement:

 self.filteredSectionData = self.sectionData.map { $0.filter { let options = NSStringCompareOptions.CaseInsensitiveSearch let found = $0.rangeOfString(sb.text, options: options) return (found != nil) } }.filter {$0.count > 0} 


You will also notice that in those places where your code relied on the Objective-C dynamics associated with message forwarding, you can do without this mechanism, since in Swift the function is first class.

Consider an example.
In my application with dictionary cards, I have a class Term, representing the Latin word. He declares many properties. Each card corresponds to one term, and each of its properties is displayed in a separate text field. When a user clicks on any of the text fields on the screen, I want the actual term in the interface to change to the following — different from the previous one — by the property that the user chose. Accordingly, the code for all three text fields will be the same; the whole difference lies in exactly what property we’ll use to select the next term to be displayed on the screen.

In Objective-C, the simplest way to express such parallelism was to use key-value pairs (g is a gesture recognizer):

  NSInteger tag = g.view.tag; // the tag tells us which text field was tapped NSString* key = nil; switch (tag) { case 1: key = @"lesson"; break; case 2: key = @"lessonSection"; break; case 3: key = @"lessonSectionPartFirstWord"; break; } //       NSString* curValue = [[self currentCardController].term valueForKey: key]; 


Now I can continue to use key-value pairs in Swift; but for this it is necessary that my class Term inherits from NSObject, and it depends on the dynamics of Objective-C / Cocoa - the translation of strings into property names - alien to the Swift spirit. It turns out that in Swift you can easily implement the same dynamics - by transforming labels into method calls - using a simple array of anonymous functions:

  let tag = g.view!.tag let arr : [(Term) -> String] = [ {$0.lesson}, {$0.lessonSection}, {$0.lessonSectionPartFirstWord} ] let curValue = arr[tag-1](self.currentCardController.term) 


Conclusion

Many Swift features are aimed precisely at making your code more reliable from the very beginning.

A common mistake when programming in Objective-C is to declare an instance property, but forget to set it to its original value; nothing happens when sending a message to nil, so you can work for a long time without noticing the problem. Swift requires you to initialize all instance properties. Next, we come to the notorious strong typing Swift; in Objective-C, it is quite possible to guess which element type an array consists of, but in Swift it is necessary to specify a strictly definite element type for an array. Do not try to fight these Swift traits, be grateful for them! If you simply manage to get your Swift code to compile, you will already learn something; Most likely, your code will be correct for the simple reason that such correctness is inherent in the very nature of the Swift language.

If you doubted whether it is worth experimenting with Swift - do not hesitate anymore! Now is the time to learn Swift and savor this language. In the beta version of Xcode 6.3, Swift 1.2 is already available; it makes sure that this language has already reached sufficient maturity. Swift is an interesting and simple language, you can quickly translate any application that was written in Objective-C to it. You might even be surprised how much clearer and simpler the resulting code will be.

Source: https://habr.com/ru/post/258185/


All Articles