NSNotification getting called multiple times - ios

I have the following code in my didFinishLaunchingWithOptions
NotificationCenter.default.addObserver(
self,
selector: #selector(addressBookDidChange),
name: NSNotification.Name.CNContactStoreDidChange,
object: nil)
This is the method it calls
#objc func addressBookDidChange(notification: NSNotification){
self.processContacts()
}
and here is the notification being removed
func applicationWillTerminate(_ application: UIApplication) {
NotificationCenter.default.removeObserver(NSNotification.Name.CNContactStoreDidChange)
}
The problem is that when I add a new contact via the method below, addressBookDidChange gets called multiple times after, not just once
func addContact(contact:ContactObject) {
let store = CNContactStore()
let contactToAdd = CNMutableContact()
contactToAdd.givenName = contact.firstName
contactToAdd.familyName = contact.lastName
contactToAdd.organizationName = contact.company
for case let contactNumber as PhoneNumberObject in contact.phoneNumbers!{
let mobileNumber = CNPhoneNumber(stringValue: contactNumber.number)
contactToAdd.phoneNumbers.append(CNLabeledValue(label: contactNumber.type.getCNLabelValue(), value: mobileNumber))
}
if let image = contact.image {
contactToAdd.imageData = UIImagePNGRepresentation(image)
}
let saveRequest = CNSaveRequest()
saveRequest.add(contactToAdd, toContainerWithIdentifier: nil)
do {
try store.execute(saveRequest)
} catch {
NSLog("Error adding contact \(contact.firstName) \(contact.lastName) : \(error)")
}
}
How can I make the notification be just called once for an addition of one contact?

I believe that it is not good idea to set notification posting based on delegate and so what should be doing is post notification from delegate checking condition whether change in notification is contact is added.

Related

How to wait for the result of a database read in swift?

In my application I use Firebase Realtime Database to store data about users. I would like to be sure that when I read this data to display it in the view (e.g. their nickname), that the reading has been done before displaying it. Let me explain:
//Initialization of user properties
    static func initUsers(){
        let usersref = dbRef.child("Users").child(userId!)
        usersref.observeSingleEvent(of: .value) { (DataSnapshot) in
            if let infos = DataSnapshot.value as? [String : Any]{
                self.username = infos["username"] as! Int
                 
                //The VC is notified that the data has been recovered
                let name = Notification.Name(rawValue: "dataRetrieved")
                let notification = Notification(name: name)
                NotificationCenter.default.post(notification)
            }
        }
    }
This is the code that runs in the model and reads the user's data when they log in.
var isFirstAppearance = true
 
override func viewDidLoad() {
        super.viewDidLoad()
         
        //We initialise the properties associated with the user
        Users.initUsers()
    }
     
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        if isFirstAppearance {
            let name = Notification.Name(rawValue: "dataRetrieved")
            NotificationCenter.default.addObserver(self, selector: #selector(registerDataToView), name: name, object: nil)
            isFirstAppearance = false
        }
        else{
            registerDataToView()
        }
    }
 
    //The user's data is assigned to the view
    #objc func registerDataToView(){
        usernameLabel.text = String(Users.username)
    }
Here we are in the VC and when the view loads we call initUsers in viewDidLoad. In viewWillAppear, if it's the first time we load the view then we create a listener which calls registerDataToView if the reading in the database is finished. Otherwise we simply call registerDataToView (this is to update the labels when we return to this VC).
I would like to know if it is possible, for example when we have a very bad connection, that the listener does not intercept the dataRetrieved notification and therefore that my UI displays only the default texts? Or does the headset wait to receive the notification before moving on?
If it doesn't wait then how can I wait for the database read to finish before initializing the labels?
Thanks for your time :)
Don't wait. Never wait. Tell the receiver that the data are available with a completion handler
static func initUsers(completion: #escaping (Bool) -> Void) {
let usersref = dbRef.child("Users").child(userId!)
usersref.observeSingleEvent(of: .value) { (DataSnapshot) in
if let infos = DataSnapshot.value as? [String : Any]{
self.username = infos["username"] as! Int
completion(true)
return
}
completion(false)
}
}
And use it
override func viewDidLoad() {
super.viewDidLoad()
//We initialise the properties associated with the user
Users.initUsers { [unowned self] result in
if result (
// user is available
} else {
// user is not available
}
}
}

