In this article I'll take you to a journey of improving SciChart.framework consumption in the Swift world. Despite the fact there's plenty of articles about Objective-C to Swift and Swift to Objective-C interoperability, and all of the techniques are well-known – I'm going to share the SciChart perspective onto this topic. As well as sharing particular use-cases we faced during this process.

Here at SciChart we have quite a lot of shared C++ code. Our underlying graphics engine is written mostly in C++ and that's why we do have quite a lot Objective-C++ files to bridge between languages. This allows us to share this "mission-critical" part of our library across all our platforms - iOS/macOS, Android, WPF/Windows and JavaScript/Web. This gives us top-notch performance as well as ability to easily support OpenGL (which we still do). We also have many customers that are using Objective-C, which with the above means that it's quite unlikely if not impossible to to fully rewrite our Framework in Swift in the foreseeable future.

Despite the fact that out-of-the box Objective-C → Swift interoperability is quite good, we decided to go an extra mile here and take care of making our Objective-C API's a top-notch experience from the Swift side, doing the same as Apple does with it's own frameworks. But it's not possible to cover all possible techniques in one article, so I will focus only on the ones listed below:

If you are not interested in detailed information about improving Swift API's for Objective-C frameworks - feel free to scroll down directly to the Summary.


Nullability annotations

This one is pretty self explanatory as it allows you to annotate all of your APIs and tell everyone to expect something that can be null, or that will never be null. So we went through our code and marked all of it with the nullability annotation, either explicitly by hand or using handy macro, which covers block of codes or even files in some cases:

  • NS_ASSUME_NONNULL_BEGIN
  • NS_ASSUME_NONNULL_END

and now instead of having whole bunch implicitly unwrapped optional:

xAxis.currentInteractivityHelper?.scroll(xAxis.visibleRange, byPixels: 100)
if let yValue = seriesInfo?.formattedYValue {
	// Only now do something with unwrapped value
}
if let strokeStyle = seriesInfo?.renderableSeries?.strokeStyle { 
	// Only now do something with unwrapped value
}
example of optionals in API

we do have nice and clean Swift APIs. Finally, no more if let, guard let and other Swift dances:

xAxis.currentInteractivityHelper.scroll(xAxis.visibleRange, byPixels: 100)
let yValue = seriesInfo.formattedYValue
let strokeStyle = seriesInfo.renderableSeries.strokeStyle
example of optionals-free API

So-called Implicitly unwrapped optionals compiler warnings are easily fixable thanks to the handy XCode features:

even compiler errors are fixable as well:

Much more information is available in the Apple docs – Designating Nullability in Objective-C APIs

Initializers - designation and availability

In this section, i'm going to touch 2(3) important macro:

  • NS_DESIGNATED_INITIALIZER
  • NS_UNAVAILABLE & NS_SWIFT_UNAVAILABLE

NS_UNAVAILABLE & NS_SWIFT_UNAVAILABLE

As we all know, there's, let's say, semi-issue in Objective-C – you have initializers adoption by default. Each and every initializers in your classes are available to your class inheritors. One might say that this is a beneficial thing, but other - that it's not. While developing the SDK you might found that this brings some issues and confusion to consumers of your APIs.

Let's consider a very simple case we have here at SciChart. We have a fairly simple class SCIXyDataSeries which purpose is self-explanatory. There's shouldn't be any questions on how to create an instance of it. But what if you take a closer look at available initialisers, do you see anything eye catching:

redundant empty initialiser on SCIXyDataSeries
redundant empty initialiser on SCIXyDataSeries

How are we supposed to initialise SCIXyDataSeries without knowing the types of X-Data and Y-Data? That's right, you can't! There's few ways of solving this issue, like for example marking this initialiser nullable, adding documentation comments that this initialiser shouldn't be used or just throwing exception with some clear explanation.

Thankfully, there's NS_SWIFT_UNAVAILABLE macro, which makes marked signature unavailable for swift, moreover there's NS_UNAVAILABLE macro, which makes signature unavailable for Objective-C as well, which is exactly what we want in situation like this:

