I currently have a function that writes a realm object into the database and writes a notification. When the object is written, it is added into a uicollectionview and displayed. The function below adds the object and sets a notification based on a UIDatePicker. That code is not shown since it is a lot that doesn't pertain to the problem:
func createTaskWithDate() {
let task = Task()
task.name = textField.text!
//notification code under here
try! realm.write {
realm.add(task)
updateData()
}
I also have another function that responds to an action on a notification set. When the user taps on the notification, they are given the option to "mark as complete". When the user taps "mark as complete", I am trying to retrieve the object from the "createTaskWithDate()" method and delete it from the collection view however I'm unsure on how to retrieve that object from that method:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
let identifier = response.actionIdentifier
let request = response.notification.request
if identifier == "complete" {
try! realm.write {
//realm.delete(task object from createTaskWithDate())
updateData()
}
}
completionHandler()
}
How could I go about doing this?
When setting up the notification, you should store the name/primary key of the object the notification is about in your notification request. You can store any information in UNMutableNotificationContent.userInfo and access it in userNotificationCenter(_:didReceive:withCompletionHandler:) by
let userInfo = response.notification.content.userInfo.
Then you can retrieve the object from Realm with the primary key stored in userInfo.
You want to create new element when your User Notification have fired or find element that already exist? If you want to find, you can query that element from data base and than delete it. You can query by date or identifier (you can implement it using User Defaults, by adding identifier field to element and increment it each time when you create new element).
For UI modification after object manipulation you can use object notifications from Realm. You can read about it here.
Related
I'm newly learning iOS/swift programming and developing an application using MongoDB Realm and using Realm sync. I'm new to programming and realm, so please feel free to correct any terminology. My question is about listening for realm notifications, which I see referred to as change listeners and notification tokens. Regardless, here is the info:
My application has a list of locations with a status (confirmed/pending/cancelled). I open this list from my realm as a realm managed object and create my notification handler:
//This is called in it's own function, but assigns the locations
locations = publicRealm?.objects(Location.self)
//This is run after the function is called
self?.notificationToken = self?.locations!.observe { [weak self] (_) in
self?.tableView.reloadData()
print("Notification Token!!!")
I then populate my table view and let a user tap on a location, which passes the location and realm to another view controller where the user can update the status. That update is made in a separate view controller.
do{
try publicRealm?.write {
selectedLocation?.statusMessage = locationStatusTextField.text!
selectedLocation?.status = selectedStatus
}
}catch{
print("Error saving location data: \(error)")
}
At this point my notification token is successfully triggered on the device where I am making the location update. The change is shown immediately. However there is no notification token or realm refresh that happens on any other open devices that are showing the locations table view. They do not respond to the change, and will only respond to it if I force realm.refresh(). The change is showing in Atlas on MongoDB server, though.
I am testing on multiple simulators and my own personal phone as well, all in Xcode.
I'm very confused how my notification token can trigger on one device but not another.
When I first started the project it was a much simpler realm model and I could run two devices in simulator and updating one would immediately fire a change notification and cause the second device to show the correct notification.
I have since updated to a newer realm version and also made the realm model more complicated. Though for this purpose I am trying to keep it simple by doing all changes via swift and in one realm.
I also have realm custom user functions running and changing data but I think reading the docs I am realizing that those will not trigger a notification - I'm not sure if that's true though? I just know right now that if I change data in the DB via a user function no notifications are triggered anywhere - but if I do realm.refresh() then the change shows.
What is it that I am missing in how I am using these notifications?
***Updating information on Public Realm:
Save the realm:
var publicRealm:Realm?
Login as an anon user and then open the realm:
let configuration = user.configuration(partitionValue: "PUBLIC")
Realm.asyncOpen(configuration: configuration) { [weak self](result) in
DispatchQueue.main.async {
switch result {
case .failure(let error):
fatalError("Failed to open realm: \(error)")
case .success(let publicRealm):
self!.publicRealm = publicRealm
guard let syncConfiguration = self?.publicRealm?.configuration.syncConfiguration else {
fatalError("Sync configuration not found! Realm not opened with sync?")
}
It is after this realm opening that the locations are loaded and notification token is created.
I use a segue to pass the location object and realm to the next VC:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVC = segue.destination as! UpdateLocationViewController
destinationVC.delegate = self
if let indexPath = tableView.indexPathForSelectedRow {
destinationVC.selectedLocation = locations?[indexPath.row]
}
if indexPathRow != nil {
destinationVC.selectedLocation = locations?[indexPathRow!]
}
destinationVC.publicRealm = self.publicRealm
}
Some notes:
original public realm opened by anon user
only a logged in user can click on a location... so in the 'UpdateLocation' VC that gets passed the public realm I am a logged in user. But, I'm just using the Dev mode of Realm to allow me to read/write however I like... and I am writing straight to that public realm to try and keep it simple. (I have a custom user function that writes to both public and the user's org realm, but I stopped trying to use the function for now)
I identify the object to update based on the passed in location object from the first VC
I needed to use try! when making my write call rather than just try. I updated my write blocks as such:
try! publicRealm?.write {
selectedLocation?.statusMessage = locationStatusTextField.text!
selectedLocation?.status = selectedStatus
}
Just wanted to follow up in case anyone finds this. Thank you!
I want to create a generic api utility that I can implement in any model. Therefore, I am currently faced with the problem of outputting error messages from the model regardless of a view. The error messages are needed to trigger a "no internet" or "poor internet connection" notification.
In general, it would help me a lot, regardless of the current view, to generate error messages in different classes in order to then inform the user of a failed process.
Here is my Code so far:
func execute(requestBody: [String: Any], withCompletion completion: #escaping (Data?, Int) -> Void) {
if !CheckApiReachability().getIsApiReachable() {
//trigger error message here
}
I have now used the Notification Center to implement an event which triggers an alert in my content view.
Emmit/ Post the Notification:
let nc = NotificationCenter.default
self.nc.post(name: Notification.Name("InternetConnectionErrorAppeared"), object: nil)
Receive/ Subscribe/ Listen to the emited event and trigger something:
.onReceive(nc.publisher(for: Notification.Name("InternetConnectionErrorAppeared"))) { output in
print("-- Api call failed triggered status code event")
}
I've been trying to solve this problem for some time, to no avail.
When accessing a managed object from UserNotification custom action and then trying to save the changes to this object I get the following message:
[error] error: CoreData: error: Failed to call designated initializer on NSManagedObject class 'NSManagedObject'.
Basically, the setup is as follows:
1. User gets a notification
2. Chooses a custom action
3. From the info in the notification the UserNotification Center Delegate extracts the URI of the object and then extracts it from the persistent store
4. Once done and type-casted the delegate calls appropriate method on the object
5. After method returns delegate tries to save the context, and that's where the error appears.
Here is some relevant code:
// - UNUserNotification Centre delegate
extension HPBUserNotificationsHandler: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
guard let object = getAssociatedObject(id: response.notification.request.identifier) else { return }
switch response.actionIdentifier {
case UNNotificationDismissActionIdentifier:
....
case UNNotificationDefaultActionIdentifier:
....
case HPBReminderAction.take.rawValue:
// get intake object
guard let reminder = object as? HPBIntake else { return }
reminder.take(at: Date())
try! dataController.saveContext() // here is when the error is raised
default:
break
}
completionHandler()
}
The function to extract an object from persistent store:
func getAssociatedObject(id: String) -> NSManagedObject? {
guard let psc = dataController.managedObjectContext.persistentStoreCoordinator else { return nil }
guard let objURL = URL(string: id) else { return nil }
guard let moid = psc.managedObjectID(forURIRepresentation: objURL) else { return nil }
return dataController.managedObjectContext.object(with: moid)
}
If I make the same changes on this object directly in the app - everything works. So I assume the matter is with getting the object from a custom action on User Notification. But I can't figure out what is the problem.
Here is some additional info. When I inspect the reminder object right before calling the take(on:) function, it shows as a fault:
Home_Pillbox.HPBIntake: 0x7fb1a9074e90 (entity: Intake; id: 0x7fb1a9069e50 x-coredata:///Intake/tAC4BBCD4-B128-4C6F-8E1B-2EE7D4EDBCB34 ; data: fault)
Of course, when the function is called, the fault is fired but the object is not initialised correctly and instead populates all properties as nil:
Home_Pillbox.HPBIntake: 0x7fb1a9074e90 (entity: Intake; id: 0x7fb1a9069e50 x-coredata:///Intake/tAC4BBCD4-B128-4C6F-8E1B-2EE7D4EDBCB34 ; data: {dosage = 0; identifier = nil; localNotification = nil; log = nil; meal = 0; medName = nil; notificationRequest = nil; profileName = nil; schedule = nil; status = 1; treatment = nil; unit = 0; userNotes = nil;})
So when the context tries to save it can't, as properties are nil, which is not allowed by the data model. What also bothers me is that the error mentions designated initialiser on NSManagedObject instead of the name of the subclass HPBIntake, even though the object is clearly correctly typed.
Any help will be highly appreciated.
EDIT:
Here's the implementation of saveContext() function in the DataController:
func saveContext() throws {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch let syserr as NSError {
throw syserror
}
}
}
One more idea: Since you get an initializer error, it seems to me that the object whose id you transfer via the notification is not yet initialized when you try to save it.
In this link I found the following:
object(with:) throws an exception if no record can be found for the
object identifier it receives. For example, if the application deleted
the record corresponding with the object identifier, Core Data is
unable to hand your application the corresponding record. The result
is an exception.
The existingObject(with:) method behaves in a similar
fashion. The main difference is that the method throws an error if it
cannot fetch the managed object corresponding to the object
identifier.
So I suggest to replace in getAssociatedObject(id: String) the call to object(with: moid) by existingObject(with: moid). If this throws an error, you know that the related object does not yet or no longer exist.
If this is allowed by your app, you had to initialize it by the designated initialiser
init(entity entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?)
before you try to store it.
EDIT:
In this answer, you can find more suggestions how to debug your core data handling.
Just an idea: You said
If I make the same changes on this object directly in the app -
everything works.
I assume you do these changes on the main thread.
Did you check if userNotificationCenter(_:didReceive:withCompletionHandler:) is also executed on the main thread? If not, you might have a problem here, since core data expects to be executed on one thread only. In this case you could try to execute the body of this function with
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
DispatchQueue.main.async {
// Your code here
}
}
You should also set the launch argument -com.apple.CoreData.ConcurrencyDebug 1 in your scheme, together with exception breakpoints:
Then your app will stop when a core data multi-thread violation happens.
This question already has answers here:
How to detect "clear" notifications
(3 answers)
Closed 4 years ago.
i want to do something when user clear notification from home screen is it possible to get trigger when user tap on clear button on push notification banner, i ended up with adding custom button on view but thats not feasible.
image is attached i want trigger when user tap on this clear action any idea ???
{
“aps” : {
“category” : “MEETING_INVITATION”
“alert” : {
“title” : “Weekly Staff Meeting”
“body” : “Every Tuesday at 2pm”
},
},
“MEETING_ID” : “123456789”,
“USER_ID” : “ABCD1234”
}
i have done this but its adding button on view like popup
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler:
#escaping () -> Void) {
// Get the meeting ID from the original notification.
let userInfo = response.notification.request.content.userInfo
let meetingID = userInfo["MEETING_ID"] as! String
let userID = userInfo["USER_ID"] as! String
// Perform the task associated with the action.
switch response.actionIdentifier {
case "ACCEPT_ACTION":
sharedMeetingManager.acceptMeeting(user: userID,
meetingID: meetingID)
break
case "DECLINE_ACTION":
sharedMeetingManager.declineMeeting(user: userID,
meetingID: meetingID)
break
// Handle other actions…
default:
break
}
// Always call the completion handler when done.
completionHandler()
}
From docs about actionIdentifier:
This parameter may contain one the identifier of one of your UNNotificationAction objects or it may contain a system-defined identifier. The system defined identifiers are UNNotificationDefaultActionIdentifier and UNNotificationDismissActionIdentifier, which indicate that the user opened the app or dismissed the notification without any further actions.
So you don’t have to use your own identifiers, use system’s.
Instead of
"ACCEPT_ACTION"
use
UNNotificationDefaultActionIdentifier
and instead of
"DECLINE_ACTION"
use
UNNotificationDismissActionIdentifier
How to handle new iOS10 Notification Action when app is closed (not in background) ?
when app is minimalized everything works fine with:
UNUserNotificationCenter.current().delegate = x
and handling it in
class x: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Swift.Void) {
}
}
but nothing is called when app is closed and user tap action in notification... maybe i can't handle background task and i always have to launch app?
The notification action buttons handling can be done in both Extension as well as in Containing App.
When the action button is tapped, the handle first goes to the Extension and then to the Containing App if required. If Extension does not handle the notification action, the handle is passed on to the containing app.
Tapping a button launches your app (either in the foreground or
background) and gives you a chance to perform the indicated action.
Handling in extension:
func didReceive(_ response: UNNotificationResponse, completionHandler completion: #escaping (UNNotificationContentExtensionResponseOption) -> Void)
{
//You need to handle all the actions that appear with notification..
completion(.dismissAndForwardAction)
}
The completion closure takes a value of type UNNotificationContentExtensionResponseOption:
enum UNNotificationContentExtensionResponseOption : UInt
{
case doNotDismiss //the custom UI is not dismissed after handling the action
case dismiss //the custom UI is dismissed after handling the action
case dismissAndForwardAction //the custom UI is dismissed after handling the action and the control is then passed to containing app for any additional handling
}
Handling in Containing App:
extension AppDelegate : UNUserNotificationCenterDelegate
{
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void)
{
// Action handling - handling for all actions is not required
completionHandler()
}
}
For more you can refer to this(https://github.com/pgpt10/RichNotificationSample) tutorial.
Yes, it always launch the app, when user tap action in notification, button launches your app.
Some lines from apple doc:
Tapping a button launches your app (either in the foreground or background) and gives you a chance to perform the indicated action. You use this class to specify the text that is displayed in the button and the information your app needs to perform the corresponding action.
"Tapping a button launches your app (either in the foreground or background) and gives you a chance ... "
These lines appear in the doc for UIUserNotificationAction, which has been deprecated in iOS10.
The original question refers to the UNUserNotificationCenterDelegate in iOS 11.
Relevant doc: Declaring Your Actionable Notification Types
Quote from the doc:
When the user selects an action, the system launches your app in the
background and notifies the shared UNUserNotificationCenter object,
which notifies its delegate. Use your delegate object's
userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:
method to identify the selected action and provide an appropriate
response.