UICollectionView for non-grid or semi-grid layout - ios

I am trying to implement a multi-row sequence of items (like Video editing sequence in Final Cut Pro or Adobe Premiere pro shown below).
While I one can always implement it using UIScrollView and placing custom views manually, it would be tedious particularly in reordering items and animating changes and also zooming across the timeline using pinch gesture. Is it possible to implement it using UICollectionView using UICollectionViewCompositionalLayout and UICollectionViewDiffableDataSource? From WWDC videos, it seems almost everything is possible using compositional layout but it isn't clear if it is possible to implement a timeline using it. Maybe UICollectionView is not the right paradigm for this use case and one should use UIScrollView? Even if I use UIScrollView, managing things like dragging & reordering items, animating datasource changes, trimming items, zooming the content are going to be issues. Any pointers to existing code base that implements these features?

Here is my playground code as a partial answer for a simple empty iOS Playground file. It should give you a basic idea how to implement it using SpriteKit. I didn't add any animations and the scene so far has a fixed width and the "camera" is also fixed and doesn't allow zooming yet. But I wanted to give you something so you can decided if this is even the right solution for you.
import UIKit
import SpriteKit
import PlaygroundSupport
class MyViewController: UIViewController {
override func loadView() {
// Setting up a basic UIView as parent
let parentView = UIView()
parentView.frame = CGRect(x: 0, y: 0, width: 600, height: 600)
parentView.backgroundColor = .black
// Defining the SKView
let tracksSKView = SKView(frame: parentView.frame)
tracksSKView.ignoresSiblingOrder = false
// Options to debug visually
// tracksSKView.showsNodeCount = true
// tracksSKView.showsPhysics = true
// tracksSKView.showsFields = true
// tracksSKView.showsLargeContentViewer = true
// Defining our subclassed SKScene
let scene = GameScene(size: tracksSKView.bounds.size)
// Presenting and adding views and sceens
tracksSKView.presentScene(scene)
parentView.addSubview(tracksSKView)
self.view = parentView
}
}
//MARK: - Custom SKScene
class GameScene: SKScene {
let trackSize = CGSize(width: 2048, height: 120)
let tracksCount = 4
// Hardcoded clips, use your data source and update when a clip has been moved in any way.
let clips: [Clip] = [
Clip(name: "SongA", track: 1, xPosition: 0, lengh: 245),
Clip(name: "SongB", track: 2, xPosition: 200, lengh: 166, color: .blue),
Clip(name: "SongC", track: 3, xPosition: 200, lengh: 256, color: .red)
]
var touchingClip = false
var touchedClip = SKNode()
// Bacically like loadView or viewDidLoad
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
self.size = CGSize(width: 1024, height: 768)
self.name = "scene"
addTracks(amount: tracksCount)
addClips(clips: clips)
}
// Adding x amount of tracks.
func addTracks(amount: Int) {
for n in 0..<amount {
let trackNode = SKSpriteNode(color: n%2 == 0 ? .systemGray : .systemGray2, size: trackSize)
// Setting up physical propeties of the border of the track
trackNode.physicsBody = SKPhysicsBody(edgeLoopFrom: trackNode.frame)
trackNode.physicsBody?.restitution = 0.2
trackNode.physicsBody?.allowsRotation = false
trackNode.physicsBody?.affectedByGravity = false
trackNode.physicsBody?.isDynamic = false
// Positioning the track
trackNode.zPosition = -1
trackNode.position.y = frame.minY + trackSize.height / 2 + CGFloat(n) * trackSize.height
addChild(trackNode)
}
}
// Adding the Clip objects stored in an array.
func addClips(clips: [Clip]) {
for clip in clips {
let clipNode = SKSpriteNode(color: clip.color, size: CGSize(width: clip.lengh, height: Int(trackSize.height) - 20))
clipNode.position.x = clip.xPosition + CGFloat(clip.lengh / 2)
clipNode.position.y = frame.minY + (trackSize.height * CGFloat(clip.track)) + 1
clipNode.zPosition = 1
clipNode.physicsBody = SKPhysicsBody(rectangleOf: clipNode.frame.size)
clipNode.physicsBody?.affectedByGravity = true
clipNode.physicsBody?.allowsRotation = false
clipNode.physicsBody?.restitution = 0.2
addChild(clipNode)
}
}
//MARK: - User interaction
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
// getting all nodes the user touched (visible and hidden below others.
let tappedNodes = nodes(at: location)
//getting the top node
if let node = tappedNodes.first {
touchedClip = node
touchingClip = true
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard touchingClip else { return }
// Moving the clip (node) based on the movement of the touch. It's very basic and can look jittery. Using the animate methods would create better results.
for touch in touches {
let location = touch.location(in: self)
touchedClip.position = location
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touchingClip = false
}
}
//MARK: - Interaction in between object like collisions etc.
extension GameScene: SKPhysicsContactDelegate {
// handle different contact cases here
}
//MARK: - Clip object
struct Clip {
var name: String
var track: Int
var xPosition: CGFloat
var lengh: Int
var color: UIColor = .green
}
PlaygroundPage.current.liveView = MyViewController()
I've added a gesture recognizer for a long press to move the clips, while touch and pan is not resizing the clip. Here is the new code:
import UIKit
import SpriteKit
import PlaygroundSupport
PlaygroundPage.current.liveView = MyViewController()
class MyViewController: UIViewController {
override func loadView() {
// Setting up a basic UIView as parent
let parentView = UIView()
parentView.frame = CGRect(x: 0, y: 0, width: 600, height: 600)
parentView.backgroundColor = .black
// Defining the SKView
let tracksSKView = SKView(frame: parentView.frame)
tracksSKView.ignoresSiblingOrder = false
// Options to debug visually
tracksSKView.showsNodeCount = true
tracksSKView.showsPhysics = true
tracksSKView.showsFields = true
tracksSKView.showsLargeContentViewer = true
// Defining our subclassed SKScene
let scene = GameScene(size: tracksSKView.bounds.size)
// Presenting and adding views and sceens
tracksSKView.presentScene(scene)
parentView.addSubview(tracksSKView)
self.view = parentView
}
}
//MARK: - Custom SKScene
class GameScene: SKScene {
let trackSize = CGSize(width: 2048, height: 120)
let tracksCount = 4
// Hardcoded clips, use your data source and update when a clip has been moved in any way.
let clips: [Clip] = [
Clip(name: "SongA", track: 1, xPosition: 0, lengh: 245),
Clip(name: "SongB", track: 2, xPosition: 200, lengh: 166, color: .blue),
Clip(name: "SongC", track: 3, xPosition: 200, lengh: 256, color: .red)
]
// Different interactions, I used a sepperate variable for each interaction instead of one to be able to add more later.
var touchingClip = false
var movingClip = false
var resizingClip = true
var touchedClip = SKNode()
var location = CGPoint()
// Bacically like loadView or viewDidLoad
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
// Using the UI gesture recognizer in the case of a long press seemed easier than trying to figure out the gestures in the touches methods.
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameScene.longPress))
self.view!.addGestureRecognizer(longPressRecognizer)
// Adding tracks and clips
addTracks(amount: tracksCount)
addClips(clips: clips)
}
// Method that handles the long press
#objc func longPress(sender: UILongPressGestureRecognizer) {
if sender.state == .began || sender.state == .changed {
movingClip = true
resizingClip = false
} else {
movingClip = false
resizingClip = true
}
location = sender.location(in: self.view)
}
//MARK: - Setting up the tracks and clips
// Adding x amount of tracks.
func addTracks(amount: Int) {
for n in 0..<amount {
let trackNode = SKSpriteNode(color: n%2 == 0 ? .systemGray : .systemGray2, size: trackSize)
// Setting up physical propeties of the border of the track
trackNode.physicsBody = SKPhysicsBody(edgeLoopFrom: trackNode.frame)
trackNode.physicsBody?.restitution = 0.2
trackNode.physicsBody?.allowsRotation = false
trackNode.physicsBody?.affectedByGravity = false
trackNode.physicsBody?.isDynamic = false
// Positioning the track
trackNode.zPosition = -1
trackNode.position.y = frame.minY + trackSize.height / 2 + CGFloat(n) * trackSize.height
addChild(trackNode)
}
}
// Adding the Clip objects stored in an array.
func addClips(clips: [Clip]) {
for clip in clips {
let clipNode = SKSpriteNode(color: clip.color, size: CGSize(width: clip.lengh, height: Int(trackSize.height) - 20))
clipNode.name = clip.name
clipNode.position.x = clip.xPosition + CGFloat(clip.lengh / 2)
clipNode.position.y = frame.minY + (trackSize.height * CGFloat(clip.track)) + 1
clipNode.zPosition = 1
clipNode.physicsBody = SKPhysicsBody(rectangleOf: clipNode.frame.size)
clipNode.physicsBody?.affectedByGravity = true
clipNode.physicsBody?.allowsRotation = false
clipNode.physicsBody?.restitution = 0.2
clipNode.physicsBody?.isDynamic = true
addChild(clipNode)
}
}
//MARK: - User interaction
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard touches.first != nil else { return }
for touch in touches {
let location = touch.location(in: self)
touchedClip = atPoint(location) as! SKSpriteNode
if clips.contains(where: { $0.name == touchedClip.name }) {
touchingClip = true
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard touchingClip else { return }
for touch in touches {
if resizingClip {
let resizeValue = touch.location(in: touchedClip).x - touch.previousLocation(in: touchedClip).x
// Checking that we're only adding width to the clip or trimming no more then the remaining width.
if resizeValue > 0 || (resizeValue < 0 && abs(resizeValue) < touchedClip.frame.size.width) {
let action = SKAction.resize(byWidth: resizeValue, height: 0, duration: 0.0)
action.timingMode = .linear
touchedClip.run(action)
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touchingClip = false
resizingClip = true
movingClip = false
}
//MARK: - Scene update
// Runs as long as scene is active once per frame (target of 60 frames per second)
override func update(_ currentTime: TimeInterval) {
// The moving needs to be done in the update method, the touches methods are unresponsive while the gesture recognizer is active.
if movingClip && touchingClip {
let newLocation = convertPoint(fromView: location)
let action = SKAction.move(to: newLocation, duration: 0.1)
action.timingMode = .easeInEaseOut
touchedClip.run(action)
}
// The physics body does not change when the clip node is resized. I'm updating it here.
if resizingClip && touchingClip {
touchedClip.physicsBody = SKPhysicsBody(rectangleOf: touchedClip.frame.size)
touchedClip.physicsBody?.affectedByGravity = true
touchedClip.physicsBody?.allowsRotation = false
touchedClip.physicsBody?.restitution = 0.2
touchedClip.physicsBody?.isDynamic = true
}
}
}
//MARK: - Interaction in between object like collisions etc.
extension GameScene: SKPhysicsContactDelegate {
// handle different contact cases here
}
//MARK: - Clip object
struct Clip {
var name: String
var track: Int
var xPosition: CGFloat
var lengh: Int
var color: UIColor = .green
}
Sources:
www.udemy.com/course/dive-into-spritekit (Pretty good, but not great)
designcode.io (Not recommended)
stackoverflow.com/questions/30337608/detect-long-touch-in-sprite-kit
as well as more SO and Apple Dev :)

Related

Collision in Spritekit not collide with another node

I describe as StackOverflow standards the following issue.
Summarize the problem
I have issue about colliding two nodes. One is composed by a crowd and each people is a single item of my crowd defined in the same way (included in a while just to be clear and using index "i" and "j" to create the row of the crowd). I wanted to make disappear once arrive a node (as civilian) to the bottom and going to the top and the crowd remove the civilian spawned along the path. Actually I have this thing and the func tells me that the colliding happens but it didn't 'cause nothing happens and the civilian node captured by crowd it didn't disappear or removed with the removefromparent(). I've got no error messages with my compiler, it works for him. My scope is : detecting node civilian during the path by the crowd and remove this one from the path.
What I've tried
I tried many things to fix this. The first thing is following a lot of tutorials about how Collision Masks etc.. work. I know what they do. But what I've tried it was to make a invisible line for the last line of crowd of people just to see if the problem is the crowd itself and making that if the civilian node arrives to collide the invisible line is like he was in contact with the crowd but it didin't this effect. I followed a lot of tutorial such as HackingWithswift, Youtube tutorials but the procedure for me it's clear but nothing happens (sorry for being repetitive).
Show code
My problem is about this GameScene.sks because it it just one file with all the functions.
import SpriteKit
import GameplayKit
enum CategoryMask: UInt32 {
case civilian_value = 1
case crowd_value = 2
case background_value = 0
}
enum GameState {
case ready
case playing
case dead
}
var gameState = GameState.ready {
didSet {
print(gameState)
}
}
class GameScene: SKScene, SKPhysicsContactDelegate {
let player = SKSpriteNode(imageNamed: "player1")
let textureA = SKTexture(imageNamed: "player1")
let textureB = SKTexture(imageNamed: "player2")
let pause = SKSpriteNode(imageNamed: "pause-button")
let resume = SKSpriteNode(imageNamed: "pause-button")
var civilian = SKSpriteNode()
let pauseLayer = SKNode()
let gameLayer = SKNode()
weak var sceneDelegate: GameSceneDelegate?
//main
override func didMove(to view: SKView) {
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
self.physicsWorld.contactDelegate = self
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
//func for dynamic background
moveBackground(image: ["background1", "background2", "background3", "background1"], x: 0, z: -3, duration: 5, size: CGSize(width: 0.5, height: 1.0))
character(player: player)
run(SKAction.repeatForever(
SKAction.sequence([
SKAction.run(civilians),
SKAction.wait(forDuration: 3.0)])))
run(SKAction.run(crowdSpawn))
pause.name="pause"
pause.position = CGPoint(x: frame.minX/1.3, y: frame.minY/1.15)
pause.size=CGSize(width: 0.1, height: 0.1)
pause.zPosition = 4
addChild(pause)
if self.scene?.isPaused == true {
resume.name="resume"
resume.position = CGPoint(x: frame.minX/1.5, y: frame.minY/1.15)
resume.size=CGSize(width: 0.1, height: 0.1)
resume.zPosition = 12
addChild(resume)
}
}
func pauseGame() {
sceneDelegate?.gameWasPaused()
let barr = SKSpriteNode()
let barrbehind = SKSpriteNode()
let buttonresume = SKSpriteNode(imageNamed: "back")
barrbehind.name = "barrbehind"
barrbehind.zPosition = 9
barrbehind.color = SKColor.black
barrbehind.size = CGSize(width: frame.width, height: frame.height)
barrbehind.alpha = 0.5
self.addChild(barradietro)
barr.name = "bar"
barr.size = CGSize(width: 0.4, height: 0.5)
barr.color = SKColor.white
barr.zPosition = 10
self.addChild(barr)
buttonresume.name = "resume"
buttonresume.zPosition = 11
buttonresume.color = SKColor.black
buttonresume.size = CGSize(width: 0.1, height: 0.1)
buttonresume.alpha = 0.5
self.addChild(buttonresume)
self.scene?.isPaused = true
}
//random func (it helps for generate randomly civilians along the path
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
//func to define civilians
func civilians() {
let civilian = SKSpriteNode(imageNamed: "PV")
civilian.name = "civilian"
//posiziono il civile
civilian.position = CGPoint(x: frame.size.width/8.0 * random(min: -1.5, max: 1.5), y: -frame.size.height * 0.45)
civilian.physicsBody = SKPhysicsBody(rectangleOf: civilian.size)
civilian.zPosition = 3
civilian.physicsBody?.categoryBitMask = CategoryMask.civilian_value.rawValue
civilian.physicsBody?.collisionBitMask = CategoryMask.crowd_value.rawValue
civilian.physicsBody?.contactTestBitMask = CategoryMask.crowd_value.rawValue
civilian.physicsBody?.isDynamic = true
//civilian size
civilian.size=CGSize(width: 0.2, height: 0.2)
//civilian movement
civilian.run(
SKAction.moveBy(x: 0.0, y: frame.size.height + civilian.size.height,duration: TimeInterval(1.77)))
addChild(civilian)
}
//func for the main character
func character(player: SKSpriteNode){
player.position = CGPoint(x: 0, y: 0)
player.size = CGSize(width: 0.2, height: 0.2)
let animation = SKAction.animate(with: [textureB,textureA], timePerFrame:0.2)
player.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(player)
player.run(SKAction.repeatForever(animation))
}
//func for generate the crowd
func crowdSpawn(){
var i = 0.0
var j = 0.25
var crowdRaw : Bool = true
while crowdRaw {
if i <= 1 {
let crowd = SKSpriteNode(imageNamed: "player1")
crowd.name = "crowd"
//posiziono il civile
crowd.size=CGSize(width: 0.15, height: 0.15)
crowd.position = CGPoint(x: -frame.size.width / 3.6 + CGFloat(i)/2 * crowd.size.width , y: frame.size.height / 2 + (CGFloat(j)*2) * -crowd.size.height)
crowd.zPosition = 3
let animation = SKAction.animate(with: [textureB,textureA], timePerFrame:0.25)
crowd.run(SKAction.repeatForever(animation))
crowd.run(SKAction.moveBy(x: frame.size.width / 16.0 + CGFloat(i) * crowd.size.width, y: 0, duration: 0))
let infectedCollision = SKSpriteNode(color: UIColor.red,
size: CGSize(width: 1, height: 0.1))
infectedCollision.physicsBody = SKPhysicsBody(rectangleOf: infectedCollision.size)
infectedCollision.physicsBody?.categoryBitMask = CategoryMask.crowd_value.rawValue
//collisionBitMask : qui la linea della folla non può collidere con il civilian
infectedCollision.physicsBody?.collisionBitMask = CategoryMask.civilian_value.rawValue
infectedCollision.physicsBody?.contactTestBitMask = CategoryMask.civilian_value.rawValue
infectedCollision.physicsBody?.isDynamic = true
infectedCollision.name = "infectedCollision"
infectedCollision.position = crowd.position
addChild(crowd)
addChild(infectedCollision)
i += 0.25
} else {
j += 0.25
i = 0.0
}
if j == 1 {
crowdRaw = false
}
}
}
func didBegin(_ contact: SKPhysicsContact) {
if contact.bodyA.node?.position == contact.bodyB.node?.position {
let actionMoveDone = SKAction.removeFromParent()
civilian.run(SKAction.sequence([actionMoveDone]))
}
}
//func about the touches
func touchDown(atPoint pos : CGPoint) {
let action = SKAction.move(to: pos, duration: 1.0)
// playerSprite is a SpriteKit sprite node.
player.run(action)
}
//func about the touches
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
switch gameState {
case .ready:
gameState = .playing
case .playing:
for t in touches {
let location = t.location(in: self)
player.position.x = location.x/2
for node in self.nodes(at: location){
if node.name == "civilian" {
let explode = SKAction.colorize(with: UIColor.systemBlue,colorBlendFactor: 5.0, duration: 2)
let vanish = SKAction.fadeOut(withDuration: 2.0)
node.run(explode , completion: {
node.run(vanish) {
node.removeFromParent()
}
})
}else if node.name == "pause" {
pauseGame()
}else if node.name == "resume" {
self.scene?.isPaused = false
}
}
}
case .dead:
print("dead")
}
}
//function to have different backgrounds in scrolling (3 backgrounds in a loop)
func moveBackground(image: [String], x: CGFloat, z:CGFloat, duration: Double, size: CGSize) {
for i in 0...3 {
let background = SKSpriteNode(imageNamed: image[i])
background.position = CGPoint(x: x, y: size.height * CGFloat(i))
background.size = size
background.zPosition = z
let move = SKAction.moveBy(x: 0, y: -background.size.height*3, duration: 0)
let back = SKAction.moveBy(x: 0, y: background.size.height*3, duration: duration)
let sequence = SKAction.sequence([move,back])
let repeatAction = SKAction.repeatForever(sequence)
addChild(background)
background.run(repeatAction)
}
}
}
Ok, it was fun to recall how all this SpriteKit stuff works :D
First problem you have is node/sprite creation. The solution could be some kind of Factory pattern with more or less abstraction. GameScene doesn't have to know how nodes are initialized/configured. Scene could know only which type of nodes exist, and thats enough to get them ready for use.
//MARK: - Factory
protocol AbstractFactory {
func getNode()-> SKNode
func getNodeConfig()->SpriteConfig
}
class CivilianFactory : AbstractFactory {
// Local Constants
private struct K {
static let size = CGSize(width: 32, height: 32)
static let name = "civilian"
static let color = UIColor.yellow
}
// Here we get Civilian sprite config
func getNodeConfig() -> SpriteConfig {
let physics = SpritePhysicsConfig(categoryMask: Collider.civilian, contactMask: Collider.player | Collider.wall, collisionMask: Collider.none)
return SpriteConfig(name: K.name, size: K.size, color: K.color, physics: physics)
}
func getNode() -> SKNode {
let config = getNodeConfig()
let sprite = SKSpriteNode(color: config.color, size: config.size)
sprite.color = config.color
sprite.name = config.name
sprite.zPosition = 1
if let physics = config.physics {
sprite.physicsBody = SKPhysicsBody(rectangleOf: config.size)
sprite.physicsBody?.isDynamic = physics.isDynamic
sprite.physicsBody?.affectedByGravity = physics.isAffectedByGravity
sprite.physicsBody?.categoryBitMask = physics.categoryMask
sprite.physicsBody?.contactTestBitMask = physics.contactMask
sprite.physicsBody?.collisionBitMask = physics.collisionMask
}
}
return sprite
}
}
Same as this, You will make other "factories" as needed (just copy the factory and change visual/physics data setup). For this example I will make PlayerFactory.
and with next method I will create my nodes:
private func getNode(factory:AbstractFactory)->SKNode{
return factory.getNode()
}
and then just use it like this:
let node = getNode(factory: self.civiliansFactory) // or self.whateverFactory
Here you just provide a factory you want (can be anything that conforms to AbstractFactory), and in return, You get a desired node (You can return here anything that is SKNode). This way, we have hid initialization process, dependencies etc. from outside world (GameScene), and put everything in one place.
So, quite flexible, plus removes a bunch of repeating code from your scene.
And here are config structs for sprites creation:
//MARK: - Sprite Config
struct SpriteConfig {
let name:String
let size:CGSize
let color:UIColor
let physics:SpritePhysicsConfig? // lets make this optional
}
struct SpritePhysicsConfig {
let categoryMask: UInt32
let contactMask: UInt32
let collisionMask:UInt32
let isDynamic:Bool
let isAffectedByGravity:Bool
init(categoryMask:UInt32, contactMask:UInt32, collisionMask:UInt32, isDynamic:Bool = true, isAffectedByGravity:Bool = false){
self.categoryMask = categoryMask
self.contactMask = contactMask
self.collisionMask = collisionMask
self.isDynamic = isDynamic
self.isAffectedByGravity = isAffectedByGravity
}
}
Now some useful extensions that I needed:
//MARK: - Extensions
//Extension borrowed from here : https://stackoverflow.com/a/37760551
extension CGRect {
func randomPoint(x:CGFloat? = nil, y:CGFloat? = nil) -> CGPoint {
let origin = self.origin
return CGPoint(x: x == nil ? CGFloat(arc4random_uniform(UInt32(self.width))) + origin.x : x!,
y: y == nil ? CGFloat(arc4random_uniform(UInt32(self.height))) + origin.y : y!)
}
}
//Extension borrowed from here: https://stackoverflow.com/a/33292919
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y))))
}
}
And the GameScene:
//MARK: - Game Scene
class GameScene: SKScene {
//MARK: - Local Constants
// It's always good to have some kind of local constants per file, so that you have all variables in one place when it comes to changing/tuning
private struct K {
struct Actions {
static let civilianSpawningKey = "civilian.spawning"
static let playerMovingKey = "player.moving"
static let spawningDuration:TimeInterval = 0.7
static let spawningRange = 0.2
static let fadeOutDuration:TimeInterval = 0.35
}
struct General {
static let playerSpeed:CGFloat = 350
}
}
//MARK: - Private Properties
private var player:SKSpriteNode?
// Just in case, nodes are removed after physics simulation is done (in didSimulatePhysics which is called in each frame)
// Frame-Cycle Events : https://developer.apple.com/documentation/spritekit/skscene/responding_to_frame-cycle_events
private var trash:[SKNode] = []
private let civilianFactory = CivilianFactory()
private let playerFactory = PlayerFactory()
//MARK: - Scene lifecycle
override func sceneDidLoad() {
physicsWorld.contactDelegate = self
spawnCivilians()
}
//MARK: - Creating & Spawning sprites
private func getNode(factory:AbstractFactory)->SKNode{
return factory.getNode()
}
private func spawnCivilian(at position: CGPoint){
let node = getNode(factory: civilianFactory)
node.position = position
addChild(node)
}
private func spawnPlayer(at position: CGPoint){
// If its a first time, create player and leave it there
guard let `player` = player else {
let node = getNode(factory: playerFactory)
node.position = position
self.player = (node as? SKSpriteNode)
addChild(node)
return
}
// If player exists, move it around
let distance = player.position.distance(point: position)
let speed = K.General.playerSpeed
// To maintain same moving speed, cause if we use constant here, sprite would move faster or slower based on a given distance
let duration = distance / speed
let move = SKAction.move(to: position, duration:duration)
// This is a good way to check if some action is running
if player.action(forKey: K.Actions.playerMovingKey) != nil {
player.removeAction(forKey: K.Actions.playerMovingKey)
}
player.run(move, withKey: K.Actions.playerMovingKey)
}
private func spawnCivilians(){
let wait = SKAction .wait(forDuration: K.Actions.spawningDuration, withRange: K.Actions.spawningRange)
let spawn = SKAction.run({[weak self] in
guard let `self` = self else {return}
self.spawnCivilian(at: self.frame.randomPoint())
})
let spawning = SKAction.sequence([wait,spawn])
self.run(SKAction.repeatForever(spawning), withKey:K.Actions.civilianSpawningKey)
}
//MARK: - Touches Handling
func touchDown(atPoint pos : CGPoint) {
spawnPlayer(at: pos)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
}
So I pretty much commented everything. Here, you :
Start spawning civilians infinitely, immediately after the scene is loaded
On touch you add player to the scene
On every next touch player travels to the touch location (by the same speed)
And contacts:
//MARK: - Physics
struct Collider{
static let player : UInt32 = 0x1 << 0
static let civilian : UInt32 = 0x1 << 1
static let wall : UInt32 = 0x1 << 2
static let none : UInt32 = 0x0
}
extension GameScene: SKPhysicsContactDelegate{
//MARK: - Removing Sprites
override func didSimulatePhysics() {
for node in trash {
// first remove node from parent (with fadeOut)
node.run(SKAction.sequence([SKAction.fadeOut(withDuration: K.Actions.fadeOutDuration), SKAction.removeFromParent()]))
}
trash.removeAll() // then empty the trash
}
//MARK: Removing
func didBegin(_ contact: SKPhysicsContact) {
guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else {
//Silliness like removing a node from a node tree before physics simulation is done will trigger this error
fatalError("Physics body without its node detected!")
}
let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch mask {
// Contact between player and civilian detected
case Collider.player | Collider.civilian:
if let civilian = (contact.bodyA.categoryBitMask == Collider.civilian ? nodeA : nodeB) as? SKSpriteNode
{
trash.append(civilian)
}
default:
break
}
}
}
I guess those contacts and node removal were your problem. The point is that nodes that have physics body, are safer to remove from a node tree when didSimulatePhysics method is finished. There is a link in comments that explains what happens each frame, but the bottom point is, that physics engine retains physics body cause simulation is not finished, but the node is removed and that often end up in some unexpected results.
So to try how this work, you just copy / paste it in your GameScene. Here is how it looks:
You can see how nodes are really removed by observing nodes count label. (to enable these labels, you just go (in your view controller class) with (self.view as? SKView)?.showsNodeCount = true, showsFPS, showsPhysics etc).

How To Have Character Slide Back and Forth Automatically and Infinitely?

I'm an ubernoob developing a game using SpriteKit from scratch and im trying to make a character that will slide back and forth off the sides of the screen (in landscape mode) until collision is detected with another node (that I will add later). Think of pong and how the paddle can move side to side except I want that movement to be completely automatic/infinite.
Side Note: I plan on having this character jump when the screen is touched but continue with the back and forth movement. idk if that makes a difference in your approach.
Ok, so this answer isn't perfect but it's working for me right now so:
basically you touch the screen to spawn a boxd, and when the paddle touches the box some stuff happens (it stops moving):
import SpriteKit
// constants!
class GameScene: SKScene, SKPhysicsContactDelegate {
// A little complicated, but basically we want to have a constant speed across all screen sizes
var sliderSpeed: CGFloat { return self.size.width / 3 }
var slider = SKSpriteNode()
var sliderVelocity = CGFloat(0)
var sliderIsContacted = false
let boxMask = UInt32(2)
let sliderMask = UInt32(4)
// For use to contact slider
func spawnBox(at pos: CGPoint) {
let rect = CGRect(origin: CGPoint.zero, size: CGSize(width: 45, height: 45))
let shape = SKShapeNode(rect: rect)
shape.fillColor = .green
shape.position = pos
let pb = SKPhysicsBody(rectangleOf: rect.size)
pb.categoryBitMask = boxMask
pb.contactTestBitMask = sliderMask
shape.physicsBody = pb
addChild(shape)
}
func setupSlider() {
sliderVelocity = sliderSpeed
let rect = CGRect(origin: CGPoint.zero, size: CGSize(width: 45, height: 10))
let shape = SKShapeNode(rect: rect)
shape.fillColor = .blue
let pb = SKPhysicsBody(rectangleOf: rect.size)
pb.categoryBitMask = sliderMask
pb.contactTestBitMask = boxMask
pb.velocity.dx = sliderVelocity // moves our slider to the right!
// A little complicated, but basically we want to have a spritenode, not a shapenode:
slider = SKSpriteNode(texture: view!.texture(from: shape))
slider.physicsBody = pb
addChild(slider)
}
func setupWorld() {
let pb = SKPhysicsBody(edgeLoopFrom: frame)
pb.categoryBitMask = UInt32(0)
self.physicsBody = pb
physicsWorld.contactDelegate = self
physicsWorld.gravity = CGVector.zero
}
override func didMove(to view: SKView) {
setupSlider()
setupWorld()
}
}
// Game loop:
extension GameScene {
// touchesBegan in iOS:
override func mouseDown(with event: NSEvent) {
let location = event.location(in: self)
spawnBox(at: location)
}
override func update(_ currentTime: TimeInterval) {
let sliderPB = slider.physicsBody!
let halfWidth = slider.size.width/2
// move slider left when it reaches far right border:
if sliderPB.velocity.dx > 0 {
if slider.position.x >= (frame.maxX - halfWidth) {
sliderVelocity = -sliderSpeed
}
}
// move slider right when it reaches far left border:
else {
if slider.position.x <= (frame.minX + halfWidth) {
sliderVelocity = sliderSpeed
}
}
// Keep slider at constant rate:
if sliderIsContacted == false {
sliderPB.velocity.dx = sliderVelocity
}
}
func didBegin(_ contact: SKPhysicsContact) {
if contact.bodyA.categoryBitMask + contact.bodyB.categoryBitMask == sliderMask + boxMask {
sliderIsContacted = true
slider.physicsBody!.velocity.dx = 0 // stop slider
}
}
}
more complicated than it shoudl be, but I"m sdrunk so this is what I got :)
}hope it helps.