What's the most efficient way to refresh a tableView every time the app is pushed back to the foreground?

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)

How to stop GCDAsyncUdpSocket send command?

I am using GCDAsyncUdpSocket for communication between my app and some smart-home hardware, and I have a problem with stopping a certain function. Logic goes something like this:
Send a command
If you didn't receive feedback from the hardware, it'll try to send it a few more times
When app receives feedback, notification DidReceiveDataForRepeatSendingHandler is posted (along with device information in userInfo)
For example, let's say I have a curtain that can react on 3 commands: Open, Close and Stop... and that curtain is currently closed.
I press Open (and don't receive feedback), and during the process I change my mind, so I press Stop. Now the app will send both commands simultaneously.
So without further ado, here's the code:
class RepeatSendingHandler: NSObject {
var byteArray: [UInt8]!
var gateway: Gateway!
var repeatCounter:Int = 1
var device:Device!
var appDel:AppDelegate!
var error:NSError? = nil
var sameDeviceKey: [NSManagedObjectID: NSNumber] = [:]
var didGetResponse:Bool = false
var didGetResponseTimer:Foundation.Timer!
//
// ================== Sending command for changing value of device ====================
//
init(byteArray:[UInt8], gateway: Gateway, device:Device, oldValue:Int) {
super.init()
appDel = UIApplication.shared.delegate as! AppDelegate
self.byteArray = byteArray
self.gateway = gateway
self.device = device
NotificationCenter.default.addObserver(self, selector: #selector(RepeatSendingHandler.didGetResponseNotification(_:)), name: NSNotification.Name(rawValue: NotificationKey.DidReceiveDataForRepeatSendingHandler), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(sameDevice(_:)), name: NSNotification.Name(rawValue: NotificationKey.SameDeviceDifferentCommand), object: nil)
sendCommand()
}
func updateRunnableList(deviceID: NSManagedObjectID) {
RunnableList.sharedInstance.removeDeviceFromRunnableList(device: deviceID)
}
// Did get response from gateway
func didGetResponseNotification (_ notification:Notification) {
if let info = (notification as NSNotification).userInfo! as? [String:Device] {
if let deviceInfo = info["deviceDidReceiveSignalFromGateway"] {
if device.objectID == deviceInfo.objectID {
didGetResponse = true
didGetResponseTimer = nil
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NotificationKey.DidReceiveDataForRepeatSendingHandler), object: nil)
}
}
}
}
func sameDevice(_ notification: Notification) {
print("NOTIFICATION RECEIVED for device with ID: ", self.device.objectID, "\n")
if let info = notification.userInfo as? [NSManagedObjectID: NSNumber] {
sameDeviceKey = info
}
}
func sendCommand () {
if sameDeviceKey != [device.objectID: device.currentValue] { print("keys have DIFFERENT values") } else { print("keys have SAME values") }
if sameDeviceKey != [device.objectID: device.currentValue] {
if !didGetResponse {
if repeatCounter < 4 {
print("Sending command. Repeat counter: ", repeatCounter)
SendingHandler.sendCommand(byteArray: byteArray, gateway: gateway)
didGetResponseTimer = Foundation.Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(RepeatSendingHandler.sendCommand), userInfo: nil, repeats: false)
repeatCounter += 1
} else {
didGetResponseTimer = nil
updateRunnableList(deviceID: device.objectID)
CoreDataController.shahredInstance.saveChanges()
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NotificationKey.DidReceiveDataForRepeatSendingHandler), object: nil)
NotificationCenter.default.post(name: Notification.Name(rawValue: NotificationKey.RefreshDevice), object: self)
}
}else{
didGetResponseTimer = nil
updateRunnableList(deviceID: device.objectID)
CoreDataController.shahredInstance.saveChanges()
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NotificationKey.DidReceiveDataForRepeatSendingHandler), object: nil)
NotificationCenter.default.post(name: Notification.Name(rawValue: NotificationKey.RefreshDevice), object: self)
}
} else {
print("Command canceled")
didGetResponseTimer = nil
return
}
}
On the ViewController where I keep my devices, I call this like:
func openCurtain(_ gestureRecognizer:UITapGestureRecognizer){
let tag = gestureRecognizer.view!.tag
let address = [UInt8(Int(devices[tag].gateway.addressOne)),UInt8(Int(devices[tag].gateway.addressTwo)),UInt8(Int(devices[tag].address))]
if devices[tag].controlType == ControlType.Curtain {
let setDeviceValue:UInt8 = 0xFF
let deviceCurrentValue = Int(devices[tag].currentValue)
devices[tag].currentValue = 0xFF // We need to set this to 255 because we will always display Channel1 and 2 in devices. Not 3 or 4. And this channel needs to be ON for image to be displayed properly
let deviceGroupId = devices[tag].curtainGroupID.intValue
CoreDataController.shahredInstance.saveChanges()
DispatchQueue.main.async(execute: {
RunnableList.sharedInstance.checkForSameDevice(device: self.devices[tag].objectID, newCommand: NSNumber(value: setDeviceValue))
_ = RepeatSendingHandler(byteArray: OutgoingHandler.setCurtainStatus(address, value: setDeviceValue, groupId: UInt8(deviceGroupId)), gateway: self.devices[tag].gateway, device: self.devices[tag], oldValue: deviceCurrentValue)
})
}
}
What I did was I made a separate class where I have a dictionary that has Device's ManagedObjectID as a key, and the command we are sending is it's value. So whenever we are sending a command for a device that's already on the list, I post a notification SameDeviceDifferentCommand with userInfo containing device's ManagedObjectID and the old command. I use it on RepeatSendingHandler to populate sameDeviceKey dictionary. That's how I tried to distinguish which function should be stopped.
public class RunnableList {
open static let sharedInstance = RunnableList()
var runnableList: [NSManagedObjectID: NSNumber] = [:]
func checkForSameDevice(device: NSManagedObjectID, newCommand: NSNumber) {
if runnableList[device] != nil && runnableList[device] != newCommand {
let oldDataToSend = [device: runnableList[device]!]
NotificationCenter.default.post(name: Notification.Name(rawValue: NotificationKey.SameDeviceDifferentCommand), object: self, userInfo: oldDataToSend)
print("Notification sent for device with ID: ", device, "\n")
}
runnableList[device] = newCommand
print("Device with ID: ", device, "received a new command", newCommand, "\n")
}
func removeDeviceFromRunnableList(device: NSManagedObjectID) {
runnableList.removeValue(forKey: device)
print("Removed from list device with ID: ", device)
}
}
However, sometimes it does it's job as it should, and sometimes it doesn't. Using a bunch of prints I tried to see in which order everything happens, and it seems that sometimes even though sameDeviceKey gets it's value from the notification - it looks like it uses old (nil) value until repeatCounter maxes out. I do not understand why.
Could anyone explain what is happening, and/or advise a better solution than the one I provided?
(There is a bit of additional code which I removed as it's irrelevant to logic/question). Please bear in mind that I am a junior and that I'm relatively new to this.

Notifications with Swift 2 and Cloudkit

I am making a "texting app" you can call it and it uses cloudkit and I have been looking everywhere to add notifications that work with cloudkit... Would someone be able to tell me the code to add push notifications for cloudkit in detail because I am very lost... Also I wan't the notifications to go to different "texting rooms" (in cloudkit it would be record types...) For instance I have one record type called "text" and another one called "text 2" I don't want notifications from "text" to get to people who use "text2" and vise versa.
Using Swift 2.0 with El Captain & Xcode 7.2.1
Elia, You need to add this to your app delegate. Which will arrive in a userInfo packet of data, which you can then parse to see which database/app sent it.
UIApplicationDelegate to the class
application.registerForRemoteNotifications() to the
func application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
Than this method
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
let notification = CKQueryNotification(fromRemoteNotificationDictionary: userInfo as! [String : NSObject])
let container = CKContainer(identifier: "iCloud.com")
let publicDB = container.publicCloudDatabase
if notification.notificationType == .Query {
let queryNotification = notification as! CKQueryNotification
if queryNotification.queryNotificationReason == .RecordUpdated {
print("queryNotification.recordID \(queryNotification.recordID)")
// Your notification
}
}
print("userInfo \(userInfo["ck"])")
NSNotificationCenter.defaultCenter().postNotificationName("NotificationIdentifier", object: self, userInfo:dataDict)
}
}
}
}
}
That'll get you started.
You can use this method to check your subscriptions programmatically, of course while your developing you can use the dashboard.
func fetchSubsInPlace() {
let container = CKContainer(identifier: "iCloud.com")
let publicDB = container.publicCloudDatabase
publicDB.fetchAllSubscriptionsWithCompletionHandler({subscriptions, error in
for subscriptionObject in subscriptions! {
let subscription: CKSubscription = subscriptionObject as CKSubscription
print("subscription \(subscription)")
}
})
}
And finally when you got it; you can this routine to ensure you capture any subscriptions you missed while your app was sleeping and make sure that subscriptions don't go to all your devices, once you treated them too.
func fetchNotificationChanges() {
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: nil)
var notificationIDsToMarkRead = [CKNotificationID]()
operation.notificationChangedBlock = { (notification: CKNotification) -> Void in
// Process each notification received
if notification.notificationType == .Query {
let queryNotification = notification as! CKQueryNotification
let reason = queryNotification.queryNotificationReason
let recordID = queryNotification.recordID
print("reason \(reason)")
print("recordID \(recordID)")
// Do your process here depending on the reason of the change
// Add the notification id to the array of processed notifications to mark them as read
notificationIDsToMarkRead.append(queryNotification.notificationID!)
}
}
operation.fetchNotificationChangesCompletionBlock = { (serverChangeToken: CKServerChangeToken?, operationError: NSError?) -> Void in
guard operationError == nil else {
// Handle the error here
return
}
// Mark the notifications as read to avoid processing them again
let markOperation = CKMarkNotificationsReadOperation(notificationIDsToMarkRead: notificationIDsToMarkRead)
markOperation.markNotificationsReadCompletionBlock = { (notificationIDsMarkedRead: [CKNotificationID]?, operationError: NSError?) -> Void in
guard operationError == nil else {
// Handle the error here
return
}
}
let operationQueue = NSOperationQueue()
operationQueue.addOperation(markOperation)
}
let operationQueue = NSOperationQueue()
operationQueue.addOperation(operation)
}
}

How to use expectationForNotification

I've tried everything, but the only way I could get a successful test is to actually send the notification in the test function, which kinda defeats the purpose.
I have a button. When I tap the button, it sends a notification. How can I use expectationForNotification to see if this notification gets sent?
func testExample() {
let app = XCUIApplication()
let button = app.buttons["Button"]
let expectation = expectationForNotification("TEST_NOTE", object: nil) {
(notification: NSNotification!) -> Bool in
print("SUCCESS")
return true
}
button.tap()
waitForExpectationsWithTimeout(5, handler: nil)
}
it looks to me that you will have to fulfill the expectation...
func testExample()
{
let app = XCUIApplication()
let button = app.buttons["Button"]
let expectation = expectationWithDescription("waiting for the tap")
expectationForNotification("TEST_NOTE", object: nil)
{
notification in
expectation.fulfill()
return true
}
button.tap()
waitForExpectationsWithTimeout(30)
{
error in
if let e = error
{
XCTFail("\(e.debugDescription)")
}
}
}

Resources