Pan gesture (hold/drag) zoom on camera like Snapchat - ios

I am trying to replicate Snapchat camera's zoom feature where once you have started recording you can drag your finger up or down and it will zoom in or out accordingly. I have been successful with zooming on pinch but have been stuck on zooming with the PanGestureRecognizer.
Here is the code I've tried the problem is that I do not know how to replace the sender.scale that I use for pinch gesture recognizer zooming. I'm using AVFoundation. Basically, I'm asking how I can do the hold zoom (one finger drag) like in TikTok or Snapchat properly.
let minimumZoom: CGFloat = 1.0
let maximumZoom: CGFloat = 15.0
var lastZoomFactor: CGFloat = 1.0
var latestDirection: Int = 0
#objc func panGesture(_ sender: UIPanGestureRecognizer) {
let velocity = sender.velocity(in: doubleTapSwitchCamButton)
var currentDirection: Int = 0
if velocity.y > 0 || velocity.y < 0 {
let originalCapSession = captureSession
var devitce : AVCaptureDevice!
let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDuoCamera], mediaType: AVMediaType.video, position: .unspecified)
let devices = videoDeviceDiscoverySession.devices
devitce = devices.first!
guard let device = devitce else { return }
// Return zoom value between the minimum and maximum zoom values
func minMaxZoom(_ factor: CGFloat) -> CGFloat {
return min(min(max(factor, minimumZoom), maximumZoom), device.activeFormat.videoMaxZoomFactor)
}
func update(scale factor: CGFloat) {
do {
try device.lockForConfiguration()
defer { device.unlockForConfiguration() }
device.videoZoomFactor = factor
} catch {
print("\(error.localizedDescription)")
}
}
//These 2 lines below are the problematic ones, pinch zoom uses this one below, and the newScaleFactor below that is a testing one that did not work.
let newScaleFactor = minMaxZoom(sender.scale * lastZoomFactor)
//let newScaleFactor = CGFloat(exactly: number + lastZoomFactor)
switch sender.state {
case .began: fallthrough
case .changed: update(scale: newScaleFactor!)
case .ended:
lastZoomFactor = minMaxZoom(newScaleFactor!)
update(scale: lastZoomFactor)
default: break
}
} else {
}
latestDirection = currentDirection
}

You can use the gesture recogniser's translation property, to calculate the displacement in points, and normalise this displacement as a zoom factor.
Fitting this into your code, you could try:
... somewhere in your view setup code, i.e. viewDidLoad....
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGesture))
button.addGestureRecognizer(panGestureRecognizer)
private var initialZoom: CGFloat = 1.0
#objc func panGesture(_ sender: UIPanGestureRecognizer) {
// note that 'view' here is the overall video preview
let velocity = sender.velocity(in: view)
if velocity.y > 0 || velocity.y < 0 {
let originalCapSession = captureSession
var devitce : AVCaptureDevice!
let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDuoCamera], mediaType: AVMediaType.video, position: .unspecified)
let devices = videoDeviceDiscoverySession.devices
devitce = devices.first!
guard let device = devitce else { return }
let minimumZoomFactor: CGFloat = 1.0
let maximumZoomFactor: CGFloat = min(device.activeFormat.videoMaxZoomFactor, 10.0) // artificially set a max useable zoom of 10x
// clamp a zoom factor between minimumZoom and maximumZoom
func clampZoomFactor(_ factor: CGFloat) -> CGFloat {
return min(max(factor, minimumZoomFactor), maximumZoomFactor)
}
func update(scale factor: CGFloat) {
do {
try device.lockForConfiguration()
defer { device.unlockForConfiguration() }
device.videoZoomFactor = factor
} catch {
print("\(error.localizedDescription)")
}
}
switch sender.state {
case .began:
initialZoom = device.videoZoomFactor
startRecording() /// call to start recording your video
case .changed:
// distance in points for the full zoom range (e.g. min to max), could be view.frame.height
let fullRangeDistancePoints: CGFloat = 300.0
// extract current distance travelled, from gesture start
let currentYTranslation: CGFloat = sender.translation(in: view).y
// calculate a normalized zoom factor between [-1,1], where up is positive (ie zooming in)
let normalizedZoomFactor = -1 * max(-1,min(1,currentYTranslation / fullRangeDistancePoints))
// calculate effective zoom scale to use
let newZoomFactor = clampZoomFactor(initialZoom + normalizedZoomFactor * (maximumZoomFactor - minimumZoomFactor))
// update device's zoom factor'
update(scale: newZoomFactor)
case .ended, .cancelled:
stopRecording() /// call to start recording your video
break
default:
break
}
}
}

