Creating an observer to check if MediaPlayer playbackState is paused or not - swift

I have a music app and I wish to determine if playback has been paused while the app was closed (due to an event like a phone call or AirPods being taken out of ear etc)
My first approach was to run a func inside of viewWillAppear that checked
if mediaPlayer.playbackState == .paused {
...
}
If it was paused I updated the play/pause button image. However, this did not work, the play/pause button would still show Play even if it was paused.
Next, I tried adding an observer to the viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(self.wasSongInterupted(_:)), name: UIApplication.didBecomeActiveNotification, object: self.mediaPlayer)
The self.wasSongInterupted I call is
#objc func wasSongInterupted(_ notification: Notification) {
DispatchQueue.main.async {
if self.mediaPlayer.playbackState == .paused {
print("paused")
self.isPlaying = false
self.playPauseSongButton.isSelected = self.isPlaying
} else if self.mediaPlayer.playbackState == .playing {
self.isPlaying = true
self.playPauseSongButton.isSelected = self.isPlaying
}
}
}
However, I am still having the same issue.
What is the best way to determine if my music player is playing or paused when I reopen the app?
Edit 1: I Edited my code based on comments.
wasSongInterrupted was not being called, and through breakpoints and errors I discovered the code was mostly not needed. I changed my code to be
func wasSongInterrupted() {
DispatchQueue.main.async {
if self.mediaPlayer.playbackState == .interrupted {
var isPlaying: Bool { return self.mediaPlayer.playbackState == .playing }
print("Playback state is \(self.mediaPlayer.playbackState.rawValue), self.isPlaying Bool is \(self.isPlaying)")
self.playPauseSongButton.setImage(UIImage(named: "playIconLight"), for: .normal)
//self.playPauseSongButton.isSelected = self.isPlaying
}
}
}
and inside my AppDelegate's applicationDidBecomeActive I have
let mediaPlayerVC = MediaPlayerViewController()
mediaPlayerVC.wasSongInterupted()
Now the code runs, however I have an issue.
If I run the following code:
if self.mediaPlayer.playbackState == .interrupted {
print("interrupted \(self.isPlaying)")
}
and then make a call and come back to the app it will hit the breakpoint. It will print out interrupted as well as false which is the Bool value for self.isPlaying
However if I try to update the UI by
self.playPauseSongButton.isSelected = self.isPlaying
or by
self.playPauseSongButton.setImage(UIImage(named: "playIconLight.png"), for: .normal)
I get an error message Thread 1: EXC_BREAKPOINT (code=1, subcode=0x104af9258)

You trying to update you player UI from viewWillAppear. From Apple Documentation:
viewWillAppear(_:)
This method is called before the view controller's view is about to be added to a view hierarchy and before any animations are configured for showing the view.
So if your app was suspended and the becomes active again, this method won't be called, because your UIViewController is already at Navigations Stack.
If you want to catch the moment when your app becomes active from suspended state, you need to use AppDelegate. From Apple Documentation:
applicationDidBecomeActive(_:)
This method is called to let your app know that it moved from the inactive to active state. This can occur because your app was launched by the user or the system.
So you need to use this method at your AppDelegate to handle app running and update your interface.
UPDATE
You saying the inside this AppDelegate method you're doing
let mediaPlayerVC = MediaPlayerViewController()
mediaPlayerVC.wasSongInterupted()
That's wrong because you're creating a new view controller. What you need to do, is to access you existing view controller from navigation stack and update it.
One of the possible solutions is to use NotificationCenter to send a notification. You view controller should be subscribed to this event of course.
At first, you need to create a notification name
extension Notification.Name {
static let appBecameActive = Notification.Name(rawValue: "appBecameActive")
}
Then in you AppDelegate add following code to post your notifications when app becomes active
func applicationDidBecomeActive(_ application: UIApplication) {
NotificationCenter.default.post(name: .appBecameActive, object: nil)
}
And finally in your view controller add to subscribe it on notifications
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(wakeUp),
name: .appBecameActive,
object: nil)
...
}
#objc func wakeUp() {
// Update your UI from here
}
Hope it helps you.

Related

How to pop up a view after application enter foreground?

