Notifications and pause mode - swift

I'm making some game with Swift and SpriteKit.
When my app is going to background it calls a function pause but it automatically unpause when the game resumes.
func pauseTheGame()
{
self.scene?.isPaused = true
}
AppDelegate
func applicationWillResignActive(_ application: UIApplication)
{
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "goToBackground"), object: self)
GameScene
NotificationCenter.default.addObserver(self, selector: #selector(GameScene.pauseTheGame), name: NSNotification.Name("goToBackground"), object: nil)
How can I fix it?

I think its not ideal to pause the whole scene, its better to have a worldNode and pause that node. This will also make your life easier for overlaying Menu nodes etc.
Apple also does this in their sample game DemoBots.
Create a world node in your scene and a isGamePause property
var isGamePaused = false
let worldNode = SKNode()
and add it in didMoveToView
addChild(worldNode)
Than add all your sprites to that node
worldNode.addChild(someSprite1)
worldNode.addChild(someSprite2)
Than in your pause function you say
func pauseTheGame() {
isGamePaused = true
worldNode.paused = true
physicsWorld.speed = 0
/// show pause menu
}
Your resume function should say
func resumeTheGame() {
isGamePaused = false
worldNode.paused = false
physicsWorld.speed = 1
// remove pause menu
}
To make extra sure that your game does not resume when paused I add a check in the update method to keep the game paused.
override func update(_ currentTime: TimeInterval) {
guard !isGamePaused else {
worldNode.paused = true
physicsWorld.speed = 0
return
}
...
}
As a tip you should always organise string keys into properties to avoid typos e.g Notification centre names, UserDefaults keys, SKAction keys etc.
With Swift 3 for Notification Center names you can now create an extension and handle them in a very neat way.
extension NSNotification.Name {
static let goToBackground = Notification.Name(rawValue: "goToBackground")
}
Now you can say
NotificationCenter.default.post(name: .goToBackground, object: self)
NotificationCenter.default.addObserver(self, selector: #selector(pauseTheGame), name: .goToBackground, object: nil)
Hope this helps

Related

Swift Multiplayer Calls Present Game Multiple Times

I am writing several Swift multiplayer games based on the Ray Wenderlich tutorial for Nine Knights. (https://www.raywenderlich.com/7544-game-center-for-ios-building-a-turn-based-game)
I use pretty much the same GameCenterHelper file except that I change to a segue instead of present scene since I am using UIKit instead of Sprite Kit with the following important pieces:
present match maker:
func presentMatchmaker() {
guard GKLocalPlayer.local.isAuthenticated else {return}
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 2
request.inviteMessage = "Would you like to play?"
let vc = GKTurnBasedMatchmakerViewController(matchRequest: request)
vc.turnBasedMatchmakerDelegate = self
currentMatchmakerVC = vc
print(vc)
viewController?.present(vc, animated: true)
}
the player listener function:
extension GameCenterHelper: GKLocalPlayerListener {
func player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch, didBecomeActive: Bool) {
if let vc = currentMatchmakerVC {
currentMatchmakerVC = nil
vc.dismiss(animated: true)
}
guard didBecomeActive else {return}
NotificationCenter.default.post(name: .presentGame, object: match)
}
}
The following extension for Notification Center:
extension Notification.Name {
static let presentGame = Notification.Name(rawValue: "presentGame")
static let authenticationChanged = Notification.Name(rawValue: "authenticationChanged")
}
In the viewdidload of the menu I call the following:
override func viewDidLoad() {
super.viewDidLoad()
createTitleLabel()
createGameImage()
createButtons()
GameCenterHelper.helper.viewController = self
GameCenterHelper.helper.currentMatch = nil
NotificationCenter.default.addObserver(self, selector: #selector(authenticationChanged(_:)), name: .authenticationChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(presentGame(_:)), name: .presentGame, object: nil)
}
and tapping the multi device buttons calls the following:
#objc func startMultiDeviceGame() {
multiPlayer = true
GameCenterHelper.helper.presentMatchmaker()
}
and the notifications call the following:
#objc func presentGame(_ notification: Notification) {
// 1
print("present game")
guard let match = notification.object as? GKTurnBasedMatch else {return}
loadAndDisplay(match: match)
}
// MARK: - Helpers
private func loadAndDisplay(match: GKTurnBasedMatch) {
match.loadMatchData { [self] data, error in
if let data = data {
do {
gameModel = try JSONDecoder().decode(GameModel.self, from: data)
} catch {gameModel = GameModel()}
} else {gameModel = GameModel()}
GameCenterHelper.helper.currentMatch = match
print("load and display")
performSegue(withIdentifier: "gameSegue", sender: nil)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
print("prepare to segue")
if let vc = segue.destination as? GameVC {vc.gameModel = gameModel}
}
Which is hopefully easy to follow.
The game starts and the menu scene adds the observer for present game
The player taps multi device, which presents the matchmaker
The player selects their game from the match maker, which I think activates the player listener function
This posts to the Notification Center for present game
The notification center observer calls present game, which calls load and display, with a little help from prepare segue
My issue is that the first time I do this it works perfectly, and per the framework from that tutorial that I can't figure out how to change (an issue for a different question I think) after a player takes their turn they are returned to the menu. The second time they enter present matchmaker and select a game the present game function is called twice, and the third time they take their turn without shutting down the app it is called 3 times, etc. (I have the print statements in both the present game and load and display functions and they are called back to back the 2nd time through and back to back to back the 3rd time etc. even though they are only called once the first time a game is selected from the matchmaker)
Console messages
present matchmaker true
<GKTurnBasedMatchmakerViewController: 0x104810000>
present game
present game
present game
load and display
prepare to segue
load and display
prepare to segue
load and display
prepare to segue
2021-03-20 22:32:26.838680-0600 STAX[4997:435032] [Presentation] Attempt to present <STAX.GameVC: 0x103894c00> on <Game.MenuVC: 0x103814800> (from < Game.MenuVC: 0x103814800>) whose view is not in the window hierarchy.
(419.60100000000006, 39.0)
2021-03-20 22:32:26.877943-0600 STAX[4997:435032] [Presentation] Attempt to present <STAX.GameVC: 0x103898e00> on < Game.MenuVC: 0x10501c800> (from < Game.MenuVC: 0x10501c800>) whose view is not in the window hierarchy.
I had thought that this was due to me not removing the Notification Center observers, but I tried the following in the view did load for the menu screen (right before I added the .presentGame observer):
NotificationCenter.default.removeObserver(self, name: .presentGame, object: nil)
and that didn't fix the issue, so I tried the following (in place of the above):
NotificationCenter.default.removeObserver(self)
and that didn't work so I tried them each, one at a time in the view did disappear of the game view controller (which I didn't think would work since self refers to the menu vc, but I was getting desperate) and that didn't work either.
I started thinking that maybe I'm not adding multiple observers that are calling present game more than once, since the following didn't work at all the second time (I'm just using a global variable to keep track of the first run through that adds the observers and then not adding them the second time):
if addObservers {
NotificationCenter.default.addObserver(self, selector: #selector(authenticationChanged(_:)), name: .authenticationChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(presentGame(_:)), name: .presentGame, object: nil)
addObservers = false
}
since it is trying to add a view that is not in the view hierarchy. (Although the background music for that screen starts playing, the menu remains and the game board is not shown...)
I wasn't sure if I'm removing the Notification Center observers incorrectly or if they aren't really the source of the problem so I decided to ask for help :)
Thank you!
I figured it out. I was trying to remove the Notifications from a deallocated instance of the view controller per the below link (The bottom most answer):
How to avoid adding multiple NSNotification observer?
The correct way to remove the notifications is in the view will disappear function like this:
override func viewWillDisappear(_ animated: Bool) {
NotificationCenter.default.removeObserver(self, name: Notification.Name.presentGame, object: nil)
NotificationCenter.default.removeObserver(self, name: Notification.Name.authenticationChanged, object: nil)
}
After implementing that I stopped making multiple calls to the notification center.

Proximity Senzor addNotification call function mutliple times

I activated the proximity senzor in application and now the function is calling multiple times.
I don't know what is wrong. Can you help me?
//MARK:- Activate Proximity Sensor
func activateProximitySensor() {
proximitySenzorActionStatus = true
device.isProximityMonitoringEnabled = true
if device.isProximityMonitoringEnabled {
NotificationCenter.default.removeObserver(self, name: UIDevice.proximityStateDidChangeNotification, object: device)
NotificationCenter.default.addObserver(self, selector: #selector(proximityStateDidChange), name:UIDevice.proximityStateDidChangeNotification, object: device)
}
}
func deactivateProximitySenzor() {
proximitySenzorActionStatus = false
device.isProximityMonitoringEnabled = false
NotificationCenter.default.removeObserver(self, name: UIDevice.proximityStateDidChangeNotification, object: device)
}
The methos in now called for 3 times:
#objc func proximityStateDidChange(notification: NSNotification) {
print("proximityStateDidChange")}
I activated the senzor in
viewWillAppear
and remove in
viewWillDisappear
You need to calculate the time of flight between every occlusion of the sensor.

Automatically playing next audio file from collection view

I'm building audio book app.
I have play button inside my collection view cell where users see list of all mp3 files. When user presses this button (play button) - I'm sending data of track to my player class and music file starts to play. I'm using global variables to send track details to my player.
Here's my code:
extension ChapterDetailsViewController: AliaCellDelegate {
func playAlia(cell: AliaCell) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "playAudio"), object: nil)
}
}
I need to implement such functionality: When track comes to an end - automatically play next file from same list. For this, I have this method inside my player class, that informs me, when it has finished playing:
func playerDidFinishPlaying(note: NSNotification) {
print("Finished playing")
}
But I don't know how to call func playAlia(cell: AliaCell) inside func playerDidFinishPlaying(note: NSNotification) in my player class and pass details of the next track from my collection view.
For now I have created only:
NotificationCenter.default.addObserver(self, selector: #selector(playNext), name: NSNotification.Name(rawValue: "playNext"), object: nil) but what should I do next?
A simple solution is to keep track of existing track within your source model. The example makes plenty of assumptions and is not it not complete.
struct Track {
let url: URL
let title: String
let artist: String
}
class AudioPlayer: UIViewController {
var currentTrackIndex: Int = 0
let tracks: [Track] = [Track(), Track(), Track()]
let player: AVPlayer()
// table view delegate for row selection
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
// assuming single section
currentTrackIndex = indexPath.row
playTrack(at: currentTrackIndex)
}
func playTrack(at index: Int) {
let nextTrack = tracks[index]
let nextItem = AVPlayerItem(url: nextTrack.url)
player.replaceCurrentItem(with: nextItem)
}
func playerDidFinishPlaying(note: NSNotification) {
// add logic to handle out of bounds index for array
currentTrackIndex += 1
playTrack(at: currentTrackIndex)
}
}

