UI (sometimes) freezes during an operation on iOS - ios

So, on my app I have a feed that I want to update every time the user returns to the app.
I'm calling a routine to do this on applicationWillEnterForeground of my AppDelegate.
Everything is working fine but, sometimes, my UI freezes during this operation.
I was able to find where this occurs using a label to show the progress of this routine. The label is updated on three major points:
Before starting the routine
Inside the routine
When the routine ends
Sometimes, this workflow works fine, and I'm able to see the progress through this label.
But sometimes, the label only shows the first message, and messages that happen inside the routine does not appear. Besides that, I can't do anything on my app, because the UI is frozen. Once the routine is over, everything comes back to normal.
Here's a simplified version of the flow my app executes to call this routine:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
let mainViewController = MainViewController()
func applicationWillEnterForeground(_ application: UIApplication) {
mainViewController.refreshLatestVideos()
}
}
class MainViewController: UITabBarController {
private var subscriptionsController: SubscriptionsController! // initialized on viewDidLoad
func refreshLatestVideos() {
subscriptionsController.refreshLatestVideos(sender: nil)
}
}
class SubscriptionsController: UITableViewController {
private var subscriptionsModelController: SubscriptionsModelController! // received on constructor
#objc func refreshLatestVideos(sender:UIButton!) {
showMessage(message: "Updating subscriptions...") // this message is always shown to me
subscriptionsModelController.loadLatestVideos()
}
}
class SubscriptionsModelController {
func loadLatestVideos() {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
DispatchQueue.global(qos: .userInitiated).async {
// bunch of requests with Just
...
// update message
showMessage(message: "Updating subscription x of y") // this message sometimes doesn't appear, because the UI is frozen
// another requests
...
// update message
showMessage(message: "Updates completed")
}
}
}
As you can see, I'm executing the update inside the global queue, so I'm not blocking the main thread.
And again, this freezing of the UI only happens sometimes.
Is there any point which I can look at to find what's going on? Is it possible that the main thread is being blocked by something else?

Dispatch updating the UI to the main thread:
DispatchQueue.main.async { showMessage(message: "Updates completed") }
Everytime you anyhow access/modify the UI, do it on the main thread to avoid having unexpected problems (link to one of several resources on this topic, I suggest you google up and read more).
That applies to the rest of the code as well, if there is something that is related to UI, do the same for it - e.g., if after finishing the task you call tableView.reloadData, do it on the main thread too.

Related

iOS requestTrackingAuthorization callback is not on the main thread

The iOS ATT Alert needs to be shown on startup before an app starts, so the remaining app code needs to be run in the completion handler (eg initialising advert code and starting the main game).
However, this crashes because the completion handler callback is not on the main thread (after the alert is actually shown and Allow or Ask App Not to Track is selected).
In iOS (Xamarin C# code):
public override void OnActivated(UIApplication application) // RequestTrackingAuthorization must be called when app is Active
{
if (!this.shownAlert)
{
this.shownAlert=true;
Debug.WriteLine("1) ThreadId={0}", Environment.CurrentManagedThreadId); // ThreadId=1
ATTrackingManager.RequestTrackingAuthorization(delegate (ATTrackingManagerAuthorizationStatus trackingManagerAuthorizationStatus)
{
Debug.WriteLine("2) ThreadId={0}", Environment.CurrentManagedThreadId); // ThreadId=7
});
}
}
Prints:
1) ThreadId=1
2) ThreadId=7
And the similarly in Unity:
Debug.WriteLine("1) ThreadId={0}", Environment.CurrentManagedThreadId); // ThreadId=1
ATTrackingStatusBinding.RequestAuthorizationTracking(delegate (int status)
{
Debug.WriteLine("2) ThreadId={0}", Environment.CurrentManagedThreadId); // ThreadId=4
});
This seems like a bug to me. So how do all the apps and games out there get this to work? Somehow switch to the main thread in the callback? I don’t see any examples of this.

WKInterfaceMap blank rendering

