I have a question as to why my SpriteKit nodes are not being removed from my parent node in the code below.
My project is currently in two parts, one is the GameScene class which comes default when you build a SpriteKit project, and the other is a Circles class which I made to operate on each circle in the game.
The circles are stored in an array here:
var circlesInPlay = [Circles]()
Basically, I'm trying to design a simple game where circles decrease in size, and when they are of 0 width, are removed from the screen.
My code to do this is here, in the
override func update(currentTime: CFTimeInterval) {
timeSinceUpdate = timeSinceUpdate + 1
print("time since update: " + String(timeSinceUpdate))
if timeForUpdate == timeSinceUpdate {
newCircle()
timeSinceUpdate = 0
//timeForUpdate = Int(arc4random_uniform(100) + 1)
timeForUpdate = 100
}
//Check to see if circle transparancies are 0
if checkGameOver() {
gameOver()
}
updateCircles()
removeCircles()
The timeSinceUpdate variable is the time since the last circle has been placed onto the screen, which increases by one for every frame updated.
updateCircles() calls this,
func updateCircles() {
for c in circlesInPlay {
c.update()
}
}
Which calls update() in the Circles class I made in another swift file:
func update() {
transparancy = transparancy - transparancyDecrease
size = size - ds
if (size <= 0) {
node.removeFromParent()
}
node.size.height = size
node.size.width = size
}
The other call from the update function to removeCircles() is here
func removeCircles() {
var posn = circlesInPlay.count - 1
for c in circlesInPlay {
if (c.size < 0) {
c.produceNode().removeFromParent()
circlesInPlay.removeAtIndex(posn)
posn = posn - 1
}
}
}
What i'm really getting at is I have no idea why the circles are sometimes getting stuck or not being removed from the screen.
Any help is greatly appreciated!
The entire game code is as follows:
import SpriteKit
class GameScene: SKScene {
var bgImage = SKSpriteNode(imageNamed: "background.png")
let screenSize = UIScreen.mainScreen().bounds
var timeSinceUpdate: Int = 0
var circlesInPlay = [Circles]()
var timeForUpdate = Int(arc4random_uniform(200) + 1)
override func didMoveToView(view: SKView) {
bgImage.position = CGPointMake(self.size.width/2, self.size.height/2)
bgImage.zPosition = -100
self.addChild(bgImage)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
if !isTouchInCircle(location) {
gameOver()
}
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
timeSinceUpdate = timeSinceUpdate + 1
print("time since update: " + String(timeSinceUpdate))
if timeForUpdate == timeSinceUpdate {
newCircle()
timeSinceUpdate = 0
//timeForUpdate = Int(arc4random_uniform(100) + 1)
timeForUpdate = 100
}
//Check to see if circle transparancies are 0
if checkGameOver() {
gameOver()
}
updateCircles()
removeCircles()
}
func newCircle(){
let sizeX = UInt32(CGRectGetMaxX(self.frame))
let randomx = CGFloat(arc4random_uniform(sizeX))
let sizeY = UInt32(CGRectGetMaxY(self.frame))
let randomy = CGFloat(arc4random_uniform(sizeY))
//let randomx = CGFloat(arc4random_uniform(UInt32(self.size.width)))
//let randomy = CGFloat(arc4random_uniform(UInt32(self.size.height)))
if (circlesInPlay.count < 5) {
circlesInPlay.append(Circles.init(x: randomx, y: randomy))
self.addChild((circlesInPlay[circlesInPlay.count - 1]).produceNode())
}
}
func checkGameOver() -> Bool {
for c in circlesInPlay {
if c.getTransparancy() == 0 {
return false
}
}
return true
}
func isTouchInCircle(touch: CGPoint) -> Bool {
for c in circlesInPlay {
if (c.getX() <= (touch.x * 1.10)) {
return true
}
}
return false
}
func updateCircles() {
for c in circlesInPlay {
c.update()
}
}
func removeCircles() {
var posn = circlesInPlay.count - 1
for c in circlesInPlay {
if (c.size < 0) {
c.produceNode().removeFromParent()
circlesInPlay.removeAtIndex(posn)
posn = posn - 1
}
}
}
func gameOver() {
}
}
import Foundation
import SpriteKit
import GameKit
class Circles {
var node = SKSpriteNode()
var size: CGFloat = 200
var ds: CGFloat = 2
var colorC: String;
var xpos: CGFloat
var ypos: CGFloat
var transparancy: Int
var transparancyDecrease: Int = 1
let arrayColors = ["circleRed.png", "circleBlue.png", "circlePink.png", "circleGreen.png", "circleYellow.png"]
//The innitial constructor
init(x: CGFloat, y: CGFloat) {
self.xpos = x
self.ypos = y
self.transparancy = 100
let randomIndex = Int(arc4random_uniform(UInt32(arrayColors.count)))
colorC = arrayColors[randomIndex]
node.texture = SKTexture(imageNamed: colorC)
node.position = CGPointMake(self.xpos, self.ypos)
node.size.height = self.size
node.size.width = self.size
}
func produceNode() -> SKSpriteNode {
return node
}
func setColor() {
let randomIndex = Int(arc4random_uniform(UInt32(arrayColors.count)))
colorC = arrayColors[randomIndex]
}
func update() {
transparancy = transparancy - transparancyDecrease
size = size - ds
if (size <= 0) {
node.removeFromParent()
}
node.size.height = size
node.size.width = size
}
func getX() -> CGFloat {
return xpos
}
func getY() -> CGFloat {
return ypos
}
/*
func getColor() -> SKColor {
return colorC
}*/
func getTransparancy() -> Int {
return transparancy
}
}
The issue is in this snippet:
var posn = circlesInPlay.count - 1
for c in circlesInPlay {
if (c.size < 0) {
c.produceNode().removeFromParent()
circlesInPlay.removeAtIndex(posn)
posn = posn - 1
}
}
You are always removing the last element from you collection and not the element which you are using in the iteration. Also be careful when you are changing the collection while iterating thrue it. The indexes will change, if you remove elements from the collection.
Related
App crashes while drawing the Waveform which is superimposed on a UIScrollView, particularly at two statements alternatively. Following are the statements and errors which I'm encountering on the Draw Method (complete code below)
Note: The below error happens one at a time, not together.
No. 1
if values.count < length {
Thread 79: EXC_BAD_ACCESS (code=1, address=0x11a668008)
No. 2
toDraw.forEach({
Thread 129: EXC_BAD_ACCESS (code=1, address=0x11e23f030)
Complete Draw method with Relevant Properties and methods
import UIKit
final class RecordWaveViewer: UIView {
// MARK: - Properties
var liveLineColor = UIColor.white {
didSet { update() }
}
var savedLineColor = UIColor.gray {
didSet { update() }
}
var lineWidth: Float = 2.0 {
didSet { update() }
}
var lineSeparation: Float = 2.0 {
didSet { update() }
}
var values = [ValueElement]() {
didSet { update() }
}
var replacementValues = [ValueElement]()
var showPlayingProgress = false {
didSet { update() }
}
var playingProgress: CGFloat = 0.0 {
didSet { update() }
}
var playingProgressLineColor = Asset.Colors.yellow.color {
didSet { update() }
}
var offsetX: CGFloat = 0.0 {
didSet { update() }
}
var waveWidth: CGFloat {
CGFloat(values.count) * CGFloat(lineWidth + lineSeparation)
}
var actualWaveWidth: CGFloat {
guard let lastActualIndex = values.lastIndex(where: { $0.type == .live }) else {
return waveWidth
}
return CGFloat(lastActualIndex + 1) * CGFloat(lineWidth + lineSeparation)
}
var sampleWidth: CGFloat {
return CGFloat(lineWidth + lineSeparation)
}
private var replacementStartIndex: Int?
private let fillingPercentage: CGFloat = 0.95
private var tiledLayer: CATiledLayer {
layer as! CATiledLayer
}
override class var layerClass: AnyClass {
CATiledLayer.self
}
override var transform: CGAffineTransform { // Prevent vertical zoom
get { super.transform }
set {
var value = newValue
value.d = 1.0
super.transform = value
}
}
// MARK: - Override
override func layoutSubviews() {
super.layoutSubviews()
// Config tile
tiledLayer.tileSize = CGSize(width: 2048, height: bounds.height * contentScaleFactor)
}
///As per apple recomendation: https://developer.apple.com/library/archive/qa/qa1637/_index.html
override func draw(_ rect: CGRect) {
super.draw(rect)
}
override func draw(_ layer: CALayer, in ctx: CGContext) {
super.draw(layer, in: ctx)
// --------------------------- Lines
let rect = ctx.boundingBoxOfClipPath
let divider = CGFloat(lineWidth + lineSeparation)
let offSet = Int(rect.origin.x / divider)
var length = offSet + Int(rect.width / divider)
if self.values.count < length {
length = self.values.count
}
if (self.values.count > 0 ) {
let toDraw = self.values[offSet..<length]
// Lines offset
var xOffset: CGFloat = self.offsetX + rect.origin.x
// Show lines
toDraw.forEach({
var lineColor: UIColor
switch $0.type {
case .live:
lineColor = self.liveLineColor
case .saved:
lineColor = self.savedLineColor
}
// Line rect
let height = rect.height * CGFloat($0.value) * self.fillingPercentage
let y = (rect.height - height) / 2.0
// Draw line
ctx.setLineWidth(CGFloat(self.lineWidth))
ctx.setLineCap(.round)
ctx.move(to: CGPoint(x: xOffset, y: y))
ctx.addLine(to: CGPoint(x: xOffset, y: y + height))
ctx.setStrokeColor(lineColor.cgColor)
if self.showPlayingProgress && xOffset >= self.playingProgress {
ctx.setStrokeColor(lineColor.withAlphaComponent(0.5).cgColor)
}
ctx.strokePath()
// Add offset
xOffset += CGFloat(self.lineWidth + self.lineSeparation)
})
}
// --------------------------- Playing progress
if showPlayingProgress && playingProgress > 0.0 {
ctx.setLineWidth(CGFloat(lineWidth))
ctx.setStrokeColor(playingProgressLineColor.cgColor)
ctx.setLineCap(.square)
ctx.move(to: CGPoint(x: 1.0 + playingProgress, y: 0.0))
ctx.addLine(to: CGPoint(x: 1.0 + playingProgress, y: rect.height))
ctx.strokePath()
}
}
// MARK: - Custom
private func update() {
DispatchQueue.main.async {
self.setNeedsDisplay()
}
}
func add(value: Float, time: TimeInterval) {
if replacementStartIndex != nil {
addReplacement(value: value, time: time)
} else {
DispatchQueue.main.async {
self.values.append(ValueElement(value, .live, time: time))
}
}
}
func addReplacement(value: Float, time: TimeInterval) {
DispatchQueue.main.async {
let replacementValue = ValueElement(value, .live, time: time)
self.replacementValues.append(replacementValue)
let index = self.replacementValues.count - 1
guard let replacementStartIndex = self.replacementStartIndex,
replacementStartIndex < self.values.count,
index >= 0 else {
return
}
let replacementIndex = replacementStartIndex + index
if replacementIndex < self.values.count {
self.values[replacementIndex] = replacementValue
} else {
self.values.append(replacementValue)
}
}
}
func reset() {
DispatchQueue.main.async {
self.values.removeAll()
}
update()
}
func convertValuesToSaved() {
DispatchQueue.main.async {
self.values = self.values.compactMap({ ValueElement($0.value, .saved, $0.time )})
self.replacementValues.removeAll()
self.replacementStartIndex = nil
}
}
func startReplacement(at time: TimeInterval) {
DispatchQueue.main.async {
self.replacementStartIndex = self.values.firstIndex(where: { $0.time >= time })
}
}
}
// MARK: - Nested types
extension RecordWaveViewer {
typealias ValueElement = (value: Float, type: ValueType, time: TimeInterval)
enum ValueType {
case saved
case live
}
}
Sharing the two other statements outside the RecordWaveViewer Class where the value is getting updated from.
Statement 1:
DispatchQueue.main.async {
self.waveViewer.add(value: value, time: time)
}
Statement 2:
DispatchQueue.main.async {
self.waveViewer.values = values.compactMap({ RecordWaveViewer.ValueElement(value: $0.power, type: .saved, time: $0.time)})
}
You're continuing to interact with values on random threads, which is not permitted. You're using CATiledLayer, which is drawn asynchronously (that's much of its reason for existing). You can see this in the fact that your crashes are not on thread 1. They're on random threads.
Every time you access values, or any other property, in your draw method, you're creating a race condition.
The easiest way to fix all of this is to make your view or layer immutable. Set it up with all its properties (including values), and when any of the properties change, make a new view or layer. For a full example of this approach, see Apple's sample code (Apple no longer publishes this code, and the original was pre-ARC, but this link is an ARC-ified version from iOS 4).
If you can't make the layer immutable, then you need to put locks around the configuration and make sure to access it exactly one time at the start of the draw routine. This most easily done by putting all the configuration together into a single struct so that it can be read and written atomically. (You'll still need a lock or queue to make it thread-safe.)
You can also reconsider using CATiledLayer. You seem to only be using it for horizontal scrolling. For many problems, it's easier to use the common technique of 3-7 views that you rearrange in the scrollview as the user scrolls. For that you can use the simpler CALayer (or even just UIView). A big advantage of CATiledLayer is handling zooming (and particularly changes in detail level as you zoom), and you're not using that here. It's also pretty nice for handling large 2-D canvases (like maps), but you're not using that either.
I am trying to construct shperical view application(same with panorama view) with ios.
there are listed images(paranonma) in UITableView.
you can view each image as 360 degrees using both device motion and finger gesture.
But, is it possible that one gesture at any image leads all image to take same effect of the gesture?
for example, if I circulate top image by finger, all image "also" circulates same with the top image.
"Bubbli" application has that function.
I tried to put pangesturerecognizer as global variable to shared gesture, but It didn't work.
How can i..?
it's tableview code.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "pic_cell", for: indexPath) as! pic_TableViewCell
let tmp = tmp_list[indexPath.row]
cell.pic_View.loadPanoramaView(image: tmp)
return cell
}
it's tableviewcell_uiview code.
class TableViewCell_UIView: UIView {
var image_name:String = ""
lazy var device: MTLDevice = {
guard let device = MTLCreateSystemDefaultDevice() else {
fatalError("Failed to create MTLDevice")
}
return device
}()
weak var panoramaView: PanoramaView?
func loadPanoramaView(image: String) {
#if arch(arm) || arch(arm64)
let panoramaView = PanoramaView(frame: view.bounds, device: device)
#else
let panoramaView = PanoramaView(frame: self.bounds) // iOS Simulator
#endif
panoramaView.setNeedsResetRotation()
panoramaView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(panoramaView)
// fill parent view
let constraints: [NSLayoutConstraint] = [
panoramaView.topAnchor.constraint(equalTo: self.topAnchor),
panoramaView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
panoramaView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
panoramaView.trailingAnchor.constraint(equalTo: self.trailingAnchor)
]
NSLayoutConstraint.activate(constraints)
// double tap to reset rotation
let doubleTapGestureRecognizer = UITapGestureRecognizer(target: panoramaView, action: #selector(PanoramaView.setNeedsResetRotation(_:)))
doubleTapGestureRecognizer.numberOfTapsRequired = 2
panoramaView.addGestureRecognizer(doubleTapGestureRecognizer)
self.panoramaView = panoramaView
panoramaView.load(UIImage(named: image)!, format: .mono)
}
}
it's PanoramaView.swift which defines PanoramaView class.
final class PanoramaView: UIView, SceneLoadable {
#if (arch(arm) || arch(arm64)) && os(iOS)
public let device: MTLDevice
#endif
public var scene: SCNScene? {
get {
return scnView.scene
}
set(value) {
orientationNode.removeFromParentNode()
value?.rootNode.addChildNode(orientationNode)
scnView.scene = value
}
}
public weak var sceneRendererDelegate: SCNSceneRendererDelegate?
public lazy var orientationNode: OrientationNode = {
let node = OrientationNode()
let mask = CategoryBitMask.all.subtracting(.rightEye)
node.pointOfView.camera?.categoryBitMask = mask.rawValue
return node
}()
lazy var scnView: SCNView = {
#if (arch(arm) || arch(arm64)) && os(iOS)
let view = SCNView(frame: self.bounds, options: [
SCNView.Option.preferredRenderingAPI.rawValue: SCNRenderingAPI.metal.rawValue,
SCNView.Option.preferredDevice.rawValue: self.device
])
#else
let view = SCNView(frame: self.bounds)
#endif
view.backgroundColor = .black
view.isUserInteractionEnabled = false
view.delegate = self
view.pointOfView = self.orientationNode.pointOfView
view.isPlaying = true
self.addSubview(view)
return view
}()
// to integrated panGesture
fileprivate lazy var panGestureManager: PanoramaPanGestureManager = {
let manager = PanoramaPanGestureManager(rotationNode: self.orientationNode.userRotationNode)
manager.minimumVerticalRotationAngle = -60 / 180 * .pi
manager.maximumVerticalRotationAngle = 60 / 180 * .pi
return manager
}()
fileprivate lazy var interfaceOrientationUpdater: InterfaceOrientationUpdater = {
return InterfaceOrientationUpdater(orientationNode: self.orientationNode)
}()
#if (arch(arm) || arch(arm64)) && os(iOS)
public init(frame: CGRect, device: MTLDevice) {
self.device = device
super.init(frame: frame)
addGestureRecognizer(panGestureManager.gestureRecognizer) // modify
//addGestureRecognizer(setGestureRecognizer())
}
#else
public override init(frame: CGRect) {
super.init(frame: frame)
addGestureRecognizer(panGestureManager.gestureRecognizer) // modify
//addGestureRecognizer(setGestureRecognizer())
}
#endif
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
orientationNode.removeFromParentNode()
}
public override func layoutSubviews() {
super.layoutSubviews()
scnView.frame = bounds
}
public override func willMove(toWindow newWindow: UIWindow?) {
if newWindow == nil {
interfaceOrientationUpdater.stopAutomaticInterfaceOrientationUpdates()
} else {
interfaceOrientationUpdater.startAutomaticInterfaceOrientationUpdates()
interfaceOrientationUpdater.updateInterfaceOrientation()
}
}
}
extension PanoramaView: ImageLoadable {}
it's PanoramaPanGestureManager.swift
final class PanoramaPanGestureManager {
let rotationNode: SCNNode
var allowsVerticalRotation = true
var minimumVerticalRotationAngle: Float?
var maximumVerticalRotationAngle: Float?
var allowsHorizontalRotation = true
var minimumHorizontalRotationAngle: Float?
var maximumHorizontalRotationAngle: Float?
lazy var gestureRecognizer: UIPanGestureRecognizer = {
let recognizer = AdvancedPanGestureRecognizer()
recognizer.addTarget(self, action: #selector(handlePanGesture(_:)))
recognizer.earlyTouchEventHandler = { [weak self] in
self?.stopAnimations()
self?.resetReferenceAngles()
}
return recognizer
}()
private var referenceAngles: SCNVector3?
init(rotationNode: SCNNode) {
self.rotationNode = rotationNode
}
#objc func handlePanGesture(_ sender: UIPanGestureRecognizer) {
guard let view = sender.view else {
return
}
switch sender.state {
case .changed:
guard let referenceAngles = referenceAngles else {
break
}
var angles = SCNVector3Zero
let viewSize = max(view.bounds.width, view.bounds.height)
let translation = sender.translation(in: view)
if allowsVerticalRotation {
var angle = referenceAngles.x + Float(translation.y / viewSize) * (.pi / 2)
if let minimum = minimumVerticalRotationAngle {
angle = max(angle, minimum)
}
if let maximum = maximumVerticalRotationAngle {
angle = min(angle, maximum)
}
angles.x = angle
}
if allowsHorizontalRotation {
var angle = referenceAngles.y + Float(translation.x / viewSize) * (.pi / 2)
if let minimum = minimumHorizontalRotationAngle {
angle = max(angle, minimum)
}
if let maximum = maximumHorizontalRotationAngle {
angle = min(angle, maximum)
}
angles.y = angle
}
SCNTransaction.lock()
SCNTransaction.begin()
SCNTransaction.disableActions = true
rotationNode.eulerAngles = angles.normalized
SCNTransaction.commit()
SCNTransaction.unlock()
case .ended:
var angles = rotationNode.eulerAngles
let velocity = sender.velocity(in: view)
let viewSize = max(view.bounds.width, view.bounds.height)
if allowsVerticalRotation {
var angle = angles.x
angle += Float(velocity.y / viewSize) / .pi
if let minimum = minimumVerticalRotationAngle {
angle = max(angle, minimum)
}
if let maximum = maximumVerticalRotationAngle {
angle = min(angle, maximum)
}
angles.x = angle
}
if allowsHorizontalRotation {
var angle = angles.y
angle += Float(velocity.x / viewSize) / .pi
if let minimum = minimumHorizontalRotationAngle {
angle = max(angle, minimum)
}
if let maximum = maximumHorizontalRotationAngle {
angle = min(angle, maximum)
}
angles.y = angle
}
SCNTransaction.lock()
SCNTransaction.begin()
SCNTransaction.animationDuration = 1
SCNTransaction.animationTimingFunction = CAMediaTimingFunction(controlPoints: 0.165, 0.84, 0.44, 1)
rotationNode.eulerAngles = angles
SCNTransaction.commit()
SCNTransaction.unlock()
default:
break
}
}
func stopAnimations() {
SCNTransaction.lock()
SCNTransaction.begin()
SCNTransaction.disableActions = true
rotationNode.eulerAngles = rotationNode.presentation.eulerAngles.normalized
rotationNode.removeAllAnimations()
SCNTransaction.commit()
SCNTransaction.unlock()
}
private func resetReferenceAngles() {
referenceAngles = rotationNode.presentation.eulerAngles
}
}
private final class AdvancedPanGestureRecognizer: UIPanGestureRecognizer {
var earlyTouchEventHandler: (() -> Void)?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if state == .possible {
earlyTouchEventHandler?()
}
}
}
private extension Float {
var normalized: Float {
let angle: Float = self
let π: Float = .pi
let π2: Float = π * 2
if angle > π {
return angle - π2 * ceil(abs(angle) / π2)
} else if angle < -π {
return angle + π2 * ceil(abs(angle) / π2)
} else {
return angle
}
}
}
private extension SCNVector3 {
var normalized: SCNVector3 {
let angles: SCNVector3 = self
return SCNVector3(
x: angles.x.normalized,
y: angles.y.normalized,
z: angles.z.normalized
)
}
}
how can I integrate all cell's gesturemanager..?
I build a circular UICollectionview by following This guide, Everything is working as expected but I don't want the items to rotate around their own angle/anchor
The top row is how my circular collectionview is working at the moment, the bottom drawing is how I would like my collectionview :
I am using following layout attribute code:
class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
var anchorPoint = CGPoint(x: 0.3, y: 0.5)
var angle: CGFloat = 0 {
didSet {
zIndex = Int(angle*1000000)
transform = CGAffineTransformMakeRotation(angle)
}
}
override func copyWithZone(zone: NSZone) -> AnyObject {
let copiedAttributes: CircularCollectionViewLayoutAttributes = super.copyWithZone(zone) as! CircularCollectionViewLayoutAttributes
copiedAttributes.anchorPoint = self.anchorPoint
copiedAttributes.angle = self.angle
return copiedAttributes
}
}
with the following layout class:
class CircularCollectionViewLayout: UICollectionViewLayout {
let itemSize = CGSize(width: 60, height: 110)
var angleAtExtreme: CGFloat {
return collectionView!.numberOfItemsInSection(0) > 0 ? -CGFloat(collectionView!.numberOfItemsInSection(0)-1)*anglePerItem : 0
}
var angle: CGFloat {
return angleAtExtreme*collectionView!.contentOffset.x/(collectionViewContentSize().width - CGRectGetWidth(collectionView!.bounds))
}
var radius: CGFloat = 400 {
didSet {
invalidateLayout()
}
}
var anglePerItem: CGFloat {
return 0.18
}
var attributesList = [CircularCollectionViewLayoutAttributes]()
override func collectionViewContentSize() -> CGSize {
return CGSize(width: CGFloat(collectionView!.numberOfItemsInSection(0))*itemSize.width,
height: CGRectGetHeight(collectionView!.bounds))
}
override class func layoutAttributesClass() -> AnyClass {
return CircularCollectionViewLayoutAttributes.self
}
override func prepareLayout() {
super.prepareLayout()
let centerX = collectionView!.contentOffset.x + (CGRectGetWidth(collectionView!.bounds)/2.0)
let anchorPointY = ((itemSize.height/2.0) + radius)/itemSize.height
let theta = atan2(CGRectGetWidth(collectionView!.bounds)/2.0, radius + (itemSize.height/2.0) - (CGRectGetHeight(collectionView!.bounds)/2.0)) //1
//let theta:CGFloat = 1.0
var startIndex = 0
var endIndex = collectionView!.numberOfItemsInSection(0) - 1
if (angle < -theta) {
startIndex = Int(floor((-theta - angle)/anglePerItem))
}
endIndex = min(endIndex, Int(ceil((theta - angle)/anglePerItem)))
if (endIndex < startIndex) {
endIndex = 0
startIndex = 0
}
attributesList = (startIndex...endIndex).map { (i) -> CircularCollectionViewLayoutAttributes in
let attributes = CircularCollectionViewLayoutAttributes(forCellWithIndexPath: NSIndexPath(forItem: i, inSection: 0))
attributes.size = self.itemSize
attributes.center = CGPoint(x: centerX, y: CGRectGetMidY(self.collectionView!.bounds))
attributes.angle = self.angle + (self.anglePerItem*CGFloat(i))
attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY)
return attributes
}
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributesList
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath)
-> (UICollectionViewLayoutAttributes!) {
return attributesList[indexPath.row]
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var finalContentOffset = proposedContentOffset
let factor = -angleAtExtreme/(collectionViewContentSize().width - CGRectGetWidth(collectionView!.bounds))
let proposedAngle = proposedContentOffset.x*factor
let ratio = proposedAngle/anglePerItem
var multiplier: CGFloat
if (velocity.x > 0) {
multiplier = ceil(ratio)
} else if (velocity.x < 0) {
multiplier = floor(ratio)
} else {
multiplier = round(ratio)
}
finalContentOffset.x = multiplier*anglePerItem/factor
return finalContentOffset
}
}
I tried many things but I was not able to change the cell rotation
I've solved this problem by rotateing the view of cell by negative angle:
Code below:
override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
super.applyLayoutAttributes(layoutAttributes)
let circularlayoutAttributes = layoutAttributes as! CircularCollectionViewLayoutAttributes
self.layer.anchorPoint = circularlayoutAttributes.anchorPoint
viewRoot.transform = CGAffineTransformMakeRotation(-circularlayoutAttributes.angle)
self.center.y += (circularlayoutAttributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds)
}
I'm making a match-3 game using SpriteKit. Explanation: http://www.raywenderlich.com/75273/make-game-like-candy-crush-with-swift-tutorial-part-2. Please refer to swm93's comment on page 4
This is a tutorial but it seems to have a memory leak in the code. Could anyone possibly download this swift project file and find what causes the memory leak and give possible solutions?
The maker of this tutorial said that there is a memory leak in "handleSwipe(swap)" method and that we can fix it by adding "weak" to field declaration. I tried to write "weak var scene: GameScene?" but if I do, it says "scene is nil" even if I initialized it like this: "scene = GameScene(size: skView.bounds.size)" in "viewDidLoad()" function.
The rest of the classes can be downloaded through my link here.
Even the view controllers have been dismissed, the memory use percentage wouldn't decrease... If I call a GameViewController, dismiss it, then call it again, the memory use is twice. In other words,
PreViewController(18MB) segue-> GameViewController(75MB) dismiss-> PreViewController(75MB) segue-> GameViewController(104MB)
import UIKit
import SpriteKit
import AVFoundation
class GameViewController: UIViewController {
// The scene draws the tiles and cookie sprites, and handles swipes.
var scene: GameScene!
// The level contains the tiles, the cookies, and most of the gameplay logic.
// Needs to be ! because it's not set in init() but in viewDidLoad().
var level: Level!
var movesLeft = 0
var score = 0
#IBOutlet weak var targetLabel: UILabel!
#IBOutlet weak var movesLabel: UILabel!
#IBOutlet weak var scoreLabel: UILabel!
#IBOutlet weak var gameOverPanel: UIImageView!
#IBOutlet weak var shuffleButton: UIButton!
var tapGestureRecognizer: UITapGestureRecognizer!
#IBAction func dismiss(sender: UIButton) {
self.dismissViewControllerAnimated(true, completion: {})
}
lazy var backgroundMusic: AVAudioPlayer = {
let url = NSBundle.mainBundle().URLForResource("Mining by Moonlight", withExtension: "mp3")
let player = AVAudioPlayer(contentsOfURL: url, error: nil)
player.numberOfLoops = -1
return player
}()
override func prefersStatusBarHidden() -> Bool {
return true
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
}
override func viewDidLoad() {
super.viewDidLoad()
// Configure the view.
let skView = view as! SKView
skView.multipleTouchEnabled = false
// Create and configure the scene.
scene = GameScene(size: skView.bounds.size)
scene.scaleMode = .AspectFill
// Load the level.
level = Level(filename: "Level_1")
scene.level = level
scene.addTiles()
scene.swipeHandler = handleSwipe
// Hide the game over panel from the screen.
gameOverPanel.hidden = true
shuffleButton.hidden = true
// Present the scene.
skView.presentScene(scene)
// Load and start background music.
backgroundMusic.play()
// Let's start the game!
beginGame()
}
func beginGame() {
movesLeft = level.maximumMoves
score = 0
updateLabels()
level.resetComboMultiplier()
scene.animateBeginGame() {
self.shuffleButton.hidden = false
}
shuffle()
}
func shuffle() {
// Delete the old cookie sprites, but not the tiles.
scene.removeAllCookieSprites()
// Fill up the level with new cookies, and create sprites for them.
let newCookies = level.shuffle()
scene.addSpritesForCookies(newCookies)
}
// This is the swipe handler. MyScene invokes this function whenever it
// detects that the player performs a swipe.
func handleSwipe(swap: Swap) {
// While cookies are being matched and new cookies fall down to fill up
// the holes, we don't want the player to tap on anything.
view.userInteractionEnabled = false
if level.isPossibleSwap(swap) {
level.performSwap(swap)
scene.animateSwap(swap, completion: handleMatches)
} else {
scene.animateInvalidSwap(swap) {
self.view.userInteractionEnabled = true
}
}
}
// This is the main loop that removes any matching cookies and fills up the
// holes with new cookies.
func handleMatches() {
// Detect if there are any matches left.
let chains = level.removeMatches()
// If there are no more matches, then the player gets to move again.
if chains.count == 0 {
beginNextTurn()
return
}
// First, remove any matches...
scene.animateMatchedCookies(chains) {
// Add the new scores to the total.
for chain in chains {
self.score += chain.score
}
self.updateLabels()
// ...then shift down any cookies that have a hole below them...
let columns = self.level.fillHoles()
self.scene.animateFallingCookies(columns) {
// ...and finally, add new cookies at the top.
let columns = self.level.topUpCookies()
self.scene.animateNewCookies(columns) {
// Keep repeating this cycle until there are no more matches.
self.handleMatches()
}
}
}
}
func beginNextTurn() {
level.resetComboMultiplier()
level.detectPossibleSwaps()
view.userInteractionEnabled = true
decrementMoves()
}
func updateLabels() {
targetLabel.text = String(format: "%ld", level.targetScore)
movesLabel.text = String(format: "%ld", movesLeft)
scoreLabel.text = String(format: "%ld", score)
}
func decrementMoves() {
--movesLeft
updateLabels()
if score >= level.targetScore {
gameOverPanel.image = UIImage(named: "LevelComplete")
showGameOver()
}
else if movesLeft == 0 {
gameOverPanel.image = UIImage(named: "GameOver")
showGameOver()
}
}
func showGameOver() {
gameOverPanel.hidden = false
scene.userInteractionEnabled = false
shuffleButton.hidden = true
scene.animateGameOver() {
self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "hideGameOver")
self.view.addGestureRecognizer(self.tapGestureRecognizer)
}
}
func hideGameOver() {
view.removeGestureRecognizer(tapGestureRecognizer)
tapGestureRecognizer = nil
gameOverPanel.hidden = true
scene.userInteractionEnabled = true
beginGame()
}
#IBAction func shuffleButtonPressed(AnyObject) {
shuffle()
// Pressing the shuffle button costs a move.
decrementMoves()
}
}
,
import SpriteKit
class GameScene: SKScene {
// This is marked as ! because it will not initially have a value, but pretty
// soon after the GameScene is created it will be given a Level object, and
// from then on it will always have one (it will never be nil again).
var level: Level!
var swipeHandler: ((Swap) -> ())?
let TileWidth: CGFloat = 32.0
let TileHeight: CGFloat = 36.0
let gameLayer = SKNode()
let cookiesLayer = SKNode()
let tilesLayer = SKNode()
let cropLayer = SKCropNode()
let maskLayer = SKNode()
var swipeFromColumn: Int?
var swipeFromRow: Int?
var selectionSprite = SKSpriteNode()
// Pre-load the resources
let swapSound = SKAction.playSoundFileNamed("Chomp.wav", waitForCompletion: false)
let invalidSwapSound = SKAction.playSoundFileNamed("Error.wav", waitForCompletion: false)
let matchSound = SKAction.playSoundFileNamed("Ka-Ching.wav", waitForCompletion: false)
let fallingCookieSound = SKAction.playSoundFileNamed("Scrape.wav", waitForCompletion: false)
let addCookieSound = SKAction.playSoundFileNamed("Drip.wav", waitForCompletion: false)
// MARK: Game Setup
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder) is not used in this app")
}
override init(size: CGSize) {
super.init(size: size)
anchorPoint = CGPoint(x: 0.5, y: 0.5)
let background = SKSpriteNode(imageNamed: "Background")
addChild(background)
gameLayer.hidden = true
addChild(gameLayer)
let layerPosition = CGPoint(
x: -TileWidth * CGFloat(NumColumns) / 2,
y: -TileHeight * CGFloat(NumRows) / 2)
tilesLayer.position = layerPosition
gameLayer.addChild(tilesLayer)
// We use a crop layer to prevent cookies from being drawn across gaps
// in the level design.
gameLayer.addChild(cropLayer)
// The mask layer determines which part of the cookiesLayer is visible.
maskLayer.position = layerPosition
cropLayer.maskNode = maskLayer
// This layer holds the Cookie sprites. The positions of these sprites
// are relative to the cookiesLayer's bottom-left corner.
cookiesLayer.position = layerPosition
cropLayer.addChild(cookiesLayer)
// nil means that these properties have invalid values.
swipeFromColumn = nil
swipeFromRow = nil
// Pre-load the label font so prevent delays during game play.
SKLabelNode(fontNamed: "GillSans-BoldItalic")
}
func addSpritesForCookies(cookies: Set<Cookie>) {
for cookie in cookies {
// Create a new sprite for the cookie and add it to the cookiesLayer.
let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
sprite.position = pointForColumn(cookie.column, row:cookie.row)
cookiesLayer.addChild(sprite)
cookie.sprite = sprite
// Give each cookie sprite a small, random delay.
sprite.alpha = 0
sprite.xScale = 0.5
sprite.yScale = 0.5
sprite.runAction(
SKAction.sequence([
SKAction.waitForDuration(0.25, withRange: 0.5),
SKAction.group([
SKAction.fadeInWithDuration(0.25),
SKAction.scaleTo(1.0, duration: 0.25)
])
]))
}
}
func removeAllCookieSprites() {
cookiesLayer.removeAllChildren()
}
func addTiles() {
for row in 0..<NumRows {
for column in 0..<NumColumns {
// If there is a tile at this position, then create a new tile
// sprite and add it to the mask layer.
if let tile = level.tileAtColumn(column, row: row) {
let tileNode = SKSpriteNode(imageNamed: "MaskTile")
tileNode.position = pointForColumn(column, row: row)
maskLayer.addChild(tileNode)
}
}
}
// The tile pattern is drawn *in between* the level tiles. That's why
// there is an extra column and row of them.
for row in 0...NumRows {
for column in 0...NumColumns {
let topLeft = (column > 0) && (row < NumRows)
&& level.tileAtColumn(column - 1, row: row) != nil
let bottomLeft = (column > 0) && (row > 0)
&& level.tileAtColumn(column - 1, row: row - 1) != nil
let topRight = (column < NumColumns) && (row < NumRows)
&& level.tileAtColumn(column, row: row) != nil
let bottomRight = (column < NumColumns) && (row > 0)
&& level.tileAtColumn(column, row: row - 1) != nil
// The tiles are named from 0 to 15, according to the bitmask that is
// made by combining these four values.
let value = Int(topLeft) | Int(topRight) << 1 | Int(bottomLeft) << 2 | Int(bottomRight) << 3
// Values 0 (no tiles), 6 and 9 (two opposite tiles) are not drawn.
if value != 0 && value != 6 && value != 9 {
let name = String(format: "Tile_%ld", value)
let tileNode = SKSpriteNode(imageNamed: name)
var point = pointForColumn(column, row: row)
point.x -= TileWidth/2
point.y -= TileHeight/2
tileNode.position = point
tilesLayer.addChild(tileNode)
}
}
}
}
// MARK: Conversion Routines
// Converts a column,row pair into a CGPoint that is relative to the cookieLayer.
func pointForColumn(column: Int, row: Int) -> CGPoint {
return CGPoint(
x: CGFloat(column)*TileWidth + TileWidth/2,
y: CGFloat(row)*TileHeight + TileHeight/2)
}
// Converts a point relative to the cookieLayer into column and row numbers.
func convertPoint(point: CGPoint) -> (success: Bool, column: Int, row: Int) {
// Is this a valid location within the cookies layer? If yes,
// calculate the corresponding row and column numbers.
if point.x >= 0 && point.x < CGFloat(NumColumns)*TileWidth &&
point.y >= 0 && point.y < CGFloat(NumRows)*TileHeight {
return (true, Int(point.x / TileWidth), Int(point.y / TileHeight))
} else {
return (false, 0, 0) // invalid location
}
}
// MARK: Detecting Swipes
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
// Convert the touch location to a point relative to the cookiesLayer.
let touch = touches.first as! UITouch
let location = touch.locationInNode(cookiesLayer)
// If the touch is inside a square, then this might be the start of a
// swipe motion.
let (success, column, row) = convertPoint(location)
if success {
// The touch must be on a cookie, not on an empty tile.
if let cookie = level.cookieAtColumn(column, row: row) {
// Remember in which column and row the swipe started, so we can compare
// them later to find the direction of the swipe. This is also the first
// cookie that will be swapped.
swipeFromColumn = column
swipeFromRow = row
showSelectionIndicatorForCookie(cookie)
}
}
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
// If swipeFromColumn is nil then either the swipe began outside
// the valid area or the game has already swapped the cookies and we need
// to ignore the rest of the motion.
if swipeFromColumn == nil { return }
let touch = touches.first as! UITouch
let location = touch.locationInNode(cookiesLayer)
let (success, column, row) = convertPoint(location)
if success {
// Figure out in which direction the player swiped. Diagonal swipes
// are not allowed.
var horzDelta = 0, vertDelta = 0
if column < swipeFromColumn! { // swipe left
horzDelta = -1
} else if column > swipeFromColumn! { // swipe right
horzDelta = 1
} else if row < swipeFromRow! { // swipe down
vertDelta = -1
} else if row > swipeFromRow! { // swipe up
vertDelta = 1
}
// Only try swapping when the user swiped into a new square.
if horzDelta != 0 || vertDelta != 0 {
trySwapHorizontal(horzDelta, vertical: vertDelta)
hideSelectionIndicator()
// Ignore the rest of this swipe motion from now on.
swipeFromColumn = nil
}
}
}
// We get here after the user performs a swipe. This sets in motion a whole
// chain of events: 1) swap the cookies, 2) remove the matching lines, 3)
// drop new cookies into the screen, 4) check if they create new matches,
// and so on.
func trySwapHorizontal(horzDelta: Int, vertical vertDelta: Int) {
let toColumn = swipeFromColumn! + horzDelta
let toRow = swipeFromRow! + vertDelta
if toColumn < 0 || toColumn >= NumColumns { return }
if toRow < 0 || toRow >= NumRows { return }
// Can't swap if there is no cookie to swap with. This happens when the user
// swipes into a gap where there is no tile.
if let toCookie = level.cookieAtColumn(toColumn, row: toRow),
let fromCookie = level.cookieAtColumn(swipeFromColumn!, row: swipeFromRow!),
let handler = swipeHandler {
// Communicate this swap request back to the ViewController.
let swap = Swap(cookieA: fromCookie, cookieB: toCookie)
handler(swap)
}
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
// Remove the selection indicator with a fade-out. We only need to do this
// when the player didn't actually swipe.
if selectionSprite.parent != nil && swipeFromColumn != nil {
hideSelectionIndicator()
}
// If the gesture ended, regardless of whether if was a valid swipe or not,
// reset the starting column and row numbers.
swipeFromColumn = nil
swipeFromRow = nil
}
override func touchesCancelled(touches: Set<NSObject>, withEvent event: UIEvent) {
touchesEnded(touches, withEvent: event)
}
// MARK: Animations
func animateSwap(swap: Swap, completion: () -> ()) {
let spriteA = swap.cookieA.sprite!
let spriteB = swap.cookieB.sprite!
// Put the cookie you started with on top.
spriteA.zPosition = 100
spriteB.zPosition = 90
let Duration: NSTimeInterval = 0.3
let moveA = SKAction.moveTo(spriteB.position, duration: Duration)
moveA.timingMode = .EaseOut
spriteA.runAction(moveA, completion: completion)
let moveB = SKAction.moveTo(spriteA.position, duration: Duration)
moveB.timingMode = .EaseOut
spriteB.runAction(moveB)
runAction(swapSound)
}
func animateInvalidSwap(swap: Swap, completion: () -> ()) {
let spriteA = swap.cookieA.sprite!
let spriteB = swap.cookieB.sprite!
spriteA.zPosition = 100
spriteB.zPosition = 90
let Duration: NSTimeInterval = 0.2
let moveA = SKAction.moveTo(spriteB.position, duration: Duration)
moveA.timingMode = .EaseOut
let moveB = SKAction.moveTo(spriteA.position, duration: Duration)
moveB.timingMode = .EaseOut
spriteA.runAction(SKAction.sequence([moveA, moveB]), completion: completion)
spriteB.runAction(SKAction.sequence([moveB, moveA]))
runAction(invalidSwapSound)
}
func animateMatchedCookies(chains: Set<Chain>, completion: () -> ()) {
for chain in chains {
animateScoreForChain(chain)
for cookie in chain.cookies {
// It may happen that the same Cookie object is part of two chains
// (L-shape or T-shape match). In that case, its sprite should only be
// removed once.
if let sprite = cookie.sprite {
if sprite.actionForKey("removing") == nil {
let scaleAction = SKAction.scaleTo(0.1, duration: 0.3)
scaleAction.timingMode = .EaseOut
sprite.runAction(SKAction.sequence([scaleAction, SKAction.removeFromParent()]),
withKey:"removing")
}
}
}
}
runAction(matchSound)
runAction(SKAction.waitForDuration(0.3), completion: completion)
}
func animateScoreForChain(chain: Chain) {
// Figure out what the midpoint of the chain is.
let firstSprite = chain.firstCookie().sprite!
let lastSprite = chain.lastCookie().sprite!
let centerPosition = CGPoint(
x: (firstSprite.position.x + lastSprite.position.x)/2,
y: (firstSprite.position.y + lastSprite.position.y)/2 - 8)
let scoreLabel = SKLabelNode(fontNamed: "GillSans-BoldItalic")
scoreLabel.fontSize = 16
scoreLabel.text = String(format: "%ld", chain.score)
scoreLabel.position = centerPosition
scoreLabel.zPosition = 300
cookiesLayer.addChild(scoreLabel)
let moveAction = SKAction.moveBy(CGVector(dx: 0, dy: 3), duration: 0.7)
moveAction.timingMode = .EaseOut
scoreLabel.runAction(SKAction.sequence([moveAction, SKAction.removeFromParent()]))
}
func animateFallingCookies(columns: [[Cookie]], completion: () -> ()) {
var longestDuration: NSTimeInterval = 0
for array in columns {
for (idx, cookie) in enumerate(array) {
let newPosition = pointForColumn(cookie.column, row: cookie.row)
let delay = 0.05 + 0.15*NSTimeInterval(idx)
let sprite = cookie.sprite!
let duration = NSTimeInterval(((sprite.position.y - newPosition.y) / TileHeight) * 0.1)
longestDuration = max(longestDuration, duration + delay)
let moveAction = SKAction.moveTo(newPosition, duration: duration)
moveAction.timingMode = .EaseOut
sprite.runAction(
SKAction.sequence([
SKAction.waitForDuration(delay),
SKAction.group([moveAction, fallingCookieSound])]))
}
}
// Wait until all the cookies have fallen down before we continue.
runAction(SKAction.waitForDuration(longestDuration), completion: completion)
}
func animateNewCookies(columns: [[Cookie]], completion: () -> ()) {
// wait that amount before we trigger the completion block.
var longestDuration: NSTimeInterval = 0
for array in columns {
let startRow = array[0].row + 1
for (idx, cookie) in enumerate(array) {
// Create a new sprite for the cookie.
let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
sprite.position = pointForColumn(cookie.column, row: startRow)
cookiesLayer.addChild(sprite)
cookie.sprite = sprite
// fall after one another.
let delay = 0.1 + 0.2 * NSTimeInterval(array.count - idx - 1)
// Calculate duration based on far the cookie has to fall.
let duration = NSTimeInterval(startRow - cookie.row) * 0.1
longestDuration = max(longestDuration, duration + delay)
let newPosition = pointForColumn(cookie.column, row: cookie.row)
let moveAction = SKAction.moveTo(newPosition, duration: duration)
moveAction.timingMode = .EaseOut
sprite.alpha = 0
sprite.runAction(
SKAction.sequence([
SKAction.waitForDuration(delay),
SKAction.group([
SKAction.fadeInWithDuration(0.05),
moveAction,
addCookieSound])
]))
}
}
// Wait until the animations are done before we continue.
runAction(SKAction.waitForDuration(longestDuration), completion: completion)
}
func animateGameOver(completion: () -> ()) {
let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3)
action.timingMode = .EaseIn
gameLayer.runAction(action, completion: completion)
}
func animateBeginGame(completion: () -> ()) {
gameLayer.hidden = false
gameLayer.position = CGPoint(x: 0, y: size.height)
let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3)
action.timingMode = .EaseOut
gameLayer.runAction(action, completion: completion)
}
// MARK: Selection Indicator
func showSelectionIndicatorForCookie(cookie: Cookie) {
if selectionSprite.parent != nil {
selectionSprite.removeFromParent()
}
if let sprite = cookie.sprite {
let texture = SKTexture(imageNamed: cookie.cookieType.highlightedSpriteName)
selectionSprite.size = texture.size()
selectionSprite.runAction(SKAction.setTexture(texture))
sprite.addChild(selectionSprite)
selectionSprite.alpha = 1.0
}
}
func hideSelectionIndicator() {
selectionSprite.runAction(SKAction.sequence([
SKAction.fadeOutWithDuration(0.3),
SKAction.removeFromParent()]))
}
}
I got the exact same problem as stated in https://stackoverflow.com/questions/28275004/uidynamicanimator-stop-reacting-to-uigravitybehavior. I can't really explain it any better either.
Does anyone know why the UIDynamicAnimator would suddenly stop animating objects/items that have the UIGravityBehavior attached to them?
EDIT
I'm running the example from the Big Nerd Ranch
I modified the example so there are only two cubes, to make reproducing the error easier. The cubes are falling down and are reacting to gravity just fine, but if you tilt the phone so that the cubes end up in the corner, touching each other, then it stops any motion, thus not reacting to the gravity behavior anymore.
I'm also wondering, if this is a Swift issue. Maybe I should try to implement this in Obj-C to see if the error persists.
Here is the example:
//
// ViewController.swift
// Rock Box
//
// Created by Steve Sparks on 7/11/14.
// Copyright (c) 2014 Big Nerd Ranch. All rights reserved.
//
import UIKit
import CoreMotion
class ViewController: UIViewController {
// Most conservative guess. We'll set them later.
var maxX : CGFloat = 320;
var maxY : CGFloat = 320;
let boxSize : CGFloat = 30.0
var boxes : Array<UIView> = []
// For getting device motion updates
let motionQueue = NSOperationQueue()
let motionManager = CMMotionManager()
override func viewDidLoad() {
super.viewDidLoad()
maxX = super.view.bounds.size.width - boxSize;
maxY = super.view.bounds.size.height - boxSize;
// Do any additional setup after loading the view, typically from a nib.
createAnimatorStuff()
generateBoxes()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
NSLog("Starting gravity")
motionManager.startDeviceMotionUpdatesToQueue(motionQueue, withHandler: gravityUpdated)
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
NSLog("Stopping gravity")
motionManager.stopDeviceMotionUpdates()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
NSLog("Memory warning")
// Dispose of any resources that can be recreated.
}
func randomColor() -> UIColor {
let red = CGFloat(CGFloat(arc4random()%100000)/100000)
let green = CGFloat(CGFloat(arc4random()%100000)/100000)
let blue = CGFloat(CGFloat(arc4random()%100000)/100000)
return UIColor(red: red, green: green, blue: blue, alpha: 0.85);
}
func doesNotCollide(testRect: CGRect) -> Bool {
for box : UIView in boxes {
var viewRect = box.frame;
if(CGRectIntersectsRect(testRect, viewRect)) {
return false
}
}
return true
}
func randomFrame() -> CGRect {
var guess = CGRectMake(9, 9, 9, 9)
do {
let guessX = CGFloat(arc4random()) % maxX
let guessY = CGFloat(arc4random()) % maxY;
guess = CGRectMake(guessX, guessY, boxSize, boxSize);
} while(!doesNotCollide(guess))
return guess
}
func addBox(location: CGRect, color: UIColor) -> UIView {
let newBox = UIButton(frame: location)
newBox.backgroundColor = color
view.addSubview(newBox)
addBoxToBehaviors(newBox)
self.boxes.append(newBox)
newBox.tag = Int(arc4random())
newBox.addTarget(self, action:"tapped:", forControlEvents: .TouchUpInside)
return newBox
}
func tapped(sender: UIButton) {
println("sender.tag: ", Int(sender.tag))
}
func generateBoxes() {
for i in 0..<2 {
var frame = randomFrame()
var color = randomColor()
var newBox = addBox(frame, color: color);
}
}
var animator:UIDynamicAnimator? = nil;
let gravity = UIGravityBehavior()
let collider = UICollisionBehavior()
let itemBehavior = UIDynamicItemBehavior()
func createAnimatorStuff() {
animator = UIDynamicAnimator(referenceView:self.view);
gravity.gravityDirection = CGVectorMake(0, 0.8)
animator?.addBehavior(gravity)
// We're bouncin' off the walls
collider.translatesReferenceBoundsIntoBoundary = true
animator?.addBehavior(collider)
itemBehavior.friction = 0.7
itemBehavior.elasticity = 0.1
animator?.addBehavior(itemBehavior)
}
func addBoxToBehaviors(box: UIView) {
gravity.addItem(box)
collider.addItem(box)
itemBehavior.addItem(box)
}
//----------------- Core Motion
func gravityUpdated(motion: CMDeviceMotion!, error: NSError!) {
let grav : CMAcceleration = motion.gravity;
let x = CGFloat(grav.x);
let y = CGFloat(grav.y);
var p = CGPointMake(x,y)
if (error != nil) {
NSLog("\(error)")
}
// Have to correct for orientation.
var orientation = UIApplication.sharedApplication().statusBarOrientation;
if(orientation == UIInterfaceOrientation.LandscapeLeft) {
var t = p.x
p.x = 0 - p.y
p.y = t
} else if (orientation == UIInterfaceOrientation.LandscapeRight) {
var t = p.x
p.x = p.y
p.y = 0 - t
} else if (orientation == UIInterfaceOrientation.PortraitUpsideDown) {
p.x *= -1
p.y *= -1
}
var v = CGVectorMake(p.x, 0 - p.y);
gravity.gravityDirection = v;
}
}
EDIT2
I just noticed that they don't have to touch each other to stop responding to UIGravityBehavior.
EDIT3
Ok, seems to be a bug in Swift somehow. I implemented the same in ObjC and there is no problem whatsoever.
The problem is that the gravity update will cause a call to gravityUpdated() on a thread other than the main thread. You should keep your UI updates to the main thread.
To fix it, either switch to the main thread in your gravityUpdated(), like so:
func gravityUpdated(motion: CMDeviceMotion!, error: NSError!) {
dispatch_async(dispatch_get_main_queue()) {
// rest of your code here
}
}
Or execute CMMotionManager on the main queue instead, like so:
This line
let motionQueue = NSOperationQueue()
changes to
let motionQueue = NSOperationQueue.mainQueue()