This is a challenging time to be a Swift developer. With three versions of Swift in the wild (2.2, 2.3, and 3.0) and two active SDKs with source-breaking changes, maintaining libraries is a daunting task. The two most common approaches to supporting multiple version of Swift and the SDKs are creating a Git branch per version, or supporting both 2.x and 3.0 using #if to isolate compatibility issues.

When I started development on a Swift-first version of BonMot, I didn’t like either of these options. However, given my release window being in the middle of the Swift 2.3 to 3.0 transition, I felt as though I should give it a shot. I was encouraged when I heard that OHHTTPStubs was able to support both 2.x and 3.0, and decided to see if I could use the #if approach in a way that minimizes the impact to the code base. This post summarizes a few strategies I found that have made maintaining both 2.x and 3.0 reasonably pain-free.

Call Site Compatibility

Call site compatibility is when a method has the same method signature in Swift 2.x and 3.0. There are two aspects of Swift 3.0 that affect this that were part of “The Great Renaming”. The first is dropping the implicit _ argument label applied to the first argument, which makes the labeling behavior of all arguments consistent. The second part is moving part of the method name into the first argument label.

For example, given a method that takes a String as an argument:

func performThing(thing: String) {} // Verbose function signatures and the call-site invocation #if swift(>=3.0) func performThing(thing thing: String) {} performThing(thing: "hello") #else func performThing(_ thing: String) {} performThing("hello") #endif 1 2 3 4 5 6 7 8 9 10 func performThing ( thing : String ) { } // Verbose function signatures and the call-site invocation # if swift ( > = 3.0 ) func performThing ( thing thing : String ) { } performThing ( thing : "hello" ) # else func performThing ( _ thing : String ) { } performThing ( "hello" ) # endif

To write code that is call-site compatible, always provide a label for your first argument. This avoids the implicit _ argument label that is inserted by Swift 2.x. By avoiding this behavior, you ensure that the signature is the same in Swift 2.x and 3.0.

For example:

// Don’t do this. Calling semantics change between 2.x and 3.0 func performThing(thing: String) {} // Avoid this even though it is call-site-compatible. This will generate a warning // in Swift 2.x because the '_' label is redundant. func performThing(_ thing: String) {} // Avoid this even though it is call-site-compatible. This will generate a warning // in Swift 3.0 because the 'thing' label is redundant. func performThing(thing thing: String) {} // Just right! The argument label and parameter name are different, // which avoids the Swift 3.0 warning mentioned above. func perform(thing theThing: String) {} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // Don’t do this. Calling semantics change between 2.x and 3.0 func performThing ( thing : String ) { } // Avoid this even though it is call-site-compatible. This will generate a warning // in Swift 2.x because the '_' label is redundant. func performThing ( _ thing : String ) { } // Avoid this even though it is call-site-compatible. This will generate a warning // in Swift 3.0 because the 'thing' label is redundant. func performThing ( thing thing : String ) { } // Just right! The argument label and parameter name are different, // which avoids the Swift 3.0 warning mentioned above. func perform ( thing theThing : String ) { }

If you really want no argument label, you will need a #if statement and a duplication of the method. This may be worth it for your API, but if you choose this option, I recommend that your Swift 2.x and 3.0 versions call out to a shared private function to minimize the duplication.

By writing all of your method signatures to be call site compatible, you provide your API consumers with a source-stable API between Swift 2.x and 3.0, and you can invoke your own methods inside your code without littering #if statements throughout your code.

Compatibility.swift

Now that we can maintain call site compatibility in our source code, we need a strategy for using the Swift Standard Library and Cocoa libraries in a call site-compatible manner. I’ve created a Compatibility.swift file that contains Swift 2.x extensions on every object that was part of The Great Renaming. This is a bit more painful and requires substantially more grunt work than I like; however, it isolates the impact of The Great Renaming to a single file.

For example:

#if swift(>=3.0) #else typealias OptionSet = OptionSetType extension UIApplication { @nonobjc static var shared: UIApplication { return sharedApplication() } } extension NSAttributedString { @nonobjc final func attributes(at location: Int, effectiveRange range: NSRangePointer) -> [String : AnyObject] { return attributesAtIndex(location, effectiveRange: range) } } // and so on #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # if swift ( > = 3.0 ) # else typealias OptionSet = OptionSetType extension UIApplication { @ nonobjc static var shared : UIApplication { return sharedApplication ( ) } } extension NSAttributedString { @ nonobjc final func attributes ( at location : Int , effectiveRange range : NSRangePointer ) - > [ String : AnyObject ] { return attributesAtIndex ( location , effectiveRange : range ) } } // and so on # endif

