Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift: Unable to downcast AnyObject to SKPhysicsBody

Apple has the following method in the SKPhysicsBody class.

 /* Returns an array of all SKPhysicsBodies currently in contact with this one */
    func allContactedBodies() -> [AnyObject]!

I noticed it returns an array of AnyObject. So I read about how to deal with down casting AnyObject Here

I want to loop through the allContactedBodies array of my physics body. The problem is, no matter what I try I just can't get things to work.

I tried this first:

for body in self.physicsBody.allContactedBodies() as [SKPhysicsBody] {

}

But I get this error.

fatal error: array cannot be downcast to array of derived

I also tried this:

for object in self.physicsBody.allContactedBodies()  {
    let body = object as SKPhysicsBody
}

But this also crashes with the following:

enter image description here

And similarly I tried this:

 for object in self.physicsBody.allContactedBodies()  {
     let body = object as? SKPhysicsBody

 }

There is no crash, but "body" becomes nil.

And if I don't cast at all, I don't get a crash. For example:

for object in self.physicsBody.allContactedBodies()  {

}

But obviously I need to cast if I want to use the actual type.


So then as a test I just tried this:

let object: AnyObject = SKPhysicsBody()
let body = object as SKPhysicsBody

And this also results in the same crash that is in the picture.


But other types won't crash. For example, this won't crash.

let object: AnyObject = SKNode()
let node = object as SKNode

So my question is, how can I correctly loop through the allContactedBodies array?

Edit: I am running Xcode 6 beta 4 on iOS 8 beta 4 device.

Edit 2: More Information

Ok so I just did some more testing. I tried this:

let bodies = self.physicsBody.allContactedBodies() as? [SKPhysicsBody]

If "allContactedBodies" is empty, then the cast is successful. But if "allContactedBodies" contains objects, then the cast fails and "bodies" will become nil, so I can't loop through it. It seems that currently it is just NOT POSSIBLE to cast AnyObject to SKPhysicsBody, making it impossible to loop through the "allContactedBodies" array, unless someone can provide a workaround.

Edit 3: Bug still in Xcode 6 beta 5. Workaround posted below still works
Edit 4: Bug still in Xcode 6 beta 6. Workaround posted below still works
Edit 5: Disappointed. Bug still in Xcode 6 GM. Workaround posted below still works

EDIT 6: I have received the following message from Apple:

Engineering has provided the following information:

We believe this issue has been addressed in the latest Xcode 6.1 beta.

BUT IT IS NOT, the bug is still in Xcode 6.1.1!!! Workaround still works.

Edit 7: Xcode 6.3, still not fixed, workaround still works.

like image 938
Epic Byte Avatar asked Jul 28 '14 20:07

Epic Byte


2 Answers

After much trial and error, I have found a workaround to my problem. It turns out that you don't need to downcast at all to access the properties of the SKPhysicsBody, when the type is AnyObject.

for object in self.physicsBody.allContactedBodies()  {
        if object.node??.name == "surface" {
            isOnSurface = true
        }
    }
like image 114
Epic Byte Avatar answered Sep 25 '22 11:09

Epic Byte


Update: This was a bug, and it's fixed in iOS 9 / OS X 10.11. Code like the following should just work now:

for body in self.physicsBody.allContactedBodies()  {
    // inferred type body: SKPhysicsBody
    print(body.node) // call an API defined on SKPhysicsBody
}

Leaving original answer text for posterity / folks using older SDKs / etc.


I noticed this in the related questions sidebar while answering this one, and it turns out to be the same underlying issue. So, while Epic Byte has a workable workaround, here's the root of the problem, why the workaround works, and some more workarounds...

It's not that you can't cast AnyObject to SKPhysicsBody in general — it's that the thing(s) hiding behind these particular AnyObject references can't be cast to SKPhysicsBody.

The array returned by allContactedBodies() actually contains PKPhysicsBody objects, not SKPhysicsBody objects. PKPhysicsBody isn't public API — presumably, it's supposed to be an implementation detail that you don't see. In ObjC, it's totally cool to cast a PKPhysicsBody * to SKPhysicsBody *... it'll "just work" as long as you call only methods that the two classes happen to share. But in Swift, you can cast with as/as?/as! only up or down the type hierarchy, and PKPhysicsBody and SKPhysicsBody are not a parent class and subclass.

You get an error casting let obj: AnyObject = SKPhysicsBody(); obj as SKPhysicsBody because even the SKPhysicsBody initializer is returning a PKPhysicsBody. Most of the time you don't need to go through this dance (and have it fail), because you get a single SKPhysicsBody back from an initializer or method that claims to return an SKPhysicsBody — all the hand-wavy casting between SKPhysicsBody and PKPhysicsBody is happening on the ObjC side, and Swift trusts the imported ObjC API (and calls back to the original API through the ObjC runtime, so it works just as it would in ObjC despite the type mismatch).

But when you cast an entire array, a runtime typecast needs to happen on the Swift side, so Swift's stricter type-checking rules come into play... casting a PKPhysicsBody instance to SKPhysicsBody fails those rules, so you crash. You can cast an empty array to [SKPhysicsBody] without error because there aren't any objects of conflicting type in the array (there aren't any objects in the array).

Epic Byte's workaround works because Swift's AnyObject works like ObjC's id type: the compiler lets you call methods of any class on it, and you just hope that at runtime you're dealing with an object that actually implements those methods.

You can get back a little bit of compile-time type safety by explicitly forcing a side cast:

for object in self.physicsBody.allContactedBodies() {
    let body = unsafeBitCast(object, SKPhysicsBody.self)
}

After this, body is an SKPhysicsBody, so the compiler will let you call only SKPhysicsBody methods on it... this behaves like ObjC casting, so you're still left hoping that the methods you call are actually implemented by the object you're talking to. But at least the compiler can help keep you honest. (You can't unsafeBitCast an array type, so you have to do it to the element, inside the loop.)

This should probably be considered a bug, so please let Apple know if it's affecting you.

like image 45
rickster Avatar answered Sep 22 '22 11:09

rickster