I would like to react to modified NSManagedObjects, therefore I setup an observer:
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil, queue: nil) { notification in
...
}
But I didn't find a solution yet how to create objects inside that block.
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil, queue: nil) { notification in
let context = notification.object as! NSManagedObjectContext
context.perform {
let insertedObjects = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>()
// insertedObjects are empty (outside of context.perform they are NOT EMPTY
}
}
Also when I do not use context.perform I do get attempt to recursively call -save: on the context aborted. How can I achieve this?
Related
In the documentation, it says:
The block is copied by the notification center and (the copy) held
until the observer registration is removed.
And it provides a one-time observer example code like so:
let center = NSNotificationCenter.defaultCenter()
let mainQueue = NSOperationQueue.mainQueue()
var token: NSObjectProtocol?
token = center.addObserverForName("OneTimeNotification", object: nil, queue: mainQueue) { (note) in
print("Received the notification!")
center.removeObserver(token!)
}
Now I expect the observer to be removed as removeObserver(_:) is called, so my code goes like this:
let nc = NotificationCenter.default
var successToken: NSObjectProtocol?
var failureToken: NSObjectProtocol?
successToken = nc.addObserver(
forName: .ContentLoadSuccess,
object: nil,
queue: .main)
{ (_) in
nc.removeObserver(successToken!)
nc.removeObserver(failureToken!)
self.onSuccess(self, .contentData)
}
failureToken = nc.addObserver(
forName: .ContentLoadFailure,
object: nil,
queue: .main)
{ (_) in
nc.removeObserver(successToken!)
nc.removeObserver(failureToken!)
guard case .failed(let error) = ContentRepository.state else {
GeneralError.invalidState.record()
return
}
self.onFailure(self, .contentData, error)
}
Surprisingly, the self is retained and not removed.
What is going on?
Confirmed some weird behavior going on.
First, I put a breakpoint on the success observer closure, before observers are removed, and printed the memory address of tokens, and NotificationCenter.default. Printing NotificationCenter.default shows the registered observers.
I won't post the log here since the list is very long.
By the way, self was captured weakly in the closures.
Printing description of successToken:
▿ Optional<NSObject>
- some : <__NSObserver: 0x60000384e940>
Printing description of failureToken:
▿ Optional<NSObject>
- some : <__NSObserver: 0x60000384ea30>
Also confirmed that observers were (supposedly) removed by printing NotificationCenter.default again after the removeObserver(_:)s were invoked.
Next, I left the view controller and confirmed that the self in the quote code was deallocated.
Finally, I turned on the debug memory graph and searched for the memory addresses and found this:
In the end, there was no retain cycle. It was just that the observers were not removed, and because the closures were alive, the captured self was alive beyond its life cycle.
Please comment if you guys think this is a bug. According to the documentation on NotificationCenter, it most likely is...
Recently I've run into similar problem myself.
This does not seem a bug, but rather undocumented feature of the token which (as you've already noticed) is of __NSObserver type. Looking closer at that type you can see that it holds the reference to a block. Since your blocks hold strong reference to the token itself (through optional var), you have a cycle.
Try to set the optional token reference to nil once it is used:
let nc = NotificationCenter.default
var successToken: NSObjectProtocol?
var failureToken: NSObjectProtocol?
successToken = nc.addObserver(
forName: .ContentLoadSuccess,
object: nil,
queue: .main)
{ (_) in
nc.removeObserver(successToken!)
nc.removeObserver(failureToken!)
successToken = nil // Break reference cycle
failureToken = nil
self.onSuccess(self, .contentData)
}
You need to use weak reference for selflike this :
let nc = NotificationCenter.default
var successToken: NSObjectProtocol?
var failureToken: NSObjectProtocol?
successToken = nc.addObserver(
forName: .ContentLoadSuccess,
object: nil,
queue: .main)
{[weak self] (_) in
guard let strongSelf = self else { return }
nc.removeObserver(successToken!)
nc.removeObserver(failureToken!)
strongSelf.onSuccess(strongSelf, .contentData)
}
failureToken = nc.addObserver(
forName: .ContentLoadFailure,
object: nil,
queue: .main)
{[weak self] (_) in
guard let strongSelf = self else { return }
nc.removeObserver(successToken!)
nc.removeObserver(failureToken!)
guard case .failed(let error) = ContentRepository.state else {
GeneralError.invalidState.record()
return
}
strongSelf.onFailure(strongSelf, .contentData, error)
}
Currently what I have is this:
AppDelegate.applicationDidBecomeActive():
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
guard let vc = self.window?.rootViewController?.children.first as! AlarmTableViewController? else {
fatalError("Could not downcast rootViewController to type AlarmTableViewController, exiting")
}
vc.deleteOldAlarms(completionHandler: { () -> Void in
vc.tableView.reloadData()
})
}
deleteOldAlarms():
func deleteOldAlarms(completionHandler: #escaping () -> Void) {
os_log("deleteOldAlarms() called", log: OSLog.default, type: .default)
let notificationCenter = UNUserNotificationCenter.current()
var activeNotificationUuids = [String]()
var alarmsToDelete = [AlarmMO]()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
notificationCenter.getPendingNotificationRequests(completionHandler: { (requests) in
for request in requests {
activeNotificationUuids.append(request.identifier)
}
for alarm in self.alarms {
guard let alarmUuids = alarm.value(forKey: "notificationUuids") as! [String]? else {
os_log("Found nil when attempting to unwrap notificationUuids in deleteOldAlarms() in AlarmTableViewController.swift, cancelling",
log: OSLog.default, type: .default)
return
}
let activeNotificationUuidsSet: Set<String> = Set(activeNotificationUuids)
let alarmUuidsSet: Set<String> = Set(alarmUuids)
let union = activeNotificationUuidsSet.intersection(alarmUuidsSet)
if union.isEmpty {
alarmsToDelete.append(alarm)
}
}
os_log("Deleting %d alarms", log: OSLog.default, type: .debug, alarmsToDelete.count)
for alarmMOToDelete in alarmsToDelete {
self.removeNotifications(notificationUuids: alarmMOToDelete.notificationUuids as [String])
managedContext.delete(alarmMOToDelete)
self.alarms.removeAll { (alarmMO) -> Bool in
return alarmMOToDelete == alarmMO
}
}
completionHandler()
})
}
but it feels disgusting. Plus, I'm calling tableView.reloadData() on a background thread now (the thread executing the completion handler). What's the best way to refresh the UI once the user opens the app back up? What I'm aiming for is for these old alarms to be deleted and for the view to be reloaded. An alarm is considered old if it doesn't have any notifications pending in the notification center (meaning the notification has already been executed).
Don't put any code in the app delegate. Have the view controller register to receive notifications when the app enters the foreground.
Add this in viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(enteringForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
Then add:
#objc func enteringForeground() {
deleteOldAlarms {
DispatchQueue.main.async {
tableView.reloadData()
}
}
}
As of iOS 13, you should register for UIScene.willEnterForegroundNotification. If your app needs to work under iOS 13 as well as iOS 12 then you need to register for both notifications but you can use the same selector.
You can use NSNotification
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
In didBecomeActive call tableView.reloadData(), that should be all. You should remember to unregister the observer in deinit.
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
I have a strange issue. I register and unregister my Notification like so:
func doRegisterNotificationListener() {
NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: "RateAlertRated"), object: nil, queue: nil, using: rateDidRate)
}
func doUnregisterNotificationListener() {
NotificationCenter.default.removeObserver(self, name: Notification.Name(rawValue: "RateAlertRated"), object: nil)
}
func rateDidRate(notification: Notification) {
let rating = notification.userInfo?["score"] as? Int
let message = notification.userInfo?["message"] as? String
let response = Response(rating: rating, message: message)
output.presentRated(response)
}
This view controller is in a UITabBarController. doRegisterNotificationListener is called in viewDidAppear and doUnregisterNotificationListener is called in viewDidDisappear. When I switch between tabs the register and unregister methods are being called correctly (I tested using a print statement). However if I fire a notification it will still be received even though doUnregisterNotificationListener was called last. Any ideas what I might be doing wrong here?
Quick note:
Also tried:
NotificationCenter.default.removeObserver(self)
This also doesn't work.
I have tested your code and once i register observer with this type it is not called when you doUnregister it. please try this.
NotificationCenter.default.addObserver(self, selector: #selector(rateDidRate(notification:)), name: Notification.Name(rawValue: "RateAlertRated"), object: nil)
If you are working with addObserver(forName:object:queue:using:) you should remove it in this way:
Create:
let center = NSNotificationCenter.defaultCenter()
let mainQueue = NSOperationQueue.mainQueue()
self.localeChangeObserver = center.addObserverForName(NSCurrentLocaleDidChangeNotification, object: nil, queue: mainQueue) { (note) in
print("The user's locale changed to: \(NSLocale.currentLocale().localeIdentifier)")
}
Remove:
center.removeObserver(self.localeChangeObserver)
This approach is taken from the documentation.
I have a private NSManagedObjectContext queue that I'm using to save an entity to Core Data. After it has finished saving, I want to send out an NSNotification. However, it doesn't seem to like me sending out the notification from the private queue. This is my code for the private queue:
let parentManagedContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
let privateManagedContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateManagedContext.persistentStoreCoordinator = parentManagedContext.persistentStoreCoordinator
privateManagedContext.performBlock {
...
// Save the entity
do {
try privateManagedContext.save()
// Send out NSNotification here
}
}
How do I add a block within performBlock to run on the main thread?
Ok so minutes after I posted this question I figured out the answer. All I had to do was add this code after the try privateManagedContext.save() code:
NSOperationQueue.mainQueue().addOperationWithBlock({
NSNotificationCenter.defaultCenter().postNotificationName(kNotificationName, object: nil)
})
Hope this helps
dispatch_async(dispatch_get_main_queue()) {
NSNotificationCenter.defaultCenter().postNotificationName(kNotificationName, object: nil)
}
I would like to access the Notification object that is sent from the method below.
var currentTrack:MPMediaItem? {
get{
return playlist?.items[index]
}
set{
print(newValue?.title!)
//self.index = locateIndex(track: newValue!)
let notif = Notification.init(name: Playlist.SongChangedName, object:self)
NotificationCenter.default.post(notif)
}
}
The Notifications name is defined as:
static let SongChangedName = Notification.Name("SongChangedNotification")
Here is the observer:
override init() {
super.init()
NotificationCenter.default.addObserver(self,
selector: #selector(testSelector),
name: Playlist.SongChangedName, //Notification.Name("songChanged"),
object: nil)
}
Here is the Method it calls:
func testSelector(notification:Notification){
queueNextTrack()
}
How do I pass testSelector a notification object? I know it has something to do with the object parameter of the addObserver method.
Thank you.
You can now get rid of your problem entirely by not using selectors in your notifications, timers, etc. There are new block based API to replace target-selector such as
NotificationCenter.default.addObserver(forName: Playlist.SongChangedName, object: nil, queue: nil, using: { notification in
self.testSelector(testSelector)
})
For the most part you won't need access to the notifications in your blocks so you could do this too
func testSelector(){
queueNextTrack()
}
NotificationCenter.default.addObserver(forName: Playlist.SongChangedName, object: nil, queue: nil) { _ in
self.testSelector()
}
or most my preferred in most scenarios:
override init() {
super.init()
let testBlock: (Notification) -> Void = {
self.queueNextTrack()
}
NotificationCenter.default.addObserver(forName: Playlist.SongChangedName, object: nil, queue: nil, using: testBlock)
}
EDIT I'd also suggest you take a look at the sample code in the description for this API