MKMapView covering SKSpriteNodes

I am experimenting with using MapKit and SpriteKit together, but I have hit a pretty major wall.
I have my map set up in my GameScene because I don't want the scene covering it, however when I attempt to add a SpriteNode to the view, it doesn't show. Upon reducing the map's alpha, I can see that the sprites are being rendered underneath the map.
Changing zPosition hasn't helped, as I'm assuming views such as MKMapViews are on a separate layer from SpriteKit objects.
Essentially what I am trying to accomplish is putting SpriteKit elements over top of my MKMapView (if possible, without storyboards - I am trying to do everything programmatically). I have spent hours trying to find an answer on the Internet but was unsuccessful.
Here is my GameScene:
import UIKit
import SpriteKit
import GameplayKit
import MapKit
import CoreLocation
class GameScene: SKScene, MKMapViewDelegate, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
var map = MKMapView()
let healthBar = SKSpriteNode(imageNamed: "Square")
let maxHealthBar = SKSpriteNode(imageNamed: "Square")
var UID = String()
var ref = DatabaseReference()
static var username = String()
static var health = Int()
static var maxHealth = Int()
var width = CGFloat()
var right = CGFloat()
var left = CGFloat()
var top = CGFloat()
var bot = CGFloat()
var margin = CGFloat()
var middleX = CGFloat()
var middleY = CGFloat()
var maxBarWidth = CGFloat()
override func didMove(to view: SKView) {
map.frame = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)
map.delegate = self
map.showsScale = false
map.showsPointsOfInterest = false
map.showsUserLocation = true
map.showsBuildings = true
map.isZoomEnabled = false
map.isScrollEnabled = false
map.isPitchEnabled = false
map.isRotateEnabled = false
let region = MKCoordinateRegionMakeWithDistance((locationManager.location?.coordinate)!, 400, 400)
map.setRegion(region, animated: false)
print("region:", map.region)
locationManager.requestWhenInUseAuthorization()
if (CLLocationManager.locationServicesEnabled()) {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
view.addSubview(map)
width = frame.width
right = frame.width
left = frame.width - frame.width
top = frame.height
bot = frame.height - frame.height
margin = frame.width / 75
middleX = frame.width / 2
middleY = frame.height / 2
maxBarWidth = width - margin * 2
let maxBarHeight = CGFloat(30)
let healthBarWidth = width - margin * 2
let barHeight = maxBarHeight * 0.75
maxHealthBar.anchorPoint = CGPoint(x: 0.0, y: 0.5)
maxHealthBar.position = CGPoint(x: middleX, y: top - margin - maxBarHeight / 2)
maxHealthBar.size = CGSize(width: maxBarWidth, height: maxBarHeight)
maxHealthBar.color = UIColor.black
maxHealthBar.colorBlendFactor = 1.0
maxHealthBar.zPosition = 1.1
self.addChild(maxHealthBar)
healthBar.anchorPoint = CGPoint(x: 0.0, y: 0.5)
healthBar.position = CGPoint(x: middleX - healthBarWidth / 2, y: maxHealthBar.position.y)
healthBar.size = CGSize(width: healthBarWidth, height: barHeight)
healthBar.color = UIColor.red
healthBar.colorBlendFactor = 1.0
healthBar.zPosition = 1000000
self.addChild(healthBar)
print("parent", healthBar.parent)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let location = t.location(in: self)
takeDamage(damage: 10)
}
}
func takeDamage(damage: Int) {
GameScene.health -= damage
updateHealth()
}
func updateHealth() {
let percentFull = CGFloat(GameScene.health / GameScene.maxHealth)
let healthWidth = maxBarWidth * percentFull
healthBar.size.width = healthWidth
}
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
map.setCenter((locationManager.location?.coordinate)!, animated: true)
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
And here is my GameViewController:
import UIKit
import SpriteKit
import GameplayKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if (SignInScene.signInLoaded == false) {
if let scene = GameScene(fileNamed: "GameScene") {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.size = skView.bounds.size
scene.scaleMode = .aspectFill
skView.presentScene(scene)
}
}
}
override var shouldAutorotate: Bool {
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .portrait
} else {
return .portrait
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override var prefersStatusBarHidden: Bool {
return false
}
}
I am extremely stuck on this one and would greatly appreciate any help!
Thanks in advance!
Your problem is your UIView heirarchy in your view controller,
Here is what your project looks like:
Controller
--SKView
----SKScene
--MKMapView
You need to change your hierarchy to be
Controller
--MKMapView
--SKView
----SKSceneView
I would recommend doing this inside your storyboard, so that you can keep design and code separated.
After you have changed your hierarchy, you need to do the following in your GameScene:
view.allowsTransparency = true
view.backgroundColor = .clear
backgroundColor = .clear
This will give you a view and scene that has no background color, which means that your map will show underneath it.
Now if you want touch events to hit the MKMapView and not the SKView, then you need to disable userInteractionEnabled on your MKMapView, or pass the touch events to the map from your SKView.