I'm working on digital banking app. I need the user to be re prompted for PIN/Password after the app enter background for more than X seconds. I look up scene delegate's functions but I have no idea how can I check how long the user has been in foreground and how to popping out the view. I use AppDelegate and SceneDelegate for lifecycle
you can do this by using local notification what you have to do is following.
easy steps for new user
manage a global object for app state // if needed
add a local notification in your main view controller
post a notification from your SceneDelegate
here is the example
add observer in your main controller which always appears when app start or launch
class MainViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(showPopup(notification:)), name:
NSNotification.Name(rawValue: "showPinCodePopup"), object: nil)
}
// remove observer
override func viewWillDisappear(_ animated: Bool)
{
NotificationCenter.default.removeObserver(self)
}
#objc func showPopup(notification: NSNotification) {
//show your popup here
}
}
call the local notification from your SceneDelegate when app becomes active or enter in Foreground.
class SceneDelegate: UIResponder, UIWindowSceneDelegate
{
var window: UIWindow?
func sceneDidBecomeActive(_ scene: UIScene) {
NotificationCenter.default.post(name: Notification.Name(rawValue:"showPinCodePopup"), object: nil, userInfo:nil)
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
}
Hope this solution may helps you 😊

Swift calling a ViewController function from the AppDelegate [duplicate]

I am building an iOS app using the new language Swift. Now it is an HTML5 app, that displays HTML content using the UIWebView. The app has local notifications, and what i want to do is trigger a specific javascript method in the UIWebView when the app enters foreground by clicking (touching) the local notification.
I have had a look at this question, but it does not seem to solve my problem. I have also come across this question which tells me about using UIApplicationState, which is good as that would help me know the the app enters foreground from a notification. But when the app resumes and how do i invoke a method in the viewController of the view that gets displayed when the app resumes?
What i would like to do is get an instance of my ViewController and set a property in it to true. Something as follows
class FirstViewController: UIViewController,UIWebViewDelegate {
var execute:Bool = false;
#IBOutlet var tasksView: UIWebView!
}
And in my AppDelegate i have the method
func applicationWillEnterForeground(application: UIApplication!) {
let viewController = self.window!.rootViewController;
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var setViewController = mainStoryboard.instantiateViewControllerWithIdentifier("FirstView") as FirstViewController
setViewController.execute = true;
}
so what i would like to do is when the app enters foreground again, i want to look at the execute variable and run the method as follows,
if execute{
tasksView.stringByEvaluatingJavaScriptFromString("document.getElementById('sample').click()");
}
Where should i put the code for the logic to trigger the javascript from the webview? would it be on viewDidLoad method, or one of the webView delegate methods? i have tried to put that code in the viewDidLoad method but the value of the boolean execute is set to its initial value and not the value set in the delegate when the app enters foreground.
If I want a view controller to be notified when the app is brought back to the foreground, I might just register for the UIApplication.willEnterForegroundNotification notification (bypassing the app delegate method entirely):
class ViewController: UIViewController {
private var observer: NSObjectProtocol?
override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [unowned self] notification in
// do whatever you want when the app is brought back to the foreground
}
}
deinit {
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
}
}
}
Note, in the completion closure, I include [unowned self] to avoid strong reference cycle that prevents the view controller from being deallocated if you happen to reference self inside the block (which you presumably will need to do if you're going to be updating a class variable or do practically anything interesting).
Also note that I remove the observer even though a casual reading of the removeObserver documentation might lead one to conclude is unnecessary:
If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method.
But, when using this block-based rendition, you really do need to remove the notification center observer. As the documentation for addObserver(forName:object:queue:using:) says:
To unregister observations, you pass the object returned by this method to removeObserver(_:). You must invoke removeObserver(_:) or removeObserver(_:name:object:) before any object specified by addObserver(forName:object:queue:using:) is deallocated.
I like to use the Publisher initializer of NotificationCenter. Using that you can subscribe to any NSNotification using Combine.
import UIKit
import Combine
class MyFunkyViewController: UIViewController {
/// The cancel bag containing all the subscriptions.
private var cancelBag: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
addSubscribers()
}
/// Adds all the subscribers.
private func addSubscribers() {
NotificationCenter
.Publisher(center: .default,
name: UIApplication.willEnterForegroundNotification)
.sink { [weak self] _ in
self?.doSomething()
}
.store(in: &cancelBag)
}
/// Called when entering foreground.
private func doSomething() {
print("Hello foreground!")
}
}
Add Below Code in ViewController
override func viewDidLoad() {
super.viewDidLoad()
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector:#selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func appMovedToForeground() {
print("App moved to foreground!")
}
In Swift 3, it replaces and generates the following.
override func viewDidLoad() {
super.viewDidLoad()
foregroundNotification = NotificationCenter.default.addObserver(forName:
NSNotification.Name.UIApplicationWillEnterForeground, object: nil, queue: OperationQueue.main) {
[unowned self] notification in
// do whatever you want when the app is brought back to the foreground
}

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.

Unable to update NSTouchBar programmatically

I am currently developing a very simple Live Scores MAC OSX app for personal use where I show a bunch of labels (scores) on the touch bar. What I am trying to achieve in a few steps:
Fetch live soccer scores from a 3rd party API every 30 seconds
Parse the scores and make them into labels
Update the touch bar with new scores
[Please note here that this app will not be published anywhere, and is only for personal use. I am aware of the fact that Apple strictly advises against such type of content in the Touch Bar.]
Here is the code that I wrote following basic Touch Bar tutorial from RW (https://www.raywenderlich.com/883-how-to-use-nstouchbar-on-macos). Skeleton of my code is picked from the RW tutorial:
In WindowController (StoryBoard entry point), override makeTouchBar like this:
override func makeTouchBar() -> NSTouchBar? {
guard let viewController = contentViewController as? ViewController else {
return nil
}
return viewController.makeTouchBar()
}
In ViewController, which is also the Touch Bar Delegate, implement the makeTouchBar fn:
override func makeTouchBar() -> NSTouchBar? {
let touchBar = NSTouchBar()
touchBar.delegate = self
touchBar.customizationIdentifier = .scoresBar
touchBar.defaultItemIdentifiers = [.match1, .flexibleSpace, .match2, ... , .match10]
return touchBar
}
NSTouchBarDelegate in ViewController. scores is where I store my fetched scores (See 5). I return nil for views if scores aren't fetched yet:
extension ViewController: NSTouchBarDelegate {
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
if (<scores not fetched yet>) {
return nil
}
// matchNum is the match number for which I am showing scores for
let customViewItem = NSCustomTouchBarItem(identifier: identifier)
customViewItem.view = NSTextField(labelWithString: self.scores[matchNum ?? 0])
return customViewItem
}
}
To fetch scores periodically I am running a scheduled task Timer in viewDidLoad() of my viewcontroller like this:
_ = Timer.scheduledTimer(timeInterval: 30.0, target: self, selector: #selector(ViewController.fetchScores), userInfo: nil, repeats: true)
And finally, this is my fetchScores function that also makes a call to update the Touch Bar:
#objc func fetchScores() {
let url = "<scores api end point>"
Alamofire.request(url).responseJSON { response in
if let json = response.result.value {
// update self.scores here and fill it with latest scores
if #available(OSX 10.12.2, *) {
//self.touchBar = nil
self.touchBar = self.makeTouchBar() // This is where I am calling makeTouchBar again to update Touch Bar content dynamically
}
}
}
My understanding from the code above is that once I make a call to makeTouchBar in fetchScores and assign it to my viewcontroller's touchBar property, it should ideally call touchBar(:makeItemForIdentifier) delegate function to update the Touch Bar view (SO thread on this). But in my case, touchBar(:makeItemForIdentifier) is never called. The only time touchBar(:makeItemForIdentifier) is called is the first time, when makeTouchBar is called from my WindowController (See point 1 above). And since scores have not been retrieved yet, my touch bar remains empty.

how to detect tap home-button twice in ios9

In my app which am writing to learn swift and iOS9, I'm trying to pause my NStimer when user double click the home button and becomes at app switcher, accoridng to programming ios9 matt neuberg, when The user double-clicks the Home button, The user can now work in the app switcher interface. If your app is frontmost, your app delegate receives this message:
applicationWillResignActive:
But my timer only pauses when I tap home button once and when I tap twice and have the app switcher, I see my timer counting, any ideas?
Try to add this lines in your AppDelegate.swift:
static let kAppDidBecomeActive = "kAppDidBecomeActive"
static let kAppWillResignActive = "kAppWillResignActive"
func applicationDidBecomeActive(application: UIApplication) {
// Your application is now the active one
// Take into account that this method will be called when your application is launched and your timer may not initialized yet
NSNotificationCenter.defaultCenter().postNotificationName("kAppDidBecomeActive", object: nil)
}
func applicationWillResignActive(application: UIApplication) {
// Home button is pressed twice
NSNotificationCenter.defaultCenter().postNotificationName("kAppWillResignActive", object: nil)
}
In addition, set your view controller as the observer to those notifications:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(pauseGame), name: AppDelegate.kAppWillResignActive, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(resumeGame), name: AppDelegate.AppDelegate.kAppDidBecomeActive, object: nil)
}