application stops working when set to background - swift

The code works fine when the window is open in the foreground and when I press the space key, the mouse moves as expected, but as soon as I move to another application and I press the space key, the mouse no longer moves as expected with the keystroke, How do I keep the application working even when it is set to the background
import Cocoa
class ViewController: NSViewController {
var timer: Timer!
#objc func movethemouse() {
let mouseLocx = NSEvent.mouseLocation.x
let mouseLocy = NSEvent.mouseLocation.y
let screenH = NSScreen.main?.frame.height
let deltaX = CGFloat(Int(1))
let deltaY = CGFloat(Int(1))
let newLoc = CGPoint(x: mouseLocx + deltaX , y: screenH! - mouseLocy - deltaY )
CGDisplayMoveCursorToPoint(CGMainDisplayID(), newLoc)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidAppear() {
view.window?.makeFirstResponder(self)
}
override var acceptsFirstResponder : Bool {
return true
}
var count3=0
var flag = false
override func keyDown(with event: NSEvent) {
if (event.keyCode == 49){
flag = true
if count3 == 0 {
timer = Timer.scheduledTimer(timeInterval: 0.001, target:self, selector: #selector(ViewController.movethemouse), userInfo: nil, repeats:true)
print(event)
}
count3 += 1
}
}
override func keyUp(with event: NSEvent) {
print(event)
if (event.keyCode == 49) {
if flag {
timer.invalidate()
count3 = 0
print(event)
}
}
}
}

You need to capture system wide events. Here's some sample code that will get everything in the system. Note that you have to enable assistive mode for XCode (during development) or your app (running outside XCode) for this to work.
func applicationDidFinishLaunching(_ aNotification: Notification) {
if getAccessibility() {
snarfKeys()
}
// etc...
}
func getAccessibility() -> Bool {
let trusted = AXIsProcessTrusted()
if !trusted {
print("process is not trusted")
}
return trusted
}
func snarfKeys() {
NSEvent.addGlobalMonitorForEvents(matching: .keyUp, handler: keyPress)
NSEvent.addGlobalMonitorForEvents(matching: .leftMouseUp, handler: mousePress)
}
snarfKeys does the magic, but as noted above and in the code, the process needs to be trusted to be able to add global event monitoring.

Related

How Can I Make A Timer Using String In xCode

So I'm trying to make a chess timer. I'm using string to make the 00:00 in a variable called storeTimed. Is there a way possible to make that used an timer to count down from that?
Here's my code:
The part I need help with is the updateTimer() function. Since the storedTime is a string trying to pass as a Int, xCode isn't liking it. I'm honestly not very sure what I could do. Mostly at the else statement, but the whole part in general
class ChessTimer: UIViewController {
#IBOutlet weak var playerTimer1: UILabel!
#IBOutlet weak var playerTimer2: UILabel!
var timer = Timer()
var time = 10
var isTimerRunning = false
var storedTime = "00:00"
override func viewDidLoad() {
super.viewDidLoad()
if isTimerRunning == false {
runTimer()
}
}
#IBAction func restartButton(_ sender: UIButton) {
}
#IBAction func pausePressed(_ sender: UIButton) {
timer.invalidate()
}
#IBAction func settingsPressed(_ sender: UIButton) {
performSegue(withIdentifier: "goToSettings", sender: self)
}
func runTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self,selector: (#selector(ChessTimer.updateTimer)),userInfo: nil, repeats: true)
isTimerRunning = true
}
#objc func updateTimer() {
if Int(storedTime) < 1 {
timer.invalidate()
playerTimer1.text = "00:00"
playerTimer2.text = "00:00"
}
else {
Int(storedTime)! -= 1
playerTimer1.text = prodTimeString(time: TimeInterval(storedTime)!)
}
}
func prodTimeString(time: TimeInterval) -> String {
let prodMinutes = Int(time) / 60 % 60
let prodSeconds = Int(time) % 60
return String(format: "%02d:%02d", prodMinutes, prodSeconds)
}
#IBAction func playerButton1(_ sender: UIButton) {
}
#IBAction func playerButton2(_ sender: UIButton) {
}
#IBAction func unwindToVC1(sender: UIStoryboardSegue) {
if let settingsController = sender.source as? SettingsController {
playerTimer1.text = settingsController.storedTime
playerTimer2.text = settingsController.storedTime
storedTime = settingsController.storedTime
}
}
}
To keep things simple, here's the code. I have removed the functions that aren't relevant to the discussion - non-implemented buttons, etc.,
The simple idea is that you use numbers (Int / Double / whatever) to store variables that you are counting with, and then have helper functions to present the variable in different formats
import UIKit
class ChessTimer: UIViewController {
var timer = Timer()
var isTimerRunning = false
var storedTime : Int = 0 // use an integer to store the time
override func viewDidLoad() {
super.viewDidLoad()
if isTimerRunning == false {
runTimer()
}
}
func runTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self,selector: (#selector(ChessTimer.updateTimer)),userInfo: nil, repeats: true)
isTimerRunning = true
}
#objc func updateTimer() {
// because we're storing the value in an Int, we don't need to convert it here
storedTime -= 1
if storedTime < 1 {
timer.invalidate()
// use the helper function to format the result for zero time as well as everything else
// that way, if you ever change it, you only have to change in one place
playerTimer1.text = prodTimeString(0)
playerTimer2.text = prodTimeString(0)
}
else {
playerTimer1.text = prodTimeString(storedTime)
}
}
func prodTimeString(_ time: Int) -> String {
// because there's only one parameter, and it's obvious, add '_' then you don't need the label when you call it
let prodMinutes = time / 60 % 60
let prodSeconds = time % 60
return String(format: "%02d:%02d", prodMinutes, prodSeconds)
}
}

