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:

[view code in blog]

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:

[view code in blog]

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:

[view code in blog]

Alternatively, you could use the Cocoa container classes, such as NSDictionary:

[view code in blog]

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:

[view code in blog]

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.

[view code in blog] [view code in blog]

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:

[view code in blog]

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:

[view code in blog]

Alternatively, force-cast the value to the concrete object type you expect:

[view code in blog]

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:

[view code in blog]

With Swift 3, we can do away with the box, and attach the object directly to the notification:

[view code in blog]

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:

[view code in blog]

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:

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:

The net result is that Swift is a more consistent language, and Cocoa APIs become more powerful when used from Swift.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章