Random EKEventStore sync issues with Google calendar - swift

I spent hours to track this bug.
My app is supposed to remove some events, then to add some other events. This works perfectly with iCloud and local calendars. However with Google calendars, this changes.
What I noticed so far:
Sometimes all works perfectly. All events which should be removed are removed and all events that should be added are added.
Sometimes, initially, all seems to be synced properly, but after 2 minutes, events get removed.
.remove works better (95 %) than .save (50-80%).
there are no catch errors. Neither for .remove, nor for .save.
It seems (?!) to be that Google calendars with few events (inside or outside the predicate - doesn't matter) gets synced correctly. Google calendars with many events (inside or outside the predicate) produce mentioned sync issues.
Here excerpts of the code (really nothing fancy!):
let evenStore = EventStore()
let predicate = eventStore.predicateForEvents(withStart: predicateStartDate!,
end: predicateEndDate!,
calendars: selectedCalendarArray)
let events = eventStore.events(matching: predicate)
for event in events {
do {
try eventStore.remove(event, span: .thisEvent, commit: false)
} catch {
print("event remove catch error")
}
}
... (new events are created and stored in eventsSave)
for event in eventsSave {
do {
try eventStore.save(event, span: .thisEvent, commit: false)
} catch {
print("event save catch error")
}
}
do {
try eventStore.commit()
} catch {
print("commit catch error")
}
I tried to avoid batch .remove or .save (true i.s.o. false - no commit()), No success.
I added eventStore.reset(). No success.
I added eventStore.refreshSourcesIfNecesary(). No success.
What I really don't get is, that iCloud works 100%. Google calendar is also a cloud calendar. What makes it behave so differently? There are some posts on SO, but they are mainly around 2013-2016. After that, virtually nothing anymore. Also never any solid solution, at the very best some workarounds.
Missing EKEvents when saving multiple Google calendar events
Batch saving EKEvents to Google calendar causing loss of random events
I am under the impression something basic and very simple is just missing, but no clue what this could be ...
Any help would be much appreciated!
UPDATE (06.08.2020)
Some events have geo location using the X-APPLE-STRUCTURED-LOCATION property. These events throw upon .save.
Error Domain=EKErrorDomain Code=19 "Locations with geo information are not supported by this account." UserInfo={NSLocalizedDescription=Locations with geo information are not supported by this account.}
Could this error be related to subject sync issue?

I believe I found the solution.
I use the calendar chooser to select a calendar. I store a default calendar in myStoredCalendar (String) which should be preselected upon calendar chooser presentation.
let selectedCalendar = myStoredCalendar
if let selectedCalendarIdentifier = eventStore.calendar(withIdentifier: selectedCalendar) {
calendarChooser.selectedCalendars = Set<EKCalendar>([selectedCalendarIdentifier])
}
Above works, but generates the following log message (apart from subject Google event issues):
Error getting shared calendar invitations for entity types 3 from daemon: Error Domain=EKCADErrorDomain Code=1014"(null)
The following code doesn't produce error 1014 anymore, and all Google event issues are gone.
let selectedCalendar = myStoredCalendar
let calendars = eventStore.calendars(for: .event)
for calendar in calendars {
if calendar.calendarIdentifier == selectedCalendar {
calendarChooser.selectedCalendars = Set<EKCalendar>([calendar])
}
}
}
UPDATE (08.08.2021)
Above wasn't the solution. The error is totally random. Sometimes it works like charm for 20 times in a row, sometimes not a single run works properly.

Related

Deleting, adding and updating Firestore documents when offline

