7

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.

Epic Byte
  • 33,840
  • 12
  • 45
  • 93
  • I think it is a known issue that SpriteKit does not work well in XCode6/ios http://stackoverflow.com/questions/24069168/swift-and-spritekit-wont-run-on-device-running-ios-7-1 http://stackoverflow.com/questions/24222174/spritekit-xcode-6-included-sample-project-crashes-on-ios-7-1-1-device-exc-bad-a – Anthony Kong Jul 28 '14 at 21:19
  • True, although the links you provided are for iOS 7. The real issue seems that it just isn't possible to cast AnyObject to SKPhysicsBody. Unless maybe there is a workaround. – Epic Byte Jul 28 '14 at 21:34
  • try let body = object! as SKPhysicsBody – nsinvocation Jul 29 '14 at 14:32
  • That doesn't work because object is not an optional type, so you can't unwrap it. The array itself is an implicitly unwrapped optional type, but the AnyObject elements in the array are not declared with an optional type. – Epic Byte Jul 29 '14 at 14:41

2 Answers2

7

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
        }
    }
rickster
  • 124,678
  • 26
  • 272
  • 326
Epic Byte
  • 33,840
  • 12
  • 45
  • 93
  • 2
    Thanks for posting the update. Let's hope the downcasting gets fixed before Swift 1.0 and iOS8 come out! Even though the workaround works, it goes completely against the whole type-safety mentality of Swift... – Thiago Campezzi Aug 06 '14 at 05:25
  • What exactly does the double question mark in the line `object.node??.name` do? I'm not sure I understand what is going on here. I know it isn't the nil coalescing operator. – mogelbuster Jun 16 '15 at 14:41
  • 1
    @mogelbuster Good question. This is actually 2 levels of optional chaining in a row. Trying to access a property on AnyObject will always return an optional because it could fail. And the property we are actually looking for (the name) is an optional itself! So it's really an optional inside an optional. First we check if the name property is actually found on this object. Then the next check is our normal check for nil to see if the name is nil or not. – Epic Byte Jun 16 '15 at 15:22
  • **Xcode 7 Update:** This solution no longer works. The return type for `allContactedBodies()` has been changed to `[SKPhysicsBody]` but the underlying type that you get is still PKPhysicsBody. Now the error du jour is `NSArray element failed to match the Swift Array Element type` You actually have to cast the result of `allContactedBodies()` back to [AnyObject] then do a side-cast using `unsafeBitCast` inside the iteration loop. You might be able to still use your workaround, but I haven't been able to get it to work. [ANSWER HERE](https://forums.developer.apple.com/message/50325#50325) – mogelbuster Oct 05 '15 at 18:16
  • @mogelbuster: I don't see that happening in Xcode 7 / iOS 9 / OS X 10.11... things like `for body in physicsBody.allContactedBodies() { print(body.node) }` work just fine, as does the [Apple sample code](https://developer.apple.com/library/ios/samplecode/DemoBots/Introduction/Intro.html) that uses such patterns. – rickster Jan 20 '16 at 20:52
  • @rickster: My Xcode 7 Update comment above was for Xcode 7.1. It looks like the current version is Xcode 7.2. I haven't had a chance to test it, but if you are right that is awesome! It was certainly a problem in Xcode 7.1 though! Thank goodness this has been resolved, and thanks for the heads up! – mogelbuster Jan 22 '16 at 23:05
5

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.

Community
  • 1
  • 1
rickster
  • 124,678
  • 26
  • 272
  • 326
  • Could you please take a look at [this question](http://stackoverflow.com/questions/34894762/swift-convenience-initializer-extension-for-skphysicsbody)? I think the OP could benefit from your deep understanding of `SKPhysicsBody` and `PKPhysicsBody`. – Epsilon Jan 20 '16 at 17:48