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

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)
}
}
}

Related

How to send a notification from UIKit to a view in SwiftUI?

I am trying to send a notification from UIViewcontroller to SwiftUI View after the user did pull to refresh.
#objc private func fetchScheduleData(_ sender: UIRefreshControl) {
NotificationCenter.default.post(name: Notification.Name(rawValue: "didPullToRefreash"), object: nil)
}
On SwiftUI view i trying to set this method .onchange()
NotificationCenter.default.addObserver(self, selector: #selector(didPullToRefreashHelper), name: Notification.Name(rawValue: "didTapNotification"), object: nil)
But onChange it's not working. I am wondering i how i will do this.
The simplest way of doing this would be to first, create the custom notification like this:
extension Notification.Name {
static let didPullToRefresh = Notification.Name("didPullToRefresh")
}
That now lets you address it with dot notation. Next, in your VC:
#objc private func fetchScheduleData(_ sender: UIRefreshControl) {
NotificationCenter.default.post(name: .didPullToRefresh, object: nil)
}
Lastly, in your SwiftUI view:
.onReceive(NotificationCenter.default.publisher(for: .didPullToRefresh)) { _ in
// If you are passing an object, this can be "notification in"
// Do something here as a result of the notification
}
edit:
If you want to send a message in a SwiftUI view when a variable changed, then you could use .onChange(of:) like this:
.onChange(of: watchedStateVar) { value in
NotificationCenter.default.post(name: .didPullToRefresh, object: value)
}

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.

Removing NSNotificationObserver From Different ViewController

I have an observer that I register in one class as so:
class ViewControllerA: UIViewController {
//create shared class reference
var sharedClassReference_A = SharedClass()
//initialize Notification Observer and store observer reference in sharedClass
override func viewDidLoad() {
super.viewDidLoad()
var observerHandler: Any? = nil
observerHandler = NotificationCenter.default.addObserver(self, selector: #selector(ViewControllerA.appDidTerminate(_:)), name: .UIApplicationWillTerminate, object: nil)
self.sharedClassReference_A.sharedHandler = observerHandler
}
//perform some action when a notification is received
#objc func appDidTerminate(_ notification: NSNotification) {
//perform some action
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueA_X" {
let destinationController = segue.destination as! ViewControllerX
destinationController.sharedClassReference_X = self.sharedClassReference_A
}
}
}
I store a reference to the observer in a shared class:
class SharedClass {
var sharedHandler: Any? = nil
}
I attempt to remove the observer once I reach a different view controller as so:
class ViewControllerX: UIViewController {
var sharedClassReference_X = SharedClass()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//Attempt to remove observer registered in ViewControllerA
if let observerHandler = self.sharedClassReference_X.sharedHandler {
NotificationCenter.default.removeObserver(observerHandler)
}
}
}
I know that removing the observer using this approach is failing, because the observer is getting called after ViewControllerX is deallocated.
My question is: How can I successfully initialize an observer in one class (ViewControllerA) and be able to remove it later in a different class (ViewControllerX)?
I think it's better to follow the general guidelines of setting the observers inside viewDidLoad/viewWillAppear and removing them inside deinit / viewDidDisappear respectively according to your case , as this
NotificationCenter.default.addObserver(self, selector: #selector(ViewControllerA.appDidTerminate(_:)), name: .UIApplicationWillTerminate, object: nil)
returns void
//
class ViewControllerX: UIViewController {
var aRef:ViewControllerA!
}
in prepareForSegue
destinationController.aRef = self
Then use
NotificationCenter.default.removeObserver(aRef, name: NSNotification.Name.UIApplicationWillTerminate, object: nil)

prefersStatusBarHidden not updating after calling setNeedsStatusBarAppearanceUpdate()

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

Got delegate nil error even it was set in the main viewcontroller

I have simple app which the welcomeVC shows a greeting message with a person's name. In another VC nameVC, users can change the name they want to be called, and it supposes to update the name on the welcomeVC when they click back.
In order to pass the name from nameVC back to welcomeVC, I set up a protocol ChangeNameDelegate in nameVC, and created a variable in my class:
protocol ChangeNameDelegate {
func updateName(name: String)
}
class nameViewController: UIViewController {
#IBOutlet weak var nameTextField: UITextField!
var changeNameDelegate: ChangeNameDelegate!
override func viewDidLoad() {
}
#IBAction func closeNameVC(_ sender: UIButton) {
if let newName = nameTextField.text {
changeNameDelegate.updateName(name: newName)
}
dismiss(animated: true, completion: nil)
}
}
In my welcomeVC, I have made sure that it register the delegate and conforms to the protocol by setting up as follows:
class welcomeViewController: UIViewController, UIViewControllerTransitioningDelegate {
let nameVC = nameViewController()
override func viewDidLoad() {
nameVC.changeNameDelegate = self
}
}
extension welcomeViewController: ChangeNameDelegate {
func updateName(name: String) {
print("The name has been updated!")
nameLabel.text = name
}
}
However when I ran my app, it got a crash because changeNameDelegate appeared to be nil. Does anybody know what could be missed there? Thanks in advance!
It seems that you are instantiating new instance of NameViewController when routing from WelcomeViewController. You should navigate to nameVC which is instantiated before.
I later solved the issue by using notification/observer.
In the parent view:
let userUpdateNotification = Notification.Name(rawValue: "nameNotificationKey")
NotificationCenter.default.addObserver(self, selector: #selector(updateUser), name: userUpdateNotification, object: nil)
}
func updateUser(not: NSNotification) {
if let userInfo = not.userInfo {
if let userName = userInfo["name"] as? String {
nameLabel.text = userName
}
}
}
In the child view:
let name = Notification.Name(rawValue: "nameNotificationKey")
NotificationCenter.default.post(name: name , object: self, userInfo: ["name": nameTextField.text!] )
}
I don't know if delegate/protocol doesn't work between view controllers that are unconnected. Anyone who knows about this is welcomed to comment!