I have array with 6 objects and I am displaying like below.
What I need is I want to get horizontal row count as 3
How Can I get that with collection view..?
I am using below code to setting cell in center of the screen
class CollectionViewRow {
var attributes = [UICollectionViewLayoutAttributes]()
var spacing: CGFloat = 0
init(spacing: CGFloat) {
self.spacing = spacing
}
func add(attribute: UICollectionViewLayoutAttributes) {
attributes.append(attribute)
}
var rowWidth: CGFloat {
return attributes.reduce(0, { result, attribute -> CGFloat in
return result + attribute.frame.width
}) + CGFloat(attributes.count - 1) * spacing
}
func centerLayout(collectionViewWidth: CGFloat) {
let padding = (collectionViewWidth - rowWidth) / 2
var offset = padding
for attribute in attributes {
attribute.frame.origin.x = offset
offset += attribute.frame.width + spacing
}
}
}
class UICollectionViewCenterLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
var rows = [CollectionViewRow]()
var currentRowY: CGFloat = -1
for attribute in attributes {
if currentRowY != attribute.frame.midY {
currentRowY = attribute.frame.midY
rows.append(CollectionViewRow(spacing: 10))
}
rows.last?.add(attribute: attribute)
}
rows.forEach { $0.centerLayout(collectionViewWidth: collectionView?.frame.width ?? 0) }
return rows.flatMap { $0.attributes }
}
}
If you're creating your own layout subclass then you can add a value to it like...
class UICollectionViewCenterLayout: UICollectionViewFlowLayout {
var numberOfRows: Int = 0
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
var rows = [CollectionViewRow]()
var currentRowY: CGFloat = -1
for attribute in attributes {
if currentRowY != attribute.frame.midY {
currentRowY = attribute.frame.midY
rows.append(CollectionViewRow(spacing: 10))
}
rows.last?.add(attribute: attribute)
}
rows.forEach { $0.centerLayout(collectionViewWidth: collectionView?.frame.width ?? 0) }
// add this bit
self.numberOfRows = rows.count
return rows.flatMap { $0.attributes }
}
}
Then in your view you can access the layout (or better still store it as a var) and get the numberOfRows from it.
if let layout = self.collectionView.collectionViewLayout as? namesLayout {
print("numberOfRows===\(layout.numberOfRows)")
rowsCount = layout.numberOfRows
}
Also... don't call your own layout UICollectionView... just call it like CenterLayout or NamesLayout or something. :D Keeps it shorter.
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 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.
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()