How to add a 30 second timer to end a game round?

I am currently experimenting with some code that I found on the internet about a game where you have to click on one set of items and avoid clicking on the other. I am currently trying to add a timer to the game so that it lasts of a total of 30 seconds but I am really struggling to do so as I am quite inexperienced with this programming language.
import UIKit
import QuartzCore
import SceneKit
class GameViewController: UIViewController, SCNSceneRendererDelegate {
var gameView:SCNView!
var SceneGame:SCNScene!
var NodeCamera:SCNNode!
var targetCreationTime:TimeInterval = 0
override func viewDidLoad() {
super.viewDidLoad()
View_in()
initScene()
initCamera()
}
func View_in(){
gameView = self.view as! SCNView
gameView.allowsCameraControl = true
gameView.autoenablesDefaultLighting = true
gameView.delegate = self
}
func initScene (){
SceneGame = SCNScene()
gameView.scene = SceneGame
gameView.isPlaying = true
}
func initCamera(){
NodeCamera = SCNNode()
NodeCamera.camera = SCNCamera()
NodeCamera.position = SCNVector3(x:0, y:5, z:10)
SceneGame.rootNode.addChildNode(NodeCamera)
}
func createTarget(){
let geometry:SCNGeometry = SCNPyramid( width: 1, height: 1, length: 1)
let randomColor = arc4random_uniform(2
) == 0 ? UIColor.green : UIColor.red
geometry.materials.first?.diffuse.contents = randomColor
let geometryNode = SCNNode(geometry: geometry)
geometryNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
if randomColor == UIColor.red {
geometryNode.name = "enemy"
}else{
geometryNode.name = "friend"
}
SceneGame.rootNode.addChildNode(geometryNode)
let randomDirection:Float = arc4random_uniform(2) == 0 ? -1.0 : 1.0
let force = SCNVector3(x: randomDirection, y: 15, z: 0)
geometryNode.physicsBody?.applyForce(force, at: SCNVector3(x: 0.05, y: 0.05, z: 0.05), asImpulse: true)
}
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
if time > targetCreationTime{
createTarget()
targetCreationTime = time + 0.6
}
cleanUp()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: gameView)
let hitList = gameView.hitTest(location, options: nil)
if let hitObject = hitList.first{
let node = hitObject.node
if node.name == "friend" {
node.removeFromParentNode()
self.gameView.backgroundColor = UIColor.black
}else {
node.removeFromParentNode()
self.gameView.backgroundColor = UIColor.red
}
}
}
func cleanUp() {
for node in SceneGame.rootNode.childNodes {
if node.presentation.position.y < -2 {
node.removeFromParentNode()
}
}
}
override var shouldAutorotate: Bool {
return true
}
override var prefersStatusBarHidden: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
}
You could use a Timer object, documented here. Just set up the timer when you want the game to start, probably once you've finished all your initializations. When you set up the timer, just wait for it to call back to your code when it finishes and run whatever logic you want to use to terminate your game.
EDIT
Create a variable representing the time you want your game will end:
var time: CGFloat = 60
Then, add an SCNAction to your scene so that each second it will decrease this variable value, for example in the viewDidLoad:
//One second before decrease the time
let wait = SCNAction.wait(forDuration: 1)
//This is the heart of this answer
// An action that reduce the time and when it is less than 1 (it reached zero) do whatever you want
let reduceTime = SCNAction.run{ _ in
self.time -= 1
if self.time < 1 {
// Do whatever you want
// for example show a game over scene or something else
}
}
}
SceneGame.rootNode.run(SCNAction.repeatForever(SCNAction.sequence([wait,reduceTime])))
If you want, you can show the remaining time by using SKLabel on an HUD, which is an SKScene used as overlay.
You can check this tutorial for how to create an HUD
As well, you can use an SCNText, this is the documentation about it

