Swift Convenience Initializer Extension for Skphysicsbody

Swift convenience initializer extension for SKPhysicsBody

As @Cristik guessed in the comments, this is the same root problem as this question and this question: the public SKPhysicsBody class is a convenience interface for the private PKPhysicsBody class that provides its implementation.

In the past, this approach relied heavily on the "duck typing" behavior of Objective-C — as long as ClassA responds to all the same selectors as ClassB, you can call any of those selectors on a pointer whose static type (the type declared to the compiler in source code) is ClassB, even if the actual object at run time is really an instance of ClassA.

Swift is stricter about runtime type correctness than ObjC, so "duck typing" alone is not enough. Since iOS 9 / OS X 10.11, SpriteKit has some workarounds that allow PKPhysicsBody instances to better pretend to be SKPhysicsBody instances.

But those don't cover all cases — in particular, it doesn't catch (ObjC) [SKPhysicsBody alloc] returning a PKPhysicsBody instance, which means any attempt to add initializers to SKPhysicsBody in Swift will fail. (Because the ObjC alloc/init process is reduced to one initializer call in Swift.)

I'd consider this a bug and recommend filing it with Apple.


Edit/update: And until that bug gets fixed (it's been a year and some now), the workaround is to make your convenience "initializer" into a class method instead. (Or a global function if you must, but... ewww.)

Creating a custom initalizer for SKScene that overrides convenience init?(fileNamed:)

Couldn't it be as simple as this?

if let gameScene = GameScene(fileNamed: "GameScene") {

self.gameScene = gameScene
self.gameScene.stage = 1
self.gameScene.setupBasedOnStage()
self.gameScene.scaleMode = .aspectFill
self.gameScene.gameSceneDelegate = self.menuSceneDelegate as! GameSceneDelegate!
self.view?.presentScene(self.gameScene, transition: SKTransition.reveal(with: .down, duration: 1.0))
}

You are able to set the stage property before revealing the page, and if you needed to you can call a setup function to load info/graphics based on the stage.

I know it's not as elegant as what you are trying to do, but maybe sometimes the easiest answer is the best?

Swift: Unable to downcast AnyObject to SKPhysicsBody

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

Circling the drain of Initialization in subclass: Swift and SpriteKit

And am instantly greeted with the lovingly cryptic:

Can not use instance member 'ringSize' within property initializer, property initializers run before 'self' is available.

So one way around this problem would be to make the default ringSize available another way, e.g.

static let defaultRingSize: CGFloat = 64

var ringSize: CGFloat = Circle.defaultRingSize
let ring = SKShapeNode(circleOfRadius: Circle.defaultRingSize)

... but I question why you even have a var ringSize property like that. Shouldn't you have a didSet observer on it, so that if you change its value, you can update the shape of ring?

Dead again:

! Expected declaration

You weren't clear, in your question, how you actually triggered this, but I guess you tried something like this:

class Circle: SKSpriteNode {

var ringSize: CGFloat = 96

var myRing = SKShapeNode()
myRing = createRing() // “Expected declaration” error on this line

The problem here is that you've placed a statement in the body of your class, but only declarations are allowed in the body.

One of them is heading into my already messy convenience initializer and adding myRing = createRing() in there... and this WORKS!

How and why does this work

All of your class's own instance variables must be initialized before a super.init call. Since myRing has a default value, the compiler effectively inserts the initialization of myRing before the call to super.init in your designated initializer, like this:

override init(texture: SKTexture?, color: UIColor, size: CGSize) {
// Compiler-inserted initialization of myRing to the default
// value you specified:
myRing = SKShapeNode()

super.init(texture: texture, color: color, size: size)
}

Since you declared var myRing, you can then change it later to the customized SKShapeNode you really want.

is this the best/right/proper way to be circling the drain of initialization?

Well, “circling the drain” means “failing”, so I guess you're asking if this is “the best/right/proper way” to fail at initialization… I suppose it's not the best way to fail, since you didn't actually fail in the end.

Or maybe you meant “I hate the way Swift does initialization so I'm going to throw some shade”, in which case, you ain't seen nothin' yet.

But maybe you really meant “is this the best/right/proper way to initialize my instance”, in which case, well, “best” and “right” and “proper” are pretty subjective.

But I can objectively point out that you're creating an SKShapeNode (as the default value of myRing) just to immediately throw it away and create another SKShapeNode. So that's a waste. You've also got calls to createRing in both of your convenience initializers, but you could factor them out into the designated initializer.

But I wouldn't even do it quite like that. SKShapeNode's path property is settable, so you can just create a default SKShapeNode and then change its path after the call to super.init. That also makes it easier to handle changes to ringSize and the other properties, because you can funnel all the changes through a single method that knows how to make myRing match the properties.

Here's how I'd probably write your class:

import SpriteKit

class Circle: SKSpriteNode {

var ringSize: CGFloat = 96 {
// Use an observer to update myRing if this changes.
didSet { configureMyRing() }
}

var ringColor = SKColor.white {
didSet { configureMyRing() }
}

var ringWidth: CGFloat = 8 {
didSet { configureMyRing() }
}

// This can be a let instead of a var because I'm never going to
// set it to a different object. Note that I'm not bothering to
// initialize myRing's path or any other property here, because
// I can just call configureMyRing in my init (after the call to
// super.init).
let myRing = SKShapeNode()

override init(texture: SKTexture?, color: SKColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)

// Call this now to set up myRing's path and other properties.
configureMyRing()
}

convenience init() {
self.init(color: SKColor.clear, size: CGSize(width: 100, height: 100))

// No need to do anything to myRing now, because my designated
// initializer set it up completely.

addChild(myRing)
print("I'm on the screen")

// Commented out because you didn't provide this property
// or method in your question.
// explodeGroup = create_explosionActionGroup()
}

convenience init(color: SKColor, size: CGSize, position: CGPoint) {
self.init(color: color, size: size)
self.position = position

// Commented out because you didn't provide this property
// or method in your question.
// explodeGroup = create_explosionActionGroup()
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func configureMyRing() {
myRing.path = CGPath(ellipseIn: CGRect(x: -ringSize / 2, y: -ringSize / 2, width: ringSize, height: ringSize), transform: nil)
myRing.strokeColor = ringColor
myRing.lineWidth = ringWidth
}
}

How: class that inherits SKSpriteNode with init, that can call init(color:, size:)

TL;DR: The texture parameter is of type SKTexture!. Since it's defined as an implicitly unwrapped optional, you can just pass nil.


First, you can't just call super(texture:, color, size:) from your convenience init, you actually have to call an init method on self. So, you'll have to add override init(texture: SKTexture!, color: SKColor!, size: CGSize) (which you can then just call super from).

Second, you can just pass nil as the texture parameter; SpriteKit will handle it appropriately.

Third (and only related because I had to make the change to get things to compile), you can't use your length property when creating your size variable because the class instance hasn't been initialized yet. Instead, I'd recommend declaring length as a var and passing it as a parameter to your convenience init.

Put all that together and you get something like this:

class TetrisCell : SKSpriteNode
{
var length: CGFloat!

override init(texture: SKTexture!, color: SKColor!, size: CGSize) {
self.length = 10 // Some sort of sensible default
super.init(texture: texture, color: color, size: size)
}

convenience init(color: SKColor, length: CGFloat = 10) {
var size = CGSize(width: length, height: length);
self.init(texture:nil, color: color, size: size)
self.length = length
}

required init?(coder aDecoder: NSCoder) {
// Decoding length here would be nice...
super.init(coder: aDecoder)
}
}

Using superclass's convenience initializers in subclass instantiation

Initializer Chaining rules as specified in the Swift Programming Guide, which reads:

Designated initializers must call a designated initializer from their immediate superclass.

If you are calling superclass's convenience initialize in sub class instantiation then this is not allowed.

Hope this helps.. :)



Related Topics



Leave a reply



Submit