Friday, March 25, 2016

Convert ObjC to Swift (1)

Recently I started to convert my previous iOS app in Objectiv-C to Swift. There are many tools available for conversion, however, I prefer to do it manually myself. I think this will be good opportunity to learn/review Swift and to understand better both programming languages.

The start did take some tough time, but it worth the try. Here I would like to take some notes on issues I have experienced.

Swift Bridging Header File

I started from my Objective-C project in Xcode. The first time I tried to add a Swift file, I got this prompt asking to create a Swift bridging header file:

This header file is a blank file when it is created. From the comments in this header, it looks like that the purpose of this bridging header is mainlly for my ObjC classes to be visible to my Swift classes, therefore, I have to add any header files if I would like to expose ObjC classes to Swift. Since I am going to convert all my .h and .m files to swift files, I don't need to add any header to this bridging header file.

However, I found one case I do need to use this bridging header. I have the following C #define macros, which are not supported in swift file:

  1. #define CALLER_SELECTOR_NAME NSStringFromSelector(_cmd)
  2. #define CALLER_CONTEXT       NSStringFromClass([self class])

The above macros were defined in a pair of .h and .m file. After conversion, I moved the above codes to a temporary header file called as ObjC2SwiftHelper.h and included this file in the bridging header file. Then removed the pair of .h and .m file from my project. With this helper header file, the macros are available for other ObjC files.
You don't need to add any codes in your ObjC or Swift files to include this bridging header file. The Xcode will automatically make this header files available for your project classes.

The bridging header file has to be named in this format: [PrjectName]-Bridging-Header.h

Hidden Swift Header: Making Swift class visible to ObjC

During the conversion, for each new conversion swift class based on a pair of .h and .m ObjC class, the class in swift has to be visible to ObjC if the class is used in ObjC. There is no header file for swift class. The way to make a swift class available for an ObjC class is to include a special hidden header in the ObjC .h or .m file.

For example, in my ObjC class I refer to a swift class, a special hidden swift header file has to be included:

  1. // This is a .m file. MyLogger class has been converted to swift.
  2. // Inorder to access to MyLogger class, include this special
  3. // header to make all swift class available
  4. #import "MyiOSApp-Swift.h"
  5. #import "Objc2SwiftHelper.h" // Helper header to provide macros
  6. @implementation MyEntity (Create) // Extend MyEnity class for creating feature
  7. #pragma mark - Private methods
  8. + (MyLogger*) logger {
  9.    MyLogger* log = [MyLogger instance:CALLER_CONTEXT];
  10.    return log;
  11. }
  12. ...

The special header file is in the format of [ProjectName]-Swift.h

It is not visible in Xcode project. However, I found you can still access to it if you highlight the header and open it from context menu Jump to Definition.

Swift String and ObjC NSString

In swift, String is equivalent to ObjC NSString. According to Apple Development documentation, swift String is automatically mapped to NSString if you refer to String in swift codes.

However, I found in one case, I have use NSString class in swift instead of String. In my project, I made extension to NSString class. When I tried to convert my extension to swift, I used String extension. I found that this extension is not available in my ObjC codes.

After struggling for a while, I realized that instead of extension to String in swift, I have to explicitly extend NSString in swift. I think that I have to make final revision to extension to String after all ObjC codes converted.

  1. import Foundation
  2. extension NSString {
  3.    // Helper getter to convert NSString to String
  4.    private var swift : String { return self as String }
  5.    func isNumeric() -> Bool {
  6.        var retVal: Bool = false
  7.        let sc: NSScanner = NSScanner(string: self.swift)
  8.        if sc.scanFloat(nil) {
  9.            retVal = sc.atEnd
  10.        }
  11.        return retVal
  12.    }
  13. ...

I also found another case that NSString is not automatically mapped to String. For example, in ObjC codes, NSFileWrapper class initializer takes NSMutalbleDictionary as parameter:

  1. NSMutableDictionary* wrappers = [NSMutableDictionary dictionary];
  2. ...
  3. NSFileWrapper* fWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrapper: wrappers];

Convert the above codes to Swift, NSMutalbleDictionary actually is the type or dictionary of [NSString : NSFileWrapper] in Swift. However, NSFileWrapper initializer takes the dictionary of [String : NSFileWrapper]. Those two are different in Swift. The converted codes have to be like:
  1. var wrappers : [String : NSFileWrapper] = [:]
  2. ...
  3. fileWrapper = NSFileWrapper(directoryWithFileWrappers: wrappers)