Note the typealias of OptionSet to OptionSetType. This allows you to write Swift 2.3 code in a way that will more easily migrate to Swift 3.0 when the time is right.

There are a few cases where this strategy falls a bit short. In particular, some Swift 3.0 API’s are not valid Swift 2.3. For instance, NSParagraphStyle.default is not valid Swift 2.3, so I was forced to use a prefixed version.

Do Not Pollute!

All of the functions in your Compatibility.swift file should not be public, because they should not pollute your consumer app’s namespace. Also, they must be marked with @nonobjc final or @nonobjc static if they are on NSObject subclasses. This will prevent the Swift compiler from creating Objective-C selectors. Instead, it will use static dispatch, which the compiler will probably end up inlining.

Type Changes

Swift 3.0 also imports String consts as Enumerations, a welcome change. However, this causes a new complication that has two compatibility challenges. The first is the type declaration changes. This is easily remedied through the use of typealias. In BonMot, I was able to typealias BonMotContentSizeCategory to String in iOS 9, and UIContentSizeCategory in iOS 10. This can get trickier, though. For instance, if I had to store this value in an attribute dictionary, I’d have to add #if or some more compatibility code. In the BonMot unit tests, I did end up having to call methods that used BonMotContentSizeCategory, so I added my own UIContentSizeCategory struct to the test target under Swift 2.3 to make the unit tests compatible. This is not something that you would want to ship with the library, but it did help making sure the tests run in both 2.x and 3.0.

Managing SDK differences

SDK differences between iOS 9 and 10 (substitute macOS, watchOS, or tvOS as appropriate) can result in compilation failures. Swift 2.3 and 3.0 don’t provide a clean mechanism for dealing with this, since the only applicable check is the Swift version check. However, you can use the Swift version as a proxy for the SDK version.

For example:

#if swift(>=2.2) // This can only be compiled for iOS 9 #elseif swift(>=2.3) // This can only be compiled for iOS 10 #elseif swift(>=3.0) // This can only be compiled for iOS 10 #endif 1 2 3 4 5 6 7 # if swift ( > = 2.2 ) // This can only be compiled for iOS 9 # elseif swift ( > = 2.3 ) // This can only be compiled for iOS 10 # elseif swift ( > = 3.0 ) // This can only be compiled for iOS 10 # endif

Hopefully, the next version of Swift will provide SDK checks to the #if system, as the intent here is masked by the use of Swift language version checks. However, this is the current preferred method of dealing with SDK differences. For instance, in iOS 10, a lot of the (NS)URL methods changed from returning (NS)URL to returning (NS)URL?.

Delegate Methods & Subclasses

One place where The Great Renaming can force #if is with delegates and subclasses. When implementing a delegate or subclass whose method signature has been impacted by The Great Renaming, you have to use #if to avoid warnings. If you are using a lot of delegates, feel free to make compilation under 3.0 be nice and clear, and leave some warnings for those stuck with 2.x. Hopefully, the transition period won’t be long enough to lose sleep over it.

Compatible Swift Subset

Another trick to minimize the amount of #if is to use a subset of Swift that is compatible with both 2.x and 3.0. Below is a non-exhaustive list of features that you should avoid, or if you need them, must be guarded in #if and disappointment.

where

Swift 3.0 deprecated the if let ... where pattern. These must be broken up into multiple statements. Also, where must be removed from generic statements.

Argument annotations

Swift 3.0 shifted the argument annotations (such as inout) so that the annotation is applied to the type instead of the label. Unfortunately, there is no way to provide compatibility without #if here.

Access Control

Swift 3.0 changed the meaning of the private keyword from “private to the file” to “private to the enclosing declaration,” and added a fileprivate keyword. This means that, in code that requires compilation in Swift 2.x and 3.0, you can use only private to mean “private to the enclosing declaration.” In the context of maintaining a library, leaving things as internal will typically be sufficient, and neatly sidesteps the issue of worrying about private versus fileprivate.

Design for the Future

When a convention changes between Swift 2.x and 3.0, use the 3.0 convention. Enumerations, for instance, should have a lowercase initial letter. It breaks the Swift 2.x convention, but it’s only for the hopefully brief transition period.

What Else?

The above samples are not an exhaustive set of all of the Swift 2.x and 3.0 compatibility issues. Attempting to write a compatible branch for your library may cause more problems than it’s worth. With these approaches, I was able to isolate the majority of the compatibility code to one file, and in my case this was a very clear win. Hopefully, Swift 2.3 is a quick blip in the evolution of Swift, and we can drop support quickly. However, Swift 2.3 is fully supported for iOS 10.0, and there will be many disappointed Swift 2.3 users if you don’t support them somehow.