How to stop GCDAsyncUdpSocket send command? - ios

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.

Related

Error when performing a `NSMetadataQuery` in iCloud Drive

I get the following error when performing a NSMetadataQuery in iCloud Drive:
[ERROR] cannot query iCloud Drive items: Error Domain=BRCloudDocsErrorDomain Code=16 "Sync is restricted for this client" UserInfo={NSDescription=Sync is restricted for this client}
This might be my problem
I have a free developer account. However I have not found anything about a requirement to use a paid developer account for using NSMetadataQuery in iCloud Drive.
Note that I found out that one has to use a paid developer account to use the iCloud Key-Value and Document Storage which is not the same as iCloud Drive.
What I tried
I used the following code to make the query (calling Test().getAllFilesIniCloud()):
class Test {
let metadataQuery = NSMetadataQuery()
func getAllFilesIniCloud() {
metadataQuery.searchScopes = [
NSMetadataQueryUbiquitousDocumentsScope,
NSMetadataQueryUbiquitousDataScope,
NSMetadataQueryAccessibleUbiquitousExternalDocumentsScope,
]
let predicate = NSPredicate { (any, dict) -> Bool in
print(any as Any, dict as Any)
return false
}
metadataQuery.predicate = predicate
NotificationCenter.default.addObserver(
self,
selector: #selector(self.queryDidFinishGathering(_:)),
name: .NSMetadataQueryDidFinishGathering,
object: metadataQuery)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.queryDidStart(_:)),
name: .NSMetadataQueryDidStartGathering,
object: metadataQuery)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.queryDidUpdate(_:)),
name: .NSMetadataQueryDidUpdate,
object: metadataQuery)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.queryGathering(_:)),
name: .NSMetadataQueryGatheringProgress,
object: metadataQuery)
metadataQuery.enableUpdates()
if metadataQuery.start() {
print("query did start")
}
}
#objc func queryGathering(_ notification: NSNotification) {
print("gathering")
self.getValuesOf(notification: notification)
}
#objc func queryDidStart(_ notification: NSNotification) {
print("didStart")
self.getValuesOf(notification: notification)
}
#objc func queryDidUpdate(_ notification: NSNotification) {
print("didUpdate")
self.getValuesOf(notification: notification)
}
#objc func queryDidFinishGathering(_ notification: NSNotification) {
print("didFinish gathering")
metadataQuery.disableUpdates()
metadataQuery.stop()
self.getValuesOf(notification: notification)
}
func getValuesOf(notification: NSNotification) {
let metadataQuery = notification.object as! NSMetadataQuery
metadataQuery.disableUpdates()
for item in metadataQuery.results as! [NSMetadataItem] {
let url = item.value(forAttribute: NSMetadataItemURLKey) as! URL
print(url)
}
metadataQuery.enableUpdates()
}
}
When starting the query it prints and nothing else:
didStart
... [default] [ERROR] cannot query iCloud Drive items: Error Domain=BRCloudDocsErrorDomain Code=16 "Sync is restricted for this client" UserInfo={NSDescription=Sync is restricted for this client}
query did start
You need to add iCloud to Capability in your Project and select key-value + iCloud Documents + add your container. I had have the same problem.

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)

NSNotification getting called multiple times

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.

access reachability with enough time to return true state

i have a reachability class which is checked from within my VC code as below:
func setupReachability (hostName:String?, useClosures: Bool) {
let reachability = hostName == nil ? Reachability() : Reachability(hostname: hostName!)
self.reachability = reachability
try! self.reachability?.startNotifier()
if useClosures {
reachability?.whenReachable = { reachability in
DispatchQueue.main.async {
self.connected = true
print("Reachable....")
}
}
reachability?.whenUnreachable = { reachability in
DispatchQueue.main.async {
self.connected = false
print("Not Connected....")
}
}
} else {
NotificationCenter.default.addObserver(self, selector: Selector(("reachabilityChanged:")), name: ReachabilityChangedNotification, object: reachability)
}
}
this is initialised from within viewDidLoad:
setupReachability(hostName: nil, useClosures: true)
print(self.connected)
the problem i am having is that it always returns false and i need this to load with enough time that it returns true (if it is really connected) at this point. I'm struggling to figure out where to place my setupReachability function and initialise it so that it can be accessed from anywhere in the app and return its connected state. Ive tried placing this in a separate class but can't seem to get it working or access the connected state. Any tips on how to achieve this?

Passing a array from notification object crashing the app in iOS 10

I am posting the below code from a view controller to another
self.genreString.append("Comedy")
self.sortByString.append("orderly")
let myDict:Dictionary<String, [String]> = ["sortoption": self.genreString, "contenttype":self.sortByString]
NSNotificationCenter.defaultCenter().postNotificationName("SecondViewControllerDismissed", object: nil, userInfo: myDict)
In receiving view controller I am accepting the data as this
if let info = sender.userInfo as? Dictionary<String,[String]> {
// Check if value present before using it
if let s = info["sortoption"] {
if info["sortoption"]?.count > 0
{
JLToast.makeText("2", duration: 2).show()
let s :[String] = info["sortoption"]!
self.genreFilter.removeAll()
self.genreFilter = []
self.genreFilter.appendContentsOf(s)
}
}
}
But the array is not initialised, its saying!!!
Following code runs fine in XCode playground
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let myDict:Dictionary<String, [String]> = ["sortoption": ["Comdedy"], "contenttype": ["orderly"]]
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "SecondViewControllerDismissed"), object: nil, queue: OperationQueue.main) {
notification in
if let info = notification.userInfo as? Dictionary<String,[String]> {
// Check if value present before using it
if let s = info["sortoption"] {
print(s)
}
}
}
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "SecondViewControllerDismissed"), object: nil, userInfo: myDict)

Resources