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!
Related
I'm currently implementing an App which has a collectionView. The cells in the collection view are filled via an API-request. The requests are to the same API but with different locations. So for example for the first cell I'm calling: "google.de/GamesInItaly" and for the second cell: "google/GamesInGermany". While I'm scrolling down the collectionView the appearing cells should also make an API-request that has different countries.
The Problem I'm facing is, that I don't know how to handle the different API calls.
I managed to make one API-request and fill every cell with the same data, but I want different data for every cell.
My code looks like this:
CollectionView:
Call with from collectionView:
func fetchMatch() {
ApiService.sharedInstance.fetchGames(from: contentURL, completion: { (content: [[contentModel]]) in
self.content = content
self.collectionView.reloadData()
})
}
API-Services:
Set the Url:
func fetchGames(from url: String, completion: #escaping ([contentModel]) -> ()) {
fetchFeedForUrlString(urlString: "\(BaseUrl)/getgames/\(url)", completion: completion)
}
Fetch function:
func fetchFeedForUrlString<T: Decodable>(urlString: String, completion: #escaping (T) -> ())
{
let url = URL(string: urlString)
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
guard let data = data else {
return
}
do {
let json = try JSONDecoder().decode(T.self, from: data)
DispatchQueue.main.async {
completion(json)
}
} catch let jsonError {
print(jsonError)
}
}
task.resume()
}
I think what I could do is make many of these fetchGames functions, but I think that would be a bad practice.
I've also read about DispatchGroup but I'm not really sure how I can implement these into my program.
Does somebody know how I can handle multiple API-calls in a good practice?
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 am wondering if there is any way to keep reloading a tableView as items are being downloaded.
There are images and info associated with those images. When the user first opens the app, the app begins downloading images and data using HTTP. The user can only see downloaded items in the tableView as they're being downloaded if he/she keeps leaving the viewController and coming back to it.
I have tried doing something like this:
while downloading {
tableView.reloadData()
}
, however, this uses too much memory and it crashes the app.
How can I asynchronously populate a tableView with images and data as they are being downloaded while still remaining in the tableViewController?
P.S. If you're interested in which libraries or APIs I'm using, I use Alamofire to download and Realm for data persistence.
The correct and usual way to do this is reload data into table, than delegate the single cell to load asyncronously the image from a link.
In swift, you can extend UIImage
extension UIImageView {
func imageFromUrl(urlString: String) {
if let url = NSURL(string: urlString) {
let request = NSURLRequest(URL: url)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {
(response: NSURLResponse?, data: NSData?, error: NSError?) -> Void in
if let imageData = data as NSData? {
self.image = UIImage(data: imageData)
}
}
}
}
}
And in your CellForRowAtIndexPath load the image from link using something like this
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
...
cell.image.imageFromUrl(dataArray[indexPath.row].imageUrl)
return cell
}
I have a UITable-View with a List of Users and their Profile Pictures.
I am loading the pictures (http://api/pictures/{userid}) one by one for each player asynchronous:
func loadImageAsync(imageUrl: URL, completionHandler handler: #escaping (_ success: Bool, _ image: UIImage?) -> Void){
DispatchQueue.global(qos: .userInitiated).async { () -> Void in
if let imgData = try? Data(contentsOf: imageUrl), let img = UIImage(data: imgData) {
DispatchQueue.main.async(execute: { () -> Void in
handler(true, img)
})
} else {
handler(false, nil)
}
}
In the completion handler in the cellForRowAt-Index-Fuction, I am setting the pictures.
loadImageAsync(imageUrl: imageUrl!, label: ip) { (success, image, backlabel) -> Void in
if(success){
cell.profilePictureView.image = image
}
}
However, when I scroll very fast, some pictures get loaded in the wrong cells.
To prevent reuse-issues, I am "resetting" the image view after every reuse:
override func prepareForReuse() {
profilePictureView.image = UIImage(named: "defaultProfilePicture")
}
But why are still some images loaded false when scrolling fastly?
hmmm, this is what I thought too.
__Update:
So, I extended the function with a Label Parameter (type Any), that is returned back as it was put in the function. I tried to compare the parameter (is used the indexpath) with the current indexpath. Actually, this should work - shouldn't it?!
loadImageAsync(imageUrl: imageUrl!, label: ip) { (success, image, backlabel) -> Void in
cell.loader.stopAnimating()
if (backlabel as! IndexPath == indexPath) {
//set image...
But however, it doesn't show any effect. Do you know why or have any other solutions to fix this?
The issue is that if you scroll fast, the download may take long enough that by the time it's complete, the cell in question has scrolled off the screen and been recycled for a different indexPath in your data model.
The trick is to ask the table view for the cell at that indexPath in the completion block and only install the image if you get a cell back:
loadImageAsync(imageUrl: imageUrl!, label: ip, for indexPath: IndexPath) { (success, image, backlabel) -> Void in
if(success){
let targetCell = tableview.cell(for: indexPath)
targetCell.profilePictureView.image = image
}
}
EDIT:
Redefine your loadImageAsync function like this:
func loadImageAsync(imageUrl: URL,
indexPath: IndexPath,
completionHandler handler: #escaping (_ success: Bool,
_ image: UIImage?,
_ indexPath: IndexPath ) -> Void) { ... }
EDIT #2
And by the way, you should really save your images to disk and load them from there rather than loading from the internet each time. I suggest using a hash of the image URL as a filename.
Modify loadImageAsync as follows:
Check to see if the file already exists on disk. If so, load it and return it.
If the file does not exist, do the async load, and then save it to disk using the hash of the URL as a filename, before returning the in-memory image.
Because your completionHandler can be called after the cell has been reused for the next user, and possibly another image request for the cell has been fired. The order of events (reuse/completion) is not predictable, and in fact a later async request could complete before an earlier one.
I have a simple iOS application written in Swift that loads JSON data downloaded over the Internet and displays it into a UITableView. When I launch the application for the first time, everything works correctly and the table displays the correct information.
In my app, I have a button that triggers a refresh when I would like to reload the data. So I call the displayTable() method when the refresh button is tapped which downloads the data again and then calls the tableView.reloadData() method on the main thread to refresh the data but the reload does not appear to work. I manually change the data on the server and hit refresh but I still have the old data cached in my application. I can exit/kill the application and the old data stays 'stuck' forever unless I delete the app and reinstall.
I've attached the code for my ViewController below. I've spent way too many hours looking at this and I cannot seem to find why the reloadData doesn't work. Any ideas, hints would be greatly appreciated.
thanks
--Vinny
class ViewController: UITableViewController {
var tableData = []
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
println("viewWillAppear was just called")
super.viewWillAppear(true)
self.displayTable()
}
func displayTable() {
getJSONData("http://www.example.com/data.json") { (results, resultError) in
if (resultError == nil) {
if var results = results {
dispatch_async(dispatch_get_main_queue(), {
self.tableData = [] //create out data on subsequent refreshes
self.tableData = results
self.tableView.reloadData() //This doesn't appear to be working!
})
} else {
self.displayErrorPopup()
}
} else {
self.displayErrorPopup()
}
}
}
func displayErrorPopup() {
let alertViewController = UIAlertController(title: "Error", message: "Couldn't connect to API", preferredStyle: .Alert)
let okButton = UIAlertAction(title: "OK", style: .Default, handler: nil)
let cancelButton = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
alertViewController.addAction(okButton)
alertViewController.addAction(cancelButton)
self.presentViewController(alertViewController, animated: true, completion: nil)
}
//todo - extract this method into it's own class
func getJSONData(ttAPIURL : String, completion: (resultsArray: NSArray?, resultError: NSError?) -> ()){
let mySession = NSURLSession.sharedSession()
let url: NSURL = NSURL(string: ttAPIURL)!
let networkTask = mySession.dataTaskWithURL(url, completionHandler : {data, response, error -> Void in
var err: NSError?
if (error == nil) {
var theJSON = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as NSMutableDictionary
let results : NSArray = theJSON["list"]!["times"] as NSArray
completion(resultsArray: results, resultError: error)
} else {
completion(resultsArray: nil, resultError: error)
}
})
networkTask.resume()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ttCell", forIndexPath: indexPath) as UITableViewCell
let timesEntry = self.tableData[indexPath.row] as NSMutableDictionary
cell.textLabel.text = (timesEntry["routeName"] as String)
return cell
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
#IBAction func refreshButtonTap(sender: AnyObject) {
self.displayTable()
}
}
Ok, so you say that reloadData is not working but that is almost certainly not your problem. To debug this you should:
Make sure reloadData is actually being called (breakpoint or println)
Check if the UITableViewDataSource methods are being called after you call reloadData (breakpoint or println)
Check the results array that you are getting back from the network request (probably println(results))
My guess is that it is failing at number 3 which means it has nothing to do with reloadData. Perhaps the shared NSURLSession has caching enabled? Try setting mySession.URLCache to nil.
Try to replace it
override func viewWillAppear(animated: Bool) {
println("viewWillAppear was just called")
super.viewWillAppear(true)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { () -> Void in
self.displayTable()
})
}
Thanks drewag and Jirom for your answers. Great comments that were very helpful. I just found my problem and its something I should have thought about earlier. The domain which is hosting my dynamic JSON data is being protected by CloudFlare and it appears that my pages rules to disable caching for a certain directory aren't working. So all of the JSON content was being cached and that was the reason my application wouldn't show the new data.
Thanks again guys for your responses.
--Vinny
PS: Per Drewag's suggestion, I added an instance of NSURLSessionConfiguration which defines the behavior and policies to use when downloading data using an NSURLSession object, and set the URLCache property to nil to disable caching.
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
config.URLCache = nil
let mySession = NSURLSession(configuration: config)