- (instancetype)init NS_UNAVAILABLE;
proper initialisers for the SCIXyDataSeries
proper initialisers for the SCIXyDataSeries

That's it, no more redundant, misleading initialisers. No more false-positive support requests. No more confusion. Everything works as it should in the first place.

Much more information is available in the Apple docs – Making Objective-C APIs Unavailable in Swift

NS_DESIGNATED_INITIALIZER

Designated initialisers explained fairly extensively in Swift documentation. In short - designated initializer guarantees that the object is fully initialised by sending an initialization message to the superclass. When you inherit a class - the implementation detail becomes important. As described in Swift docs the rules for designated initializer are:

  • A designated initializer must call a designated initializer from its immediate superclass.
  • A convenience initializer must call another initializer from the same class.
  • A convenience initializer must ultimately call a designated initializer.

A simple way to remember this is:

  • Designated initializers must always delegate up.
  • Convenience initializers must always delegate across.

In Objective-C if your class has a designated initializer and rules are not met - you'll get a friendly warning. Swift on the other hand is much more strict and will give you the compilation error. So marking the designated Objective-C initializers helps the compiler to enforce the rules and make less mistake while do custom work with SciChart.framework

Consider a simple case of creating a custom labels for your SCINumericAxis. You'd better read the documentation on this topic, but even after that I assume you would start with something like the following:

class CustomLabelProvider: SCILabelProviderBase<ISCINumericAxis> {
    override func formatLabel(_ dataValue: ISCIComparable!) -> ISCIString! {
		return super.formatLabel(dataValue)
	}
}

After applying it to your Axis - it will crash with an exception : < Parameterless initializer of CustomLabelProvider class shouldn't be used. Please use one of the designated initializers instead >.

Let's assume you wan't to pass some string format through the initialiser to be used during label formatting, so you would add something like below:

let format: String
init(format: String) {
	self.format = format
}

override func formatLabel(_ dataValue: ISCIComparable) -> ISCIString {
	let formattedString = String(format: format, dataValue.toDouble())
	return NSString(string: formattedString)
}

which also will fail. Which initialiser to call - nobody knows, no hint from compiler whatsoever. All you would get is - < 'super.init' isn't called on all paths before returning from initializer >, which isn't much of help.

This brings a lot of confusion, support requests for such a simple use-case and other pain in the ... Thankfully that's easily fixable using designated initializers (in conjunction with marking some initializers unavailable) so it's fairly clear which initializer to call from an inheritor. Now if you want to add new initializer, and just call super.init() you will got two errors:

'init()' is unavailable / Must call a designated initializer of the superclass

and if you try to call initializer on super, there's only proper and valid initializers are available:

designated initializers

everything now clear and simple without any confusion.

Much more information is available in the Apple docs – Designated Initializers and Convenience Initializers

Just for Swift refinements

As mentioned at the top of this article – Objective-C to Swift interoperability is outstanding by default, but at times it's a little bit awkward. All of that is possible thanks to the Swift's ClangImporter, which is used to import Objective-C (and C) code into Swift. Here is some of the tricks we used to improve importer behaviour and emit better and cleaner Swift API:

  • Grouping Related Objective-C Constants
  • NS_SWIFT_NAME a.k.a. swift_name attribute
  • NS_REFINED_FOR_SWIFT a.k.a. swift_private attribute

As per Apple Documentation there's a bunch of macros to Objective-C types to group their values in Swift, such as:

  • NS_ENUM – simple enumerations
  • NS_CLOSED_ENUM – simple enumerations that can never gain new cases
  • NS_OPTIONS – enumerations whose cases can be grouped into sets of options
  • NS_TYPED_ENUM – enumerations with a raw value type that you specify
  • NS_TYPED_EXTENSIBLE_ENUM – enumerations that you expect might gain more cases

Those are pretty self explanatory and I want to emphasise on the last one only.

Since a regular NS_ENUM isn't an extensible type, we often use constants to provide list of possible values, which might be extended by a framework consumer. As an example here's what we used to have in our SCIThemeManager:

