didSet observer does not get called when initialising property - swift

Given the following class,
class Spaceship {
var position: CGPoint! {
didSet {
node.position = position
}
}
var node: SKSpriteNode!
init(frame: CGRect) {
node = SKSpriteNode(
color: UIColor.red,
size: CGSize(
width: frame.size.width / 5,
height: frame.size.width / 5
)
)
self.position = CGPoint(x: frame.midX, y: frame.midY)
}
}
it looks as though the didSet observer of the position property does not get called.
Since the didSet observer should set the SpriteKit node's position when the position property gets modified, I thought that instead of using the same line of code contained within the didSet block, I could trigger the latter instead, but it doesn't seem to work; when the scene gets created (in the main GameScene, which simply creates a Spaceship objects and adds the spaceship.node to the scene's node children), the node's position seems to be 0; 0.
Any clue on why does this happen? Thank you.
Update: although the defer statement works, the assignment does not allow to make use of it.

Rather than using didSet to update your node, directly forward queries to it:
class Spaceship {
let node: SKSpriteNode
init(frame: CGRect) {
self.node = SKSpriteNode(
color: UIColor.red,
size: CGSize(
width: frame.size.width / 5,
height: frame.size.width / 5
)
)
self.position = CGPoint(x: frame.midX, y: frame.midY)
}
var position: CGPoint {
get { return node.position }
set { node.position = newValue }
}
}

The willSet and didSet observers of superclass properties are called when a property is set in a subclass initializer, after the superclass initializer has been called. They are not called while a class is setting its own properties, before the superclass initializer has been called.
One possible solution could be to set the position property outside of the Spaceship's initialiser, or also set directly the node's position.

I personally would just subclass Spaceship as an SKSpriteNode. Spaceship does not seem to be a complex object that involves composition over inheritance.
class Spaceship : SKSpriteNode {
convenience init(frame: CGRect) {
self.init(
color: UIColor.red,
size: CGSize(
width: frame.size.width / 5,
height: frame.size.width / 5
)
)
self.position = CGPoint(x: frame.midX, y: frame.midY)
}
}
Now I am not sure why you are passing in frame like this.... I am assuming frame is your scene? If this is the case, you are going to come across issues when anchor point is not (0,0).

Related

Force Custom NSView from PaintCode to Redraw

I created an icon in PaintCode that is drawn programmatically (but this question isn't necessarily specific to that tool) and I'm trying to get that icon to redraw.
I use a custom class like this:
class IconTabGeneral: NSView {
override func draw(_ dirtyRect: NSRect) {
StyleKitMac.drawTabGeneral()
}
}
The drawTabGeneral() is a method in the StyleKitMac class (generated by PaintCode) that looks like this (I'll omit all the bezierPath details):
#objc dynamic public class func drawTabGeneral(frame targetFrame: NSRect = NSRect(x: 0, y: 0, width: 22, height: 22), resizing: ResizingBehavior = .aspectFit) {
//// General Declarations
let context = NSGraphicsContext.current!.cgContext
//// Resize to Target Frame
NSGraphicsContext.saveGraphicsState()
let resizedFrame: NSRect = resizing.apply(rect: NSRect(x: 0, y: 0, width: 22, height: 22), target: targetFrame)
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 22, y: resizedFrame.height / 22)
//// Bezier Drawing
let bezierPath = NSBezierPath()
...
bezierPath.close()
StyleKitMac.accentColor.setFill() ⬅️Custom color set here
bezierPath.fill()
NSGraphicsContext.restoreGraphicsState()
}
The accentColor defined there is a setting that can be changed by the user. I can't get my instance of IconTabGeneral to redraw to pick up the new color after the user changes it.
I've tried this without any luck:
iconGeneralTabInstance.needsDisplay = true
My understanding is that needsDisplay would force the draw function to fire again, but apparently not.
Any idea how I can get this icon to redraw and re-fill its bezierPath?
How about using a NSImageView? Here is a working example:
import AppKit
enum StyleKit {
static func drawIcon(frame: CGRect) {
let circle = NSBezierPath(ovalIn: frame.insetBy(dx: 1, dy: 1))
NSColor.controlAccentColor.setFill()
circle.fill()
}
}
let iconView = NSImageView()
iconView.image = .init(size: .init(width: 24, height: 24), flipped: true) { drawingRect in
StyleKit.drawIcon(frame: drawingRect)
return true
}
import PlaygroundSupport
PlaygroundPage.current.liveView = iconView
I think I got it. It turns out the PaintCode-generated StyleKitMac class was caching the color. The icon was, in fact, being redrawn after needsDisplay was being set. So I just had to refresh the cache's color value.

IBInspectable variable value is not changing in interface builder and simulator

I am creating custom parallelogram view and view is rendering fine with default offset value. But when i change the offset value from ib it's not working
#IBDesignable class CustomParallelogramView: UIView {
#IBInspectable var offset: Float = 10.0
var path = UIBezierPath()
lazy var shapeLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.path = path.cgPath
return layer
}()
override init(frame: CGRect) {
super.init(frame:frame)
drawParallelogram()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
drawParallelogram()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
drawParallelogram()
}
override func layoutSubviews() {
super.layoutSubviews()
updateFrame()
}
func drawParallelogram() {
print(offset)
path.move(to: CGPoint(x: bounds.minX + CGFloat(offset), y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX - CGFloat(offset), y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
self.layer.mask = shapeLayer
}
func updateFrame() {
shapeLayer.frame = bounds
}
}
I changed the offset value from IB but it doesn't changes IB and simulator
A couple of thoughts:
As Daniel said, you really want to call drawParallelgram in your offset observer.
Also, in layoutSubviews, you’re updating the frame of your shape layer. You want to reset its path and update your mask again.
You’re just adding more and more strokes to your UIBezierPath. You probably just want to make it a local variable and avoid adding more and more strokes to the existing path.
The prepareForInterfaceBuilder suggests some misconception about the purpose of this method. This isn’t for doing initialization when launching from IB. This is for doing some special configuration, above and beyond what is already done by the init methods, required by IB.
E.g. If you have a sophisticated chart view where you’ll be programmatically providing real chart data programmatically later, but you wanted to see something in IB, nonetheless, you might have prepareForInterfaceBuilder populate some dummy data, for example. But you shouldn’t repeat the configuration you already did in init methods.
It’s not relevant here (because I’m going to suggest getting rid of these init methods), but for what it’s worth, if I need to do configuration during init, I generally write two init methods:
override init(frame: CGRect = .zero) {
super.init(frame: frame)
<#call configure routine here#>
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
<#call configure routine here#>
}
Note that in init(frame:) I also supply a default value of .zero. That ensures that I’m covered in all three scenarios:
CustomView()
CustomView(frame:); or
if the storyboard calls CustomView(decoder:).
In short, I get three init methods for the price of two. Lol.
All that being said, you can greatly simplify this:
#IBDesignable public class CustomParallelogramView: UIView {
#IBInspectable public var offset: CGFloat = 10 { didSet { updateMask() } }
override public func layoutSubviews() {
super.layoutSubviews()
updateMask()
}
private func updateMask() {
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.minX + offset, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX - offset, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
layer.mask = shapeLayer
}
}
Try:
#IBInspectable var offset: Float = 10.0 {
didSet {
drawParallelogram()
}
}