WKCrownSequencer not working after going back to main interface controller

I have a WKCrownSequencer that triggers an action in my pushed interface controller and the first time through everything works fine. When I go back to root interface controller regardless of the method (pop or reloadRootcontrollers) the digital crown no longer works in first interface controller nor the second one. The StartInterfaceController is the rootInterfaceController and the MidWorkoutInterfaceController is the pushed one.
import WatchKit
import Foundation
class StartInterfaceController:
WKInterfaceController,CLLocationManagerDelegate {
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
#IBAction func start() {
WKInterfaceController.reloadRootControllers(
withNames: ["midWorkout"], contexts: []
)
}
The second interface controller is below.
import WatchKit
import Foundation
class MidWorkoutInterfaceController: WKInterfaceController, WKCrownDelegate {
override func awake(withContext context: Any?) {
super.awake(withContext: context)
print("viewdidAwake")
print("ViewWillActivate")
crownSequencer.delegate = self
crownSequencer.focus()
WKInterfaceDevice.current().play(.success)
currentPhase = 0
let workoutType = UserDefaults.standard.object(forKey: "CurrentType") as? [String] ?? [ "Swimming", "T1"]
orderOfEventsSetter = workoutType
updateCurrentPhase()
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
crownSequencer.focus()
}
var clockTimer: Timer!
func workoutStarted(){
clockTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
self?.totalTimeOutlet.setText( String(describing: -1 * Int(self!.startDate!.timeIntervalSinceNow)))
self?.splitTimeOutlet.setText( String(describing: -1 * Int(self!.currentStartDate!.timeIntervalSinceNow)))
}
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
var startDate: Date?
var currentStartDate: Date?
//Outlets
#IBOutlet var currentPhaseOutlet: WKInterfaceLabel!
#IBOutlet var totalTimeOutlet: WKInterfaceLabel!
#IBOutlet var splitTimeOutlet: WKInterfaceLabel!
#IBOutlet var currentPaceOutlet: WKInterfaceLabel!
#IBOutlet var totalDistanceOutlet: WKInterfaceLabel!
var orderOfEventsSetter: Array<String>{
get{
return orderOfEvents
}
set{
var tempArray = ["GPS Locking In"]
for phase in newValue {
tempArray.append(phase)
}
orderOfEvents = tempArray
}
}
private var orderOfEvents: [String] = []
var currentPhase = 0 {
didSet{
if !orderOfEvents.isEmpty {
updateCurrentPhase()
}
}
}
func updateCurrentPhase(){
currentPhaseOutlet.setText(orderOfEvents[currentPhase])
}
//timing for location requests
//Corelocation Section
//CoreMotion Section
///crown control
var currentDialRotation = 0.0
let dialRotationRange = Range(uncheckedBounds: (lower: -Double.pi / 4, upper: Double.pi / 4))
let constantForTimer: TimeInterval = 0.1
var justTransitioned = false
func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) {
currentDialRotation += rotationalDelta
if !dialRotationRange.contains(currentDialRotation){
currentDialRotation = 0.0
justTransitioned = true
makeTransition()
//make so two transitions cannot happen right after each other
}
print(currentDialRotation)
}
func crownDidBecomeIdle(_ crownSequencer: WKCrownSequencer?) {
print(String(describing: orderOfEvents[currentPhase]))
print("crown stopped")
}
func makeTransition(){
print(currentPhase)
print(orderOfEvents.count)
if (currentPhase) == orderOfEvents.count - 1 {
endWorkout()
}
else if (currentPhase == 0){
WKInterfaceDevice.current().play(.start)
let dateFormat = DateFormatter()
dateFormat.dateFormat = "mm/dd/yyyy"
startDate = Date()
currentStartDate = Date()
workoutStarted()
currentPhase += 1
}
else{
WKInterfaceDevice.current().play(.start)
print("transitioning to " + String(describing: orderOfEvents[currentPhase + 1]))
currentStartDate = Date()
stopTimers()
currentPhase += 1
}
}
#IBAction func endWorkoutButton() {
endWorkout()
}
func endWorkout(){
stopTimers()
clockTimer.invalidate()
alerts()
}
func alerts(){
let saveAction = WKAlertAction(title: "Save",
style: WKAlertActionStyle.default) {
self.goToStartScreen()
}
let discardAction = WKAlertAction(title: "Discard Workout",
style: WKAlertActionStyle.cancel) {
self.goToStartScreen()
}
presentAlert(withTitle: "Workout Complete",
message: "Would you like to save the workout?",
preferredStyle: WKAlertControllerStyle.alert,
actions: [saveAction, discardAction])
}
func goToStartScreen(){
crownSequencer.resignFocus()
self.popToRootController()
}
func stopTimers(){
if orderOfEvents[currentPhase] == "Running"{
}
if orderOfEvents[currentPhase] == "Biking"{
}
if currentPhase == orderOfEvents.count {
clockTimer.invalidate()
}
}
}
According to Apple's document here:
"...Only one object in your interface can have focus at any given time, so if your interface also contains picker objects or has scrollable scenes, you must coordinate changes in focus accordingly. For example, calling the sequencer's focus method causes any picker objects or interface controllers to resign focus. When the user taps on a picker object, the currently active sequencer resigns focus, and the selected picker object gains the focus... "
And you are to lose the focus at any time unpredictable...
"...If the user taps a picker object or a scrollable scene in your interface, the system automatically removes the focus from any active crown sequencer..."

