Skip to content

Instantly share code, notes, and snippets.

@jckarter
Last active August 22, 2016 18:05
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jckarter/5ffa9ba75050b94cd2cf9092b332a866 to your computer and use it in GitHub Desktop.
Save jckarter/5ffa9ba75050b94cd2cf9092b332a866 to your computer and use it in GitHub Desktop.
id-as-Any migration and known issues

id as Any

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, and Set no longer implicitly convert to the corresponding Cocoa class types NSString, NSArray, etc.
  • Instead, Objective-C APIs with the id type now use the Any type when used in Swift instead of AnyObject. 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 of Any. Since Dictionary and Set require keys that are Hashable, untyped NSDictionary and NSSet containers use keys of the new AnyHashable type from the Swift standard library, which can hold a value of any Hashable 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.

Overriding methods and conforming to protocols

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 { ... }
}

Untyped Collections

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
  }
}

Unbridged Contexts

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.

AnyObject Dynamic Lookup

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)
}

Known Limitations

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.

Literal Dictionarys and Sets with AnyHashable Keys

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.

Indexing NSDictionarys

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.

Literal NS Containers

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.

Transitive Coercions

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

Nested Hetergeneous Container Literals

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment