QOS_CLASS_USER_INITIATED blocking UI in iOS8? - ios

I am porting a card-melding-game from Android to iOS (see https://play.google.com/store/apps/details?id=com.pipperpublishing.fivekings). Each turn you choose from the drawpile or discard pile, meld your cards, and then discard. To keep it responsive, I have the computer player start pre-calculating its "best action" based on your discard, before it is apparently playing. In Android, I do my own thread management; in iOS I am trying to use GCD.
What I am finding is that the computer pre-calculations running on a QOS_CLASS_USER_INITIATED queue sometimes blocks the UI, especially when testing on iOS8 on an iPhone 6. I can barely imagine that happening for USER_INTERACTIVE. otoh, I've read some confusing stuff about GCD reusing the UI thread for such queues, so maybe I am not understanding.
Here's the relevant code:
EasyComputerPlayer definition of my own queue (things were even slower when I used a global queue):
class EasyComputerPlayer : Player {
static let qos_attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, 0)
static let concurrentDiscardTestQueue = dispatch_queue_create("com.pipperpublishing.FiveKings", qos_attr)
...
Here is the pre-calculation, which is called immediately after the human player discards - testHand.main() does the actually calculations for the possible choices of the computer's discard if the computer picked up the card that the human just discarded.
override func findBestHandStart(isFinalTurn : Bool, addedCard : Card) {
let cardsWithAdded : CardList = CardList(cardList: hand!);
cardsWithAdded.add(addedCard);
//Create the different test hands
testHandSets[method.rawValue].removeAll(keepCapacity: true)
for disCard in cardsWithAdded.cards {
let cards : CardList = CardList(cardList: cardsWithAdded);
cards.remove(disCard);
testHandSets[method.rawValue].append(ThreadedHand(parent: self, roundOf: self.hand!.getRoundOf(), cards: cards, discard: disCard, method: self.method, isFinalTurn: isFinalTurn)) //creates new hand with replaced cards
}
//and then dispatch them
dispatchGroups[method.rawValue] = dispatch_group_create()
for (iTask,testHand) in testHandSets[method.rawValue].enumerate(){
let card = testHand.hand.discard
dispatch_group_enter(dispatchGroups[method.rawValue])
dispatch_async(EasyComputerPlayer.concurrentDiscardTestQueue) {
testHand.main() //calls meldAndEvaluate
dispatch_group_leave(self.dispatchGroups[self.method.rawValue])
}
}
}
In the log, I will see the tasks dispatched, and then the UI sometimes hangs until they all finish (which in later rounds can take 5 seconds).
I replaced QOS_CLASS_USER_INITIATED with QOS_CLASS_UTILITY which seems to have fixed the problem temporarily, but of course I am worried that I have just reduced the frequency :)

Related

Track the time it takes a user to navigate through an iOS app's UI

I want to measure how long (in seconds) it takes users to do certain things in my app. Some examples are logging in, pressing a button on a certain page, etc.
I am using an NSTimer for that. I am starting it in the viewDidLoad of a specific page, and stopping it at the point that I want to measure.
I also want to measure cumulative time for certain things. I would like to start the timer on the log-in screen, and then continue the timer until the user gets to the next view controller and clicks on a certain button.
I'm not sure how to do this. Should create a global variable in my app delegate? Or is there some other better way?
No need for an NSTimer, you just need to record the start times and compare them to the stop times. Try using a little helper class such as:
class MyTimer {
static let shared = MyTimer()
var startTimes = [String : Date]()
func start(withKey key: String) {
startTimes[key] = Date()
}
func measure(key: String) -> TimeInterval? {
if let start = startTimes[key] {
return Date().timeIntervalSince(start)
}
return nil
}
}
To use this, just call start(withKey:) right before you start a long-running task.
MyTimer.shared.start(withKey: "login")
Do something that takes a while and then call measure(key:) when you're done. Because MyTimer is a singleton, it can be called from anywhere in your code.
if let interval = MyTimer.shared.measure("login") {
print("Logging in time: \(interval)")
}
If you're using multiple threads, you may to to add some thread safety to this, but it should work as is in simple scenarios.

How to update variable based on an external event in Swift?

I am using a Particle Core to get the temperature from my room. The temperature is accessed through the cloud, which is being constantly updated in a variable. This is how I access the variable and display it:
func updateTemp(){
let seconds = 3.0
let delay = seconds * Double(NSEC_PER_SEC) // nanoseconds per seconds
let dispatchTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
self.myPhoton?.getVariable("tempF", completion: { (result:AnyObject!, error:NSError!) -> Void in
if let _ = error {
print("Failed reading temperature from device")
}
else {
if let larry = result as? Int {
self.temp.text="\(larry)˚"
self.truth++ //Once a value has been found, update the count.
}
}
})
})
}
override func viewDidLoad() {
sparkStart()
}
override func viewDidLayoutSubviews() {
updateTemp()
NSTimer.scheduledTimerWithTimeInterval(100.0, target: self, selector: "updateTemp", userInfo: nil, repeats: true) //Gaurantees that the app is updated every 100 seconds. That way we have a fresh temperature often.
//Stop the spinning once a value has been found
if truth == 1{
activity.stopAnimating()
activity.removeFromSuperview()
}
}
Since this is my Particle Core detecting the temperature from environment, the temperature variable is constantly changing. However, when I use NSTimer, the code does not get updated in the time specified. Instead, it begins by updating based on the specified time, but then the time starts decreases exponentially and the variable is updated every 0.001 seconds or so. Any thoughts?
Im assuming what we see is not the full code. In your viewDidLayoutSubviews function, you call updateTemp twice. Once explicitly and once via timer callback.
Your updateTemp function schedules the network call in the main run loop, that's where the timer is also running. The dispatch_after function queues the execution of the readout updates one after the other. I am now assuming, that something in your display code causes repeated triggers of viewDidLayoutSubviews, each of which schedules two new updates etc. Even if the assumption is false (there are a couple of other possibilities due to network code being slow and the timer also running in the main run loop), I am guessing if you drop the explicit call to updateTemp you'll lose the "exponential" and should be fine.
In general, as the web call is largely asynchronous, you could just use the timer and call your sensor directly or if you feel GCD has an important performance advantage switch to dispatch_async and apply for the next available queue with each call via calling dispatch_get_global_queue

