The Strange Behaviour of Swift's Anyobject

The strange behaviour of Swift's AnyObject

Similar as in Objective-C, where you can send arbitrary messages to id,
arbitrary properties and methods can be called on an instance of AnyObject
in Swift. The details are different however, and it is documented in
Interacting with Objective-C APIs
in the "Using Swift with Cocoa and Objective-C" book.

Swift includes an AnyObject type that represents some kind of object. This is similar to Objective-C’s id type. Swift imports id as AnyObject, which allows you to write type-safe Swift code while maintaining the flexibility of an untyped object.

...

You can call any Objective-C method and access any property on an AnyObject value without casting to a more specific class type. This includes Objective-C compatible methods and properties marked with the @objc attribute.

...

When you call a method on a value of AnyObject type, that method call behaves like an implicitly unwrapped optional. You can use the same optional chaining syntax you would use for optional methods in protocols to optionally invoke a method on AnyObject.

Here is an example:

func tryToGetTimeInterval(obj : AnyObject) {
let ti = obj.timeIntervalSinceReferenceDate // NSTimeInterval!
if let theTi = ti {
print(theTi)
} else {
print("does not respond to `timeIntervalSinceReferenceDate`")
}
}

tryToGetTimeInterval(NSDate(timeIntervalSinceReferenceDate: 1234))
// 1234.0

tryToGetTimeInterval(NSString(string: "abc"))
// does not respond to `timeIntervalSinceReferenceDate`

obj.timeIntervalSinceReferenceDate is an implicitly unwrapped optional
and nil if the object does not have that property.

Here an example for checking and calling a method:

func tryToGetFirstCharacter(obj : AnyObject) {
let fc = obj.characterAtIndex // ((Int) -> unichar)!
if let theFc = fc {
print(theFc(0))
} else {
print("does not respond to `characterAtIndex`")
}
}

tryToGetFirstCharacter(NSDate(timeIntervalSinceReferenceDate: 1234))
// does not respond to `characterAtIndex`

tryToGetFirstCharacter(NSString(string: "abc"))
// 97

obj.characterAtIndex is an implicitly unwrapped optional closure. That code
can be simplified using optional chaining:

func tryToGetFirstCharacter(obj : AnyObject) {
if let c = obj.characterAtIndex?(0) {
print(c)
} else {
print("does not respond to `characterAtIndex`")
}
}

In your case, TestClass does not have any @objc properties.

let xyz = typeAnyObject.xyz // error: value of type 'AnyObject' has no member 'xyz'

does not compile because the xyz property is unknown to the compiler.

let name = typeAnyObject.name // String!

does compile because – as you noticed – NSException has a name property.
The value however is nil because TestClass does not have an
Objective-C compatible name method. As above, you should use optional
binding to safely unwrap the value (or test against nil).

If your class is derived from NSObject

class TestClass : NSObject {
var name : String?
var xyz : String?
}

then

let xyz = typeAnyObject.xyz // String?!

does compile. (Alternatively, mark the class or the properties with @objc.)
But now

let name = typeAnyObject.name // error: Ambigous use of `name`

does not compile anymore. The reason is that both TestClass and NSException
have a name property, but with different types (String? vs String),
so the type of that expression is ambiguous. This ambiguity can only be
resolved by (optionally) casting the AnyObject back to TestClass:

if let name = (typeAnyObject as? TestClass)?.name {
print(name)
}

Conclusion:

  • You can call any method/property on an instance of AnyObject if that
    method/property is Objective-C compatible.
  • You have to test the implicitly unwrapped optional against nil or
    use optional binding to check that the instance actually has that
    method/property.
  • Ambiguities arise if more than one class has (Objective-C) compatible
    methods with the same name but different types.

In particular because of the last point, I would try to avoid this
mechanism if possible, and optionally cast to a known class instead
(as in the last example).

Strange Any? as AnyObject behaviour

a as AnyObject will cast a to NSNull so that b is not nil

You can check it with type(of:)

let a: Any? = nil
let b: AnyObject? = a as AnyObject

if let c: AnyObject = b {
print(c)
print(type(of: c)) // will print "NSNull"
print("That's not right, is it?")
} else {
print("I'd expect this to be printed")
}

Strange behaviour of optionals in Swift 3

You can do something like:

do {
let json = try JSONSerialization.jsonObject(with: data!) as! [String: Any]
let items = json["items"] as! [[String: Any]]
for item in items {
let id = item["id"] as! String
let info = item["volumeInfo"] as! [String: Any]
let title = info["title"] as! String

print(id)
print(info)
print(title)
}
} catch {
print("error thrown: \(error)")
}

I might suggest excising the code of the ! forced unwrapping (if the JSON was not in the form you expected, do you really want this to crash?), but hopefully this illustrates the basic idea.

Optional binding bug on Swift 2.2?

