Show/Hide UIButton in another controller - ios

I have a button in my ViewController.swift:
#IBOutlet weak var exampleButton: UIButton!
I would like to show/hide that button from the AppDelegate, when something specific happens (i.e. the app enter background etc.).
How can i do that?

One Approach can be
- You can use Notifications for this
Add Observer in your view controller where button needs to be hidden
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: "hideButton",
name: #"HIDE_BUTTON_NOTIFICATION",
object: nil)
func hideButton() -> Void {
// Hide your button here
// Remember to hide on main thread
}
From wherever you want to hide the button (like from AppDelegate), you can post this notification
NSNotificationCenter.defaultCenter().postNotificationName(#"HIDE_BUTTON_NOTIFICATION", object: nil)

Rather than let another object directly manipulate it, use a notification to indicate a state change. The state dictates whether the button appears or not.
Make sure in your notification listener that you only touch the button on the main thread.

Notification is a great idea, but what if your ViewController is not your initial ViewController, or hasn't been initialized yet? It will not be able to catch this notification. Possible solution (maybe not elegant) in extension to other answers is to provide a flag. Your ViewController will be checking it, e.g. in viewDidLoad(). Your flag could be stored in a singleton object which will be catching notification from AppDelegate.
To sum up, you should add notification observer in your ViewController, to catch event from AppDelegate. (like in other answers)
Create singleton class to store appropriate information.
Check condition in viewDidLoad:
if YOUR_SINGLETON.YOUR_FLAG == true {
showButton()
} else {
hideButton()
}
Don't forget to add notification observer also in your singleton class.

Related

Call method on all the existing instances of UIViewController

I want to be able to allow the user to change some properties of the graphical user interface immediately through all the app. To achieve that, I thought about creating a protocol like
protocol MyProtocol {
func changeProperties()
}
so each UIViewController can change those properties in its own way and then call this method in all the current instantiated controllers.
However, I don't know if this is possible. My first idea was to access the most root controller of the app, and then iterate through all the child recursively calling the method. Something like
func updatePropertiesFrom(_ vc: UIViewController) {
for child in vc.childViewControllers {
if let target = child as? MyProtocol {
target.changeProperties()
}
updatePropertiesFrom(child)
}
}
let appRootController = ...
updatePropertiesFrom(appRootController)
I don't know how to get that appRootController and I would like to know if there's any more elegant way for doing this. Thanks.
You can use NotificationCenter. E.g. define a notification name:
extension Notification.Name {
static let changeViewProperties = Notification.Name(Bundle.main.bundleIdentifier! + ".changeViewProperties")
}
And your various view controllers can then register to be notified when this notification is posted:
NotificationCenter.default.addObserver(forName: .changeViewProperties, object: nil, queue: .main) { _ in
// do something here
}
(If you're supporting iOS versions prior to iOS 9, make sure to remove your observer in deinit.)
And to post the notification when you want to initiate the change:
NotificationCenter.default.post(name: .changeViewProperties, object: nil)
You can access the app's root controller with:
UIApplication.shared.keyWindow.rootViewController
Of course this assumes the current key window is the one with your app's root controller.
You could also access the window property of your app delegate and get the rootViewController from that window.
Your general approach is valid. There is no other "more elegant" way to walk your app's current view controllers. Though you should also traverse presented view controllers as well as the child view controllers.
However, a simpler solution might be to post a notification using NotificationCenter and have all of your view controllers respond accordingly.
You could do this with a central dispatcher and having each UIViewController register with it.
Example:
class MyDispatcher{
static let sharedInstance = MyDispatcher()
var listeners: [MyProtocol]()
private init() {}
public func dispatch(){
for listener in listeners{
listener.changeProperties()
}
}
}
From each UIViewController you want to update the properties, you call MyDispatcher.sharedInstance.listeners.append(self). When you want to update the properties, you call MyDispatcher.sharedInstance.dispatch().

how to call a method in a view controller from Appdelegate in Swift?

this Main Menu VC will be opened when the app launched for the first time or after the user back to the app (the app become active after enter the background state).
every time this main menu VC is opened, ideally I need to update the time that the date time data comes from the server. in this main menu vc class I call getDateTimeFromServer() after that I updateUI().
but to update the data after the app enter the background and back to the foreground, the getDateTimeFromServer() and updateUI() shall be activated from Appdelegate using function.
func applicationWillEnterForeground(application: UIApplication) {
}
so how do I activate a method that are exist in Main Menu VC from AppDelegate
You don’t need to call the view controller method in app delegate. Observe foreground event in your controller and call your method from there itself.
Observe for the UIApplicationWillEnterForeground notification in your viewController viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.yourMethod), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
Implement this to receive callback when user enters foreground
#objc func yourMethod() {
// Call getDateTimeFromServer()
}
These types of messaging are in most cases done with static context. As it was already mentioned you could alternatively use notification center within the within the view controller to be notified of your application entering foreground. I discourage you creating custom notifications for this though (but is a possible solution as well).
Anyway for your specific case I suggest you have a model that contains your data. Then create a shared instance of it.
class MyDataModel {
static var shared: MyDataModel = {
let model = MyDataModel()
model.reloadData()
return model
}()
var myObjects: [MyObject]?
func reloadData() {
// load data asynchronously
}
}
Now when your view controller needs to reload it simply uses MyDataModel.shared.myObjects as data source.
In app delegate all you do is reload it when app comes back to foreground using MyDataModel.shared.reloadData().
So now a delegate is still missing so we add
protocol MyDataModelDelegate: class {
func myDataModel(_ sender: MyDataModel, updatedObjects objects: [MyObject]?)
}
class MyDataModel {
weak var delegate: MyDataModelDelegate?
static var shared: MyDataModel = {
Now when your view controller appears it needs to assign itself as a delegate MyDataModel.shared.delegate = self. And implement the protocol in which a reload on the view must be made.
A callout to the delegate can simply be done in a model setter:
}()
var myObjects: [MyObject]? {
didSet {
delegate.myDataModel(self, updatedObjects: myObjects)
}
}
func reloadData() {
You can do something like that, using a technique called Key-Value Observation:
class CommonObservableData: NSObject {
// Use #objc and dynamic to ensure enabling Key-Value Observation
#objc dynamic var dateTime: Date?
static let shared = CommonObservableData()
func updateFromWeb() {
// callWebThen is a function you will define that calls your Web API, then
// calls a completion handler you define, passing new value to your handler
callWeb(then: { self.dateTime = $0 })
}
}
Then you observe on it using Swift 4 's new NSKeyValueObservation.
class SomeViewController: UIViewController {
var kvo: NSKeyValueObservation?
func viewDidLoad() {
...
kvo = CommonObservableData.shared.observe(
\CommonObservableData.dateTime, { model, change in
self.label.text = "\(model.dateTime)"
})
}
}
Key-Value Observation is originally an Objective-C technique that is "somewhat revived" by Swift 4, this technique allows you to observe changes on a property (called a Key in Objective-C) of any object.
So, in the previous code snippets, we made a class, and made it a singleton, this singleton has an observable property called dateTime, where we could observe on change of this property, and make any change in this property automatically calls a method where we could update the UI.
Read about KVO here:
Key-Value Observation Apple Programming Guide
Key-Value Observation using Swift 4
Also, if you like Rx and RFP (Reactive Functional Programming), you can use RxSwift and do the observation in a cleaner way using it.
In swift 4 and 5, the notification name is changed the below code working for both.
notifyCenter.addObserver(self, selector: #selector(new), name:UIApplication.willEnterForegroundNotification, object: nil)
#objc func new(){}

How to pass in a nil notification variable in Swift

I am relatively new to Swift and so my head often returns to Objective-C solutions when coding.
I am coding an audio cell for a podcast player. This has a custom UIView slider inside and an audio manager singleton. The slider communicates with its parent UITableViewCell when it is manually changed by sending a notification, the cell then communicates with the audio manager to play/pause the audio.
I am currently writing the code to make the audio react to a manual change of the slider, for this I am sending the percent drag on the slider to the Cell so it can play the appropriate audio.
// Notification sent containing information - notification sent from SlideView
NotificationCenter.default.post(name: Notification.Name("SlidebarManuallyMovedNotification"), object: nil, userInfo: ["percent": Double(self.getCurrentPercent())])
// Notification observed in UITableViewCell
NotificationCenter.default.addObserver(self, selector: #selector(self.refreshCell(notification:)), name: Notification.Name("SlidebarManuallyMovedNotification"), object: nil)
The sending and receiving of notifications is working fine:
// Some info needs to be refreshed depending on the cell state
func refreshCell(notification: NSNotification) {
// If we have user information it means we have manually changed the position so want to refresh the information
if (notification.userInfo != nil) {
... // I then run code depending on whether we have user info or not
As you can see I receive the notification and then move to refresh the cell.
The issue now comes from trying to call the refreshCell manually from inside my UITableViewCell:
// 1
self.refreshCell(notification: nil)
// 2
self.refreshCell(notification: NSNotification())
// 3
let notify: NSNotification = NSNotification()
self.refreshCell(notification: notify)
I tried the first method as in Objective-C it was often easy to load a different view with or without information initWithCellInfo: (NSDictionary *) userInfo. In this scenario I want to do the same, call the function without any userInfo so that I can refresh my cell slightly differently.
I then tried adding an empty notification so that it would be passed in and would return nil userInfo. When I do this I receive the following warning:
[NSConcreteNotification init]: should never be used'
This points me towards this answer but I don't want to create an actual notification, just point towards a dummy on to call the function.
Other thoughts:
I have thought of creating a two functions:
func refreshCellInfo(notification: NSNotification) {
let userInfo = "foo"
self.refreshCellInfo(userInfo)
}
func refreshCellInfo(info: String) {
I could then just call refreshCellInfo("") and add an if statement checking the string instead of the notification.
This just doesn't seem a very elegant way of doing it, adding a completely unnecessary function and fudging the if statement to get round the issue instead of understanding it.
If anyone can explain what is happening here and whether there is a good solution for this I would be very appreciative, I feel that this really highlights a gap in my understanding and although I can probably patch the issue I really want to learn from it to move forward as a developer.
You need to refactor slightly. Separate the function that receives the notification from the function that refreshes the cell and then call the second from the first.
You could say something like:
func receiveDragNotification(notification: NSNotification) {
self.refreshCellInfo(notification)
}
func refreshCellInfo(_ notification: Notification?) {
// process update
}
Now, since the notification parameter is optional in refreshCellInfo, you can pass nil if you want to.
But really, you should further separate the functionality, so that you have one function that is responsible for receiving the notification and updating the data model and a second that is responsible for updating the cell based on the data model (Note that you haven't provided all the details so I am using a fictitious
self.position as a data model)
func receiveDragNotification(notification: NSNotification) {
// update data model based on notification
self.position = someDataFromNotification
// Now refresh the cell based on this new data
self.refreshCellInfo()
}
func refreshCellInfo() {
// use data model self.position to update cell
}
Now you can call refreshCellInfo any time you need without needing to pass a parameter.

Go back to previous controller from class swift

I have an app that use http calls to stream video from external storage.
When the user's device isn't connected to a network service, I need the app to go back to the previous controller.
the flow is the following: the user access a list of elements (table view cell), select one, then the app goes to the player controller. On this controller, the call is made to stream the file.
I use an api call handler in a class outside of the controller and I don't know how to proceed to make it go back to the previous controller from here (the element list).
Connectivity issues errors are all catched within the api class.
I don't show any code as Im not sure it would be relevant. If you need to see anything, let me know and I will update the question. What should be the way to do that? (of course I use a navigation controller)
Thanks
If you want go back to the previous view controller you should use:
navigationController?.popViewControllerAnimated(true)
If you need to use this function not in the view-controller but in another class you can use NSNotificationCenter for notify the view-controller when it's needed to show the previous controller, just like this:
YourViewController
override func viewDidLoad()
{
...
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: "goBack:",
name: "goBackNotification",
object: nil)
...
}
func goBack(notification: NSNotification)
{
navigationController?.popViewControllerAnimated(true)
}
AnotherClass
NSNotificationCenter.defaultCenter().postNotificationName("goBackNotification", object: nil)
Don't forget to remove the observer in your YourViewController:
deinit
{
NSNotificationCenter.defaultCenter().removeObserver(self)
}
EDIT 1: you can use obviously a delegate instead of a NSNotification method. If you don't know the differences between NSNotification and delegate I recommend to you this answer.
A common approach besides NSNotificationCenter is to utilize closures or delegates to inform your ViewController that the streaming attempt failed. Using closures, the API of the class responsible for the streaming could be extended to take a completion closure as parameter, and call it with an NSError, if one occurred, or nil if it didn't.
func streamFile(completion: NSError? -> Void) {
// Try to start the streaming and call the closure with an error or nil
completion(errorOrNil)
}
When the call to the API in the ViewController is made you can then pass a closure to the method and check for errors. In case something went wrong an error should be present and the ViewController should be dismissed
func startStream() {
StreamingAPIClient.streamFile(completion: { [weak self] error in
if error != nil {
// Handle error
self.dismissViewControllerAnimated(true, completion: nil)
} else {
// Proceed with the streaming
}
})
}

Do I need to add observer if I have a func to take NSNotification as parameter

Just like the question title
for say I have code like
func receieveNotification(notification : NSNotification) {
......verify notification
......retrieve userInfo
}
Do I still need to add observer to NSNotificationCenter.defaultCenter() ?
If I do. How to?
An NSNotification is sent when some object calls the post method on an NSNotificationCenter. The notification center then calls the designated receiving method on each object that has been registered with it.
If you do not register with a notification center, then there is no way for the system to know that it should send the notification to you. Although there can be other registration centers, in iOS you will almost always use the default.
When you register for a notification, you specify which object is to receive the notification, what method on that object to call, which notification you are registering for, and what sender you want to receive notifications from. If you want to receive every one of a particular kind of notification (that is, you don't care which object sent it), you can specify nil for the sender.
Thus, to register for the notification, "MyNotification", and you don't care what object sent it, you call the following:
NSNotificationCenter.defaultCenter().addObserver(self, "gestureHandler", "MyNotification", nil)
Where to place this call depends on when you want this object to listen for it. For example, if the receiver is a UIView, you probably want to register when the view is about to be shown, not when the view is created.
It is extremely important that you unregister when you want to stop receiving notifications, such as when the receiver goes out of scope. You do this by calling 'removeObserver()`.
You should search through Xcode's documentation and read the Notification Programming Topics.
Yes it's required.
Use it like: Snippet taken from a great tutorial:
http://natashatherobot.com/ios8-where-to-remove-observer-for-nsnotification-in-swift/
class FirstViewController: UIViewController {
#IBOutlet weak var sentNotificationLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateNotificationSentLabel", name: mySpecialNotificationKey, object: nil)
}
// 2. Post notification using "special notification key"
#IBAction func notify() {
NSNotificationCenter.defaultCenter().postNotificationName(mySpecialNotificationKey, object: self)
}
func updateNotificationSentLabel() {
self.sentNotificationLabel.text = "Notification sent!"
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
}
Further topic: Swift and removing Observers:
http://natashatherobot.com/ios8-where-to-remove-observer-for-nsnotification-in-swift/

Resources