I have been using the following code in my iOS Swift app:
class ProfileController
{
func remove(pid: String, completion: #escaping ErrorCompletionHandler)
{
guard let uid = self.uid else
{
completion(Errors.userIdentifierEmpty)
return
}
let db = Firestore.firestore()
let userDocument = db.collection("profiles").document(uid)
let collection = userDocument.collection("profiles")
let document = collection.document(pid)
document.delete()
{
error in
completion(error)
}
}
}
When the device is online, everything works fine. The completion handler of the deletecall is properly executed. However, when I am offline, I have noticed that the completion handler will not be executed as long as I am offline. As soon as I get back online, the completion handler will be called almost immediately.
I don't want to wait until the user is back online (which could take forever), so I changed the code a little bit and added a ListenerRegistration:
class ProfileController
{
func remove(pid: String, completion: #escaping ErrorCompletionHandler)
{
guard let uid = self.uid else
{
completion(Errors.userIdentifierEmpty)
return
}
let db = Firestore.firestore()
let userDocument = db.collection("profiles").document(uid)
let collection = userDocument.collection("profiles")
let document = collection.document(pid)
var listener: ListenerRegistration?
listener = document.addSnapshotListener(includeMetadataChanges: false)
{
snapshot, error in
listener?.remove() // Immediately remove the snapshot listener so we only receive one notification.
completion(error)
listener = nil
}
document.delete()
}
}
Although this works fine, I am not sure if this is the right way to go. I have read online that a snapshot listener can be used in real-time scenarios, which is not really what I am looking for (or what I need).
Is this the right approach or is there another (better) way? I only want to get notified once (thus I added the includeMetadataChanged property and set it to false). I also remove the ListenerRegistration once the completion handler was called once.
If the first approach does not work properly when being offline - what are the use cases of this approach? Before I change my entire codebase to use listeners, is there any way of executing the completion handler of the first approach when the device is offline?
TL;DR: The second implementation works fine, I am simply unsure if this is the proper way of receiving notifications when the device is offline.
If the first approach does not work properly when being offline - what are the use cases of this approach?
It depends on what you mean by "work properly". The behavior you're observing is exactly as intended. Write operations are not "complete" until they're registered at the server.
However, all writes (that are not transactions) are actually committed locally before they hit the server. These local changes will eventually be synchronized with the server at some point in the future, even if the app is killed and restarted. The change is not lost. You can count on the synchronization of the change to eventually hit the server as long as the user continues to launch the app - this is all you can expect. You can read more about offline persistence in the documentation.
If you need to know if a prior change was synchronized, there is no easy way to determine that if the app was killed and restarted. You could try to query for that data, if you know the IDs of the documents written, and you could check the metadata of the documents to find out the source of the data (cache or server). But in the end, you are really supposed to trust that changes will be synchronized with the server at the earliest convenience.
If your use case requires more granularity of information, please file a feature request with Firebase support.

Possible to get latest data with observeSingleEvent + persistence enabled?

I am trying to get some data from firebase. Any idea how can I get the latest data (not from cache) when I have persistence enabled? I tried keepSynced; I still get stale data. Is this the correct usage?
userRef = FIRDatabase.database().reference().child("<path>")
userRef.keepSynced(true)
userRef.observeSingleEvent(of: .value, with: { snapshot in
...stale data here...
})
Or the only option is to use observe instead of observeSingleEvent? I don't like the fact that with observe I get the cache data first, and then the event triggers a second time with data from the server. So with observe, when I navigate to this screen, first I see a blank table, then I see the table with stale data, and then I see the table with latest data.
Thanks.
EDIT:
https://stackoverflow.com/a/34487195/1373592 -
This post says keeySynced should work. But it's not working for me. I would like to know if I am doing something wrong.
I retrieve some explanation, I think it might help you in your case :
ObserveSingleEventType with keepSycned will not work if the Firebase
connection cannot be established on time. This is especially true
during appLaunch or in the appDelegate where there is a delay in the
Firebase connection and the cached result is given instead. It will
also not work at times if persistence is enabled and
observeSingleEvent might give the cached data first. In situations
like these, a continuous ObserveEventType is preferred and should be
used if you absolutely need fresh data.
I think you don't have the choice to use a continuous listener. But to avoid performance issues why you don't remove yourself your listeners when you don't it anymore.
Here is an example on how to ALWAYS get latest data from firebase when persistence is turned on. Use observe event, keepSynced on your ref and terminate listener if you don't want to keep it always. After several trials, I came up with this and it is working.
func readFromFB() {
let refHandle: DatabaseHandle?
let ref: DatabaseReference? = firebase.child(nodeName)
ref?.keepSynced(true)
refHandle = ref!.observe(.value, with:
{ snapshot in
if snapshot.exists() {
for item in ((snapshot.value as! NSDictionary).allValues as Array) {
//do whatever tasks
}
}
})
if let rf = ref {
rf.removeObserver(withHandle: refHandle!)
}
}

HTTP requests simultaneously in Swift

On start my app I do some http requests, some heavy http requests (downloading some images) and some heavy tasks with UIGraphics (for example doing icon for GMSMarker from two UIImages and other operations with GraphicsContext). It costs some time, so I want to do all that tasks simultaneously. Can you show me best way make it?
On start I have to:
Download and write to local database all devices
Download and write to local database all geofences
Download and write to local database all users
Download and write to local database all positions
Download images for devices, users and geofences
Setup GMSMarkers for devices, users and geofences (after images for that objects will be available - for setting icon of marker)
Code of my login function (it works, but too slow):
func loginPressed(_ sender: UIButton) {
guard
let username = self.usernameTextField.text,
let password = self.passwordTextField.text,
!username.isEmpty,
!password.isEmpty
else {
return
}
self.loginButton.isEnabled = false
self.activityIndicator.startAnimating()
WebService.shared.connect(email: username, password: password) { error, loggedUser in
guard
error == nil,
let loggedUser = loggedUser
else {
self.showAlert(title: "Ошибка подключения", message: error?.localizedDescription ?? "", style: .alert)
self.activityIndicator.stopAnimating()
self.loginButton.isEnabled = true
return
}
DB.users.client.insert(loggedUser)
print("Start loading user photo...")
loggedUser.loadPhoto() { image in
if let image = image {
loggedUser.photo = UIImageJPEGRepresentation(image, 0.0)
}
print("User photo loaded...")
loggedUser.marker = UserMarker(loggedUser, at: CLLocation(latitude: 48.7193900, longitude: 44.50183))
DB.users.client.modify(loggedUser)
}
DB.geofences.server.getAll() { geofences in
DB.devices.server.getAll() { devices in
DB.positions.server.getAll() { positions in
for device in devices {
device.loadPhoto() { image in
if let image = image {
device.photo = UIImageJPEGRepresentation(image, 0.0)
}
if let position = positions.findById(device.positionId) {
device.marker = DeviceMarker(device, at: position)
}
device.attributes.battery = device.lastKnownBattery(in: positions)
}
}
geofences.forEach({$0.marker = GeofenceMarker($0)})
DB.geofences.client.updateAddress(geofences) { geofences in
if DEBUG_LOGS {
print("Geofences with updated addresses: ")
geofences.forEach({print("\($0.name), \($0.address ?? "")")})
}
DB.devices.client.insert(devices)
DB.geofences.client.insert(geofences)
DB.positions.client.insert(positions)
self.activityIndicator.stopAnimating()
WebService.shared.addObserver(DefaultObserver.shared)
self.performSegue(withIdentifier: "toMapController", sender: self)
}
}
}
}
}
}
Not sure it's good idea post here code snippets of all classes and objects, hope you'll get idea.
Any help would be appreciated.
P.S. In case you wonder what is DB, it's database, which consists of two parts - server side and client side for each group of objects, so first task is get all objects from server and write them in memory (in client database)
P.S. I've changed logic from "download everything" on login to "download all I need right now and download rest later". So now after I've got all devices, geofences and positions I'm performing segue to MapController, on which I show all those objects. Just after login I'm showing deviceMarkers (GMSMarker) with default iconView. SO question is - can I after show map with all objects start download photos of devices in background and refresh markers with that photos after that (in main thread of course)?
Network requests are asynchronous by default, so what you are asking for has already the intended behavior.
You can, however, make your life much more simple by using a Promises library such as then.
An example usage might look like:
login(email: "foo#bar.com", password: "pa$$w0rd")
.whenAll(syncDevices(), syncGeofences(), syncUsers(), syncPositions(), syncImages())
.onError { err in
// process error
}
.finally {
// update UI
}
Your login function is slow because you are downloading AND writing to disk (as you mentioned in your question) "all" the data in your closures (geofences, devices and/or positions). Furthermore, all your operations are being executed in the main thread. You should never do I/O (networking, writing to disk) in the main thread, as this thread is used mainly for UI updates. You should offload the expensive tasks to another thread using GCD.
Also, it is worth mentioning that writing to disk is a relatively slow operation, especially if you are doing it for EVERY item you are downloading.
I would recommend you to JUST download any data to be displayed and then use an async task in a DispatchQueue (GCD) to persist the data downloaded to disk AFTER you've displayed the data in your UI.
I am not sure what the DB.geofences.server.getAll() lines do for geofences, devices and positions (in regards to how you handle your networking or database fetching), so I cannot advice you on that. What I can advice you on, is to structure your code the following way:
When user logs in, do the validation against the DB (remote) and guard against a valid login. Transition to your next view controller next (don't execute all your logic), since I can see that you are delegating way too much responsibility to your login action (for your login button).
From that second view controller, get your data through networking calls asynchronously on another thread using a .UserInitiated priority (to get results fast).
After doing all your networking operations, call DispatchQueue.main.async { ... } to update your UI Asynchronously in the main thread with the data you just got.
AFTER you've displayed the downloaded data, you can persist it to your local DB, ideally using another DispatchQueue async task.
If anything I said above does not make any sense to you, please read AppCoda's article about GCD here and RayWenderlich's GCD article here. They will give you the basic knowledge about GCD in iOS. Once you've done that, come back and try to structure your code the way I recommended above.
I hope this helps!

Key data not storing to iCloud with NSUbiquitousKeyValueStore.defaultStore

So I have a simple goal, just to get this working. So theoretically if you ruined this.
Triggered save players
waited a bit
Deleted the app
Rebuilt (downloaded)
Triggered restore then the word "MEDO" would print to the console
But instead it is null, making me pretty sure that it is not saving to iC cloud some reason. I followed this tutorial. Perhaps it is too outdated to work?
func c_restoreCharecters()
{
let icloud = NSUbiquitousKeyValueStore.defaultStore()
println(icloud.objectForKey("username"))
}
func c_savePlayers()
{
let icloud = NSUbiquitousKeyValueStore.defaultStore()
icloud.setObject("MEDO", forKey: "username")
println(icloud.synchronize())
}
Obviously I am going to use this concept for something completely different in the future, but I have to get the basics down first!
Some other stuff:
iCloud set up in my settings:
My Entitlements file:

WMI and Win32_DeviceChangeEvent - Wrong event type returned?

I am trying to register to a "Device added/ Device removed" event using WMI. When I say device - I mean something in the lines of a Disk-On-Key or any other device that has files on it which I can access...
I am registering to the event, and the event is raised, but the EventType propery is different from the one I am expecting to see.
The documentation (MSDN) states : 1- config change, 2- Device added, 3-Device removed 4- Docking. For some reason I always get a value of 1.
Any ideas ?
Here's sample code :
public class WMIReceiveEvent
{
public WMIReceiveEvent()
{
try
{
WqlEventQuery query = new WqlEventQuery(
"SELECT * FROM Win32_DeviceChangeEvent");
ManagementEventWatcher watcher = new ManagementEventWatcher(query);
Console.WriteLine("Waiting for an event...");
watcher.EventArrived +=
new EventArrivedEventHandler(
HandleEvent);
// Start listening for events
watcher.Start();
// Do something while waiting for events
System.Threading.Thread.Sleep(10000);
// Stop listening for events
watcher.Stop();
return;
}
catch(ManagementException err)
{
MessageBox.Show("An error occurred while trying to receive an event: " + err.Message);
}
}
private void HandleEvent(object sender,
EventArrivedEventArgs e)
{
Console.WriteLine(e.NewEvent.GetPropertyValue["EventType"]);
}
public static void Main()
{
WMIReceiveEvent receiveEvent = new WMIReceiveEvent();
return;
}
}
Well, I couldn't find the code. Tried on my old RAC account, nothing. Nothing in my old backups. Go figure. But I tried to work out how I did it, and I think this is the correct sequence (I based a lot of it on this article):
Get all drive letters and cache
them.
Wait for the WM_DEVICECHANGE
message, and start a timer with a
timeout of 1 second (this is done to
avoid a lot of spurious
WM_DEVICECHANGE messages that start
as start as soon as you insert the
USB key/other device and only end
when the drive is "settled").
Compare the drive letters with the
old cache and detect the new ones.
Get device information for those.
I know there are other methods, but that proved to be the only one that would work consistently in different versions of windows, and we needed that as my client used the ActiveX control on a webpage that uploaded images from any kind of device you inserted (I think they produced some kind of printing kiosk).
Oh! Yup, I've been through that, but using the raw Windows API calls some time ago, while developing an ActiveX control that detected the insertion of any kind of media. I'll try to unearth the code from my backups and see if I can tell you how I solved it. I'll subscribe to the RSS just in case somebody gets there first.
Well,
u can try win32_logical disk class and bind it to the __Instancecreationevent.
You can easily get the required info
I tried this on my system and I eventually get the right code. It just takes a while. I get a dozen or so events, and one of them is the device connect code.