prefersStatusBarHidden not updating after calling setNeedsStatusBarAppearanceUpdate() - swift

Different vcs inside my app show the status bar visible and others are hidden. This is set to YES in the info.pList
"View controller-based status bar appearance": YES
// also tried togging this between yes and no
"Status bar is initially hidden": YES
The app has 2 windows, the main window and a second window. The second window gets presented it front of the main window on a button push. The vc in the second window has the status bar hidden.
The problem is if I'm on a vc (mainVC) inside the main window that shows the status bar, I press the button to show the second window, mainVC's status bar disappears. The second window gets presented and after I dismiss it I send a notification to mainVC to call setNeedsStatusBarAppearanceUpdate() but prefersStatusBarHidden isn't triggered so the status bar stays hidden even though it shouldn't be. I even subclassed a Navigation Controller and added the code there with mainVC as it's root.
Why isn't prefersStatusBarHidden getting called?
I added prefersStatusBarHidden inside the mainVC by itself, the nav to the mainVC by itself, and then in both the mainVC and it's nav at the same time. It's still not getting called after setNeedsStatusBarAppearanceUpdate() gets called in either places.
Subclassed nav:
class MainVCNavController: UINavigationController {
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
NotificationCenter.default.addObserver(self, selector: #selector(updateStatusBar), name: NSNotification.Name(rawValue: "updateStatusBar"), object: nil)
}
let statusBarHidden: Bool = false
#objc func updateStatusBar() {
self.setNeedsStatusBarAppearanceUpdate() // this gets called when the notification is triggered
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden // this doesn't get called after setNeedsStatusBarAppearanceUpdate() is called
}
// I added this just to see if it would make a difference but it didn't
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
override open var childViewControllerForStatusBarStyle: UIViewController? {
return self.topViewController
}
override open var childViewControllerForStatusBarHidden: UIViewController? {
return self.topViewController
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
}
MainVC is the rootVC of the above nav
class MainVCController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(updateStatusBar), name: NSNotification.Name(rawValue: "updateStatusBar"), object: nil)
}
let statusBarHidden: Bool = false
#objc func updateStatusBar() {
self.setNeedsStatusBarAppearanceUpdate() // this gets called when the notification is called
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden // this doesn't get called after setNeedsStatusBarAppearanceUpdate() is triggered
}
// I added this just to see if it would make a difference but it didn't
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
}
SecondVC inside the second window has it's status bar hidden. It sends the notification when it's dismissed to the above mainVC:
class SecondController: UIViewController {
override var prefersStatusBarHidden: Bool {
return true
}
if dismissed {
NotificationCenter.default.post(name: Notification.Name(rawValue: "updateStatusBar"), object: nil)
}
}
I also read that I need to call the below to trigger the prefersStatusBarHidden but even when I added these to the updateStatusBar() it didn't make a difference.
navigationController?.setNavigationBarHidden(false, animated: false)
// or
navigationController?.navigationBar.isHidden = false

Updating the status bar needs to be on the main thread.
There are two ways to ensure that:
Add notification observer on the main thread: (you don't need to expose the func to objc c):
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "updateStatusBar"), object: nil, queue: .main, using: updateStatusBar)
func updateStatusBar(_ notification: Notification) {
setNeedsStatusBarAppearanceUpdate()
}
Or update the status bar on the main thread:
NotificationCenter.default.addObserver(self, selector: #selector(updateStatusBar(_:)), name: NSNotification.Name(rawValue: "updateStatusBar"), object: nil)
#objc func updateStatusBar(_ notification: Notification) {
DispatchQueue.main.sync {
self.setNeedsStatusBarAppearanceUpdate()
}
}

#zombie’s answer about the updating on the main thread 100% worked. On another note he also suggested I use symbolic breakpoints to diagnose the problem. He provided a great link to help:
Symbolic Breakpoints

Related

NotificationCenter - addObserver not called

I am trying a very simple code with NotificationCenter. But the addObserver is not getting called. Can any one of you check and let me know what i am missing. There are 2 simple class, one which post notification and another which listens to it. When i run the program, i just see "sending notification" in the console.
Thanks in advance.
Class 1:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("sending notification")
NotificationCenter.default.post(name: Notification.Name("test"), object: nil)
}
}
Class 2:
class secondvc: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("second vc")
NotificationCenter.default.addObserver(self,
selector: #selector(doThisWhenNotify(_:)),
name: Notification.Name("test"),
object: nil)
}
#objc func doThisWhenNotify(_ notification: Notification) {
print("inside notification")
}
}
If, at the time ViewController comes into existence, secondvc does not yet exist, then there is no one there to receive the posted notification and that is why you don't see the notification being received later when secondvc does come into existence.

What is the best way of updating a variable in a view controller from scene delegate?

I am using Spotify SDK. I want to change labels in some view controllers when a user changes his/her player state. Here is my scene delegate:
var playerViewController = MatchViewController()
func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) {
playerViewController.stateChanged(playerState)
}
A view controller:
func stateChanged(_ playerState: SPTAppRemotePlayerState) {
// aLabel.text = playerState.track.name
}
The problem is labels or other outlets are nil when the state is changed because the view controllers are not loaded at that time. How can I fix that? (I tried isViewLoaded)
If you have a more than a few places to update according to a change that occurs at one place use observers. Here's how,
Post notification in SceneDelegate like this:
func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "stateChanged"), object: nil, userInfo: ["playerState": playerState])
}
Observe in ViewControllers like this:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(stateChanged), name: NSNotification.Name("stateChanged"), object: nil)
}
#objc func stateChanged(_ notification: Notification) {
if let playerState = notification.userInfo?["playerState"] as? SPTAppRemotePlayerState {
print(playerState)
}
}
}

How to push controller after dismiss presented controller in swift?

Hey I am showing a controller (A) in main controller with presentation style (not push), and I want to button tapped and push another controller (B) after dismiss this (A) controller, this situation occurred in main controller. I am using protocol for this situation. Any idea for that ? Code like below.`
//this is dismiss button action
var segueDelegate: segueFromController?
#objc func dismissController() {
self.dismiss(animated: true) {
self.segueDelegate?.segueFromController()
}
//and this one is protocol function in main controller
func segueFromController() {
let contProfile = ContViewController(collectionViewLayout: UICollectionViewFlowLayout())
navigationController?.pushViewController(contProfile, animated: true)
}
// and I am making "self" this protocol in main controller's didload
let aCont = AController()
override func viewDidLoad() {
super.viewDidLoad()
AController.segueDelegate = self
}
// protocol
protocol segueFromController {
func segueFromController()
}
// this is presenting (A) controller code in main page
func openController() {
let preController = AController()
preController.modalPresentationStyle = .fullScreen
self.present(preController, animated: true, completion: nil)
}
First you need to make this segueDelegate weak
protocol segueFromController : class {
func segueFromController()
}
weak var segueDelegate: segueFromController?
func openController() {
let preController = AController()
preController.segueDelegate = self
preController.modalPresentationStyle = .fullScreen
self.present(preController, animated: true, completion: nil)
}
Try to dismiss without animation
self.dismiss(animated: false) {
self.segueDelegate?.segueFromController()
}

swift - How to detect from what ViewController application enter background?

In my swift app I need to know from what screen my application entered background. Im trying to use NotificationCenter this way:
class MainViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(appMovedToBackgroundMain), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
#objc func appMovedToBackgroundMain() {
print("main - App moved to Background!")
}
}
class InitViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(appMovedToBackgroundInit), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
#objc func appMovedToBackgroundInit() {
print("init - App moved to Background!")
}
}
and when I'm press Home button at the MainViewController I got in Xcode's console these lines:
init - App moved to Background!
main - App moved to Background!
and I expected only one line there - main - App moved to Background!. How can I reach this?
On AppDelegate Methods: applicationDidEnterBackground or applicationWillEnterForeground, you can get the top most UIViewController. It is well explained on this question: Get top most UIViewController
You can use following function:
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
Here you can check which controller is on the top in your navigation controller's controller.
print(self.navigationController.topViewController)
When application enter in background state below method will call.
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}

Return control to function when modal viewcontroller dismissed

I need to present a modal VC that sets a property in my presenting VC, and then I need to do something with that value back in the presenting VC. I have to be able to pass pointers to different properties to this function, so that it's reusable. I have the code below (KeyPickerTableViewController is the modal VC).
It should work, except not, because the line after present(picker... gets executed immediately after the picker is presented.
How do I get my presenting VC to "wait" until the modal VC is dismissed?
#objc func fromKeyTapped(_ button: UIBarButtonItem) {
print("from tapped")
setKey(for: &sourceKey, presentingFrom: button)
}
#objc func toKeyTapped(_ button: UIBarButtonItem) {
print("from tapped")
setKey(for: &destKey, presentingFrom: button)
}
fileprivate func setKey(for key: inout Key!, presentingFrom buttonItem: UIBarButtonItem) {
let picker = KeyPickerTableViewController()
picker.delegate = self
picker.modalPresentationStyle = .popover
picker.popoverPresentationController?.barButtonItem = buttonItem
present(picker, animated: true, completion: nil)
if let delKey = delegatedKey {
key = delKey
}
}
You could use delegate pattern or closure.
I would do the following
1. I would not use inout pattern, I would first call the popover and then separately update what is needed to be updated
2. In KeyPickerTableViewController define property var actionOnDismiss: (()->())? and setting this action to what we need after initialisation of KeyPickerTableViewController
I could show it in code, but the abstract you've shown is not clear enough to come up with specific amendments. Please refer the illustration below.
import UIKit
class FirstVC: UIViewController {
var key = 0
#IBAction func buttonPressed(_ sender: Any) {
let vc = SecondVC()
vc.action = {
print(self.key)
self.key += 1
print(self.key)
}
present(vc, animated: true, completion: nil)
}
}
class SecondVC: UIViewController {
var action: (()->())?
override func viewDidLoad() {
onDismiss()
}
func onDismiss() {
action?()
}
}
While presenting VC, add dismissing modal VC action in its completion handler, so that Viewcontroller will be presented after dismissal is completed
present(picker, animated: true, completion: { (action) in
//dismissal action
if let delKey = delegatedKey {
key = delKey
}
})