I am getting a very strange crash while trying to change the position of an SKSpriteNode. This crash happens in iOS8 and not in iOS9.
// if the powerup is full, spawn the icon on screen
if orangeBallsCollected == numberOfBallsRequiredToActivatePowerup {
// add the powerup icon to the array of powerupiconsonscreen
powerupIconsOnScreen.append(fewerColorsPowerupIcon)
// set the lighting mask of the powerup icon so that we know what positon it is onscreen for alter
fewerColorsPowerupIcon.lightingBitMask = UInt32(powerupIconsOnScreen.count - 1)
// remove the ball from its current parent
fewerColorsPowerupIcon.removeFromParent()
print(fewerColorsPowerupIcon, fewerColorsPowerupIcon.parent, fewerColorsPowerupIcon.physicsBody, fewerColorsPowerupIcon.superclass)
// place the ball of the screen so that we can bring it on later
fewerColorsPowerupIcon.position = CGPointMake((width * -0.1) , (height * -0.1))
// set the size of the icon
fewerColorsPowerupIcon.xScale = scaleFactor
fewerColorsPowerupIcon.yScale = scaleFactor
// add it the scene
self.addChild(fewerColorsPowerupIcon)
// animate it moving down to the first avaliable position
let animation = SKAction.moveTo(CGPoint(x: width * 0.1, y: height * 0.1), duration: 0.5)
// run the animation!
fewerColorsPowerupIcon.runAction(animation)
// activate the poweurp
activateFewerColors()
}
The crash happens when I try to set the position (fewerColorsPowerupIcon.position) and this is the crash message:
Thread 1: EXC_BAD_ACCESS(code=EXC_I386_GPFLT)
This crash still happens if I put the .removeFromParent() piece of code after I set the position of the node.
I think that if you remove the sprite, you can't do anithing.
so try to do this:
let spriteCopy = fewerColorsPowerupIcon.copy()
// place the ball of the screen so that we can bring it on later
spriteCopy.position = CGPointMake((width * -0.1) , (height * -0.1))
// set the size of the icon
spriteCopy.xScale = scaleFactor
spriteCopy.yScale = scaleFactor
// add it the scene
self.addChild(spriteCopy)
or you can first add in the scene and after change property:
// remove the ball from its current parent
fewerColorsPowerupIcon.removeFromParent()
// add it the scene
self.addChild(fewerColorsPowerupIcon)
// place the ball of the screen so that we can bring it on later
fewerColorsPowerupIcon.position = CGPointMake((width * -0.1) , (height * -0.1))
// set the size of the icon
fewerColorsPowerupIcon.xScale = scaleFactor
fewerColorsPowerupIcon.yScale = scaleFactor
If fewerColorsPowerupIcon were declared something like:
weak var fewerColorsPowerupIcon: Type!
Then as soon as you call removeFromParent(), there might be no strong references to it any more. If that were so, you are not guaranteed that fewerColorsPowerupIcon will be set to nil or that it would be detected. The next time you use it, you might get EXC_BAD_ACCESS.
One way to find something like this is to use the zombie instrument. Under it, no objects are really freed when they have no references, but they complain if you try to use them once all strong references are released.
Another option is to change the ! to ? (and update the code that uses the var). It might be a pain, but there are more guarantees about how ? acts.
Related
I have a moving background which is 1500 x 600 pixels and constantly moves vertically down the screen using this code:
let bgTexture = SKTexture(imageNamed: "bg.png")
let moveBGanimation = SKAction.move(by: CGVector(dx: 0, dy: -bgTexture.size().height), duration: 4)
let shiftBGAnimation = SKAction.move(by: CGVector(dx: 0, dy: bgTexture.size().height), duration: 0)
let moveBGForever = SKAction.repeatForever(SKAction.sequence([moveBGanimation, shiftBGAnimation]))
var i: CGFloat = 0
while i < 3 {
bg = SKSpriteNode(texture: bgTexture)
bg.position = CGPoint(x: self.frame.midX, y: bgTexture.size().height * i)
bg.size.width = self.frame.width
bg.zPosition = -2
bg.run(moveBGForever)
self.addChild(bg)
i += 1
}
I now want a new background to come onto the screen after x amount of time to give the feel the player is moving into a different part of the game.
Could I put this code into a function and trigger it with NSTimer after say 20 seconds but change the start position of the new bg to be off screen?
The trouble with repeatForever actions is you don't know where they are at a certain moment. NSTimers are not as precise as you'd like, so using a timer may miss the right time or jump in too early depending on rendering speeds and frame rate.
I would suggest replacing your moveBGForever with a bgAnimation as a sequence of your move & shift actions. Then, when you run bgAnimation action, you run it with a completion block of { self.cycleComplete = true }. cycleComplete would be a boolean variable that indicates whether the action sequence is done or not. In your scene update method you can check if this variable is true and if it is, you can run the sequence action once again. Don't forget to reset the cycleComplete var to false.
Perhaps it sounds more complex but gives you control of whether you want to run one more cycle or not. If not, then you can change the texture and run the cycle again.
Alternatively you may leave it as it is and only change the texture(s) after making sure the sprite is outside the visible area, e.g. its Y position is > view size height.
In SpriteKit you can use wait actions with completion blocks. This is more straightforward than using a NSTimer.
So, to answer your question - when using actions for moving the sprites on-screen, you should not change the sprite's position at any time - this is what the actions do. You only need to make sure that you update the texture when the position is off-screen. When the time comes, obviously some of the sprites will be displayed, so you can't change the texture of all 3 at the same time. For that you may need a helper variable to check in your update cycle (as I suggested above) and replace the textures when the time is right (sprite Y pos is off-screen).
I have an App with Today Widget. So I would like to perform some UI testing on it.
I found a way to open Today/Notifications panel. It seems easy:
let statusBar = XCUIApplication().statusBars.elementBoundByIndex(0)
statusBar.swipeDown()
But then I can't find a way to do something useful. It is possible to record UI interactions in Today/Notifications panel, but such code can't reproduce my actions.
First you need to open Today View, you can use this way:
let app = XCUIApplication()
// Open Notification Center
let bottomPoint = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 2))
app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)).press(forDuration: 0.1, thenDragTo: bottomPoint)
// Open Today View
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
springboard.scrollViews.firstMatch.swipeRight()
Then, to access everything you need, just use springboard, for example:
let editButton = springboard.buttons["Edit"]
There's a similar problem testing extensions. I've found that what you must do is tap the element at where it is on the screen rather than the element itself in order to drive the interaction. I haven't tested this with your scenario, but I haven't found anything un-tappable via this method yet.
Here is a Swift example of tapping the "X" button on the Springboard for an app icon, which similarly cannot be tapped via typical interaction:
let iconFrame = icon.frame // App icon on the springboard
let springboardFrame = springboard.frame // The springboard (homescreen)
icon.pressForDuration(1.3) // tap and hold
// Tap the little "X" button at approximately where it is. The X is not exposed directly
springboard.coordinateWithNormalizedOffset(CGVectorMake((iconFrame.minX + 3) / springboardFrame.maxX, (iconFrame.minY + 3) / springboardFrame.maxY)).tap()
By getting the frame of the superview and the subview, you can calculate where on the screen the element should be. Note that coordinateWithNormalizedOffset takes a vector in the range [0,1], not a frame or pixel offset. Tapping the element itself at a coordinate doesn't work, either, so you must tap at the superview / XCUIApplication() layer.
More generalized example:
let myElementFrame = myElement.frame
let appFrame = XCUIApplication().frame
let middleOfElementVector = CGVectorMake(iconFrame.midX / appFrame.maxX, iconFrame.midY / appFrame.maxY)
// Tap element from the app-level at the given coordinate
XCUIApplication().coordinateWithNormalizedOffset(middleOfElementVector).tap()
If you need to access the Springboard layer and go outside your application, you can do so with:
let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")
springboard.resolve()
But you'll need to expose some private XCUITest methods with Objective-C:
#interface XCUIApplication (Private) {
- (id)initPrivateWithPath:(id)arg1 bundleID:(id)arg2;
}
#interface XCUIElement (Private) {
- (void) resolve;
}
I have a UIView whose perspective I'd like to slightly change as the user rotates his device. For example when the user tilts to the right, I want the left side of the view to 'rise' while the right size 'goes in' but only to a certain point.
Here is my code so far:
var dynamicTransform = CATransform3DIdentity
dynamicTransform.m34 = 1/(-500)
// Device Tilt
if motionManager.gyroAvailable {
motionManager.gyroUpdateInterval = 0.1/30
motionManager.startGyroUpdates()
motionManager.startGyroUpdatesToQueue(NSOperationQueue.mainQueue(), withHandler: { (gyroData, NSError) -> Void in
// Perspective Transform
dynamicTransform = CATransform3DRotate(dynamicTransform, CGFloat(Double(gyroData!.rotationRate.y) * M_PI / 1080.0), 0, 0.1, 0)
dynamicTransform = CATransform3DRotate(dynamicTransform, -CGFloat(Double(gyroData!.rotationRate.x) * M_PI / 1080.0), 0.1, 0, 0)
self.blueView.layer.transform = dynamicTransform
})
Everything works great except for a few caveats:
1) When I return to the device's original orientation as when the device was launched, the view is distorted (i.e.there's still a perspective applied to it). There's probably something wrong with my logic but I don't see what..
2) Sometimes the view rotates clockwise/counterclockwise and doesn't remain still.
Any help would be greatly appreciated,
thanks!
Have you tried to send layoutIfNeeded to the parent view to force a re-layout after your orientation change?
I'm teaching myself how to do SpriteKit programming by coding up a simple game that requires that I lay out a square "game field" on the left side of a landscape-oriented scene. I'm just using the stock 1024x768 view you get when creating a new SpriteKit "Game" project in XCode - nothing fancy. When I set up the game field in didMoveToView(), however, I'm finding the coordinate system to be a little weird. First of all, I expected I would have to place the board at (0, 0) for it to appear in the lower-left. Not so -- it turns out the game board has to be bumped up about 96 pixels in the y direction to work. So I end up with this weird code:
let gameFieldOrigin = CGPoint(x:0, y:96) // ???
let gameFieldSize = CGSize(width:560, height: 560)
let gameField = CGRect(origin: gameFieldOrigin, size: gameFieldSize)
gameBorder = SKShapeNode(rect: gameField)
gameBorder.strokeColor = UIColor.redColor()
gameBorder.lineWidth = 0.1
self.addChild(gameBorder) // "self" is the SKScene subclass GameScene
Furthermore, when I add a child to it (a ball that bounces inside the field), I assumed I would just use relative coordinates to place it in the center. However, I ended up having to use "absolute" coordinate and I had to offset the y-coordinate by 96 again.
Another thing I noticed is when I called touch.locationInNode(gameBorder), the coordinates were again not relative to the border, and start at (0, 96) at the bottom of the border instead of (0, 0) as I would have guessed.
So what am I missing here? Am I misunderstanding something fundamental about how coordinates work?
[PS: I wanted to add the tag "SpriteKit" to this question, but I don't have enough rep. :/]
You want to reference the whole screen as a coordinate system, but you're actually setting all the things on a scene loading from GameScene.sks. The right way to do is modify one line in your GameViewController.swift in order to set your scene size same as the screen size. Initialize scene size like this instead of unarchiving from .sks file:
let scene = GameScene(size: view.bounds.size)
Don't forget to remove the if-statement as well because we don't need it any more. In this way, the (0, 0) is at the lower-left corner.
To put something, e.g. aNode, in the center of the scene, you can set its position like:
aNode.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));
I am new to swift a Sprite kit. In the app I am trying to make I have a submarine moving through the ocean. Every time the user clicks the screen the gravity starts pulling the sub in the opposite direction. My problem is that i can't find a way to keep the sub from leaving the screen. I have tried to solve it by making a physicsBody around the screen, but the sub still leaves the screen. I have also tried the following code in the updateCurrentTime fund.
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
self.physicsWorld.gravity = CGVectorMake(0,gravity)
if (sub.position.y >= self.size.height - sub.size.height / 2){
sub.position.y = self.size.height - self.sub.size.height / 2
}
if (sub.position.y <= sub.size.height / 2) {
sub.position.y = self.sub.size.height / 2
}
}
But this doesn't do anything either.
Any help would be greatly appreciated!!!!! thanks in advance!
P.S. I can't believe that it is that hard to keep things on the screen!!!
frustrating!
Try SKConstraint - it doesn't require a physicsBody. The code would look something like this, and would constrain the sub sprite to the scene:
let width2 = sub.size.width/2
let height2 = sub.size.height/2
let xRange = SKRange(lowerLimit:0+width2,upperLimit:size.width-width2)
let yRange = SKRange(lowerLimit:0+height2,upperLimit:size.height-height2)
sub.constraints = [SKConstraint.positionX(xRange,Y:yRange)]
Try this in the update:
if sub.frame.maxY >= view!.frame.height {
sub.position.y = view!.frame.height - sub.size.height / 2
sub.physicsBody!.affectedByGravity = false
}
if sub.frame.minY <= 0 {
sub.position.y = sub.size.height / 2
sub.physicsBody!.affectedByGravity = false
}
And then inside of the event where you want to reverse gravity don't forget to do this:
sub.physicsBody!.affectedByGravity = true
Alternatively, instead of using gravity you could use this which is a better option in my opinion:
// This moves the object to the top of the screen
let action = SKAction.moveToY(view!.frame.height - character.size.height / 2, duration: 5.0) // Or however much time you want to the action to run.
action.timingMode = .EaseInEaseOut // Or something else
character.runAction(action)
// Change view!.frame.height - character.size.height / 2 to just character.size.height / 2 to move to the bottom.