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 }
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
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:
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;
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:
and if you try to call initializer on super, there's only proper and valid initializers are available:
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
Grouping Related Objective-C Constants
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";
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 }
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)
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:
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:
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
andNS_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.
Top comments (0)