Creating a stopwatch in swift

I have tried searching other answers and I cannot find one that applies to my scenario. I am writing a game in swift and want to create a stopwatch that determines how the long the player is playing. When the user touches down, the stopwatch will start and when a certain action happens, then the timer stops and resets. I'd like to use minutes, seconds, milliseconds (i.e. 00:00.00).
Currently, the time function kind of works. It doesn't start at 0, it starts at the current seconds in time (I know thats when I start it, but I don't know how to start it at 0). It also only updates when I touch the screen, I need it to count from 00:00.00 and update on its own until the cancel action is fired.
Thank you for your time.
Here is what I have so far:
class GameScene: SKScene {
var activeTimer = SKLabelNode()
//var bestTime = SKLabelNode()
var startTime = TimeInterval()
//var currentTime = TimeInterval()
//var countingTime = TimeInterval()
var updateTimerAction = SKAction()
override func didMove(to view: SKView) {
activeTimer = self.childNode(withName: "active_timer") as! SKLabelNode
//bestTime = self.childNode(withName: "best_time") as! SKLabelNode
startTime = TimeInterval(Calendar.current.component(.second, from:Date()))
updateTimerAction = SKAction.sequence([SKAction.run(updateTimer), SKAction.wait(forDuration: 1.0)])
}
func startGame() {
// Other code
startGameTimer()
}
func resetGame() {
// Other code
stopGameTimer()
}
func startGameTimer() {
run(SKAction.repeatForever(updateTimerAction), withKey: "timer")
}
func updateTimer() {
activeTimer.text = stringFromTimeInterval(interval: startTime) as String
}
func stringFromTimeInterval(interval: TimeInterval) -> NSString {
let ti = NSInteger(interval)
let ms = Int((interval.truncatingRemainder(dividingBy: 1)) * 1000)
let seconds = ti % 60
let minutes = (ti / 60) % 60
return NSString(format: "%0.2d:%0.2d.%0.2d",minutes,seconds,ms)
}
func stopGameTimer() {
removeAction(forKey: "timer")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
startGame()
for touch in touches {
// other code
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
// other code
}
}
override func update(_ currentTime: TimeInterval) {
updateTimer()
if <action> {
// stop timer and reset game
resetGame()
}
}
Instead of using a TimeInterval object, it might be better to use a Timer object which calls a function that increments the value of a label (or an internal variable) from 0 onwards accordingly after every 1 second(or can be made lesser). And for stopping the clock just call the Timer object's invalidate function.
Check the documentation for more info: https://developer.apple.com/documentation/foundation/timer
The following lines of code come from an actual macOS game that I submitted to Mac App Store some 3 weeks ago. It counts down a number to 0. In your case, change
self.currentSeconds -= 1
to
self.currentSeconds += 1
timeBackNode is an empty node that holds SKLabelNode objects timeNode0 and timeNode1. So create it when the game starts with didMove.
var currentSeconds = Int() // currentSeconds
var timeNode0 = SKLabelNode() // timeNode0
var timeNode1 = SKLabelNode() // timeNode1
func setupTimeGoal() {
enumerateChildNodes(withName: "//TimerBack", using: { timeBackNode, _ in
let goal = self.timeDict["num"] as! Int
self.currentSeconds = goal // In your case, goal is 0 since you are going to count up.
self.timeNode0 = SKLabelNode(fontNamed: "Your font's font name")
self.timeNode0.text = self.makeTimeWithSeconds(secs: goal)
self.timeNode0.fontSize = 26
self.timeNode0.fontColor = SKColor.black
self.timeNode0.horizontalAlignmentMode = .center
self.timeNode0.position = CGPoint(x: 1, y: -22)
timeBackNode.addChild(self.timeNode0)
self.timeNode1 = SKLabelNode(fontNamed: "Your font's font name")
self.timeNode1.text = self.makeTimeWithSeconds(secs: goal) // this function translate a counting number into a time code like 00:00:00
self.timeNode1.fontSize = 26
self.timeNode1.fontColor = SKColor.cyan
self.timeNode1.horizontalAlignmentMode = .center
self.timeNode1.position = CGPoint(x: 0, y: -23)
timeBackNode.addChild(self.timeNode1)
})
}
func repeatTime() {
let waitAction = SKAction.wait(forDuration: 1.0)
let completionAction = SKAction.run {
if self.currentSeconds == 0 && self.gameState == .playing {
self.removeAction(forKey: "RepeatTime")
self.proceedLoss()
return
}
self.currentSeconds -= 1
self.timeNode0.text = self.makeTimeWithSeconds(secs: self.currentSeconds)
self.timeNode1.text = self.makeTimeWithSeconds(secs: self.currentSeconds)
}
let seqAction = SKAction.sequence([waitAction, completionAction])
let repeatAction = SKAction.repeatForever(seqAction)
self.run(repeatAction, withKey: "RepeatTime")
}
Here is what I ended up doing. While El Tomato's answer appears to work, given the setup of my code I went a little different route. After talking with some other friends, I chose to user a timer due to the design of my application. If I happen to run into any problems using a Timer object I will update this question.
The code below will start on a touch, update in the format: "00:00.00" and stop when the action is fired. This time will pause until another touch and the timer will then start from zero.
class GameScene: SKScene {
var seconds = 0
var timer = Timer()
var timeStarted = Bool()
var activeTimer = SKLabelNode()
//var bestTime = SKLabelNode()
override func didMove(to view: SKView) {
activeTimer = self.childNode(withName: "active_timer") as! SKLabelNode
//bestTime = self.childNode(withName: "best_time") as! SKLabelNode
timeStarted = false
}
func startGame() {
startGameTimer()
timeStarted = true
}
func resetGame() {
timeStarted = false
stopGameTimer()
seconds = 0
}
func startGameTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(updateTimer)), userInfo: nil, repeats: true)
}
func updateTimer() {
seconds += 1
activeTimer.text = timeString(time: TimeInterval(seconds))
}
func timeString(time:TimeInterval) -> String {
let hours = Int(time) / 3600
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i.%02i", hours, minutes, seconds)
}
func stopGameTimer() {
timer.invalidate()
removeAction(forKey: "timer")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if !timeStarted {
startGame()
}
for touch in touches {
// other code
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
// other code
}
}
override func update(_ currentTime: TimeInterval) {
if timeStarted {
updateTimer()
}
if <action> {
// stop timer and reset game
resetGame()
}
}
}

