Oct 12, 2016
Objective-C id as Swift Any
Swift 3 interfaces with Objective-C APIs in a more powerful way than previous versions. For instance, Swift 2 mapped the id type in Objective-C to the AnyObject type in Swift, which normally can hold only values of class types. Swift 2 also provided implicit conversions to AnyObjectfor some bridged value types, such as String, Array, Dictionary, Set, and some numbers, as a convenience so that the native Swift types could be used easily with Cocoa APIs that expected NSString, NSArray, or the other container classes from Foundation. These conversions were inconsistent with the rest of the language, making it difficult to understand what exactly could be used as an AnyObject, resulting in bugs.
In Swift 3, the id type in Objective-C now maps to the Any type in Swift, which describes a value of any type, whether a class, enum, struct, or any other Swift type. This change makes Objective-C APIs more flexible in Swift, because Swift-defined value types can be passed to Objective-C APIs and extracted as Swift types, eliminating the need for manual “box” types. These benefits also extend to collections: Objective-C collection types NSArray, NSDictionary, and NSSet, which previously only accepted elements of AnyObject, now can hold elements of Any type. For hashed containers, such as Dictionary and Set, there’s a new type AnyHashable that can hold a value of any type conforming to the Swift Hashable protocol. 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> |
In many cases, your code will not have to change significantly in response to this change. Code that in Swift 2 relied on value types implicitly converting to AnyObject will continue to work as-is in Swift 3 by passing as Any. However, there are places where you will have to change the declared types of variables and methods and get the best experience in Swift 3. Also, if your code is explicitly using AnyObject or Cocoa classes such as NSString, NSArray, or NSDictionary, you will need to introduce more explicit casts using as NSString or as String, since the implicit conversions between the objects and value types are no longer allowed in Swift 3. The automatic migrator in Xcode will make minimal changes to keep your code compiling when moving from Swift 2 to 3, but the result may not always be the most elegant thing. This article will describe some of the changes you may need to make, as well as some pitfalls to look out for when changing your code to take advantage of id as Any.
Overriding methods and conforming to protocols
When subclassing an Objective-C class and overriding its methods, or conforming to an Objective-C protocol, the type signatures of methods need to be updated when the parent method uses id in Objective-C. Some common examples are the NSObject class’s isEqual: method and the NSCopying protocol’s copyWithZone: method. In Swift 2, you would write a subclass of NSObject conforming to NSCopying like this:
// Swift 2
class Foo: NSObject, NSCopying {
override func isEqual(_ x: AnyObject?) -> Bool { ... }
func copyWithZone(_ zone: NSZone?) -> AnyObject { ... }
}
In Swift 3, in addition to making the naming change from copyWithZone(_:) to copy(with:), you will also need to change the signatures of these methods to use Any instead of AnyObject:
// Swift 3
class Foo: NSObject, NSCopying {
override func isEqual(_ x: Any?) -> Bool { ... }
func copy(with zone: NSZone?) -> Any { ... }
}
Untyped Collections
Property lists, JSON, and user info dictionaries are common in Cocoa, and Cocoa natively represents these as untyped collections. In Swift 2, it was necessary to build Array, Dictionary, or Set with AnyObject or NSObject elements 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
}
}
let california = State(name: "California",
abbreviation: "CA",
population: 39_000_000)
NSNotification(name: "foo", object: nil,
userInfo: california.asPropertyList)
Alternatively, you could use the Cocoa container classes, such as NSDictionary:
// Swift 2
struct State {
var name: String
var abbreviation: String
var population: Int
var asPropertyList: NSDictionary {
var result = NSMutableDictionary()
// 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.copy()
}
}
let california = State(name: "California",
abbreviation: "CA",
population: 39_000_000)
// NSDictionary then implicitly converts to [NSObject: AnyObject] here.
NSNotification(name: "foo", object: nil,
userInfo: california.asPropertyList)
In Swift 3, the implicit conversions are gone, so neither of the above snippets will work as is. The migrator may suggest individually converting each value using as conversions to to keep this code working, but there’s a better solution. Swift now imports Cocoa APIs as accepting collections of Any and/or AnyHashable, so we can change the collection type to use [AnyHashable: Any] instead of [NSObject: AnyObject] or NSDictionary, without changing any other code:
// Swift 3
struct State {
var name: String
var abbreviation: String
var population: Int
// Change the dictionary type to [AnyHashable: Any] here...
var asPropertyList: [AnyHashable: Any] {
var result: [AnyHashable: Any] = [:]
// No implicit conversions necessary, since String and Int are subtypes
// of Any and AnyHashable
result["name"] = self.name
result["abbreviation"] = self.abbreviation
result["population"] = self.population
return result
}
}
let california = State(name: "California",
abbreviation: "CA",
population: 39_000_000)
// ...and you can still use it with Cocoa API here
Notification(name: "foo", object: nil,
userInfo: california.asPropertyList)
The AnyHashable Type
Swift’s Any type can hold any type, but Dictionary and Set require keys that are Hashable, so Any is too general. Starting with Swift 3, the Swift standard library provides a new type AnyHashable. Similar to Any, it acts as a supertype of all Hashable types, so values of String, Int, and other hashable types can be used implicitly as AnyHashable values, and the type inside an AnyHashable can be dynamically checked with the is, as!, or as? dynamic cast operators. AnyHashable is used when importing untyped NSDictionary or NSSet objects from Objective-C, but is also useful in pure Swift as a way of building heterogeneous sets or dictionaries.
Explicit Conversion for Unbridged Contexts
Under certain limited circumstances, Swift cannot automatically bridge C and Objective-C constructs. For example, some C and Cocoa APIs use id * pointers as “out” or “in-out” parameters, and since Swift is not able to statically determine how the pointer is used, it cannot perform the bridging conversions on the value in memory automatically. In cases like this, the pointer will still appear as an UnsafePointer<AnyObject>. If you need to work with one of these unbridged APIs, you can 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) // parameter imports as UnsafeMutablePointer<NSString>
let finishedString = string as String
var object = "string" as AnyObject
foo.updateObject(&object) // parameter imports as UnsafeMutablePointer<AnyObject>
let finishedObject = object as Any
return (finishedString, finishedObject)
}
Additionally, Objective-C protocols are still class-constrained in Swift, so you cannot make Swift structs or enums directly conform to Objective-C protocols or use them with lightweight generic classes. You will need to explicit convert String as NSString, Array as NSArray, etc. with these protocols and APIs.
AnyObject Member Lookup
Any does not have the same magic method lookup behavior as AnyObject. This may break some Swift 2 code that looked up a property or sent a message to an untyped Objective-C object. For example, this Swift 2 code:
// Swift 2
func foo(x: NSArray) {
// Invokes -description by magic AnyObject lookup
print(x[0].description)
}
will complain that description is not a member of Any in Swift 3. You can convert the value with x[0] as AnyObject to get the dynamic behavior back:
// 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, force-cast the value to the concrete object type you expect:
func foo(x: NSArray) {
// Cast to the concrete object type you expect
print((x[0] as! NSObject).description)
}
Swift Value Types in Objective-C
Any can hold any struct, enum, tuple, or other Swift type you can define in the language. The Objective-C bridge in Swift 3 can in turn present any Swift value as an id-compatible object to Objective-C. This makes it much easier to store custom Swift value types in Cocoa containers, userInfo dictionaries, and other objects. For example, in Swift 2, you would need to either change your data types into classes, or manually box them, to attach their values to an NSNotification:
// Swift 2
struct CreditCard { number: UInt64, expiration: NSDate }
let PaymentMade = "PaymentMade"
// We can't attach CreditCard directly to the notification, since it
// isn't a class, and doesn't bridge.
// Wrap it in a Box class.
class Box<T> {
let value: T
init(value: T) { self.value = value }
}
let paymentNotification =
NSNotification(name: PaymentMade,
object: Box(value: CreditCard(number: 1234_0000_0000_0000,
expiration: NSDate())))
With Swift 3, we can do away with the box, and attach the object directly to the notification:
// Swift 3
let PaymentMade = Notification.Name("PaymentMade")
// We can associate the CreditCard value directly with the Notification
let paymentNotification =
Notification(name: PaymentMade,
object: CreditCard(number: 1234_0000_0000_0000,
expiration: Date()))
In Objective-C, the CreditCard value will appear as an id-compatible, NSObject- conforming object that implements isEqual:, hash, and description using Swift’s Equatable, Hashable, and CustomStringConvertible implementations if they exist for the original Swift type. From Swift, the value can be retrieved by dynamically casting it back to its original type:
// Swift 3
let paymentCard = paymentNotification.object as! CreditCard
print(paymentCard.number) // 1234000000000000
Be aware that, in Swift 3.0, some common Swift and Objective-C struct types will bridge as opaque objects instead of as idiomatic Cocoa objects. For instance, whereas Int, UInt, Double, and Bool bridge to NSNumber, the other sized numeric types such as Int8, UInt16, etc. only bridge as opaque objects. Cocoa structs such as CGRect, CGPoint, and CGSize also bridge as opaque objects even though most Cocoa API that works with them as objects expects them boxed in NSValue instances. If you see errors like unrecognized selector sent to _SwiftValue, that indicates that Objective-C code is trying to invoke a method on an opaque Swift value type, and you may need to manually box that value in an instance of the class the Objective-C code expects.
One particular issue to look out for is Optionals. A Swift Any can hold anything, including an Optional, so it becomes possible to pass a wrapped Optional to an Objective-C API without checking it first, even if the API is declared as taking a nonnull id. This will generally manifest as a runtime error involving _SwiftValue rather than a compile-time error. Swift 3.0.1 included in Xcode 8.1 beta handles number types, Objective-C structs, and Optionals transparently by implementing these proposals that address the aforementioned limitations in NSNumber, NSValue, and Optional bridging:
- SE–0139: Bridge Numeric Types to NSNumber and Cocoa Structs to NSValue
- SE–0140: Warn when Optional converts to Any, and bridge Optional As Its Payload Or NSNull
To avoid forward compatibility problems, you should not rely on implementation details of opaque objects of the _SwiftValue class, since future versions of Swift may allow more Swift types to bridge to idiomatic Objective-C classes.
Linux Portability
Swift programs running on Linux with the Swift Core Libraries use a version of Foundation natively written in Swift, without an Objective-C runtime to bridge to. id-as-Any allows the Core Libraries to use the native Swift Any and standard library value types directly, while remaining compatible with code on Apple platforms using the Objective-C Foundation implementation. Since Swift does not interoperate with Objective-C on Linux, there is no support for bridging conversions such as string as NSString or value as AnyObject. Swift code that intends to be portable across Cocoa and the Swift Core Libraries should use the value types exclusively.
Learning More
id-as-Any is a great example of a Swift language improvement inspired by user feedback with earlier versions of Swift and refined by review from the open Swift Evolution process. If you want to learn more about the motivations and design decisions behind id-as-Any, the original Swift Evolution proposals are available on GitHub in the swift-evolution repository:
- SE-0072: Fully eliminate implicit bridging conversions from Swift
- SE–0116: Import Objective-C id as Swift Any type
- SE–0131: Add AnyHashable to the standard library
The net result is that Swift is a more consistent language, and Cocoa APIs become more powerful when used from Swift.