Related

Running swift condition just once on translation

I created a UIView and a UIImageView which is inside the UIView as a subview, then I added a pan gesture to the UIImageView to slide within the UIView, the image slides now but the problem I have now is when the slider gets to the end of the view if movex > xMax, I want to print this just once print("SWIPPERD movex"). The current code I have there continues to print print("SWIPPERD movex") as long as the user does not remove his/her hand from the UIImageView which is used to slide
private func swipeFunc() {
let swipeGesture = UIPanGestureRecognizer(target: self, action: #selector(acknowledgeSwiped(sender:)))
sliderImage.addGestureRecognizer(swipeGesture)
swipeGesture.delegate = self as? UIGestureRecognizerDelegate
}
#objc func acknowledgeSwiped(sender: UIPanGestureRecognizer) {
if let sliderView = sender.view {
let translation = sender.translation(in: self.baseView) //self.sliderView
switch sender.state {
case .began:
startingFrame = sliderImage.frame
viewCenter = baseView.center
fallthrough
case .changed:
if let startFrame = startingFrame {
var movex = translation.x
if movex < -startFrame.origin.x {
movex = -startFrame.origin.x
print("SWIPPERD minmax")
}
let xMax = self.baseView.frame.width - startFrame.origin.x - startFrame.width - 15 //self.sliderView
if movex > xMax {
movex = xMax
print("SWIPPERD movex")
}
var movey = translation.y
if movey < -startFrame.origin.y { movey = -startFrame.origin.y }
let yMax = self.baseView.frame.height - startFrame.origin.y - startFrame.height //self.sliderView
if movey > yMax {
movey = yMax
// print("SWIPPERD min")
}
sliderView.transform = CGAffineTransform(translationX: movex, y: movey)
}
default: // .ended and others:
UIView.animate(withDuration: 0.1, animations: {
sliderView.transform = CGAffineTransform.identity
})
}
}
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return sliderImage.frame.contains(point)
}
You may want to use the .ended state instead of .changed state, based on your requirements. And you've mentioned you want to get the right direction only. You could try below to determine if the swipe came from right to left, or vice-versa, change as you wish:
let velocity = sender.velocity(in: sender.view)
let rightToLeftSwipe = velocity.x < 0

Scaling a SCNNode in ARKit

I am trying to scale a 3D model of a chair in ARKit using SceneKit. Here is my code for the pinch gesture:
#objc func pinched(recognizer :UIPinchGestureRecognizer) {
var deltaScale :CGFloat = 0.0
deltaScale = 1 - self.lastScale - recognizer.scale
print(recognizer.scale)
let sceneView = recognizer.view as! ARSCNView
let touchPoint = recognizer.location(in: sceneView)
let scnHitTestResults = self.sceneView.hitTest(touchPoint, options: nil)
if let hitTestResult = scnHitTestResults.first {
let chairNode = hitTestResult.node
chairNode.scale = SCNVector3(deltaScale,deltaScale,deltaScale)
self.lastScale = recognizer.scale
}
}
It does scale but for some weird reason it inverts the 3D model upside down. Any ideas why? Also although the scaling works but it is not as smooth and kinda jumps from different scale factors when used in multiple progressions using pinch to zoom.
Here is how I scale my nodes:
/// Scales An SCNNode
///
/// - Parameter gesture: UIPinchGestureRecognizer
#objc func scaleObject(gesture: UIPinchGestureRecognizer) {
let location = gesture.location(in: sceneView)
let hitTestResults = sceneView.hitTest(location)
guard let nodeToScale = hitTestResults.first?.node else {
return
}
if gesture.state == .changed {
let pinchScaleX: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.x))
let pinchScaleY: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.y))
let pinchScaleZ: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.z))
nodeToScale.scale = SCNVector3Make(Float(pinchScaleX), Float(pinchScaleY), Float(pinchScaleZ))
gesture.scale = 1
}
if gesture.state == .ended { }
}
In my example current node refers to an SCNNode, although you can set this however you like.

