Swift 3 preview 6 implements the language changes from proposal
SE-0116, which change the way Objective-C APIs that use id
are expressed
in Swift to make Swift value types interoperate more naturally with Cocoa
frameworks. The proposal enacts the following changes:
- Swift value types, such as
String
,Array
,Dictionary
, andSet
no longer implicitly convert to the corresponding Cocoa class typesNSString
,NSArray
, etc. - Instead, Objective-C APIs with the
id
type now use theAny
type when used in Swift instead ofAnyObject
. This allows Swift value types to be used seamlessly with these APIs without error-prone implicit conversion behavior affecting the entire language. - Similarly, Objective-C APIs that use untyped
NSArray
containers appear in Swift as collections ofAny
. SinceDictionary
andSet
require keys that areHashable
, untypedNSDictionary
andNSSet
containers use keys of the newAnyHashable
type from the Swift standard library, which can hold a value of anyHashable
type.
In summary, the following type mappings change from Swift 2 to Swift 3:
Objective-C | Swift 2 | Swift 3 |
---|---|---|
id |
AnyObject |
Any |
NSArray * |
[AnyObject] |
[Any] |
NSDictionary * |
[NSObject: AnyObject] |
[AnyHashable: Any] |
NSSet * |
Set<NSObject> |
Set<AnyHashable> |
The collection conversions for Array
, Dictionary
, and Set
have also been generalized to work with Swift protocol types and Any
, so a [String]
array can be implicitly converted to an [Any]
array, for example.
When passing Swift value types to untyped Objective-C APIs, in many cases no change is necessary at all, and things will work as they did in Swift 2. However, there are some places where code changes are necessary.
The type signatures of class methods that override methods from a base class or conform to Objective-C protocols need to be updated when the parent method uses id
in Objective-C. The NSObject
class's isEqual(_:)
method and the NSCopying
protocol's copy(with:)
method are common examples of this:
// Swift 2
class Foo: NSObject, NSCopying {
override func isEqual(_ x: AnyObject?) -> Bool { ... }
func copyWithZone(_ zone: NSZone?) -> AnyObject { ... }
}
In Swift 3, change the signatures to use Any
instead of AnyObject
:
// Swift 3
class Foo: NSObject, NSCopying {
override func isEqual(_ x: Any?) -> Bool { ... }
func copy(with zone: NSZone?) -> Any { ... }
}
Property lists, JSON, and user info dictionaries are common in Cocoa. In Swift 2, it was necessary to build collections of AnyObject
and/or NSObject
for this purpose, relying on implicit bridging conversions to handle value types:
// Swift 2
struct State {
var name: String
var abbreviation: String
var population: Int
var asPropertyList: [NSObject: AnyObject] {
var result: [NSObject: AnyObject] = [:]
// Implicit conversions turn String into NSString here…
result["name"] = self.name
result["abbreviation"] = self.abbreviation
// …and Int into NSNumber here.
result["population"] = self.population
return result
}
}
In Swift 3, the implicit conversions are gone, so this code will no longer work as-is. However, Cocoa APIs now accept collections of Any
and/or AnyHashable
. We can change the collection types of our APIs to use Any
and AnyHashable
, allowing the natural subtyping relationship to collect heterogeneous members:
// Swift 3
struct State {
var name: String
var abbreviation: String
var population: Int
var asPropertyList: [AnyHashable: Any] {
var result: [AnyHashable: Any] = [:]
// No implicit conversions necessary, since String and Int are subtypes
// of Any
result["name"] = self.name
result["abbreviation"] = self.abbreviation
result["population"] = self.population
return result
}
}
Under certain limited circumstances, Swift cannot automatically bridge C and Objective-C constructs. For example, some C and Cocoa APIs use pointers as "out" or "in-out" parameters. In such cases, use explicit bridging conversions, written explicitly using as Type
or as AnyObject
in your code.
// ObjC
@interface Foo
- (void)updateString:(NSString **)string;
- (void)updateObject:(id *)obj;
@end
// Swift
func interactWith(foo: Foo) -> (String, Any) {
var string = "string" as NSString // explicit conversion
foo.updateString(&string)
let finishedString = string as String
var object = "string" as AnyObject
foo.updateObject(&object)
let finishedObject = object as Any
return (finishedString, finishedObject)
}
Objective-C generics and protocols are still imported into Swift as class-constrained, so manual bridging to objects may also be necessary when working with generic Objective-C APIs.
Any
does not have the same magic method lookup behavior as AnyObject
.
To get it back, explicitly coerce the Any
back to an object with as AnyObject
, or do a checked cast to the concrete type whose method you're
attempting to invoke:
// Swift 2
func foo(x: NSArray) {
// Invokes -description by magic AnyObject lookup
print(x[0].description)
}
// Swift 3
func foo(x: NSArray) {
// Result of subscript is now Any, needs to be coerced to get method lookup
print((x[0] as AnyObject).description)
// Alternatively, cast to the concrete object type you expect
print((x[0] as! NSObject).description)
}
Some preview versions of the Swift 3 compiler there are some limitations to id
-as-Any
that lead to more explicit conversions being necessary than desirable. The issues that have been addressed are linked to the patches.
Dictionary
and Set
provide special overloads of their indexing and membership operations when they contain AnyHashable
keys so that they work with any type that conforms to Hashable
. This support did not yet extend to literal syntax, so keys have to be individually wrapped in AnyHashable
. This is fixed by
apple/swift#4022, which makes all Hashable
types
subtypes of AnyHashable
.
NSDictionary
's subscript operator was still imported as taking NSCopying
, so explicit conversions to object were necessary when indexing. This is fixed
by apple/swift#3945.
NSArray
, NSDictionary
, and NSSet
conformed to ExpressibleBy{Array,Dictionary}Literal
with classes as their associated element types, necessitating explicit bridging of elements in literals of those types. This is fixed by
apple/swift#3945.
Due to compiler bugs, some explicit conversions with as
don't always successfully apply transitively, making it necessary to do two conversions when going from a string to a type such as Notification.Name
.
"foo" as NSNotification.Name // should work, but doesn't
"foo" as NSString as NSNotification.Name // workaround
This also occasionally comes up with literals that require explicit coercion:
["foo": "bar"] as NSDictionary // should work, but doesn't
["foo": "bar"] as Dictionary as NSDictionary // workaround
The type checker sometimes failed to infer the element type of nested heterogeneous collections, necessitating explicit as
annotations on the inner literals:
let x: [AnyHashable: Any] = [
"foo": 1,
"bar": "two",
"bas": [
1,
"two",
"three"
] as [Any] // shouldn't be necessary, but is
]
This is fixed by apple/swift#4027.