Saturday, March 26, 2016

Convert ObjC to Swift (2)

There are some differences between Objective-C and Swift. During the conversion, I found that the customized constructor or initializer in ObjC is different from Swift. For example, in ObjC, the initializer is in the format of initWith...:... parameters, while in Swift, all initializers are in the same name of init(...), like other modern languages.

This presents a challenge, how this kind of initWith...:... ObjC constructor be directly converted to Swift?

Class Factory

I come to Design Pattern to find a solution: Class Factory. In Swift, a static function can be defined as class factory to create an instance of the class!

For example, in the following swift file, all init() constructors are private. As a result, I force to use class methods to create instance of the class MyLogger:

  1. import Foundation
  2. //FIXME: Remove objc and inheritance from NSObject after convertion to Swift is completed
  3. @objc
  4. class MyLogger : NSObject {
  5. //MARK: Init methods
  6.    //Make all init as private constructors
  7.    private override init() {
  8.        self.level = MyLoggerLevel.LogLevelDebug
  9.    }
  10.    private convenience init(loggingContext: String) {
  11.        self.init()
  12.        self.context = loggingContext
  13.    }

  14.    //Force to use these two class methods to create instance!
  15.    static func instance(loggingContext: String) -> MyLogger {
  16.        return MyLogger(loggingContext:loggingContext)
  17.    }
  18. ...
I have to make some changes in my ObjC codes to adopt this design pattern. This actually makes my ObjC codes much clean and beautiful, getting rid of [[MyLogger alloc] initWith...]. In this way, the converted ObjC codes looks like this.

  1. #import "MyiOSApp-Swift.h"
  2. #import "ObjC2SwiftHelper.h"
  3. @interface MyObjCClass ()
  4. @property (strong, nonatomic) MyLogger* logger;
  5. @synthesize logger = _logger;
  6. - (MyLogger *) logger {
  7.    if (!_logger) {
  8.        /* Remove alloc init codes
  9.        _logger = [[MyLogger alloc] init];
  10.        _logger.context = CALLER_CONTEXT; */
  11.        //Use class method to get instance
  12.        _logger = [MyLogger instance:CALLER_CONTEXT];
  13.    }
  14.    return _logger;
  15. }
  16. ...
Even I made my init(...) constructor in swift as private, but I found this is still accessible from ObjC. Not sure why.

Actually it is good to find it out. If I did not write this blog, I would not go so deep to find the issue.

Method Signature Mapping

In Swift, the method signature includes both method name and parameter names. Therefore, the conversion to ObjC has to be matched completely in both.

I find out that a method definition in Swift, as in the following example:

  1. //Case 1: method with two named parameters
  2. func swiftMethod(para1 sValue : String, para2 aInt : Int) {
  3. ...
  4. }

where: swiftMethod is the name of method, para1 and para2 are parameter names, and sValue and aInt are parameter value names.

The first parameter name can be omitted without a name, which is very common in both swift and ObjC:

  1. //Case 1: method with 1st parameter with no name, 2nd with a name
  2. func swiftMethod(sValue : String, para2 aInt : Int) {
  3. ...
  4. }

The 2nd case is very simple to map to ObjC:

  1. [swiftMethod:@"Test" para2: 1];

How about the first case, the first parameter with a specified name? I found out the interesting point, the first parameter name has to be linked with With in between method name and parameter name.

  1. [swiftMethodWithPara1:@"Test" para2: 1];

Notice that camel case of capital letter of the parameter name in part of method name(swiftMethodWithPara1), even it is defined in Swift in lower case as para1.

The above swift example can be further simplified with all parameters with no names
  1. //Case 3: method with all parameters with no names
  2. func swiftMethod(sValue : String, para2 : Int) {
  3. ...
  4. }

To convert the 3rd case to ObjC, the parameter names after 1st parameter have to use the swift parameter value names as default names:

  1. [swiftMethod:@"Test" para2: 1];

Based on above findings, it seems that an alternative ObjC solution for swift init(...) customized constructors is found, as in the following example:

  1. let defaultInitInt = 1
  2. class MyClass {
  3.  //init with two named parameters
  4.  init(para1 sValue : String, para2 aInt : Int) {
  5.  ...
  6.  }
  7.  //init with only one parameter without name
  8.  convenience init(sValue : String) {
  9.    self.init(para1: sValue, para2: defaultInitInt)
  10.  }
  11. }

The corresponding ObjC code can be something like this:

  1. //Create instance with init of two named parameters
  2. MyClass* instance1 = [[MyClass alloc] initWithPara1: @"Test" para2:1];
  3. //Create instance with init of only one parameter
  4. MyClass* instance2 = [[MyClass alloc] initWithSValue:@"Test2"];