Background: We expect many changes in Objective-C APIs between Swift 3 and Swift 4, but our primary goal is to allow an app to upgrade to Swift 4 without having to update its dependencies. This necessarily means that a Swift 4 app needs to be able to use Swift 3 APIs, where “use” includes:
The situation gets even more complicated when
So let's start with the simplest possible changes, and work up to some of the more complicated ones. These are changes we expect Swift (and Objective-C) library authors to want to make in Swift 4 without breaking source compatibility. (Authors of Swift libraries also have another alternative: treat their Swift 4 update as a major and breaking release, in the Semantic Versioning sense, just as Swift 4 itself will be a major and breaking release.)
This document is written in the form of a guide for app authors and framework authors migrating to Swift 4, but its primary purpose is really to discover any compiler work we need to do to support this.
Note that this only applies to public and open APIs. Anything internal or private should not affect source compatibility.
This isn‘t usually recommended at all, but there is one good reason: you’re matching an Objective-C or standard library API whose name changed.
All of these changes should result in existing Swift 3 and Swift 4 clients continuing to work even when they fetch a new version of your library...barring a few awkward edge cases like a reference to a function by its base name becoming ambiguous when only argument names are changed.
It is recommended to mark any post-rename declarations as “introduced in Swift 4”, to minimize the chances of affecting overload resolution in Swift 3 code. It's considered acceptable for migrating from Swift 3 to Swift 4 to require minor fixes.
Unfortunately, there are a few declarations that cannot be renamed without breaking clients.
If a Swift 3 library has a class ‘Sub’ that overrides an Objective-C method using the method's original name, what should Swift 4 client code do when given an instance of ‘Sub’? There are two options:
Obviously the second one is preferable, as it‘s more forward-looking. For plain renames, it’d be possible to make either choice work with some amount of compiler hacking. However, when we get to changes in types (below), the choice becomes more clear.
Like renames, this usually isn't recommended, but once again the change may be to match a change in an Objective-C API.
There are a few cases where IUO will let you preserve source compatibility. Note, however, that this is very dangerous since existing clients won't expect a nil value in these locations:
And there are a few cases where there's just no good answer.
This change is pretty much not possible to do without breaking clients. Nearly every use of an optional value could end up in an if let
or optional chain, which is not valid to perform on non-optional values. There's really only one safe place to do this: parameters where nil
was never a valid value anyway:
Any other changes are likely to break clients, even more so if overrides are a possibility.
Unfortunately, the remaining changes are clearly unsafe, but may still happen because of APIs that were mis-annotated.
If you own an Objective-C framework that needs to make these changes and you want to preserve Swift 3 compatibility, you can use the “API notes” mechanism to lie and mark the API as non-nullable when compiling in Swift 3 mode. (FIXME: link to a document about API notes.) If you're a client of such a framework, you may be able to minimize the impact of some of these changes by following the steps for something being made *non-*optional.
(FIXME: to be written)
Like with the Swift 3 API case, this is pretty much guaranteed to break clients in non-parameter cases. The workaround for a client is to add as Optional
to their use of the value.
// FIXME: example
When a parameter has been made non-optional, there is no way to continue passing nil
to it save by using a helper method or function written in Objective-C. Again, this is only a client-side workaround, and the client should probably just not pass nil
to begin with.
It is generally allowed to override a method that takes a non-optional value with one that takes an optional value, so that particular change is no problem. In the case where it is a property type or a method result type being made non-optional, the change is inherently source-breaking, and the client must be updated to change its return type and to make sure it does not return nil.
FIXME: Most specifically, Objective-C pointers to pointers.
NS_STRING_ENUM
FIXME: Hm.
FIXME: Ugh.
FIXME: Can we expose both versions? Maybe?
FIXME: No.
Changing a designated initializer to a convenience initializer is almost guaranteed to break subclasses, since it's entirely possible that a subclass is overriding this initializer, or using super.init
to chain to it.
In Objective-C, such code may have always been incorrect if the initializer really was a convenience initializer---that is, if it delegated to another initializer on self
---which may result in double-initialization of properties of the subclass (possibly resulting in leaks). However, it is also possible that some subclasses would have worked correctly, and it is important for those to keep building. The best option is to update your headers, but reverse the change in Swift 3 mode using the “API Notes” mechanism.
Changing a convenience initializer to a designated initializer can also break subclasses that were relying on convenience initializers being inherited. In Swift, convenience initializers are only inherited if all designated initializers are overridden, or if no designated initializers are overridden and all new instance properties have initial values.
Once again, Objective-C clients should update their headers to match the implementations, but reverse the change in Swift 3 mode using the “API Notes” mechanism.
FIXME: We could make sure overloading properties and methods actually works, and then we can present both.
@escaping
This is pretty much always a breaking change. In addition to the trivial case of forcing clients to write self.
on property accesses within the closure, it may also ruin their assumptions about their object graph.
If you‘re doing this just to deal with an API that takes an escaping closure, use the withoutActuallyEscaping
helper instead, and make sure that the API you’re using won't hold on to the closure by the time your function exits.
@escaping
(or, for Objective-C, “an API adds the noescape
attribute”)
For an API that is not overridden, this is a source-compatible change---any existing calls will still work. Keep in mind that the reverse is not allowed, though, and plan for the future.
FIXME: Seems problematic. Do we allow overriding an escaping parameter with a non-escaping one? That would help a lot. Otherwise we may need to make these insert an implicit “withoutActuallyEscaping”.
An Objective-C enum may be newly flagged as an error enum (e.g. by the “NSErrorDomain” annotation in API notes). This change is nearly source-compatible anyway; although the name now refers to a struct wrapping an NSError instance instead of a raw error code, all of the enum constants are still available as members, and the domain string will still be exposed. This means that nearly all code working directly with NSError and error codes in Swift 3 will continue to work in Swift 4; it's only if someone is explicitly using the type itself that there may be problems.
(Which can happen, but is usually rare. Nevertheless, this Objective-C-side change is source-breaking, and should be reversed when compiling under Swift 3 compatibility mode using API notes.)
FIXME: Are there any other major categories of changes that don‘t manifest as type changes? At the very least there are a few things supported by API notes (or at least considered) that don’t fall into those buckets.