I am using parse to store and retrieve some data, which I then load into a UITableview, each cell contains some text and image, however when I open my tableview, any cells in the view do not show images until I scroll them out of view and back into view (I guess this is calling cellForRowAtIndexPath). Is there a way to check when all images are downloaded and then reload the tableview?
func loadData(){
self.data.removeAllObjects()
var query = PFQuery(className:"Tanks")
query.orderByAscending("createdAt")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
// The find succeeded.
for object in objects {
self.data.addObject(object)
}
} else {
// Log details of the failure
NSLog("Error: %# %#", error, error.userInfo!)
}
self.tableView.reloadData()
}
}
override func tableView(tableView: UITableView?, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell {
self.cell = tableView!.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath!) as TankTableViewCell // let cell:TankTableViewCell
let item:PFObject = self.data.objectAtIndex(indexPath!.row) as PFObject
self.cell.productName.alpha = 1.0
self.cell.companyName.alpha = 1.0
self.cell.reviewTv.alpha = 1.0
self.rating = item.objectForKey("rating") as NSNumber
cell.productName.text = item.objectForKey("prodName") as? String
cell.companyName.text = item.objectForKey("compName") as? String
self.cell.reviewTv.text = item.objectForKey("review") as? String
let userImageFile = item.objectForKey("image") as PFFile
userImageFile.getDataInBackgroundWithBlock({
(imageData: NSData!, error: NSError!) -> Void in
if (error == nil) {
let image = UIImage(data:imageData)
self.cell.productImage.image = image
}
}, progressBlock: {
(percentDone: CInt) -> Void in
if percentDone == 100{
}
})
self.setStars(self.rating)
// Configure the cell...
UIView.animateWithDuration(0.5, animations: {
self.cell.productName.alpha = 1.0
self.cell.companyName.alpha = 1.0
self.cell.reviewTv.alpha = 1.0
self.cell.reviewTv.scrollRangeToVisible(0)
})
return cell
}
The problem is that you use self.cell and that you change that reference each time a cell is returned. So, when the images are loaded they are all set into the last cell to be returned, which probably isn't on screen (or at least not fully).
Really you should be capturing the cell in the completion block of the image download (and checking that the cell is still linked to the same index path).
Also, you should cache the downloaded images so you don't always download the data / recreate the image.
You could set up a delegate method in your UITableViewController that gets called by another controller class that fetches the images. I doubt that's what you want to do though.
What you should do is initialize the cells with a default image, and have the cell controller itself go and fetch the image in the background, and update its UIImageView when the fetch completes. You definitely don't want to wait around for all images to load before reloading the table because a.) that takes a long time, and b.) what if one fails or times out?
Once the cell has loaded its image, if it is swapped out by the recycler and swapped back in, you can simply get the cached image by calling getData instead of getDataInBackground as long as isDataAvailable is true.
After your line:
self.cell.productImage.image = image
Try Adding:
cell.layoutSubviews() or self.cell.layoutSubviews()
It should render the subview, or your image in this case, on the first table view.
Related
I have added UITableView into UIScrollView, I have created an IBOutlet for height constraint of UITableView which helps me in setting the content size of UITableview.
I have 3 tabs and I switch tabs to reload data with different data source . Also the i have different custom cells when the tab changes.
So when the tab changes I call reloadData
here is my cellForRow function
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Configure the cell...
var cell:UITableViewCell!
let event:Event!
if(tableView == self.dataTableView)
{
let eventCell:EventTableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier, forIndexPath: indexPath) as! EventTableViewCell
eventCell.delegate = self
event = sectionsArray[indexPath.section].EventItems[indexPath.row]
eventCell.eventTitleLabel?.text = "\(event.title)"
eventCell.eventImageView?.image = UIImage(named: "def.png")
if let img = imageCache[event.imgUrl] {
eventCell.eventImageView?.image = img
}
else {
print("calling image of \(indexPath.row) \(event.imgUrl)")
// let escapedString = event.imgUrl.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
let session = NSURLSession.sharedSession()
do {
let encodedImageUrl = CommonEHUtils.urlEncodeString(event.imgUrl)
let urlObj = NSURL(string:encodedImageUrl)
if urlObj != nil {
let task = session.dataTaskWithURL(urlObj!, completionHandler: { ( data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
guard let realResponse = response as? NSHTTPURLResponse where
realResponse.statusCode == 200 else {
print("Not a 200 response, url = " + event.imgUrl)
return
}
if error == nil {
// Convert the downloaded data in to a UIImage object
let image = UIImage(data: data!)
// Store the image in to our cache
self.imageCache[event.imgUrl] = image
// Update the cell
dispatch_async(dispatch_get_main_queue(), {
if let cellToUpdate:EventTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as? EventTableViewCell {
cellToUpdate.eventImageView?.image = image
}
})
}
})
task.resume()
}
} catch {
print("Cant fetch image \(event.imgUrl)")
}
}
cell = eventCell
}
else if(secodTabClicked)
{
let Cell2:cell2TableViewCell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier1, forIndexPath: indexPath) as! cell2TableViewCell
//Image loading again takes place here
cell = Cell2
}
else if(thirdTabClicked)
{
let Cell3:cell3TableViewCell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier2, forIndexPath: indexPath) as! cell3TableViewCell
//Image loading again takes place here
cell = Cell3
}
return cell
}
As you can see each tab has different custom cells with images.
Below are the problems I am facing
1) it takes time to reload data when I switch tabs and their is considerable lag time. On iphone 4s it is worse
2) When I open this page, first tab is selected by default, so when i scroll, everything works smoothly. But when i switch tabs, and when i scroll again after reloading of data, the scroll becomes jerky and immediately i get memory warning issue.
What I did so far?
1) I commented the image fetching code and checked whether that is causing jerky scroll, but its not.
2) I used time profiler, to check what is taking more time, and it points the "dequeueReusableCellWithIdentifier". So I dont know what is going wrong here.
Your code does not look "symmetric" with respect to cell set up when secodTabClicked and thirdTabClicked. I do not see firstTabClicked, and it looks to me that the condition that you are using to determine which tab is clicked overlaps with secodTabClicked and thirdTabClicked. In other words, you are probably getting into the top branch, and return EventTableViewCell when cell2TableViewCell or cell3TableViewCell are expected.
Refactoring your code to make type selection "symmetric" with respect to all three cell types should fix this problem.
Another solution could be making separate data sources for different tabs, and switching the data source instead of setting xyzTabClicked flags. You would end up with thee small functions in place of one big function, which should make your code easier to manage.
I have a pretty elaborate problem and I think someone with extensive async knowledge may be able to help me.
I have a collectionView that is populated with "Picture" objects. These objects are created from a custom class and then again, these objects are populated with data fetched from Parse (from PFObject).
First, query Parse
func queryParseForPictures() {
query.findObjectsInBackgroundWithBlock { (objects: [PFObject]?, err: NSError?) -> Void in
if err == nil {
print("Success!")
for object in objects! {
let picture = Picture(hashtag: "", views: 0, image: UIImage(named: "default")!)
picture.updatePictureWithParse(object)
self.pictures.insert(picture, atIndex: 0)
}
dispatch_async(dispatch_get_main_queue()) { [unowned self] in
self.filtered = self.pictures
self.sortByViews()
self.collectionView.reloadData()
}
}
}
}
Now I also get a PFFile inside the PFObject, but seeing as turning that PFFile into NSData is also an async call (sync would block the whole thing..), I can't figure out how to load it properly. The function "picture.updatePictureWithParse(PFObject)" updates everything else except for the UIImage, because the other values are basic Strings etc. If I would also get the NSData from PFFile within this function, the "collectionView.reloadData()" would fire off before the pictures have been loaded and I will end up with a bunch of pictures without images. Unless I force reload after or whatever. So, I store the PFFile in the object for future use within the updatePictureWithParse. Here's the super simple function from inside the Picture class:
func updateViewsInParse() {
let query = PFQuery(className: Constants.ParsePictureClassName)
query.getObjectInBackgroundWithId(parseObjectID) { (object: PFObject?, err: NSError?) -> Void in
if err == nil {
if let object = object as PFObject? {
object.incrementKey("views")
object.saveInBackground()
}
} else {
print(err?.description)
}
}
}
To get the images in semi-decently I have implemented the loading of the images within the cellForItemAtIndexPath, but this is horrible. It's fine for the first 10 or whatever, but as I scroll down the view it lags a lot as it has to fetch the next cells from Parse. See my implementation below:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(Constants.PictureCellIdentifier, forIndexPath: indexPath) as! PictureCell
cell.picture = filtered[indexPath.item]
// see if image already loaded
if !cell.picture.loaded {
cell.loadImage()
}
cell.hashtagLabel.text = "#\(cell.picture.hashtag)"
cell.viewsLabel.text = "\(cell.picture.views) views"
cell.image.image = cell.picture.image
return cell
}
And the actual fetch is inside the cell:
func loadImage() {
if let imageFile = picture.imageData as PFFile? {
image.alpha = 0
imageFile.getDataInBackgroundWithBlock { [unowned self] (imageData: NSData?, err: NSError?) -> Void in
if err == nil {
self.picture.loaded = true
if let imageData = imageData {
let image = UIImage(data: imageData)
self.picture.image = image
dispatch_async(dispatch_get_main_queue()) {
UIView.animateWithDuration(0.35) {
self.image.image = self.picture.image
self.image.alpha = 1
self.layoutIfNeeded()
}
}
}
}
}
}
}
I hope you get a feel of my problem. Having the image fetch inside the cell dequeue thing is pretty gross. Also, if these few snippets doesn't give the full picture, see this github link for the project:
https://github.com/tedcurrent/Anonimg
Thanks all!
/T
Probably a bit late but when loading PFImageView's from the database in a UICollectionView I found this method to be much more efficient, although I'm not entirely sure why. I hope it helps. Use in your cellForItemAtIndexPath in place of your cell.loadImage() function.
if let value = filtered[indexPath.row]["imageColumn"] as? PFFile {
if value.isDataAvailable {
cell.cellImage.file = value //assign the file to the imageView file property
cell.cellImage.loadInBackground() //loads and does the PFFile to PFImageView conversion for you
}
}
I am using parse to retrieve my images and labels and display it on a collection view. The problem was that the collection view loads all the images and labels at once making the load time long and memory usage was high. I was thinking that I would load 10 cells each time however I was recommended to use SDWebImage to make the app lighter. However I don't know how to implement it with parse using swift. I am suspecting that I would put some code in this piece of code below
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("newview", forIndexPath: indexPath) as! NewCollectionViewCell
let item = self.votes[indexPath.row]
let gesture = UITapGestureRecognizer(target: self, action: Selector("onDoubleTap:"))
gesture.numberOfTapsRequired = 2
cell.addGestureRecognizer(gesture)
// Display "initial" flag image
var initialThumbnail = UIImage(named: "question")
cell.postsImageView.image = initialThumbnail
// Display the country name
if let user = item["uploader"] as? PFUser{
item.fetchIfNeeded()
cell.userName!.text = user.username
var profileImgFile = user["profilePicture"] as! PFFile
cell.profileImageView.file = profileImgFile
cell.profileImageView.loadInBackground { image, error in
if error == nil {
cell.profileImageView.image = image
}
}
var sexInt = user["sex"] as! Int
var sex: NSString!
if sexInt == 0 {
sex = "M"
}else if sexInt == 1{
sex = "F"
}
var height = user["height"] as! Int
cell.heightSexLabel.text = "\(sex) \(height)cm"
}
if let votesValue = item["votes"] as? Int
{
cell.votesLabel?.text = "\(votesValue)"
}
// Fetch final flag image - if it exists
if let value = item["imageFile"] as? PFFile {
println("Value \(value)")
cell.postsImageView.file = value
cell.postsImageView.loadInBackground({ (image: UIImage?, error: NSError?) -> Void in
if error != nil {
cell.postsImageView.image = image
}
})
}
return cell
}
I have implemented SDWebImage using Pods and have imported through the Bridging Header. Is there anyone who knows how to implement SDWebImage with parse using Swift?
You should rethink your approach -
I believe you are using collectionViewDelegate method - collectionView(_:cellForItemAtIndexPath:)
this fires every time the collection view needs a view to handle.
In there you can access the cell imageView and set its image (For Example)-
cell.imageView.sd_setImageWithURL(url, placeholderImage:placeHolderImage, completed: { (image, error, cacheType, url) -> Void in })
And if you wish to fade in the image nicely, you could -
cell.imageView.sd_setImageWithURL(url, placeholderImage:placeHolderImage, completed: { (image, error, cacheType, url) -> Void in
if (cacheType == SDImageCacheType.None && image != nil) {
imageView.alpha = 0;
UIView.animateWithDuration(0.0, animations: { () -> Void in
imageView.alpha = 1
})
} else {
imageView.alpha = 1;
}
})
EDIT
I see the you use Parse, so you don't need SDWebImage, you need to use Parse - PFImageView, It will handle your background fetch for the image when it loads. You will need to save reference to your PFObject, but I believe you already do that.
For example (inside your cellForItemAtIndexPath)-
imageView.image = [UIImage imageNamed:#"..."]; // placeholder image
imageView.file = (PFFile *)someObject[#"picture"]; // remote image
[imageView loadInBackground];
How many objects are displaying in the collection view?
Since you mentioned SDWebImage, are you downloading the images in the background as well?
If you want to load the images as the user scrolls, have a look at the documentation for SDWebImage. The first use case describes how to display images in table view cells withouth blocking the main thread. The implementation for collection view cells should be similar.
I'm implementing an activity indicator to show while the image is fetched/loads. However, the activity indicator sometimes shows up twice in the same frame.
I checked the code numerous times and even tried other methods such as a counter matching the row number. Any idea why this is showing up twice? (see image below)
Activity Indicator Code (inside cellForRowAtIndexPath):
// start indicator when loading images
var indicatorPhoto: MaterialActivityIndicatorView! = MaterialActivityIndicatorView(style: .Small)
indicatorPhoto.center = cell.mainRestaurantImageView.center
cell.mainRestaurantImageView.addSubview(indicatorPhoto)
indicatorPhoto!.startAnimating()
cell.mainRestaurantImageView.loadInBackground {
(success: UIImage!, error: NSError!) -> Void in
if ((success) != nil) {
// stop indicator when loading images
if indicatorPhoto?.isAnimating == true {
indicatorPhoto!.stopAnimating()
indicatorPhoto!.removeFromSuperview()
}
} else {
println("Unsuccessful Fetch Image")
if indicatorPhoto?.isAnimating == true {
indicatorPhoto!.stopAnimating()
indicatorPhoto!.removeFromSuperview()
}
}
}
Update:
This is the rest of the cellForRowAtIndexPath code
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("RestaurantCell", forIndexPath: indexPath) as FeedCell
cell.nameLabel.text = restaurantNames[indexPath.row]
cell.photoNameLabel.text = photoNames[indexPath.row]
cell.cityLabel.text = " " + addressCity[indexPath.row]
cell.distanceLabel?.text = arrayRoundedDistances[indexPath.row] + "mi"
// check if there are images
if foodPhotoObjects.isEmpty { } else {
var restaurantArrayData = self.foodPhotoObjects[indexPath.row] as PFObject
cell.mainRestaurantImageView.image = UIImage(named: "") // set placeholder
cell.mainRestaurantImageView.file = restaurantArrayData["SmPhotoUploaded"] as PFFile
// start indicator when loading images
var indicatorPhoto: MaterialActivityIndicatorView! = MaterialActivityIndicatorView(style: .Small)
indicatorPhoto.center = cell.mainRestaurantImageView.center
cell.mainRestaurantImageView.addSubview(indicatorPhoto)
indicatorPhoto!.startAnimating()
cell.mainRestaurantImageView.loadInBackground {
(success: UIImage!, error: NSError!) -> Void in
if ((success) != nil) {
// stop indicator when loading images
if indicatorPhoto?.isAnimating == true {
indicatorPhoto!.stopAnimating()
indicatorPhoto!.removeFromSuperview()
}
} else {
println("Unsuccessful Fetch Image")
if indicatorPhoto?.isAnimating == true {
indicatorPhoto!.stopAnimating()
indicatorPhoto!.removeFromSuperview()
}
}
}
cell.mainRestaurantImageView.contentMode = .ScaleAspectFill
cell.mainRestaurantImageView.clipsToBounds = true
}
return cell
}
Update 2
// create indicator when loading images
var indicatorPhoto : MaterialActivityIndicatorView? = cell.mainRestaurantImageView.viewWithTag(123) as? MaterialActivityIndicatorView;
if indicatorPhoto == nil{
indicatorPhoto = MaterialActivityIndicatorView(style: .Small)
indicatorPhoto!.center = cell.mainRestaurantImageView.center
indicatorPhoto!.tag = 123
cell.mainRestaurantImageView.addSubview(indicatorPhoto!)
indicatorPhoto!.startAnimating()
}
This is showing multiple times because you're adding it multiple times. In fact every time when the else case of foodPhotoObjects.isEmpty is called.
This is because, the first line of your method:
let cell = tableView.dequeueReusableCellWithIdentifier("RestaurantCell", forIndexPath: indexPath) as FeedCell
dequeues the cell from table view. The dequeue works as follow:
It maintains a queue based on the identifier.
If there's no cell in the queue, it creates a new cell.
If there's already a cell, it returns that cell to you. Which will be re-used.
So what you're doing is, you're adding MaterialActivityIndicatorView every time to the cell, whether it was added previously or not.
Solution:
Add a custom view to your cell from xib, and set its class to
MaterialActivityIndicatorView. And get the reference here to
hide/show and animation.
Check the sub-views of cell.mainRestaurantImageView and see if
there's already a MaterialActivityIndicatorView, get its reference
and do animation and stuff. If there's no subview as MaterialActivityIndicatorView, create one and add it to the image view as subview. You'll use the tag property for this.
The second approach can be done something like this:
//first find the activity indication with tag 123, if its found, cast it to its proper class
var indicatorPhoto : MaterialActivityIndicatorView? = cell.mainRestaurantImageView.viewWithTag(123) as? MaterialActivityIndicatorView;
if indicatorPhoto == nil{
//seems it wasn't found as subview initialize here and add to mainRestaurantImageView with tag 123
}
//do rest of the stuff.
I'm having a rather common issue, but the solutions so far have not given me the desired result.
I have a UITableView that I am populating with information that I have parsed from a JSON pulled from the web at run time. The JSON retrieval starts in func viewDidLoad() using the NSURLSession.dataTaskWithURL(NSURL, completionHandler) function. The cells of the table are then populated within the completionHandler.
The cells used in the table are custom, and all UI elements in the cell have default values set through the interface builder.
When the table appears it is completely empty, though it does have the proper cell height. The reason I found for this behavior is that the reloadData function isn't being called at the right time due to multithreading/multiprocessing and the suggested solution is to have
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
somewhere in the code to allow the reload to occur correctly. However, this only partially works. The table flickers with what looks like content, and then immediately goes blank again until I scroll. Once I scroll the reloadData function works as expected, but only after I scroll.
I've tried having the reloadData function in various locations (such as in viewWillAppear) with no luck. Are there any ideas or troubleshooting tips that I can try?
Edit #1 - Request for completion handler
var listTask = session.dataTaskWithURL(listUrl, completionHandler: {(data, response, error) in
if error == nil {
var json:NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: nil) as NSDictionary
var topGames = json["top"] as [NSDictionary]
var currGame:NSDictionary
var toAdd:Game
for var i=0; i < topGames.count; ++i {
currGame = topGames[i]["game"] as NSDictionary
toAdd = Game(
id: String(currGame["_id"] as CLong),
name: currGame["name"] as NSString,
boxArtImageUrl: (currGame["box"] as NSDictionary)["medium"] as NSString,
boxArtImage: nil,
isFetchingBoxArt: false,
totalViewers: topGames[i]["viewers"] as Int,
totalChannels: topGames[i]["channels"] as Int,
topChannel: "N/A")
self.games.append(toAdd)
}
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
}
}
Note that the self.games array is used in func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCel to fill in the information in the cells.
Edit #2 - Request for cellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:TopGameListCell = self.tableView.dequeueReusableCellWithIdentifier("topGameCell") as TopGameListCell
if games.count > 0 {
var game:Game = games[indexPath.row]
cell.nameLabel?.text = game.name
cell.totalViewersLabel.text = String(game.totalViewers)
cell.totalChannelsLabel.text = String(game.totalChannels)
cell.topChannelLabel.text = game.topChannel
if game.boxArtImage != nil {
cell.boxArtImageView.image = game.boxArtImage
} else {
if(!game.isFetchingBoxArt) {
cell.boxArtImageView.image = placeholderImage
gatherGameBoxArtImageForCell(game.boxArtImageUrl, indexPath: indexPath)
}
}
}
return cell;
}
I forgot about the extra fetch that I start within the gatherGameBoxArtImageForCell. Basically if I don't already have the image, download it. The if/else for the image seems to be causing the flicker to occur. If I let it sit long enough, it finally shows the table. The idea for me is that the placeholder image would show until the image is downloaded, and then reloadData is called after the image is downloaded. Here is the image fetch completionHandler:
var task = session.dataTaskWithURL(url, completionHandler: {(data, response, error) in
var game = self.games[indexPath.row]
if error == nil {
if(game.boxArtImage == nil) {
var cellForImage:TopGameListCell? = self.tableView.cellForRowAtIndexPath(indexPath) as? TopGameListCell
if cellForImage != nil {
self.games[indexPath.row].boxArtImage = UIImage(data: data)
cellForImage?.boxArtImageView.image = self.games[indexPath.row].boxArtImage
}
}
} else {
println(error)
}
game.isFetchingBoxArt = false
self.tableView.reloadData()
})
It appears that when I was trying to have the images load in the background it was hogging up the main thread. What I did was changed the following from
if(!game.isFetchingBoxArt) {
cell.boxArtImageView.image = placeholderImage
gatherGameBoxArtImageForCell(game.boxArtImageUrl, indexPath: indexPath)
}
to
if(!game.isFetchingBoxArt) {
cell.boxArtImageView.image = placeholderImage
dispatch_async(dispatch_get_main_queue(), {
self.gatherGameBoxArtImageForCell(game.boxArtImageUrl, indexPath: indexPath)
})
}
I decided to do this after thinking on how the normal solution is to place reloadData in a dispatch_async. Apparently the reasoning is the same for the issue I was having, so now the download of the images runs asynchronously.