Waiting with method execution until a condition is met in Swift - ios

I have a MKMapView with some annotations added by code representing users sharing their location. I get updates about location changes via websockets and update the corresponding MKAnnotation.coordinate that represent the annotations in MKMapView.
This mostly works fine, I see the users moving on the map. The problem is that when a MKAnnotation.cooridnate is updated while the user pans or zooms the map then MKMapView crashes saying that a hash table was update while being evaluated. It is basically this problem: MKMapView crashing if zooming while adding annotations
Update: Crashes only occur when clustering is enabled, no need to even pan or zoom, just waiting a bit is usually enough.
The recommended solution is not doing anything (like updating MKAnnotation.cooridnate in my case) with MKMapView between regionWillChangeAnimated and regionDidChangeAnimated and I am tryintg to implement this.
The problem is that my data is being received via websockets so I cannot just "stop" listening for the new data between those 2 delegate methods. Theoretically I can but that would mean losing some data which is not a solution.
My idea was that I need some kind of queue that I will use to add my updates to MKAnnotation.cooridnate and will get paused in regionWillChangeAnimated and resumed in regionDidChangeAnimated.
I tried using DispatchQueue in the way described but it locks after a while. I am not sure if it is because of the high amount of operations being added to it or the calls to suspend and resume. If it does not lock it just crashes the app after a while.
My question is what mechanism will work here?
As stated in the question title, I basically need a way to make some methods wait for a condition, say toggling a flag between regionWillChangeAnimated and regionDidChangeAnimated that is reliable and can take the load.

If you want to not perform tasks while a certain condition is met, use a DispatchQueue and just suspend or resume it. And if this is for tasks that should be performed on the main queue, make the main queue the target for this queue that will be suspended and resumed:
let annotationChangeQueue = DispatchQueue(label: "...", target: .main)
But this begs the question: Under what condition should one should suspend and resume this queue? It would be reasonable to assume (as that other question you reference does) that one could suspend in regionWillChangeAnimated and resume in regionDidChangeAnimated. Alas, this appears to be insufficient. You’ll find that even if you make sure to not do any annotation changes until after regionDidChangeAnimated, it can still crash. And MKMapView doesn’t appear to expose any methods/properties that we can observe to avoid this.
I don’t think there is a proper solution to this problem given that Apple isn’t exposing any property that we could possibly detect when it’s safe to mutate our annotations.
FWIW, I am only able to manifest this issue if I turn on clustering. Also, while the focus of this question was on panning/zooming, I found I was able to reproduce the crash without any panning/zooming. Just was just updating annotations (that participate in clustering) and not touching my device at all, and it eventually crashed with this error. So while it’s easier to manifest with panning/zooming, I think the problem is more fundamental, namely how the map view handles updates to annotations (presumably, only those that may be clustered).

Related

When does the SpriteKit game loop run for the first time?

