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’sid
type. Swift importsid
asAnyObject
, 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 ofAnyObject
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 onAnyObject
.
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:
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 NSDictionary
→ Dictionary
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
Swift3 Optionals Chaining in If Conditions Bug
Function with Datatask Returning a Value
How to Create Custom Notifications in Swift 3
Using as a Concrete Type Conforming to Protocol Anyobject Is Not Supported
One-Line Closure Without Return Type
How to Change Back Button Title on Navigation Controller in Swift3
Firebase Query Ordering Not Working Properly
What Is '@_Silgen_Name' in Swift Language
Retrieve Only 5 Users At a Time :Firebase [Like Instagram]
Iboutlet and Ibaction in Swift
How to Create Swift Class for Category
Dispatchsourcetimer and Swift 3.0
Convert Avaudiopcmbuffer to Nsdata and Back
Stop a Dispatchqueue That Is Running on the Main Thread
Swift - Could Not Cast Value of Type '_Nscfstring' to 'Nsdictionary'