SpriteKit, Swift 2.0 - ScrollView in reverse

I managed to get a scrollview working and scrolling, but now when I go to scroll it only scrolls from right to left and was wondering how I go about reversing it so it scrolls from left to right instead.
Here is my menu code that contains my scrollview:
var moveableNode = SKNode()
var scrollView: CustomScrollView!
private var spriteSize = CGSize.zero
let kMargin: CGFloat = 40
var sprite = SKSpriteNode()
class Menu: SKScene {
override func didMoveToView(view: SKView) {
addChild(moveableNode)
spriteSize = SKSpriteNode (imageNamed: "card_level01").size
let initialMargin = size.width/2
let marginPerImage = kMargin + spriteSize.width
scrollView = CustomScrollView(frame: CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height), scene: self, moveableNode: moveableNode)
scrollView.contentSize = CGSizeMake(initialMargin*2 + (marginPerImage * 7), size.height)
// scrollView.contentSize = CGSizeMake(self.frame.size.width * 2, self.frame.size.height)
view.addSubview(scrollView)
for i in 1...8 {
let sprite = SKSpriteNode(imageNamed: String(format: "card_level%02d", i))
sprite.position = CGPoint (x: initialMargin + (marginPerImage * (CGFloat(i) - 1)), y: size.height / 2)
moveableNode.addChild(sprite)
}
Here is my scrollView Class that is a subclass of UIScrollView:
var nodesTouched: [AnyObject] = [] // global
class CustomScrollView: UIScrollView {
// MARK: - Static Properties
/// Touches allowed
static var disabledTouches = false
/// Scroll view
private static var scrollView: UIScrollView!
private static var contentView: UIView!
// MARK: - Properties
/// Current scene
private var currentScene: SKScene?
/// Moveable node
private var moveableNode: SKNode?
// MARK: - Init
init(frame: CGRect, scene: SKScene, moveableNode: SKNode) {
print("Scroll View init")
super.init(frame: frame)
CustomScrollView.scrollView = self
currentScene = scene
self.moveableNode = moveableNode
self.frame = frame
indicatorStyle = .White
scrollEnabled = true
//self.minimumZoomScale = 1
//self.maximumZoomScale = 3
canCancelContentTouches = false
userInteractionEnabled = true
delegate = self
//flip for spritekit (only needed for horizontal)
let verticalFlip = CGAffineTransformMakeScale(-1,-1)
self.transform = verticalFlip
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Touches
extension CustomScrollView {
/// began
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
print("Touch began scroll view")
guard !CustomScrollView.disabledTouches else { return }
currentScene?.touchesBegan(touches, withEvent: event)
}
/// moved
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
print("Touch moved scroll view")
guard !CustomScrollView.disabledTouches else { return }
currentScene?.touchesMoved(touches, withEvent: event)
}
/// ended
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
print("Touch ended scroll view")
guard !CustomScrollView.disabledTouches else { return }
currentScene?.touchesEnded(touches, withEvent: event)
}
/// cancelled
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
print("Touch cancelled scroll view")
guard !CustomScrollView.disabledTouches else { return }
currentScene?.touchesCancelled(touches, withEvent: event)
}
}
// MARK: - Touch Controls
extension CustomScrollView {
/// Disable
class func disable() {
print("Disabled scroll view")
CustomScrollView.scrollView?.userInteractionEnabled = false
CustomScrollView.disabledTouches = true
}
/// Enable
class func enable() {
print("Enabled scroll view")
CustomScrollView.scrollView?.userInteractionEnabled = true
CustomScrollView.disabledTouches = false
}
}
// MARK: - Delegates
extension CustomScrollView: UIScrollViewDelegate {
/// did scroll
func scrollViewDidScroll(scrollView: UIScrollView) {
print("Scroll view did scroll")
moveableNode!.position.x = scrollView.contentOffset.x // Left/Right
//moveableNode!.position.y = scrollView.contentOffset.y // Up/Dowm
}
}
You need get my updated helper on gitHub (v1.1) first before reading the rest.
My helper only really works well when your scene scale mode (gameViewController) is set to
.ResizeFill
so your scenes do not crop. If you use a different scaleMode such as
.AspectFill
than it might crop stuff in your scrollView which you would need to adjust for.
It also doesnt work if your game/app supports both portrait and landscape, which is unlikely for a game anyway.
So as I said and you have also noticed when using a ScrollView in spriteKit the coordinates are different compared to UIKit. For vertical scrolling this doesn't really mean anything, but for horizontal scrolling everything it is in reverse. So to fix this you do the following
Set up your scrollView for horizontal scrolling, passing along the new scrollDirection property (.Horizontal in this case)
scrollView = CustomScrollView(frame: CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height), scene: self, moveableNode: moveableNode, scrollDirection: .Horizontal)
scrollView.contentSize = CGSizeMake(self.frame.size.width * 3, self.frame.size.height) // * 3 makes it twice as wide as screen
view.addSubview(scrollView)
you need to also add this line of code after adding it to the view.
scrollView.setContentOffset(CGPoint(x: 0 + self.frame.size.width * 2, y: 0), animated: true)
this is the line you use to tell the ScrollView on which page to start. Now in this example the scrollView is three times as wide as the screen, therefore you need to offset the content by 2 screen lengths
Now to make things easier for positioning I would do this, create sprites for each page of the scrollView. This makes positioning much easier later on.
let page1ScrollView = SKSpriteNode(color: SKColor.clearColor(), size: CGSizeMake(self.frame.size.width, self.frame.size.height))
page1ScrollView.position = CGPointMake(CGRectGetMidX(self.frame) - (self.frame.size.width * 2), CGRectGetMidY(self.frame))
moveableNode.addChild(page1ScrollView)
let page2ScrollView = SKSpriteNode(color: SKColor.clearColor(), size: CGSizeMake(self.frame.size.width, self.frame.size.height))
page2ScrollView.position = CGPointMake(CGRectGetMidX(self.frame) - (self.frame.size.width), CGRectGetMidY(self.frame))
moveableNode.addChild(page2ScrollView)
let page3ScrollView = SKSpriteNode(color: SKColor.clearColor(), size: CGSizeMake(self.frame.size.width, self.frame.size.height))
page3ScrollView.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
moveableNode.addChild(page3ScrollView)
and now you can positioning your actual labels, sprites much easier.
/// Test label page 1
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
myLabel.text = "Hello, World!"
myLabel.fontSize = 45
myLabel.position = CGPointMake(0, 0)
page1ScrollView.addChild(myLabel)
/// Test sprite page 2
let sprite = SKSpriteNode(color: SKColor.redColor(), size: CGSize(width: 50, height: 50))
sprite.position = CGPointMake(0, 0)
page2ScrollView.addChild(sprite)
/// Test sprite page 3
let sprite2 = SKSpriteNode(color: SKColor.blueColor(), size: CGSize(width: 50, height: 50))
sprite2.position = CGPointMake(0, 0)
page3ScrollView.addChild(sprite2)
Hope this helps.
I also updated my GitHub project to explain this better
https://github.com/crashoverride777/Swift2-SpriteKit-UIScrollView-Helper