I'm trying to understand when a SpriteKit scene's frame cycle runs within the main iOS run loop. Specifically, I am concerned about the AppDelegate's applicationDidBecomeActive(_:) method. I always thought that method was called after the app becomes active, but before your presented scene's frame cycle runs.
This is important to the project I am building because I use the applicationDidBecomeActive(_:) method to perform some time-sensitive tasks like examining a timestamp, setting flags, starting timers, etc. So I need to reliably anticipate when this method will be called during the frame cycle (let's just call it the "game loop").
I did some testing which suggests that the game loop runs at different times in relation to the applicationDidBecomeActive(_:) method, depending on which version of iOS the app is running on. This is a concern because it means I cannot rely on a single implementation of this method to perform the tasks I need at the correct time.
I want to know definitively when applicationDidBecomeActive(_:) is called in relation to the SpriteKit game loop. That seems like a fundamental thing that anyone writing a SpriteKit game needs to understand. And I'm shocked to see that it seems to vary depending on the OS version. It's possible that I made a mistake in my testing and assumptions. But I'll report what I found here and see if anyone else has noticed this, and if anyone can explain this odd behavior.
In my current project, I have been testing on my physical iPhone running iOS 12.4 and sometimes using the Simulator for an iPhone running iOS 13. By using print statements, I have observed that the AppDelegate's applicationDidBecomeActive(_:) method and the SKScene's update(_:) method are being called in a different order, depending on which version of iOS is used.
Note that my project uses UIViewController's viewDidLoad() method to present the scene. I tried using viewWillLayoutSubviews() instead, hoping that things might work more reliably that way. But that proved to be even less reliable, so I won't discuss that here.
Order of Method Calls (iOS 12.4):
didFinishLaunchingWithOptions
viewDidLoad
didMove
update
applicationDidBecomeActive
update
...
Order of Method Calls (iOS 13):
didFinishLaunchingWithOptions
viewDidLoad
didMove
?
applicationDidBecomeActive
update
...
You can see that both versions of the OS call AppDelegate's application(_:didFinishLaunchingWithOptions:) method first, then they load the view. In viewDidLoad(), I make my call to have the view present my SKScene. As expected, the scene's didMove(to:) method is called after the view presents the scene. But what happens next is the odd part.
In iOS 12.4, the scene's update(_:) method is called, which indicates that the scene performed a single run of its game loop. Then the AppDelegate calls its applicationDidBecomeActive(_:) method. Next, the update(_:) method runs again. Then update(_:) keeps being called over and over as the scene's game loop fires 60 times per second, as expected.
In iOS 13, the update(_:) method does not get called immediately after didMove(to:) is called. Instead, applicationDidBecomeActive(_:) is called right after didMove(to:). Only then does the update(_:) method run (and then continues running, as expected).
So basically, the issue here is that in iOS 12.4, the game loop appears to run once immediately after it is presented, before applicationDidBecomeActive(_:) is called. But in iOS 13 this does not happen.
It's a problem that the game loop in iOS 12.4 runs one extra time, before applicationDidBecomeActive(_:) is called. This makes the game's lifecycle inconsistent among different versions of the OS, and it means I will have to write different code to handle cases for different OS versions. Either that, or I must redesign the parts of the app that rely on applicationDidBecomeActive(_:) to find a more consistent way of handling them. It also makes me wonder if the extra run of the game loop is a bug in iOS 12.
I always assumed that the app's lifecycle was consistent between OS versions (at least regarding the order of method calls for AppDelegate and SKScene). But this discovery throws all of that into question. I have not yet tested with other versions of iOS, because even if this is the only discrepancy between all of the OS versions, it still means that your code must handle things differently depending on the OS version.
To add one more wrinkle to this analysis...
I also made a new SpriteKit template project and performed the same test. I found the same discrepancy, with one added peculiarity: in iOS 12.4, the update(_:) method is called twice immediately following didMove(to:), before applicationDidBecomeActive(_:) is called. In iOS 13, the behavior is the same as described above.
I'm not sure why update(_:) is firing twice rather than once as it does in my other project. That seems quite odd. But this test in a "clean" template project suggests that this is a real issue, rather than some error in my own code.
To reiterate my question...
I would like to know if anyone else has noticed this. Maybe I am mistaken in my conclusion. If this is a real issue, I wonder if there is any "fix" that can be done to make the game loop work in a consistent way for all OS versions. If not, can anyone suggest a good workaround so that your code in applicationDidBecomeActive(_:) consistently runs before the game loop first fires? I already have some ideas. But first, I want to confirm if this is an actual issue with iOS or just a mistake in my own code.
This isn't directly relevant but might give you a way forward...
SpriteKit tries to do automatic pausing and unpausing when the game goes into the background and back to the foreground. I had a situation where I wanted more control, and in particular did not want the game to always start up again on resume. My hack (which was working on iOS 12 and 13, though I don't recall versions) was to override isPaused in my derived game scene, e.g.,
override var isPaused: Bool {
get { super.isPaused }
set {
if forcePause && !newValue {
os_log("holding isPaused at true because forcePause is true", log: .app, type: .debug)
}
super.isPaused = newValue || forcePause
}
}
Then I'd have the additional variable forcePause to keep the game under control.
Perhaps you could do something similar, setting forcePause when you go into the background and then having applicationDidBecomeActive clear it. Perhaps that would be sufficient to keep iOS 12 from jumping the gun on you.
I used one of the code-level support incidents that I get as part of the Apple Developer Program to ask this question of one of their support engineers. What he told me was that there is no guarantee that update(_:) is always called after applicationDidBecomeActive(_:), so your app should not rely on such behavior. I had already implemented a workaround, and he told me I should either continue using that approach, or explore a different design that does not rely on the active state.
My workaround is:
Set a global Boolean variable called applicationDidBecomeActiveRan, initialized to false.
At the end of applicationDidBecomeActive(_:), set the global variable to true.
At the beginning of applicationWillResignActive(_:), set the global variable to false.
In the update(_:) method of the SKScene subclass, put this guard statement at the beginning of the method:
guard applicationDidBecomeActiveRan else { return }
This way, the rest of the code in my update(_:) method won't run unless applicationDidBecomeActive(_:) has already run. This ensures that the order of code execution for applicationDidBecomeActive(_:) and update(_:) will be consistent, no matter which iOS version the game is running on.
I was surprised to hear that the order of these two method calls is not guaranteed by default, because in every situation I have encountered up until this point, it has behaved in a consistent way (unless I just did not notice the inconsistency). Admittedly, I've never seen any Apple documentation that states that it should behave consistently. But when you see something happen a certain way every time, you naturally come to expect that it always works that way and indeed is supposed to work that way. It's helpful to learn that in rare edge cases like this one, it may not work in the usual way.

dynamic MKMap points loading

Im trying to find the best way to work with MKMaps with my needs.
My code is very long so ill just explain.
What do i need to do? (*)
understand what is the location that the user is interested in.
get points from the internet for this location.
load them into the map.
update the map (cluster annotations) with animation.
How do I do it now?
in regionDidChangeAnimated I call GetPoints.
GetPoints starts a NSURLConnection in order to get the points in the region (using region.span.latitudeDelta and region.span.longitudeDelta).
in connectionDidFinishLoading I'm calling AddPoints.
AddPoints is checking for every point if its already on the map, if not its added (not directly to the MapView but to a "hidden" AllPointsMap).
after adding all points into the AllPointsMap Im calling updateVisibleAnnotations.
updateVisibleAnnotations is updating the MapView (by separating the visibleMapRect to gridMapRects and shows only one MKAnnotation for each one) and takes care to animate the annotations in and out the way they should.
What is the problem?
it works! but its not so smooth, updateVisibleAnnotations takes a lot of time and the map is not responding in that time...
So I'm calling updateVisibleAnnotations in background, like that:
[self performSelectorInBackground:#selector(updateVisibleAnnotations) withObject:nil];
Its awesome, really smooth mapView animations, not disturbing user interface.
BUT from time to time i get "Collection <__NSArrayM: 0x76c11b0> was mutated while being enumerated".
I can't tell exactly what causing it. But my guess is that main thread is trying to add or remove annotations while background threads are using them.
I have tried using #synchronized in updateVisibleAnnotations so main thread is not interrupting background thread, but it remained the same.
I have tried using a self.waitinigForUpdate BOOL #property to indicate that updateVisibleAnnotations is running in background, still get the same error…
Makes me wonder: Maybe this is not the best way doing what I need to do (*), is it??

Macro Recording in iOS

Is it possible to record set of touch events on iPhone and then playback?
I have searched alot but could not find any answer. if its possible, can anyone explain with an example.
I m not looking for testing purpose. Within my application, instead of creating animation, i just want to record set of events and then want to playback to explain the app flow to the users.
Regards.
Recording is pretty simple. Look at the various "Responding to Touch Events" and "Responding to Motion Events" methods on UIResponder. Just create your own UIView subclass (since UIView inherits from UIResponder) and keep a copy of the events passed into the relevant methods.
Playback is a bit more complicated; there's no way to make UITouch or UIEvent objects (so you can't make a fake event and pass it on to -[UIApplication sendEvent:]). But, there's nothing stopping you from manually parsing an array of Event objects and handling it on your own (aside from it being some kind of ugly code).
There's no built-in macro capability, but you could certainly build that ability into your application. You'll need to do more than just play back events, though. Touches aren't normally visible, but if you're trying to explain how to use your app to the user you'll probably want to have some sort of visual representation for the touches that trigger different responses similar to the way the iOS Simulator uses white dots to represent multiple touches when you hold down the option key.
Assuming that you can solve that problem, two strategies for easily recording user actions come to mind:
Use the Undo Manager: NSUndoManager is already set up to "record" undoable events. If you invest some time into making everything in your app undoable, you could (maybe) perform a set of actions, undo them all to move them to the redo stack, and then save the events in the redo stack as your script.
Use Accessibility: The Accessibility framework sends notifications whenever user interface elements are touched. Your app could use those notifications to create a playback script. You'll still need to write the code to play back the events in the script, though.
You could mirror your application with AirServer and use any screen capture software to make the video.

Why are my iPad touch events being delayed (sometimes up to a second)?

I'm working on a game, and I'm using a CAEAGLLayer backed UIView subclass to present the game engine. Touch handling is done using -touchesBegan:withEvent: et. al.
Everything works fine, except that very rarely, if one of the on-screen controls is tapped rapidly, -touchesBegan:withEvent: doesn't get called for somewhere between 0.1 and 1-2 seconds. This happens maybe one in 20 times, and only if you tap the screen rapidly (4-5 times) first. It seems to be more likely to happen if I am also holding down another finger on a different control on the screen.
Thinking that it was something to do with my own code, I subclassed UIApplication so I could add a logging statement to -sendEvent:. When the laggy touch happens, -sendEvent: doesn't get called until some period of time after the touch has started, so it the touch handling code inside my UIView subclass doesn't appear to be at fault.
Does anyone have any idea what's going on here (other than iOS having some obscure bug)? Is there some sort of internal "events queue" that makes event delivery become laggy when it fills up? Has anyone else experienced this?
Touch events are only dispatched in the main UI run loop, and sometimes only when the main run loop goes idle for a bit. So if your app is busy handling several previous touch events in a row without taking a break, the main UI run loop might be saturated, and thus not get any further touch events until done with the current stuff.
Touch events also have time stamps. So you can check if they're coming too fast (faster than your event handlers and resulting UI updates can run), and skip or combine some of the event handlers, as appropriate for your app, if you want the app to stay maximally responsive.

Trouble toggling the userInteractionEnabled property in iOS

I am developing a tic tac toe game for iOS and I am using a combination of UIButtons and UIImageViews to allow for user interaction and to display the moves made. My problem is that the buttons continue to accept user input before the cpu makes it's move, which breaks my game logic. I have made several attempts to toggle the userInteractionEnabled property, but I have only been able to turn it off. The engine that gets everything started in the game is my buttonPressed method. I also toggle the userInteractionEnabled property within this method and therein lies my problem: How do I re-enable the property after disabling user interaction? Is there a method that is called in between events that I can overwrite?
I have searched the web and I have searched through the developer documentation provided by Apple and I found information on the touchesBegan and touchesEnded methods. However, from what I understand, those methods need to be explicitly called which brings me back to my original problem of not being able to call those functions without the user's interaction.
If anyone can help, I would greatly appreciate it! I have been racking my brain over this for the past couple of weeks and I am just not seeing a solution.
I'd think that for a game like tic-tac-toe, calculating the countermove should be so fast that it can be done immediately in response to the first button press. If you're doing something complicated to calculate the next move, like kicking off a thread, you might want to reconsider that.
Let's say, though, that your game is something like chess or go, where coming up with a countermove might take a bit longer. Your view controller should have a method to make a move for the current player, let's call it -makeMove:. Your -buttonPressed action should call that method to make a move for the user. In response, -makeMove: should update the state of the game, switch the current player to the next player. If the new current player is the computer, it should then disable the controls and start the process of calculating the next move. Let's imagine that's done by invoking some NSOperation, so that coming up with the next move is an asynchronous task. Once the operation has come up with a move, it should again invoke -makeMove: (by calling -performSelectorOnMainThread:), which will again update the game state and the current player. This time, though, it should see that the new current player is not the computer, and so it should re-enable the controls.

Resources