WKInterfaceMap sometimes renders blank in a very simple app I am developing. The map appears completely empty (no grid of lines or anything similar). It looks as though the thread responsible for drawing the tiles gets blocked.
In order to reproduce the issue just add the code below to the extension delegate.
func applicationDidEnterBackground() {
let watchExtension = WKExtension.shared()
// Schedule the background refresh task.
watchExtension.scheduleBackgroundRefresh(withPreferredDate: Date().addingTimeInterval(15.0*60.0), userInfo: nil) { (error) in
// Check for errors.
if let error = error {
print("ExtensionDelegate: \(error.localizedDescription)")
return
}
print("ExtensionDelegate: Background Task scheduled successfuly")
}
}
Add a WKInterfaceMap to the main view and run once on the simulator. Close the app using the crown and stop it from XCode. Wait for at least 15 minutes and open the app again directly from the simulator.
The map renders then as in the image below.

swift ios 10 execute code asynchronously or in the background

When user launch the app or finish editing the data I need to update local notifications, basically it takes around 2-3 seconds in async way. I need to make sure that this code executes even if app leave foreground. What I have now:
func buildLocalNotifications()
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
//recreate the notifications
}
}
And I can call this method from didFinishLaunchingWithOptions or when user save the form and everything works like a charm while app stays active for more then 3-4 seconds and its not blocking UI of course.. but if user lock the screen or terminate the app - this code won;t finished and notifications won't be created. How to safely execute sensitive code?
What is coming on my mind - show up a loader while performing this action - but it will block the user interaction
Ok I found the solution for the task which requires some time and should not be interrupted when app leaves foreground.
So we need beginBackgroundTask and endBackgroundTask
Small manager which you can use to execute code even when app is not in foreground
class BackgroundTaskManager {
let backgroundDQ = DispatchQueue.global(qos: .background)
var backgroundUpdateTask: UIBackgroundTaskIdentifier!
init(withName: String) {
self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(withName: withName) {}
}
/* Using completion handler to know when code is done*/
func runBackgroundTask(withCode: #escaping (_ cH: #escaping () -> Void) -> Void)
{
backgroundDQ.async {
withCode() {
self.endBackgroungTask()
}
}
}
func endBackgroungTask() {
if backgroundUpdateTask != nil && backgroundUpdateTask != UIBackgroundTaskInvalid {
UIApplication.shared.endBackgroundTask(backgroundUpdateTask)
backgroundUpdateTask = UIBackgroundTaskInvalid
}
}
}
And you can use it like
let taskManager = BackgroundTaskManager(withName: "LocalNotifications")
taskManager.doBackgroundTask() { (cH) in
//Your code goes here
//Send back completion handler so system knows when to finish background task
cH()
}
More information you can find on the Medium
If you want to make sure your code gets executed even if the user closes your app, you need to call your function in applicationWillTerminate. However, you only have ~5 seconds to execute code, before the system closes your app, so asynchronous execution is not encouraged here. It also doesn't matter if you execute code synchronously, since the user already quit your app, so you won't be blocking any UI updates.
Try to excute your code in background
DispatchQueue.global(qos: .background).async {
// your code here
}

Why is popViewControllerAnimated taking so long to run?

I have a secondary viewController that allows me to delete images from the camera roll. The problem is, the completionHandler fires like it's suppose to, but the popViewController doesn't actually seem to run for about 8 seconds. It definitely fires, because I can see the optional output. And I checked just doing the pop, and it runs correctly. I checked the viewWillDisapear event, and it fires late as well, which I expected considering the nav controller hadn't popped the view current viewController yet.
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
PHAssetChangeRequest.deleteAssets(assetsToDelete)
return
}, completionHandler: { success, error in
if success {
println("success")
println(navigationController.popViewControllerAnimated(true))
println("so slow")
}
if let error = error {
println(error)
}
return
})
This is what the documentation says:
Photos executes both the change block and the completion handler block
on an arbitrary serial queue. To update your app’s UI as a result of a
change, dispatch that work to the main queue.
The navigation controller needs to be executed from the main thread, so you need to wrap the call to something like
dispatch_async(dispatch_get_main_queue()) {
navigationController.popViewControllerAnimated(true)
}
For Swift 3
DispatchQueue.main.async() {
self.navigationController?.popViewController(animated: true)
}

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