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
How to Apply a Grace Time Using Rx
Why Can't I Use Self in a Func Swift
Spritekit: Howto Make Holes in Layer with Blendmode
How to Implement a 'Next' Property to a Caseiterable Enum in Swift
How to Edit Swift Error Breakpoint
Avplayer Seektotime Not Working Properly
Selectively Remove and Delete Objects from a Nsmutablearray in Swift
Swift Repl: How to Save/Load the Repl State? (A.K.A. Suspend/Resume, Snapshot, Clone)
Classes in Swift Files Inside Folder References Not Seen by Xcode 10's Compiler
How to Apply Animation for One Specific Modifier Change Only
Swift: Print Name of a Function Stored in a Variable
Swift: Nstextfield Allow Only Specified Characters
Tap Gesture Not Working as Expected When Added to Uiview in Collectionview Cell
How to Stop/Cancel Playsoundfilenamed in Swift
Xcode 7 Run on MAC Osx 10.10 Yosemite
How to Get Part of a String in Swift
How to Add Document with Custom Id to Firebase (Firestore) on Swift