How to add a sprite from another class on the gameScene

can anyone explain how to add a node to the gameScene please.
I subclassed my Boss class but i dont know how i can display my Boss1 on the GameScene
class Boss: GameScene {
var gameScene : GameScene!
var Boss1 = SKSpriteNode(imageNamed: "boss1")
override func didMove(to view: SKView) {
Boss1.position = CGPoint(x: size.width * 0.1, y: size.height * 0.5)
Boss1.zPosition = 2
self.gameScene.addChild(Boss1)
}
}
im using swift 4 and xcode 9
You don't generally subclass the scene for instances such as this. More likely you are meaning to make boss a subclass of SKSpriteNode and adding it to your scene. While there is probably dozens of ways that you could subclass this, this is just 1 way.
Also worth noting that it is not generally acceptable practice to make your variable names capitalized, classes yes, variables no.
class Boss: SKSpriteNode {
init() {
let texture = SKTetxure(imageNamed: "boss1")
super.init(texture: texture , color: .clear, size: texture.size())
zPosition = 2
//any other setup such as zRotation coloring, additional layers, health etc.
}
}
...meanwhile back in GameScene
class GameScene: SKScene {
let boss1: Boss!
override func didMove(to view: SKView) {
boss1 = Boss()
boss1.position = CGPoint(x: size.width * 0.1, y: size.height * 0.5)
self.gameScene.addChild(boss1)
}
}
in GameScene you create an instance of the new class and position it and add it in the GameScene.
edit
the init function has a short cut in swift.
Boss()
is the same as
Boss.init()
you can also add custom parameters to your init in order to further clarify or add more features to your class. for example...
class Boss: SKSpriteNode {
private var health: Int = 0
init(type: Int, health: Int, scale: CGFloat) {
let texture: SKTetxure!
if type == 1 {
texture = SKTetxure(imageNamed: "boss1")
}
else {
texture = SKTetxure(imageNamed: "boss2")
}
super.init(texture: texture , color: .clear, size: texture.size())
self.health = health
self.setScale(scale)
zPosition = 2
//any other setup such as zRotation coloring, additional layers, health etc.
}
}