Creation of buttons for SpriteKit

I am creating the main menu for a sprite kit application I am building. Throughout my entire project, I have used SKScenes to hold my levels and the actual gameplay. However, now I need a main menu, which holds buttons like "Play," "Levels," "Shop," etc... However, I don't feel really comfortable the way I am adding buttons now, which is like this:
let currentButton = SKSpriteNode(imageNamed: button) // Create the SKSpriteNode that holds the button
self.addChild(currentButton) // Add that SKSpriteNode to the SKScene
And I check for the touch of the button like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self)
for node in self.nodes(at: touchLocation) {
guard let nodeName = node.name else {
continue
}
if nodeName == ButtonLabel.Play.rawValue {
DispatchQueue.main.asyncAfter(deadline: .now()) {
let transition = SKTransition.reveal(with: .left, duration: 1)
self.view?.presentScene(self.initialLevel, transition: transition)
self.initialLevel.loadStartingLevel()
}
return
}
if nodeName == ButtonLabel.Levels.rawValue {
slideOut()
}
}
}
However, I don't know if this is considered efficient. I was thinking of using UIButtons instead, but for that would I have to use an UIView?
Or can I add UIButtons to an SKView (I don't really get the difference between an SKView, SKScene, and UIView) What is recommended for menus?
I totally agree with #Whirlwind here, create a separate class for your button that handles the work for you. I do not think the advice from #ElTomato is the right advice. If you create one image with buttons included you have no flexibility on placement, size, look and button state for those buttons.
Here is a very simple button class that is a subclass of SKSpriteNode. It uses delegation to send information back to the parent (such as which button has been pushed), and gives you a simple state change (gets smaller when you click it, back to normal size when released)
import Foundation
import SpriteKit
protocol ButtonDelegate: class {
func buttonClicked(sender: Button)
}
class Button: SKSpriteNode {
//weak so that you don't create a strong circular reference with the parent
weak var delegate: ButtonDelegate!
override init(texture: SKTexture?, color: SKColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
setScale(0.9)
self.delegate.buttonClicked(sender: self)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
setScale(1.0)
}
}
This button can be instantiated 2 ways. You can create an instance of it in the Scene editor, or create an instance in code.
class MenuScene: SKScene, ButtonDelegate {
private var button = Button()
override func didMove(to view: SKView) {
if let button = self.childNode(withName: "button") as? Button {
self.button = button
button.delegate = self
}
let button2 = Button(texture: nil, color: .magenta, size: CGSize(width: 200, height: 100))
button2.name = "button2"
button2.position = CGPoint(x: 0, y: 300)
button2.delegate = self
addChild(button2)
}
}
func buttonClicked(sender: Button) {
print("you clicked the button named \(sender.name!)")
}
You have to remember to make the scene conform to the delegate
class MenuScene: SKScene, ButtonDelegate
func buttonClicked(sender: Button) {
print("you clicked the button named \(sender.name!)")
}
For simple scenes what you are doing is fine, and actually preferred because you can use the .SKS file.
However, if you have a complex scene what I like to do is subclass a Sprite and then override that node's touchesBegan.
Here is a node that I use in all of my projects... It is a simple "on off" button. I use a "pointer" to a Boolean via the custom Reference class I made, so that way this node doesn't need to be concerned with your other scenes, nodes, etc--it simply changes the value of the Bool for the other bits of code to do with what they want:
public final class Reference<T> { var value: T; init(_ value: T) { self.value = value } }
// MARK: - Toggler:
public final class Toggler: SKLabelNode {
private var refBool: Reference<Bool>
var value: Bool { return refBool.value }
var labelName: String
/*
var offText = ""
var onText = ""
*/
func toggleOn() {
refBool.value = true
text = labelName + ": on"
}
func toggleOff() {
refBool.value = false
text = labelName + ": off"
}
/*init(offText: String, onText: String, refBool: Reference<Bool>) {
ref = refBool
super.init(fontNamed: "Chalkduster")
if refBool.value { toggleOn() } else { toggleOff() }
isUserInteractionEnabled = true
}
*/
init(labelName: String, refBool: Reference<Bool>) {
self.refBool = refBool
self.labelName = labelName
super.init(fontNamed: "Chalkduster")
isUserInteractionEnabled = true
self.refBool = refBool
self.labelName = labelName
if refBool.value { toggleOn() } else { toggleOff() }
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if refBool.value { toggleOff() } else { toggleOn() }
}
public required init?(coder aDecoder: NSCoder) { fatalError("") }
override init() {
self.refBool = Reference<Bool>(false)
self.labelName = "ERROR"
super.init()
}
};
This is a more elaborate button than say something that just runs a bit of code when you click it.
The important thing here is that if you go this route, then you need to make sure to set the node's .isUserInteractionEnabled to true or it will not receive touch input.
Another suggestion along the lines of what you are doing, is to separate the logic from the action:
// Outside of touches func:
func touchPlay() {
// Play code
}
func touchExit() {
// Exit code
}
// In touches began:
guard let name = node.name else { return }
switch name {
case "play": touchPlay()
case "exit": touchExit()
default:()
}
PS:
Here is a very basic example of how to use Toggler:
class Scene: SKScene {
let spinnyNode = SKSpriteNode(color: .blue, size: CGSize(width: 50, height: 50))
// This is the reference type instance that will be stored inside of our Toggler instance:
var shouldSpin = Reference<Bool>(true)
override func didMove(to view: SKView) {
addChild(spinnyNode)
spinnyNode.run(.repeatForever(.rotate(byAngle: 3, duration: 1)))
let toggleSpin = Toggler(labelName: "Toggle Spin", refBool: shouldSpin)
addChild(toggleSpin)
toggleSpin.position.y += 100
}
override func update(_ currentTime: TimeInterval) {
if shouldSpin.value == true {
spinnyNode.isPaused = false
} else if shouldSpin.value == false {
spinnyNode.isPaused = true
}
}
}