I am having a little bit of trouble. I am trying to use the notification center to alert the app that some content is done loading. After the user signs up or logs in successfully I create the view controller and then and then make it the root view controller using the blow
func finishLoggingIn() {
// print("Finish logging in from LoginController")
let homeController = HomeViewController()
self.loginButton.stopAnimation(animationStyle: .expand, completion: {
self.view.window?.rootViewController = homeController
self.view.window?.makeKeyAndVisible()
})
}
The login button just creates a loading animation on the button for UI purposes.
When I first enter the controller I add the observer for the notification.
let MainVCSetup = Notification.Name("mainVCComplete")
NotificationCenter.default.addObserver(self, selector: #selector(handleRootViewSwitch), name: MainVCSetup, object: nil)
When the content in my mainVC is done loading I post this same notification to the Notification Center like so
NotificationCenter.default.post(name: MainVCSetup, object: nil)
However this function never fires off no matter what I do
#objc func handleRootViewSwitch(){
print("Trying to handle root view switch attack")
NotificationCenter.default.removeObserver(self, name: MainVCSetup, object: nil)
}
If anyone notices where I went wrong I would greatly appreciate it.
Related
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.
In my app, there is a ViewController.swift file and a popupViewController.swift file. Inside the app, when I open the popupViewController with storyboard segue as presentModally and then come back from popupViewController to ViewController with the code dismiss(), the methods viewDidLoad, viewWillAppear, viewDidAppear, ViewWillLayoutSubviews etc. nothing works, they execute just once and don't repeat when I go and return back. So, I want to execute the code every time when viewController.swift is active. I couldn't find a useful info in stackoverflow about this.
Meanwhile, I don't know much about notification and observers(if certainly needed), therefore, can you tell step by step in detail how to do that in Swift (not objective-c)? I mean how to determine if current view controller is active.
Edit: I am navigating from StoryBoard segue, presentModally. There is no Navigation Controller in storyboard.
I tried some codes but nothing happens. The point I came so far is:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector:#selector(appWillEnterForeground), name:UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func appWillEnterForeground() {
print("asdad") //nothing happens
if self.viewIfLoaded?.window != nil {
// viewController is visible
print("CURRENT VİEW CONTROLLER") //nothing happens
}
}
As mention in my comments, I don't use storyboards. There may be a way to create an unwind segue - or maybe not - but [here's a link][1] that may help you with a storyboard-only way of fixing your issue. A quick search on "modal" turned up 9 hits, and the second one starts going into details.
I'm thinking the issue is with what modality is. Basically, your first view controller, which properly executed viewDidAppear, is still visible. So it's effectively not executing viewDidDisappear when your second VC is presented.
You might want to change your concept a bit - an application window (think AppDelegate and/or SceneDelegate become active, where a UIViewController has a is initialized and deinitialized, along with a root UIView that is loaded, appears* and disappears*. This is important, because what you want to do is send your notification from the modal VC's viewDidDisappear override.
First, I find it easiest to put all your notication definitions in an extension:
extension Notification.Name {
static let modalHasDisappeared = Notification.Name("ModalHasDisappeared")
}
This helps not only reduce string typos but also is allows Xcode's code completion to kick in.
Next, in your first view controller, ad an observer to this notification:
init() {
super.init(nibName: nil, bundle: nil)
NotificationCenter.default.addObserver(self, selector: #selector(modalHasDisappeared), name: .modalHasDisappeared, object: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
NotificationCenter.default.addObserver(self, selector: #selector(modalHasDisappeared), name: .modalHasDisappeared, object: nil)
}
#objc func modalHasDisappeared() {
print("modal has disappeared")
}
I've added both forms of init for clarity. Since you are using a storyboard, I'd expect that init(coder:) is the one you need.
Finally, just send the notification when the modal has disappeared:
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.post(name: .modalHasDisappeared, object: nil, userInfo: nil)
}
This sends no data, just the fact that the modal has disappeared. If you want to send data - say, a string or a table cell value, change the object parameter to it:
NotificationCenter.default.post(name: .modalHasDisappeared, object: myLabel, userInfo: nil)
And make the following changes in your first VC:
NotificationCenter.default.addObserver(self, selector: #selector(modalHasDisappeared(_:)), name: .modalHasDisappeared, object: nil)
#objc func modalHasDisappeared(_ notification:Notification) {
let label = notification.object as! UILabel!
print(label.text)
}
Last notes:
To repeat, note that by declaring an extension to Notification.Name, I've only have one place where I'm declaring a string.
There is no code in AppDelegate or SceneDelegate, nor any references to `UIApplication(). Try to think of the view (and view controller) as appearing/disappearing, not background/foreground.
While the first view is visually in the background, it's still visible. So the trick is to code against the modal view disappearing instead.
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.
I'm adding deep linking to my app and getting stuck at handing off the URL to the view controller. I try to use notification but it doesn't work all the times.
My App Delegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
let notification = Notification(name: "DeepLink", object: url)
NotificationCenter.default.post(notification)
return true
}
}
And View Controller:
class ViewController: UIViewController {
func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.handleDeepLink), name: "DeepLink", object: nil)
}
#objc private func handleDeepLink(notification: Notification) {
let url = notification.object as! URL
print(url)
}
}
Problem: when my app is cold launched the notification is not handled, which I guess is due to the View Controller not having time to register itself as the observer yet. If I press the Home button, go to Notes and click on the deep link a second time, everything works as it should.
Question: how can I register the View Controller to observe the notification when the app is cold launched? Or is there a better way to send the URL from the App Delegate to the View Controller?
I think an initial assumption is to assume your view controller does not have time to register itself which means that the connection between URL and View controller must be decoupled and reside outside of the view controller.
I would then use some kind of lookup to instantiate the view controller when a URL is received.
For example,
if url.contains(“x”) {
let vc = ViewController(...)
cv.url = URL // Pass contextual data to the view controller.
// Present or push cv
}
As your app gets more complex you have to manage more challenging scenarios, like standing-up entire controller stacks, or removing presented controllers.
I followed the example in the LaunchMe app. Here's what solved my problem:
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
// My root view controller is a UINavigationController
// Cast this to whatever class your root view controller is
guard let navigationController = self.window?.rootViewController as? UINavigationController else {
return false
}
// Navigate to the view controller that handles the URL within the navigation stack
navigationController.popToRootViewController(animated: false)
// Handle the URL
let vc = navigationController.topViewController as! ViewController
vc.handleDeepLink(url: url)
return true
}
Store if notification is not handled and load it when viewDidLoad.
var pendingNotificationInfo: PushNotificationInfo?
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)
}