wondering use of computed property

before.
class DrawingView: UIView {
var arcCenter = CGPoint(x: frame.midX, y: frame.midY) // error
}
after.
class DrawingView: UIView {
var arcCenter: CGPoint {
return CGPoint(x: frame.midX, y: frame.midY)
}
}
I know why that error occur, but I can't understand why the second codes are not makes error.
The first form is not a computed property.
It is a stored property with a default value.
it uses self which causes the error because at the moment the default value is going to be assigned the instance is not guaranteed to be instantiated.
An alternative to a computed property – which is computed only at the moment it's called – is a lazy instantiated property.
lazy var arcCenter: CGPoint = CGPoint(x: self.frame.midX, y: self.frame.midY)
Unlike the stored property the default value is assigned when the property is accessed the first time.

Swift SpriteKit Mario style rotating platform that stays horizontal

I am trying to create a mario style rotating platform that stays horizontal.
What I have done so far is create a simple class for this for testing purposes.
class PlatformRound: SKNode {
let platform: Platform
// MARK: - Init
init(barSize: CGSize, color: SKColor, pos: CGPoint) {
/// Base
let base = SKShapeNode(circleOfRadius: 6)
//base.name = Platform.Name.normal
base.fillColor = SKColor.darkGrayColor()
base.strokeColor = base.fillColor
base.position = pos
let rotatingAction = SKAction.rotateByAngle(CGFloat(-M_PI), duration: 8)
base.runAction(SKAction.repeatActionForever(rotatingAction))
/// Bar
let bar = Platform(size: barSize, color: color, pos: CGPointMake(0, 0 - (barSize.height / 2)), ofType: .Normal)
bar.zPosition = -200
/// Platform that supposed to stay horizontal
let platformSize = CGSizeMake(40, GameplayConfig.World.platformHeight)
let platformPos = CGPointMake(0, 0 - (bar.size.height / 2))
platform = Platform(size: platformSize, color: color, pos: platformPos, ofType: .Normal)
super.init()
addChild(base)
base.addChild(bar)
bar.addChild(platform)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I am creating a roundBase that I can rotate. I than create a bar that goes down from the base, that is added to the base node. Finally I create the platform that is supposed to stay horizontal at all times.
I am using another Platform subclass to create the bar and platform, but they are not relevant to this question.
How can I make the platform stay horizontal. I tried the following which didnt work.
1) In update in my gameScene I constantly update the platform position or zRotation
platformRound.platform.zRotation = ...
2) Create a zRotation property that gets set once the platform is added and than use that property to constantly update the zRotation.
3) Tried playing around with physicsJoints
Im sure there is a easy way that I am missing. I would appreciate any help.
This should work:
class GameScene: SKScene{
override func didMoveToView(view: SKView) {
backgroundColor = .blackColor()
let centerSprite = SKSpriteNode(color: .whiteColor(), size: CGSize(width: 10, height: 10))
centerSprite.zPosition = 3
let platform = SKSpriteNode(color: .orangeColor(), size: CGSize(width: 70, height: 20))
platform.zPosition = 2
platform.name = "platform"
let container = SKNode()
container.position = CGPoint(x: frame.midX, y: frame.midY)
container.addChild(centerSprite) //Just for debugging
container.addChild(platform)
let radius = 120
let chain = SKSpriteNode(color: .grayColor(), size: CGSize(width: 3, height: radius))
chain.position = CGPoint(x: 0, y: radius/2)
container.addChild(chain)
platform.position = CGPoint(x: 0, y: radius)
let rotatingAction = SKAction.rotateByAngle(CGFloat(-M_PI), duration: 8)
container.runAction(SKAction.repeatActionForever(rotatingAction), withKey: "rotating")
addChild(container)
}
override func didEvaluateActions() {
self.enumerateChildNodesWithName("//platform") { node,stop in
if let parent = node.parent{
node.zRotation = -parent.zRotation
}
}
}
}
What I did, is that I have added platform node into container node and applied rotation to that container. Later on, in didEvaluateActions I've adjusted the rotation of platform (to have a negative value of its parent's zRotation). And that's it.
Here is the result of what I am seeing:
The adjusting is needed, because otherwise the platform will end-up rotating along with its parent (notice how white, center sprite is being rotated along with container node).