static NSString * _Nonnull const SCIChart_BlackSteelStyleKey = @"SCIChart_BlackSteelStyleKey";
static NSString * _Nonnull const SCIChart_SciChartv4DarkStyleKey = @"SCIChart_SciChartv4DarkStyleKey";
static NSString * _Nonnull const SCIChart_Bright_SparkStyleKey = @"SCIChart_Bright_SparkStyleKey";
static NSString * _Nonnull const SCIChart_ChromeStyleKey = @"SCIChart_ChromeStyleKey";
static NSString * _Nonnull const SCIChart_ElectricStyleKey = @"SCIChart_ElectricStyleKey";
static NSString * _Nonnull const SCIChart_ExpressionLightStyleKey = @"SCIChart_ExpressionLightStyleKey";
static NSString * _Nonnull const SCIChart_OscilloscopeStyleKey = @"SCIChart_OscilloscopeStyleKey";
static NSString * _Nonnull const SCIChart_ExpressionDarkStyleKey = @"SCIChart_ExpressionDarkStyleKey";
static NSString * _Nonnull const SCIChart_DefaultThemeKey = @"SCIChart_DefaultThemeKey";
SCIThemeManager themeKey constants

At the moment in Swift 5 it's just going to be simply imported as public constants:

public let SCIChart_BlackSteelStyleKey: String
public let SCIChart_SciChartv4DarkStyleKey: String
public let SCIChart_Bright_SparkStyleKey: String
public let SCIChart_ChromeStyleKey: String
public let SCIChart_ElectricStyleKey: String
public let SCIChart_ExpressionLightStyleKey: String
public let SCIChart_OscilloscopeStyleKey: String
public let SCIChart_ExpressionDarkStyleKey: String
public let SCIChart_DefaultThemeKey: String

But if we add the typedef for a theme with a NS_TYPED_EXTENSIBLE_ENUM macro to it and use if with our constants:

typedef NSString * _Nonnull SCIChartTheme NS_TYPED_EXTENSIBLE_ENUM;
static SCIChartTheme const SCIChartThemeBlackSteel = @"SCIChartThemeBlackSteel";

magic happens:

public struct SCIChartTheme : Hashable, Equatable, RawRepresentable {
    public init(_ rawValue: String)
    public init(rawValue: String)
}
extension SCIChartTheme {
    public static let blackSteel: SCIChartTheme
    public static let v4Dark: SCIChartTheme
    public static let brightSpark: SCIChartTheme
    public static let chrome: SCIChartTheme
    public static let electric: SCIChartTheme
    public static let expressionLight: SCIChartTheme
    public static let oscilloscope: SCIChartTheme
    public static let expressionDark: SCIChartTheme
    public static let `default`: SCIChartTheme
}
NS_TYPED_EXTENSIBLE_ENUM magic for the Swift side

Now, SCIChartTheme itself become RawRepresentable. It has an associated type, and every single constant with the same type in Objective-C will now become SCIChartTheme. So now Swift can enforce type safety, and use only type he needs comparing to any other NSString like hundreds of notification constants/default keys. This is now consumed by Swift - much, much much more cleaner:

SCIThemeManager.applyTheme(.v4Dark, to: self.surface)
// as opposed to
SCIThemeManager.applyTheme(to: self.surface, withThemeKey: SCIChart_SciChartv4DarkStyleKey)
SCIThemeManager API comparison

In addition - you can easily extend this on the Swift side like so:

extension SCIChartTheme {
     static let berryBlue: SCIChartTheme = SCIChartTheme(rawValue: "SciChart_BerryBlue")
}

As a bonus point, it works also for the Objective-C side, so we not only have better Swift API, but better API for the both worlds:

[SCIThemeManager applyTheme:SCIChartThemeBlackSteel toThemeable:self.surface];
// as opposed to
[SCIThemeManager applyThemeToThemeable:self.surface withThemeKey:SCIChart_BlackSteelStyleKey];
Much more information is available in the Apple docs – Grouping Related Objective-C Constants