Gathering Accelerometer data in background

I'm writing an app, that has to do some calculations every time the phone moves. I've read every question here, but couldn't get the Accelerometer to gather data in the background (after the user navigates away from the app). I've set the Location updates flag in the Plist.info. This is the code I'm using:
let motionManager = CMMotionManager()
func startMotionUpdates() {
var timestamps = [NSDate]()
if motionManager.accelerometerAvailable {
motionManager.accelerometerUpdateInterval = 1
motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue(), withHandler: { (data: CMAccelerometerData?, error: NSError?) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let time = NSDate(timeIntervalSince1970: data!.timestamp)
timestamps.append(time)
print(timestamps)
})
})
}
}
I tried every combination out there, I've tried using CMDeviceMotion, I've tried using CLLocationManager.startUpdatingLocation() to simulate background activity, but nothing works. Does anybody have any ideas?
You cannot be woken up every time the accelerometer changes. You can be woken up whenever the location of the device changes significantly (at least several meters) using CLLocationManager, and that's what "location" means in the background modes.
To track finer-grained motion information, you need to ask the system to start recording the data using CMSensorRecorder, and then later you can ask for the data and compute what you want from it. But you won't be allowed to run in the background continuously watching every jiggle of the device. That would eat too much battery.
See also CMPedometer which addresses certain use cases more directly.

iOS Motion Detection: Count of times the device moves up and down

How can i use the gravity or motion sensors inside iPhone to calculate how many times the device moved up and down. e.g. as if it were lifted like a dumbbell for a couple of times, i wanted to count it.
Forgive me if this is something very simply achievable but I'm pretty new to iOS development and hence the question.
You need to use the Core Motion framework to access the gyroscope and accelerometer data.
let manager = CMMotionManager()
if manager.gyroAvailable {
// CMMotionManager is available.
manager.gyroUpdateInterval = 0.1
manager.startGyroUpdates()
// get gyro data...
let queue = NSOperationQueue.mainQueue
manager.startGyroUpdatesToQueue(queue) {
(data, error) in
// ... get data here
}
}
// accelerometer data
if manager.accelerometerAvailable {
manager.accelerometerUpdateInterval = 0.01
manager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()) {
[weak self] (data: CMAccelerometerData!, error: NSError!) in
// get data here ...
}
}
Combining those 2 you can get the detection going. Experiment with the results until you get the right motion you want.
I experimented a bit with the HTML5 methods on the iPhone. I wanted to build something similar to you, an app that would count the number of chin-ups or sit-ups automatically. I tried a combination of these:
navigator.geolocation
window.ondevicemotion
window.ondeviceorientation
The app would count up when moving in one direction, then wait after movement in the opposite direction ends, then start counting again. I get a count, but it lacks precision. Calibration is difficult... Code available if needed.

My iOS app freezes but no error appears

Does any body know what I need to check if app freezes after some time? I mean, I can see the app in the iPhone screen but no view responds.
I did some google and i found that, i've blocked the main thread somehow.
But my question is how to identify which method causes blocking of main thread? is there any way to identify?
Launch your app and wait for it to freeze. Then press the "pause" button in Xcode. The left pane should show you what method is currently running.
Generally, it is highly recommended to perform on the main thread all animations method and interface manipulation, and to put in background tasks like download data from your server, etc...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//here everything you want to perform in background
dispatch_async(dispatch_get_main_queue(), ^{
//call back to main queue to update user interface
});
});
Source : http://www.raywenderlich.com/31166/25-ios-app-performance-tips-tricks
Set a break point from where the freeze occurs and find which line cause that.
Chances may be,Loading of large data,disable the controls,overload in main thread,Just find out where that occurs using breakpoints and rectify based on that.
I believe it should be possible to periodically check to see if the main thread is blocked or frozen. You could create an object to do this like so:
final class FreezeObserver {
private let frequencySeconds: Double = 10
private let acceptableFreezeLength: Double = 0.5
func start() {
DispatchQueue.global(qos: .background).async {
let timer = Timer(timeInterval: self.frequencySeconds, repeats: true) { _ in
var isFrozen = true
DispatchQueue.main.async {
isFrozen = false
}
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + self.acceptableFreezeLength) {
guard isFrozen else { return }
print("your app is frozen, so crash or whatever")
}
}
let runLoop = RunLoop.current
runLoop.add(timer, forMode: .default)
runLoop.run()
}
}
}
Update October 2021:
Sentry now offers freeze observation, if you don't wanna roll this yourself.
I reached an error similar to this, but it was for different reasons. I had a button that performed a segue to another ViewController that contained a TableView, but it looked like the application froze whenever the segue was performed.
My issue was that I was infinitely calling reloadData() due to a couple of didSet observers in one of my variables. Once I relocated this call elsewhere, the issue was fixed.
Most Of the Time this happened to me when a design change is being called for INFINITE time. Which function can do that? well it is this one:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
Solution is to add condition where the function inside of viewDidLayoutSubviews get calls only 1 time.
It could be that another view is not properly dismissed and it's blocking user interaction! Check the UI Debugger, and look at the top layer, to see if there is any strange thing there.

Resources