How to be able to zoom with front facing camera

In my custom camera, I implemented a pinch gesture recognizer. Then i added a function for the video to be able to zoom. The camera zooms when I am on the back camera, but when i switch it to the front facing camera, I am not allowed to zoom. This is my function
var zoomFactor: CGFloat = 1.0
#objc func pinch(_ pinch: UIPinchGestureRecognizer) {
print("123")
guard let device = videoCaptureDevice else { return }
func minMaxZoom(_ factor: CGFloat) -> CGFloat { return min(max(factor, 1.0), device.activeFormat.videoMaxZoomFactor) }
func update(scale factor: CGFloat) {
do {
try device.lockForConfiguration()
defer { device.unlockForConfiguration() }
device.videoZoomFactor = factor
} catch {
debugPrint(error)
}
}
let newScaleFactor = minMaxZoom(pinch.scale * zoomFactor)
switch pinch.state {
case .began: fallthrough
case .changed: update(scale: newScaleFactor)
case .ended:
zoomFactor = minMaxZoom(newScaleFactor)
update(scale: zoomFactor)
default: break
}
}

UIKit Dynamics with pan, rotate and pinch gesture

I have added UIDynamics to imageview and used pan gesture for that. It is working fine with pan gesture but when I apply pinch gesture with that it is not working. It is showing large imageview but when I start dragging then it is changed to original size.
Here is my code:
func handleAttachmentGesture(_ sender: UIPanGestureRecognizer) {
let location = sender.location(in: emojiSuperView!)
let boxLocation = sender.location(in: self)
switch sender.state {
case .began:
print("Your touch start position is \(location)")
print("Start location in image is \(boxLocation)")
animator.removeAllBehaviors()
let centerOffset = UIOffset(horizontal: boxLocation.x - self.bounds.midX, vertical: boxLocation.y - self.bounds.midY)
attachmentBehavior = UIAttachmentBehavior(item: self, offsetFromCenter: centerOffset, attachedToAnchor: location)
animator.addBehavior(attachmentBehavior)
case .ended:
print("Your touch end position is \(location)")
print("End location in image is \(boxLocation)")
animator.removeAllBehaviors()
// 1
let velocity = sender.velocity(in: emojiSuperView!)
let magnitude = sqrt((velocity.x * velocity.x) + (velocity.y * velocity.y))
if magnitude > ThrowingThreshold {
// 2
let pushBehavior = UIPushBehavior(items: [self], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: velocity.x / 10, dy: velocity.y / 10)
pushBehavior.magnitude = magnitude / ThrowingVelocityPadding
self.pushBehavior = pushBehavior
animator.addBehavior(pushBehavior)
// 3
let angle = Int(arc4random_uniform(20)) - 10
itemBehavior = UIDynamicItemBehavior(items: [self])
itemBehavior.friction = 0.2
itemBehavior.allowsRotation = true
itemBehavior.addAngularVelocity(CGFloat(angle), for: self)
animator.addBehavior(itemBehavior)
}
default:
attachmentBehavior.anchorPoint = sender.location(in: emojiSuperView!)
break
}
}
func recognizePinchGesture(sender: UIPinchGestureRecognizer)
{
weak var dynamicItem: UIDynamicItem?
// whatever your item is, probably a UIView
dynamicItem = self
let behavior = UIGravityBehavior(items: [dynamicItem!])
let animator = UIDynamicAnimator(referenceView: emojiSuperView!)
// or however you're getting your animator
animator.addBehavior(behavior)
sender.view!.transform = sender.view!.transform.scaledBy(x: sender.scale, y: sender.scale)
animator.updateItem(usingCurrentState: self)
self.animator.updateItem(usingCurrentState: self)
sender.scale = 1
}
When a user does any transform event, save current transform to a global variable.
after that when panning start assigns new transform in began state using UIAttachmentBehavior's action property.
attachmentBehavior.action = {
self.attachmentBehavior.items[0].transform = self.aTransform
}

Memory Leak and its solutions using weak declaration in Swift

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()]))
}
}

Resources