NS_SWIFT_NAME a.k.a. swift_name attribute  

The NS_SWIFT_NAME macro allow you to customize how the Objective-C declarations are imported. Effectively it gives you an ability to provide a full blown new signature specially for Swift. You can mark any declaration with it - enum, class, constant or function. You just pass the desired Swift name for selector you want to import, e.g.:

- (instancetype)initWithDefaultRange:(id<ISCIRange>)defaultNonZeroRange andAxisModifierSurface:(id<ISCIAxisModifierSurface>)axisModifierSurface;
// mark the above initializer with the following macro
NS_SWIFT_NAME(init(defaultNonZeroRange:axisModifierSurface:))

Here is the before/after of the Swift API:

// before
init(defaultRange defaultNonZeroRange: ISCIRange, andAxisModifierSurface axisModifierSurface: ISCIAxisModifierSurface)
// after
init(defaultNonZeroRange: ISCIRange, axisModifierSurface: ISCIAxisModifierSurface)

which is fixed by a one click thanks to the XCode:

XCode automatically can fix API changes made for Swift

Most of the time XCode helps you automatically fix the change - as above or below:

but sometimes doesn't - as with function below:

// before
func `is`(ofValidType dataSeries: ISCIDataSeries) -> Bool
// after
func isOfValidType(dataSeries: ISCIDataSeries) -> Bool

so you have to do that manually:

Much more information is available in the Apple docs – Renaming Objective-C APIs for Swift

NS_REFINED_FOR_SWIFT a.k.a. swift_private attribute

NS_REFINED_FOR_SWIFT macro on the other hand modifies the declaration adding a double underscore prefix, making it effectively private in Swift. But since double underscores are there, you can create a new function and call the private one from inside. This gives the opportunity of making clean Swift API where it can't be achieved via simple renames.

Consider simple SCIIntegerValues initializer example which accepts c-style array  and count:

- (instancetype)initWithItems:(int *)items count:(NSInteger)count

The above gets imported to swift as follows:

public convenience init(items: UnsafeMutablePointer<Int32>, count: Int)

This API is not that convenient on the swift side:

Inconvenient SCIIntegerValues initializer

Rename can't help with UnsafeMutablePointer, so here is when NS_REFINED_FOR_SWIFT comes in very handy. Instead of just changing parameter names, we can change method signature in an extension, making it accept a Swift Sequence:

convenience init<S: Sequence>(_ s: S) where S.Element == Int {
    let array = Array(s.map { Int32($0) })
    self.init(__items: array, count: array.count)
}

Now instead of hassling with pointers we have a super convenient API on the Swift side:

let val2 = SCIIntegerValues([1, 2, 3, 4, 5])
// or even
let val1 = SCIIntegerValues(1...5)
Much more information is available in the Apple docs – Improving Objective-C API Declarations for Swift

Summary

Building an SDK on Apples platforms still means using Objective-C. Even if out-of-the box Objective-C → Swift interoperability is outstanding – you'll still need to go an extra mile. Yes, interoperability is hard, and building bridges is not as easy as you might think it is. But if youto provide an excellent experience for consumers of your APIs – you might found this article helpful. Otherwise you can leave everything as it is or just build everything in Swift.

As a conclusion I'd recommend using the following techniques:

  • nullability annotations - by adding type safety to the Swift API you also improve your Objective-C code.
  • designated initializers - in conjunction with NS_UNAVAILABLE on your initializers you make them stupidly clear and eliminate mistakes upfront.
  • lightweight generics - same as above. Here's more info in documentation

And the most important do the 4R's:

  • Rename - NS_SWIFT_NAME
  • Refine - NS_REFINED_FOR_SWIFT
  • Rinse - NS_UNAVAILABLE and NS_SWIFT_UNAVAILABLE
  • Repeat

All of the above were just simple examples on how available approaches can be used in real-world scenarios. This and much more subtle improvements to SciChart.framework will be available in our next release SciChart iOS/macOS SDK v4.3.