How to pass in a nil notification variable in Swift - ios

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.

Related

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

Show/Hide UIButton in another controller

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.

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/

ios swift parse: methods with async results

When I go to a viewController I call within my viewDidAppear Method a function:
override func viewDidAppear(animated: Bool) {
getLessons()
}
This methods loads from parse.com a list of data I want to use in a pickerView.
The function itself:
func getLessons(){
var query = PFQuery(className:"Lesson")
query.orderByAscending("name")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
for object in objects {
var name = object["name"] as String
self.languagePickerKeys.append(object.objectId)
self.languagePickerValues.append(name)
self.selectedLanguage.text = self.languagePickerValues.first // set the first lessons name into the text field
self.selectedLessonObjectId = self.languagePickerKeys.first // set the first objectId for the lesson
self.languagePicker?.reloadAllComponents()
}
} else {
// Log details of the failure
println("\(error.userInfo)")
}
}
println("getLessons done")
}
The thing is, that the textfield is empty, as the getLesson() gets the data async and the data is not available to the textfield.
I also tried to put the getLesson into the viewDidAppear method, but this doesn't help me, the textfield is empty anyway.
What can I do, to have the data from the getLessons() method ready and loaded its first value into my textfield when the view is shown to the user?
You certainly have to get the data from asyncTask before setting it to pickerView.
Here's the ViewController lifecycle after instantiation:
Preparation if being segued to.
Outlet setting
Appearing and Disappearing.
So, you have two options:
Load the data in previous ViewController and then perform the segue. You need to follow these steps for it.
a. Create a segue from previous ViewController to your ViewController.
b. Call the function when you want to go next ViewController which fetches the data, and the end (after getting the data) call performSegueWithIdentifier which will lead to your ViewController.
c. Set the data in prepareForSegue
let navigationController = segue.destinationViewController as UINavigationController
navigationController.data = yourData //you got from async call
Now when you reach your ViewController, you are sure that your data is present, and you can set it to your pickerView.
If you want to do it in the same ViewController: here's is the lifeCycle of ViewController:so you need to call your function in viewDidLoad, and always set your pickerView after completion of the async network call.
Make sure that you initiate all changes to the UI from the main thread e.g. like so:
dispatch_async(dispatch_get_main_queue(), {
selectedLanguage.text = languagePickerValues.first
self.languagePicker?.reloadAllComponents()
})
The problem is that findObjectsInBackgroundWithBlock is an asynchronous method, so even if you fire it in the ViewDidLoad you will never know when you will receive the response data and you can't be sure that the data will be ready by the time you view appear.
I think you have just 2 possibility:
The first one is to load the data in the previous view controller and then just pass the data that got ready to you view controller.
The second is to use a synchronous method (the findobject method maybe?) and put the call in a method that is fired BEFORE the view appear (like the viewWillAppear: method). But your view will stuck for a moment (I think) while the data is retreiving... However this second solution probably resolve your problem but using synchronous method to retrieve data from a slower data source is usually bad design solution.
D.

Resources