Together with iOS 10, watchOS 3 and Xcode 8 Swift 3 made its way into the public. Of course there were betas of Xcode (and with it Swift 3) before the public release. And of course I was playing around with them, too.
While more and more projects are converted to Swift 3, I hear lots of complains about how tedious work it is and how long it takes. First of all: I won't say anything different: Depending on the amount of LOC, it's not an easy task to properly convert the code base from Swift 2.2 / 2.3 to Swift 3.
Last week I've been converting a codebase of around 10 KLOC to Swift 3 and it took me about 3 days. Of course I did a little bit more than just running the migrator and cleaning up after it. A proper migration includes renaming some methods to conform to the new coding guidelines.
The migrator
To be honest, the migrator is only really helpful for the obvious things (renaming simple enums to lowercase, adapting to the new structs in foundation, converting GCD calls). And even there it fails, if it can't know the type which happens if for example the call which assigns the variable / constant could not be compiled. Take the following example (after the migrator is done with it):
enum Style { case normal, bold, italic }
let myStyle = inferStyleFromHTML(someHTML)
switch myStyle {
case .Normal: print("Normal style")
case .Bold: print("Normal style")
case .Italic: print("Normal style")
}
You'll notice, that the enum cases in the enum definition have been properly renamed. Yet, in the switch they weren't. Why? Because the Method inferStyleFromHTML
was renamed to inferStyle(fromHTML:)
. This change, however, wasn't properly detected (although the new method name is only logic and not that far away). Now, for the compiler myStyle
is an <<error type>>
, because it couldn't infer a type (of course not, inferStyleFromHTML
doesn't exist anymore). And due to that, all code dealing with myStyle
won't be properly converted. If you're lucky, you'll get fix-it's after changing the method call to inferStyle(fromHTML: someHTML)
. For small enums you usually do. For larger ones (making the switch
larger as well), it sometimes just only adds a fix-it for the first few cases, while throwing simple errors for the rest.
Another quirk of the migrator is, that it converts all uses of private
to fileprivate
, and (almost) all uses of public
to open
. The explanation is simple: By doing so, the code should still work in Swift 3.
However, it ignores the fact, that sometimes private
is even fine in Swift 3. Or that open
is not really necessary (which is almost impossible to know, though). Another example here:
struct TimeStamp {
private let intervalSinceRefDate: NSTimeInterval
var date: NSDate {
return NSDate(timeIntervalSinceReferenceDate: intervalSinceRefDate)
}
init(date: NSDate) {
intervalSinceRefDate = date.timeIntervalSinceReferenceDate
}
init() { self.init(date: NSDate()) }
}
The migrator will likely convert private let intervalSinceRefDate: NSTimeInterval
to fileprivate let intervalSinceRefDate: NSTimeInterval
. While it's impossible to detect subclasses of a previously public class
(which will be turned into an open class
), it would be perfectly detectable if a private var/let
is used outside it's private
scope and hence should become fileprivate
in Swift 3. Actually, I've reverted all fileprivate
changes to private
and the compiler later told me, that a certain property / method is inaccessible due to private
protection level.
Another thing it does, is converting e.g. NSIndexPath
to IndexPath
(which is correct), but then adding as NSIndexPath
wherever section
, row
or item
of such an IndexPath
object is used, although IndexPath
also has these convenience accessors.
But there are also good parts of the migrator:
I was really impressed how it converted the "old" GCD and CGContext code to the new style. And that's sometimes quite a complex change.
Long story short, the migrator is only helpful for very simple stuff. And it breaks easily. The last thing you'd want, is to just let it automatically convert the codebase, though. You could miss some pretty bad choices it took. The best thing is to first go through every file it changed and review the changes (correcting / reverting them if necessary). That's only the first part of the conversion process, though. The hard part's about to follow.
The afterwork
Now, after the migrator's done, you need to fix all the errors which the migrator couldn't fix. And to do the job properly, all existing methods should be revisited and checked, if the name still matches the new coding guidelines.
Usually I did the latter while going through the migrator changes. If I came across a method with an "old name", I directly changed it to what I thought matches the Swift 3 style more closely.
When building the app later, errors would pop up for all renamed methods, since the old one can't be found anymore.
The easiest way to fix this is to make the compiler generate fix-its.
There's an interesting behaviour about fix-its. As soon as the base name of a method matches the new one (leaving away all the new argument labels), you'll usually get a fix-it for the argument labels. There's one exception to that rule: If there are multiple versions of a method which have the same base name but different argument labels (in the "worst case" even with the same amount of arguments), then it might fail to generate a fix it.
Once there are a bunch of fix-it's in a file, there's this neat feature "Fix All in Scope" (Shortcut: ^⌥⌘F
). However, make sure the fix-it's are really fix-it's and not "break-it's". Sometimes it'll suggest nonsense.
Is Swift 3 bad?
Once you're done with all the renaming and your project builds again, you've finally made it. And unless you somehow distinctively count the errors, you've had and fixed, you'll never know how many there were exactly. Because the counts Xcode gives you, are only what it found during a build. And it usually never finds all of them in one run. But believe me, you've fixed enough of them.
So what does that mean for Swift 3? Is it a bad change? Is it too much?
My answers straight and clear: No!
Yes, it's hard to convert. Yes, Xcode is not of that much help.
But that's the tools, not the language. And we all knew that Swift is not yet ABI stable.
After all, I'm really happy with Swift 3. I'm happily spending days for converting a few KLOCs if it means that the language doesn't take any drawbacks just to make the conversion easier. There's a whole lot of stuff that became really cool in Swift 3. I'm speaking of all the new value types, the new GCD APIs (or generally the C APIs). Not to forget all the bugs that have been fix (introducing new ones, of course, but that's ok).
For example I really understood why they changed the enum cases to lowercase. While converting the codebase, I was often thinking: Is this now a enum case or a static property? Or is it a nested type?
Given the following enum in Swift 2:
struct ErrorHandling {
enum FailureReason {
case Unknown
case NetworkFailure(Error)
case ParsingFailure(Error)
var error: Error? {
switch self {
case .NetworkFailure(let error): return error
case .ParsingFailure(let error): return error
case .Unknown(let error): return error
}
}
}
}
If you now see the following line in some completely different file, you wouldn't know if Unknown
is a nested type and error
static property or if Unknown
is an enum case (or static property) and error
an instance property:
let error = ErrorHandling.FailureReason.Unknown.error
In Swift 3 this becomes
let error = ErrorHandling.FailureReason.unknown.error
which is much clearer since you can tell the types and property apart.
Also I really like the shorter method names achieved by removing repetition in naming (e.g. objectAtIndexPath(_:)
-> object(at:)
).
Final words
Swift 3 isn't ABI stable, either. So there's a fair chance, that we're going to convert all of our Swift codebases again for new minor versions of Swift 3 and again for Swift 4. And I'm happily doing it as long as I see that Swift is really pushing for becoming a great language instead of just being easy to migrate.