Titanium

How to Write Titanium iOS Native Modules with Swift

We are thrilled to announce that with the upcoming Titanium SDK 8.0.0 release, you can finally write native modules for iOS in Swift! With this blog post, we want to give you a little introduction and show you how to get started with native modules and Swift.

Getting started

First, make sure you have Titanium 8.0.0 installed and selected:

ti sdk install -b 8_0_X -d

 

When you create a new module using Titanium SDK 8.0.0, you will be able to choose what code base you want to use for iOS. Choosing Swift here will use the new Swift project template and you are ready to go. You can also directly pass the new --code-base option to the ti create command.

ti create -t module --name ti.swift --id ti.swift --code-base swift

 

If you don’t need Swift in your new module, you can still choose objc for the Objective-C project template.

Short intro to Swift module development

Now, if you open the Xcode project of your new Swift-based native module, you will see two Swift source files: one for the module and one for the example proxy.

Let’s take a closer look at the module and elaborate on a few things that you commonly encounter when writing native modules in Swift.

Tip: If you are new to native module development, make sure to take a look at the iOS Module Development Guide. There, you’ll learn the basics of native module development for iOS. The conventions, rules and guidelines described there for Objective-C will also apply when you write native modules using Swift.

The module class

The main entry point to your module is the module class. It looks something like this:

import UIKit
import TitaniumKit

@objc(TiSwiftModule)
class TiSwiftModule: TiModule {

  @objc public let testProperty: String = "Hello World"

  // ...

  @objc(tryThis:)
  func tryThis(arguments: Array?) -> String {
    guard let arguments = arguments, let message = arguments.first else { return "No arguments" }

    return "\(message) from TiSwift!"
  }
}

 

You import the Titanium core module with import TitaniumKit. The module class can now simply extend from TiModule. This is all pretty standard for native modules, however there are two important things here, namely the @objcattributes.

The first one is @objc(TiSwiftModule) for the class. This will make sure the class gets exported to the Objective-C runtime as TiSwiftModule instead of <module-name>.TiSwiftModule, which is important for Titanium to be able to instantiate the module. You only need to do that for classes that need to be available from the Objective-C runtime, i.e. module and view proxies automatically created by Titanium, or Swift classes that you want to use in Objective-C source files in mixed language modules.

Second, the tryThis method is marked with @objc(tryThis:). Through this attribute, you make sure the method will be available in JavaScript as tryThis instead of tryThisArguments. This is due to Swift trying to rename the method by default to match Objective-C naming conventions. You need to add this to all methods that you want to access via JavaScript.

Note: When you open the project, Xcode might show you a warning: “The use of Swift 3 @objc inference in Swift 4 mode is deprecated.” To resolve this, you need to change the “Swift Language Version” build setting to “4.2” (see TIMOB-26770 for an upcoming fix in the template). For more info about @objc inference, see the Limiting @objc inference proposal.

Proxies

To see a few more examples of how to implement proxy properties and methods, let’s switch over to the titanium-onboarding module. It implements the awesome PaperOnboarding Swift UI library and is our first official native module written in Swift.

To not blow this blog out of proportion, we will concentrate on a few selected examples from TiOnboardingViewProxy.swift.

Defining Proxy Properties

Let’s start with a simple property definition:

@objc public var pageItemRadius: NSNumber

 

This will define the property pageItemRadius on the proxy. Again, note the use of @objc attribute to make this property available to the Objective-C runtime, which is required for Titanium to be able to expose it to JavaScript. This simple declaration is enough for most simple data types you can directly assign like String, NSNumber (note: Swift’s Numeric is not Objective-C compatible), KrollCallback functions or even arrays and dictionaries.

However, when it comes to more complex types and you want to retain Swift’s type safety, there is a little trick. Color values, for example, are typically specified as a string on the JavaScript side and handled as TiColor or UIColor values on the native side. Now is the perfect time to make use of Swift’s computed properties.

private var _defaultItemColor: TiColor
@objc public var defaultItemColor: String {
  get { return _defaultItemColor.name }
  set {
    _defaultItemColor = TiUtils.colorValue(newValue)
    self.replaceValue(newValue, forKey: "defaultItemColor", notification: false)
  }
}

 

This will expose the property defaultItemColor as a String and store the converted native value in an additional private property named _defaultItemColor, which you can use throughout your code and benefit from Swift’s type safety and IDE auto-completion features.

Tip: If you are wondering what replaceValue does, check the Setting Proxy Values section of the iOS Module Architecture Guide.

Everything that is documented in the official Swift Properties guide, like computed properties or property observers, can be applied to proxy properties.

Defining proxy methods

We have already seen a proxy method definition in the module class (which is just a special purpose proxy) above. To give you one more example for proxy methods in Swift and how to handle arguements, take a look at the following method from TiOnboardingViewProxy.swift, which performs a simple transition to an onboarding page with the given index:

jc(transitionToIndex:)
public func transitionToIndex(args: Array?) {
  guard let args = args, let index = args.first as? NSNumber else {
    return;
  }
  var animated = NSNumber(value: true)
  if args.count > 1 {
    animated = args[1] as? NSNumber ?? animated
  }
  (self.view as! TiOnboardingView).onboardingView.currentIndex(index.intValue, animated: animated.boolValue)
}

 

Since arguments will be passed as an array of Any, we need to explicitly cast them to the types we want. For that, Swift’s excelent type casting serves as replacement for the various utility macros from Objective-C like ENSURE_SINGLE_ITEM(args, type). In the above method, these checks are implemented by a guard as well as an additional if statement to check for an optional second argument.

The initial guard makes sure we have any arguments at all and also that the first one is of type NSNumber. If those requirements are not met, we simply return early and do no transition at all.

To check for the optional second parameter, we first define the default value of true. If a second argument was provided a conditional cast can be used to see if it’s of type NSNNumber. If that’s the case, Swift will use the casted value. Otherwise, it will assign our default value.

In a nutshell (or tl;dr)

Those are the most important things you need to know to get started with Swift native module development. For the tl;dr faction among you, here is everything condensed in a few bullet points:

  • Use the @objc annotation to expose your class to Objective-C (used by the Titanium core)
  • Use the @objc annotation to expose your properties and methods to Objective-C as well
  • Method arguments always have the Array<Any>? type, specifying a various number of arguments. Unwrap them like you would do in Swift, e.g. guard let arguments = arguments, let message = arguments.first and so on.
  • You can use any public Titanium API like before, e.g. TiUtils as long as it’s part of TitaniumKit.

Current limitations

This is the first iteration of Swift support in native modules, so naturally there are a few limitations while we work hard to make the rest of the core Swift-compatible.

  • Access to types is restricted to those that are in TitaniumKit. For example, extending existing UI classes like TiUINavigationView or TiUIImageView that are not yet part of a module is not yet possible.
  • No convenient macros like in Objective-C (ENSURE_ARG etc.) to handle argument extraction from methods. Luckily, this can easily be done with Swift’s guard and if let checks as well as type casting.