Issue with recognising touch in SKLabelNode

I have been programming in Swift for about four months now using Sprite Kit to build some simple Arcade games for IOS. Until recently I haven't had any problems with recognising touches in specific nodes. In the main screen in one of my projects, I have added another SKLabelNode for the latest addition to the app, following the same layout of implementation as the others, but this one doesn't work. When the label is tapped it is supposed run a function but doesn't even get that far, I figured out using breakpoints. Here is all of the relevant code, please have a look, I have going over this four hours and it has been driving me crazy.
import SpriteKit
let twistedLabelName = "twisted"
class StartScene: SKScene {
var play1P = SKLabelNode(fontNamed: "HelveticaNeue-Thin")
var play2P = SKLabelNode(fontNamed: "HelveticaNeue-Thin")
var playTwisted = SKLabelNode(fontNamed: "HelveticaNeue-Thin")
override func didMoveToView(view: SKView) {
initializeValues()
self.userInteractionEnabled = true
}
func initializeValues () {
backgroundColor = UIColor.whiteColor()
play1P.name = "1p"
play1P.text = "Play 1P"
play1P.fontColor = SKColor.blackColor()
play1P.fontSize = 40
play1P.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetHeight(self.frame) * 0.8)
play1P.zPosition = 100
self.addChild(play1P)
play2P.name = "2p"
play2P.text = "Play 2P"
play2P.fontColor = SKColor.blackColor()
play2P.fontSize = 40
play2P.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetHeight(self.frame) * 0.6)
play2P.zPosition = 100
self.addChild(play2P)
playTwisted.text = "Twisted"
playTwisted.fontColor = SKColor.blackColor()
playTwisted.name = twistedLabelName
playTwisted.fontSize = 40
playTwisted.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetHeight(self.frame) * 0.4)
self.addChild(playTwisted)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
let location = touch.locationInNode(self)
if let theName = self.nodeAtPoint(location).name {
if theName == "1p" {
// Some function
}
else if theName == "2p" {
// Some function
}
else if theName == twistedLabelName {
// This is the one that doesn't work
// Some function
}
}
}
}
}
Note that you can extend any SKNode (including SKLabelNode) and handle touches in the subclass. Set isUserInteractionEnabled = true first though. e.g.
import SpriteKit
class MyLabelNode: SKLabelNode {
override init() {
super.init()
isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print( #file + #function )
}
}

Resources