Using acceptsMouseMovedEvents for SpriteKit mouse actions with Storyboards and Swift

I have created a SpriteKit scene by referencing a custom NSView using Storyboards in Xcode. However, I cannot implement any mouseMoved events using SpriteKit because I do not know how to reference the program's NSWindowto set its acceptsMouseMovedEvents property to "true".
How can I create an #IBOutlet reference to my NSWindow in my AppDelegate.swift file so that I can change this property?
You can configure an NSTrackingArea object to track the movement of the mouse as well as when the cursor enters or exits a view. To create an NSTrackingArea object, you specify a region of a view where you want mouse events to be tracked, the owner that will receive the mouse event messages, and when the tracking will occur (e.g., in the key window). The following is an example of how to add a tracking area to a view. Add to your SKScene subclass, such as GameScene.swift.
Swift 3 and 4
override func didMove(to view: SKView) {
// Create a tracking area object with self as the owner (i.e., the recipient of mouse-tracking messages
let trackingArea = NSTrackingArea(rect: view.frame, options: [.activeInKeyWindow, .mouseMoved], owner: self, userInfo: nil)
// Add the tracking area to the view
view.addTrackingArea(trackingArea)
}
// This method will be called when the mouse moves in the view
override func mouseMoved(with theEvent: NSEvent) {
let location = theEvent.location(in: self)
print(location)
}
Swift 2
override func didMoveToView(view: SKView) {
// Create a tracking area object with self as the owner (i.e., the recipient of mouse-tracking messages
let trackingArea = NSTrackingArea(rect: view.frame, options: NSTrackingAreaOptions.ActiveInKeyWindow | NSTrackingAreaOptions.MouseMoved, owner: self, userInfo: nil)
// Add the tracking area to the view
view.addTrackingArea(trackingArea)
}
// This method will be called when the mouse moves in the view
override func mouseMoved(theEvent: NSEvent) {
let location = theEvent.locationInNode(self)
println(location)
}
An update for 0x141E's answer:
override func didChangeSize(_ oldSize: CGSize) {
guard let newRect = view?.bounds else {return}
let options = NSTrackingArea.Options(rawValue: NSTrackingArea.Options.activeInKeyWindow.rawValue | NSTrackingArea.Options.mouseMoved.rawValue)
let userInfo = ["SKMouseInput": 1]
let trackingArea = NSTrackingArea(rect: newRect, options: options, owner: self, userInfo: userInfo)
if let previousTrackingAreas = view?.trackingAreas {
for area in previousTrackingAreas {
if let theInfo = area.userInfo {
if let _ = theInfo["SKMouseInput"] {
view?.removeTrackingArea(area)
}
}
}
}
view?.addTrackingArea(trackingArea)
}
This SKScene method override will be called shortly after initialization, will allow immunity to window size changes and will clean up the old tracking area. Note that it will still require the mouseMoved override as well.