If you look at currentTitle, you'll see it is likely inferred to be String??. For example, go to currentTitle in Xcode and hit the esc key to see code completion options, and you'll see what type it thinks it is:

Sample Image

I suspect you have this in a method defining sender as AnyObject, such as:

@IBAction func didTapButton(sender: AnyObject) {
if let mathematicalSymbol = sender.currentTitle {
brain.performOperation(mathematicalSymbol)
}
}

But if you explicitly tell it what type sender is, you can avoid this error, namely either:

@IBAction func didTapButton(sender: UIButton) {
if let mathematicalSymbol = sender.currentTitle {
brain.performOperation(mathematicalSymbol)
}
}

Or

@IBAction func didTapButton(sender: AnyObject) {
if let button = sender as? UIButton, let mathematicalSymbol = button.currentTitle {
brain.performOperation(mathematicalSymbol)
}
}

Swift trouble with optionals

There are optionals on three levels:

  • The sender: AnyObject? parameter is an optional.
  • Sending arbitrary messages to AnyObject behaves similar to optional
    chaining and returns an implicitly unwrapped optional
    (compare The strange behaviour of Swift's AnyObject).
  • The var urlString: String? property is an optional.

To resolve the first two optionals, use optional binding/casting to the
concrete button class:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "segueToWebView") {
if let button = sender as? TestButton {
// The type of `button` is `TestButton`
// ...
}
}
}

Now

var destUrl: String
if button.urlString == nil {
destUrl = "http://www.google.com"
} else {
destUrl = button.urlString!
}

would compile, but that is better handled with the
nil-coalescing operator:

let destUrl = button.urlString ?? "http://www.google.com"

Note that there is no forced unwrapping ! operator anymore!

element as AnyObject vs element vs AnyObject?

Maybe the snippet is from some Swift 2 code, in Swift 3+ it's

private static func keychainQuery(withService service: String, account: String? = nil, accessGroup: String? = nil) -> [String : Any] {
var query = [String : Any]()
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrService as String] = service

if let account = account {
query[kSecAttrAccount as String] = account
}

if let accessGroup = accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
}

return query
}

Very unusual Xcode compile behaviour

It must be a bug around optimizations in Swift compiler. I think, it's around bridging NSDictionary to Dictionary<String,AnyObject>.

I reproduced the problem with following setup.


Environment: Xcode 6.1 (6A1052d) / iPhone 6 / iOS 8.1

Template: Single View Application

CategoriesSettings.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ct1</key>
<string>test</string>
</dict>
</plist>

AppDelegate.swift

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let result = loadPlist()
println("result: \(result)")
return true
}

func loadPlist() -> [String: AnyObject]? {
let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
let dict = NSDictionary(contentsOfURL: categoriesURL!)
println(dict)
let result = dict as? [String:AnyObject]
return result
}
}

// EOF

outputs (with -O):

Optional({
ct1 = test;
})
result: nil

outputs (with -Onone):

Optional({
ct1 = test;
})
result: Optional(["ct1": test])

I don't know the best workaround, though.

Maybe this works:

class CategoryParser {
var categoriesSettingsDictionary : [String: AnyObject]?
init() {
let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
categoriesSettingsDictionary = NSDictionary(contentsOfURL: categoriesURL!) as? Dictionary<String, AnyObject>
if categoriesSettingsDictionary == nil {
// NOTICE: to other developers about this workaround
println("_")
println("_")
}
}
}

Encapsulating them in autoreleasepool also works:

class CategoryParser {
var categoriesSettingsDictionary : [String: AnyObject]?

init() {
autoreleasepool {
let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
self.categoriesSettingsDictionary = NSDictionary(contentsOfURL: categoriesURL!) as? Dictionary<String, AnyObject>
}
}
}

But, as of now, I think, you should use NSDictionary as is, because as long as you only read from it, there is almost no practical difference between NSDictionary and Dictionary<String,AnyObject> in most cases.

class CategoryParser {
var categoriesSettingsDictionary : NSDictionary?
init() {
let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
categoriesSettingsDictionary = NSDictionary(contentsOfURL: categoriesURL!)
}
}

OR, this may be too aggressive, but you can implement your own NSDictionaryDictionary converter.

extension Dictionary {
init?(nsDictionaryOrNil:NSDictionary?) {
if let dict = nsDictionaryOrNil? {
self = [Key:Value](minimumCapacity: dict.count)
for (k,v) in dict {
if let key = k as? Key {
if let val = v as? Value {
self[key] = val
continue
}
}
return nil
}
}
else {
return nil
}
}
}

class CategoryParser {
var categoriesSettingsDictionary : [String:AnyObject]?
init() {
let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
let dict = NSDictionary(contentsOfURL: categoriesURL!)
categoriesSettingsDictionary = [String:AnyObject](nsDictionaryOrNil: dict)
}
}


Related Topics



Leave a reply



Submit