My app gets stuck in the icon for about 4-5 seconds when entering foreground, if the networking is bad. In almost all the main loading pages I am fetching images from a server. I have nothing on applicationDidEnterBackground() nor applicationWillEnterForeground().
Here the code in the Main View controller where I have a collection view with images:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(homeReuseIdentifier, forIndexPath: indexPath)as! HomeViewCell
// Reload the collection view after insert item at indexPath
cvHome.reloadItemsAtIndexPaths([indexPath])
let entry = dataCell.infoCell[indexPath.row]
cell.backImageView.image = nil
let stringURL = "https://.....\(entry.filename)"
let image = UIImage(named: entry.filename)
cell.backImageView.image = image
if cell.backImageView.image == nil {
if let image = dataCell.cachedImage(stringURL) {
cell.backImageView.image = image
} else {
request = dataCell.getNetworkImage(stringURL) { image in
cell.backImageView.image = image
}
}
}
cell.titleLabel.text = entry.title
cell.authorLabel.text = entry.author
// Fix the collection view cell in size
cell.contentView.frame = cell.bounds
cell.contentView.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
// To edit the cell for deleting
cell.editing = editing
return cell
}
And this is the getNetworkImage function with Alamofire:
func getNetworkImage(urlString: String, completion: (UIImage -> Void)) -> (ImageRequest) {
let queue = decoder.queue.underlyingQueue
let request = Alamofire.request(.GET, urlString)
let imageRequest = ImageRequest(request: request)
imageRequest.request.response(
queue: queue,
responseSerializer: Request.imageResponseSerializer(),
completionHandler: { response in
guard let image = response.result.value else {
return
}
let decodeOperation = self.decodeImage(image) { image in
completion(image)
self.cacheImage(image, urlString: urlString)
}
imageRequest.decodeOperation = decodeOperation
}
)
return imageRequest
}
I'm not sure if you're networking code is bad since you haven't provided any.
My guess is, fetching the data is taking way too long, and it's blocking the UI. So currently, your app is synchronous. So tasks are executed one at a time. the reason your UI is blocked is because it has to wait for the networking to finish.
What you need to do is perform the fetching in the background, and always keep your UI on the main queue.
If you are calling Synchronous you have to wait till the task is finishing.We can start next task once we finish the current task.When you are fetching image from server make sure that whether you are using Synchronous or not.When you don't wait for a second go with Asynchronous.You don't need to wait while fetch the images from server.You can do other tasks when fetching the images from server.
For this GCD is good one to use.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// Do the back ground process here
......Fetch the image here
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI here
.....Do your UI design or updation design part here
});
});
Global Queue
Asynchronous task will be performed here.
It does not wait.
Asynchronous function does not block the current thread of execution from proceeding on to the next function.
Main Queue
We must always access UI Kit classes on the main thread.
If you don't want to wait a seconds,go with GCD
Related
I have a UITableView with about 1000 rows. I also have a timer running every 6 seconds that fetches data from a web service. Each time I call reloadData() there is a blip - my app freezes very noticeably for a brief moment. This is very evident when scrolling.
I tried fetching about 400 rows only and the blip disappears. Any tips how to get rid of this while still fetching the 1000 rows?
var items: [Item] = []
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
OperationQueue.main.addOperation {
strongSelf.items = itemsFromResponse
strongSelf.itemsTableView.reloadData()
}
}
}
UITableViewDataSource code:
extension ItemViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].name
return cell
}
}
The problem is being caused because you are storing the items from the response and then updating the table view from the same OperationQueue, meaning that the UI thread is being blocked while your array is being updated. Using an operation queue in itself is not an optimal way to schedule tasks if you do not need fine grain control over the task (such as cancelling and advanced scheduling, like you don't need here). You should instead be using a DispatchQueue, see here for more.
In order to fix your issue, you should update your array from the background completion handler, then update your table.
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
strongSelf.items = itemsFromResponse
// update the table on the main (UI) thread
DispatchQueue.main.async {
strongSelf.itemsTableView.reloadData()
}
}
}
You should also maybe look into a more efficient way to fetch new data, because reloading the entire dataset every 6 seconds is not very efficient in terms of data or CPU on the user's phone.
The problem is you are reloading data every 6 seconds, so if the data is so big you're reloading 1000 rows every 6 seconds. I recommend you request the data and compare if there's new data so in that case you need to reload data or you simply ask to refresh once. For example:
var items: [Item] = []
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
OperationQueue.main.addOperation {
if(strongSelf.items != itemsFromResponse){
strongSelf.items = itemsFromResponse
strongSelf.itemsTableView.reloadData()
}
}
}
I know any updates to UI should be run inside the main queue using the below syntax:
dispatch_async(dispatch_get_main_queue()) {
UI update code here
}
What about these other cases?
In my viewDidLoad(), I have code to stylize the navbar and toolbar, like below:
let nav = self.navigationController?.navigationBar
nav?.barStyle = UIBarStyle.Default
nav?.tintColor = UIColor.blackColor()
nav?.barTintColor = UIColor(red:133.0/255, green:182.0/255, blue:189.0/255, alpha:1.0)
let toolBar = self.navigationController?.toolbar
toolBar?.barTintColor = UIColor(red:231/255, green:111/255, blue:19.0/255, alpha:1.0)
toolBar?.tintColor = UIColor.blackColor()
Should I wrap this code inside the main queue as well?
In my tableView cellForRowAtIndexPath function, should I wrap all the code setting up the UI of each table cell in the main queue as well?
When I present a new modal controller (self.presentViewController(modalController, animated: true, completion: nil), should I wrap this inside the main queue?
The answer to all your questions is "no"
Unless specified in the documentation, all UIKit functions will be called on the main queue.
Generally, you'll need to specifically run on the main queue after calling an async func with a completion handler that runs on a background queue. Something like…
// downloadImage is some func where the
// completion handler runs on a background queue
downloadImage(completion: { image in
DispatchQueue.main.async {
self.imageView.image = image
}
})
Yeah, calling the main queue is something you should only have to do if a function was already being performed asynchronously on a background queue. And that doesn't tend to happen by itself.
Like Ashley says, UIKit methods are automatically called from the main queue – that's just how they are. It stands to reason, then, that some frameworks have methods that automatically call from a background queue.
I know that URLSession's dataTask's resume function automatically executes on a background queue (so that the app isn't slowed down by a web connection), which means the completion handler that executes afterward ALSO works on a background queue. That's why any UI updates that happen in the completion handler certainly require a call on the main queue.
let dataTask = URLSession.shared.dataTask(with: request) {
// begin completion handler
(data, response, error) in
guard error == nil else { print("error \(error.debugDescription)"); return }
guard let data = data else { print("no data"); return }
do {
if let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [[String: Any]] {
print("sending returned JSON to handler")
self.responseHandler.handleAPIResponse(jsonArray: json)
>>>> DispatchQueue.main.async { <<<<
self.tableView.reloadData()
}
}
} catch {
print("get tasks JSONSerialization error")
}
}
dataTask.resume()
I'm working on an app in iOS wherein I need to start spinning a UIActivityIndicatorView, upload an image to a server, and when the upload is completed, stop spinning the activity indicator.
I'm currently using XCode 7 Beta and am testing the app on the iOS simulator as an iPhone 6 and iPhone 5. My issue is that the activity indicator won't end immediately after file upload, but several (~28 seconds) later. Where should I place my calls to cause it to end?
I have an #IBOutlet function attached to the button I use to start the process, which contains the startAnimating() function, and which calls a dispatch_async method that contains the call to uploadImage, which contains the signal, wait, and stopAnimating() functions.
Note that
let semaphore = dispatch_semaphore_create(0)
let priority = DISPATCH_QUEUE_PRIORITY_HIGH
are defined at the top of my class.
#IBAction func uploadButton(sender: AnyObject) {
self.activityIndicatorView.startAnimating()
dispatch_async(dispatch_get_global_queue(priority, 0)) {
self.uploadImage(self.myImageView.image!)
} // end dispatch_async
} // works with startAnimating() and stopAnimating() in async but not with uploadImage() in async
func uploadImage(image: UIImage) {
let request = self.createRequest(image)
let session : NSURLSession = NSURLSession.sharedSession()
let task : NSURLSessionTask = session.dataTaskWithRequest(request, completionHandler: {
(data, response, error) in
if error != nil {
print(error!.description)
} else {
let httpResponse: NSHTTPURLResponse = response as! NSHTTPURLResponse
if httpResponse.statusCode != 200 {
print(httpResponse.description)
} else {
print("Success! Status code == 200.")
dispatch_semaphore_signal(self.semaphore)
}
}
})! // end dataTaskWithResult
task.resume()
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER)
self.activityIndicatorView.stopAnimating()
} // end uploadImage
This is just one version of my code, I have moved several things around several different ways. I have tried this:
#IBAction func uploadButton(sender: AnyObject) {
self.activityIndicatorView.startAnimating()
dispatch_async(dispatch_get_global_queue(priority, 0)) {
self.uploadImage(self.myImageView.image!)
dispatch_semaphore_signal(self.semaphore)
} // end dispatch_async
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER)
self.activityIndicatorView.stopAnimating()
}
And several, several other ways of moving my code around to attempt to get the activity indicator to display for the duration of the image upload and then immediately quit. In some cases the spinner doesn't appear at all for the duration of program execution. I read this post and this question and have migrated my dispatch_semaphore_wait and stopAnimating() to the uploadImage() method to circumvent this, but can't find enough information in the UIActivityIndicatorView documentation about the UI updating to know any other way of updating it, though I believe this might be at the core of the problem.
All I need is for the spinner to start before the upload process begins (dataTaskWithRequest) and end once it has succeeded or failed. What am I doing wrong?
Instead of using semaphores, you could just dispatch directly to the main thread in your async task,
func uploadImage(image: UIImage) {
let request = self.createRequest(image)
let session : NSURLSession = NSURLSession.sharedSession()
let task : NSURLSessionTask = session.dataTaskWithRequest(request, completionHandler: {
(data, response, error) in
if error != nil {
print(error!.description)
} else {
let httpResponse: NSHTTPURLResponse = response as! NSHTTPURLResponse
if httpResponse.statusCode != 200 {
print(httpResponse.description)
} else {
print("Success! Status code == 200.")
}
}
// dispatch to main thread to stop activity indicator
dispatch_async(disptach_get_main_queue()) {
self.activityIndicatorView.stopAnimating()
}
})! // end dataTaskWithResult
task.resume()
} // end uploadImage
I have a header view for every UITableViewCell. In this header view, I load a picture of an individual via an asynchronous function in the Facebook API. However, because the function is asynchronous, I believe the function is called multiple times over and over again, causing the image to flicker constantly. I would imagine a fix to this issue would be to load the images in viewDidLoad in an array first, then display the array contents in the header view of the UITableViewCell. However, I am having trouble implementing this because of the asynchronous nature of the function: I can't seem to grab every photo, and then continue on with my program. Here is my attempt:
//Function to get a user's profile picture
func getProfilePicture(completion: (result: Bool, image: UIImage?) -> Void){
// Get user profile pic
let url = NSURL(string: "https://graph.facebook.com/1234567890/picture?type=large")
let urlRequest = NSURLRequest(URL: url!)
//Asynchronous request to display image
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) { (response:NSURLResponse!, data:NSData!, error:NSError!) -> Void in
if error != nil{
println("Error: \(error)")
}
// Display the image
let image = UIImage(data: data)
if(image != nil){
completion(result: true, image: image)
}
}
}
override func viewDidLoad() {
self.getProfilePicture { (result, image) -> Void in
if(result == true){
println("Loading Photo")
self.creatorImages.append(image!)
}
else{
println("False")
}
}
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
headerView.headerImage.image = self.creatorImages[section]
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
As seen by the program above, I the global array that I created called self.creatorImages which holds the array of images I grab from the Facebook API is always empty and I need to "wait" for all the pictures to populate the array before actually using it. I'm not sure how to accomplish this because I did try a completion handler in my getProfilePicture function but that didn't seem to help and that is one way I have learned to deal with asynchronous functions. Any other ideas? Thanks!
I had the same problem but mine was in Objective-C
Well, the structure is not that different, what i did was adding condition with:
headerView.headerImage.image
Here's an improved solution that i think suits your implementation..
since you placed self.getProfilePicture inside viewDidLoad it will only be called once section==0 will only contain an image,
the code below will request for addition image if self.creatorImages's index is out of range/bounds
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
if (section < self.creatorImages.count) // validate self.creatorImages index to prevent 'Array index out of range' error
{
if (headerView.headerImage.image == nil) // prevents the blinks
{
headerView.headerImage.image = self.creatorImages[section];
}
}
else // requests for additional image at section
{
// this will be called more than expected because of tableView.reloadData()
println("Loading Photo")
self.getProfilePicture { (result, image) -> Void in
if(result == true) {
//simply appending will do the work but i suggest something like:
if (self.creatorImages.count <= section)
{
self.creatorImages.append(image!)
tableView.reloadData()
println("self.creatorImages.count \(self.creatorImages.count)")
}
//that will prevent appending excessively to data source
}
else{
println("Error loading image")
}
}
}
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
You sure have different implementation from what i have in mind, but codes in edit history is not in vain, right?.. hahahaha.. ;)
Hope i've helped you.. Cheers!
i am trying to load set of images from the server and updating UI with returned images and displaying it by fade animation. it will be repeated forever.
Code snippet:
override func viewDidLoad(animated: Bool) {
super.viewDidLoad(animated)
self.loadImages()
}
func loadImages(){
var urlImage:UIImage?
//self.array_images contains preloaded images (not UIImage) objects which i get from different api call.
if imageCount >= self.array_images!.count{
imageCount = 0
}
var img = self.array_images[imageCount]
var url = NSURL(string: img.url!)
var data = NSData(contentsOfURL: url!)
if (data != nil){
urlImage = UIImage(data: data!)
UIView.transitionWithView(self.imageView_Promotion, duration: 1.2, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
println("Begin Animation")
self.imageView_Promotion.image = realImage
}, completion:{ finished in
println("Completed")
self.imageCount++
self.loadImages() //another issue: it calls the function more than couple of times
})
}
}else{
var image:UIImage = UIImage(named: "test_Image.jpg")!
imageView_Promotion.image = image
}
The above one hangs the UI. i have tried to call loadImages in dispatch_async and animation in dispatch_main queue but the issue still persist.
let priority = DISPATCH_QUEUE_PRIORITY_BACKGROUND
dispatch_async(dispatch_get_global_queue(priority, 0)) {
self.loadImages()
}
dispatch_async(dispatch_get_main_queue(),{
//UIView animation
})
What is the proper way to handle this.
Thanks
Thread-safe problem
Basically, UIKit APIs are not thread-safe. It means UIKit APIs should be called only from the main thread (the main queue). But, actually, there are some exceptions. For instance, UIImage +data: is thread-safe API.
Your code includes some unsafe calls.
UIView.transitionWithView(self.imageView_Promotion, duration: 1.2, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
I don't think UIView.transitionWithView is thread-safe.
var image:UIImage = UIImage(named: "test_Image.jpg")!
UIImage +named: is not thread-safe, it uses a global cache or so on. According to UIImage init(named:) Discussion.
You can not assume that this method is thread safe.
Block the main thread
If the thread-safe problem was fixed, it would block the main thread.
Your code calls loadImages method, that downloads an image from the server or loads an image from the file system, from the completion block of UIView.transitionWithView. UIView.transitionWithView should be called from the main thread, so the completion block also will be called from the main thread. It means downloading or loading an image on the main thread. It blocks the main thread.
Example code
Thus, loadImages should be like the following.
Swift Playground code:
import UIKit
import XCPlayground
func loadImage(file:String) -> UIImage {
let path = NSBundle.mainBundle().pathForResource(file, ofType: "")
let data = NSData(contentsOfFile: path!)
let image = UIImage(data: data!)
return image!
}
var index = 0
let files = ["image0.png", "image1.png", "image2.png"]
let placementImage = loadImage(files[0])
let view = UIImageView(image:placementImage)
// loadImages function
func loadImages() {
let priority = DISPATCH_QUEUE_PRIORITY_BACKGROUND
dispatch_async(dispatch_get_global_queue(priority, 0)) {
/*
* This block will be invoked on the background thread
* Load an image on the background thread
* Don't use non-background-thread-safe APIs like UIImage +named:
*/
if index >= files.count {
index = 0
}
var file = files[index++]
let image = loadImage(file)
dispatch_async(dispatch_get_main_queue()) {
/*
* This block will be invoked on the main thread
* It is safe to call any UIKit APIs
*/
UIView.transitionWithView(view, duration: 1.2, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
/*
* This block will be invoked on the main thread
* It is safe to call any UIKit APIs
*/
view.image = image
}, completion:{ finished in
/*
* This block will be invoked on the main thread
* It is safe to call any UIKit APIs
*/
loadImages()
})
}
}
}
// call loadImages() from the main thread
XCPShowView("image", view)
loadImages()
XCPSetExecutionShouldContinueIndefinitely()
Your UI freezes as the comletion block for transitionWithView is called on the main thread (a.k.a. UI thread)- that's basically how you end up running "loadImages()" on the main thread. Then, when the load method is being called on the main thread, you create an instance of NSData and initialize it with contents of a URL - this is being done synchronously, therefore your UI is freezing. For what it's worth, just wrap the loadImages() call (the one which is inside the completion block) into a dispatch_async call to a background queue and the freeze should be gone.
P.S. I would recommend queuing calls of loadImages() instead of relying on the completion blocks of UI animations.
You have to separate loadImages & update View into 2 GCD thread. Something like that:
override func viewDidLoad(animated: Bool) {
super.viewDidLoad(animated)
dispatch_async(dispatch_queue_create("com.company.queue.downloadImages", nil {
() -> Void in
self.loadImages()
}))
}
/**
Call this function in a GCD queue
*/
func loadImages() {
... first, download image
... you have to add stop condition cause I see that self.loadImages is called recursively in 'completion' clause below
... then, update UIView if data != nil
if (data != nil) {
dispatch_async(dispatch_get_main_queue(),nil, {
() -> Void in
UIView.transitionWithView.... {
}, completion: { finished in
dispatch_async(dispatch_queue_create("com.company.queue.downloadImages", nil, {
() -> Void in
self.imageCount++
self.loadImages()
}))
})
})
}
}