swift NSTimer userinfo

I'm trying to pass a UIButton with a NSTimer's userinfo. I've read every post on stackoverflow on NSTimers. I'm getting very close but can't quite get there. This post has helped
Swift NSTimer retrieving userInfo as CGPoint
func timeToRun(ButonToEnable:UIButton) {
var tempButton = ButonToEnable
timer = NSTimer.scheduledTimerWithTimeInterval(4, target: self, selector: Selector("setRotateToFalse"), userInfo: ["theButton" :tempButton], repeats: false)
}
the function the timer runs
func setRotateToFalse() {
println( timer.userInfo )// just see whats happening
rotate = false
let userInfo = timer.userInfo as Dictionary<String, AnyObject>
var tempbutton:UIButton = (userInfo["theButton"] as UIButton)
tempbutton.enabled = true
timer.invalidate()
}
I realise you've managed to fix this but I thought I would give you a little more information about using NSTimer. The correct way to access the timer object and hence user info is to use it like below. When initialising the timer you can create it like this:
Swift 2.x
NSTimer.scheduledTimerWithTimeInterval(4, target: self, selector: Selector("setRotateToFalse:"), userInfo: ["theButton" :tempButton], repeats: false)
Swift 3.x<
Timer.scheduledTimer(timeInterval: 1, target: self, selector:#selector(ViewController.setRotateToFalse), userInfo: ["theButton" :tempButton], repeats: false)
Then the callback looks like this:
func setRotateToFalse(timer:NSTimer) {
rotate = false
let userInfo = timer.userInfo as Dictionary<String, AnyObject>
var tempbutton:UIButton = (userInfo["theButton"] as UIButton)
tempbutton.enabled = true
timer.invalidate()
}
Therefore you don't need to keep a reference to the timer and avoid often nasty global variables where possible. You may run into an issue in swift if your class doesn't inherit from NSObject where it says there is no callback defined but this can be easily fixed by adding #objc at the beginning of the function definition.
macOS 10.12+ and iOS 10.0+ introduces a block based API of Timer which is a more convenient way
func timeToRun(buttonToEnable: UIButton) {
timer = Timer.scheduledTimer(withTimeInterval:4, repeats: false) { timer in
buttonToEnable.enabled = true
}
}
A one shot timer will be invalidated automatically after it fires.
An similar convenient way for a one shot timer is using GCD (DispatchQueue.main.asyncAfter)
func timeToRun(buttonToEnable: UIButton) {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4)) {
buttonToEnable.enabled = true
}
}
I was just going to post this as I read over it before I posted. I noticed that I had timer.invalidate() before userinfo so that's why it wasn't working. I will post it as it may help somebody else.
func setRotateToFalse(timer:NSTimer) {
rotate = false
timer.invalidate()
let userInfo = timer.userInfo as Dictionary<String, AnyObject>
var tempbutton:UIButton = (userInfo["theButton"] as UIButton)
tempbutton.enabled = true
}