I have an array with all the images I got from the camera. They are saved in my Core Data. If a user takes a new photo, the photo will be saved in the core data and then I replace the normal array data with the Core Data images. So if I go back to the collection view, only the first and the last image is shown. When I print the images[indexpath.item] out, I got the images. But they aren't shown in the cells. I have a custom class for the imageView in the cell and the imageView has constraints, so the image can't be out of the visible area I think. I look forward to solve this problem with your help. Thank you.
Here is the code from camera catch :
func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage!, editingInfo: [NSObject : AnyObject]!) {
photoImage.image = image
print(images)
addPhotoLabel.removeFromSuperview()
if isEdit == false {
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let entinity = NSEntityDescription.entityForName("Image", inManagedObjectContext: context)
let newImage = Image(entity: entinity!, insertIntoManagedObjectContext: context)
newImage.image = UIImageJPEGRepresentation(image, 1)
do {
try context.save()
} catch let error as NSError {
print(error)
}
images.removeAll()
let request = NSFetchRequest()
request.entity = entinity
let fetched = try? context.executeFetchRequest(request)
if fetched != nil {
let imagesS = fetched!.map { UIImage(data: ($0 as! Image).image!)}
images.appendContentsOf(imagesS)
}
}
self.dismissViewControllerAnimated(true, completion: nil);
}
And the code where I create the cell:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//1
let cell : FlickrPhotoCell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! FlickrPhotoCell
if searchBarActive == true
{
cell.nameLabel.text = researchResults[indexPath.item]
cell.imageView.image = sFunc_imageFixOrientation(resultImages[indexPath.item])
}
else
{
cell.nameLabel.text = names[indexPath.item]
cell.imageView.image = images[indexPath.item]
print(images[indexPath.item])
}
cell.nameLabel.hidden = false
cell.frame.size = CGSizeMake(UIScreen.mainScreen().bounds.width/2 , UIScreen.mainScreen().bounds.width/2)
cell.imageView.frame = cell.frame
cell.backgroundColor = UIColor.clearColor()
cell.layer.borderColor = themeColor.CGColor
cell.layer.borderWidth = 4
cell.layer.cornerRadius = CGFloat(10.0)
cell.placeHolderName.frame.size = CGSize(width: cell.frame.width, height: cell.frame.height/4)
return cell
}
Ask me if you need more code. Thank you
This answer might be of some help: https://stackoverflow.com/a/14438875/2678994
Also, imo you may want to (in your Storyboard or programmatically) setup a UIImageView within the UICollectionViewCell, then update that image view's .image value.
Related
I have a UICollectionView which displays images in a grid but as I scroll rapidly it displays the wrong image in the cell momentarily until the image is downloaded from the S3 storage and then the correct image is displayed.
I have seen questions and answers relating to this problem on SO before but none of the solutions are working for me. The dictionary let items = [[String: Any]]() is filled after an API call. I need the cell to discard the image from the recycled cell. Right now there is an unpleasant image "dancing" effect.
Here is my code:
var searchResults = [[String: Any]]()
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MyCollectionViewCell
let item = searchResults[indexPath.item]
cell.backgroundColor = .white
cell.itemLabel.text = item["title"] as? String
let imageUrl = item["img_url"] as! String
let url = URL(string: imageUrl)
let request = Request(url: url!)
cell.itemImageView.image = nil
Nuke.loadImage(with: request, into: cell.itemImageView)
return cell
}
class MyCollectionViewCell: UICollectionViewCell {
var itemImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 0
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .white
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
---------
}
override func prepareForReuse() {
super.prepareForReuse()
itemImageView.image = nil
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I changed my cell image loading to the following code:
cell.itemImageView.image = nil
APIManager.sharedInstance.myImageQuery(url: imageUrl) { (image) in
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCollectionViewCell
else { return }
cell.itemImageView.image = image
cell.activityIndicator.stopAnimating()
}
Here is my API manager.
struct APIManager {
static let sharedInstance = APIManager()
func myImageQuery(url: String, completionHandler: #escaping (UIImage?) -> ()) {
if let url = URL(string: url) {
Manager.shared.loadImage(with: url, token: nil) { // internal to Nuke
guard let image = $0.value as UIImage? else {
return completionHandler(nil)
}
completionHandler(image)
}
}
}
If the user scrolls past the content limit my collection view will load more items. This seems to be the root of the problem where cell reuse is reusing images. Other fields in the cell such as item title are also swapped as new items are loaded.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.contentOffset.y + view.frame.size.height) > (scrollView.contentSize.height * 0.8) {
loadItems()
}
}
Here is my data loading function
func loadItems() {
if loadingMoreItems {
return
}
if noMoreData {
return
}
if(!Utility.isConnectedToNetwork()){
return
}
loadingMoreItems = true
currentPage = currentPage + 1
LoadingOverlay.shared.showOverlay(view: self.view)
APIManager.sharedInstance.getItems(itemURL, page: currentPage) { (result, error) -> () in
if error != nil {
}
else {
self.parseData(jsonData: result!)
}
self.loadingMoreItems = false
LoadingOverlay.shared.hideOverlayView()
}
}
func parseData(jsonData: [String: Any]) {
guard let items = jsonData["items"] as? [[String: Any]] else {
return
}
if items.count == 0 {
noMoreData = true
return
}
for item in items {
searchResults.append(item)
}
for index in 0..<self.searchResults.count {
let url = URL(string: self.searchResults[index]["img_url"] as! String)
let request = Request(url: url!)
self.preHeatedImages.append(request)
}
self.preheater.startPreheating(with: self.preHeatedImages)
DispatchQueue.main.async {
// appendCollectionView(numberOfItems: items.count)
self.collectionView.reloadData()
self.collectionView.collectionViewLayout.invalidateLayout()
}
}
When the collection view scrolls a cell out of bounds, the collection view may reuse the cell to display a different item. This is why you get cells from a method named dequeueReusableCell(withReuseIdentifier:for:).
You need to make sure, when the image is ready, that the image view is still supposed to display that item's image.
I recommend you change your Nuke.loadImage(with:into:) method to take a closure instead of an image view:
struct Nuke {
static func loadImage(with request: URLRequest, body: #escaping (UIImage) -> ()) {
// ...
}
}
That way, in collectionView(_:cellForItemAt:), you can load the image like this:
Nuke.loadImage(with: request) { [weak collectionView] (image) in
guard let cell = collectionView?.cellForItem(at: indexPath) as? MyCollectionViewCell
else { return }
cell.itemImageView.image = image
}
If the collection view is no longer displaying the item in any cell, the image will be discarded. If the collection view is displaying the item in any cell (even in a different cell), you'll store the image in the correct image view.
Try this:
Whenever the cell is reused, its prepareForReuse method is called. You can reset your cell here. In your case, you can set a default image in the image view here till the original image is downloaded.
override func prepareForReuse()
{
super.prepareForReuse()
self.imageView.image = UIImage(named: "DefaultImage"
}
#discardableResult
public func loadImage(with url: URL,
options: ImageLoadingOptions = ImageLoadingOptions.shared,
into view: ImageDisplayingView,
progress: ImageTask.ProgressHandler? = nil,
completion: ImageTask.Completion? = nil) -> ImageTask? {
return loadImage(with: ImageRequest(url: url), options: options, into: view, progress: progress, completion: completion)
}
all Nuke API's return an ImageTask when requesting unless the image was in the cache. Hold reference to this ImageTask if there is one. In the prepareForReuse function. call ImageTask.cancel() in it and set the imageTask to nil.
From the Nuke project page:
Nuke.loadImage(with:into:) method cancels previous outstanding request associated with the target. Nuke holds a weak reference to a target, when the target is deallocated the associated request gets cancelled automatically.
And as far as I can tell you are using their example code correctly:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
cell.imageView.image = nil
Nuke.loadImage(with: url, into: cell.imageView)
...
}
You are explicitly setting the UIImageViews image to nil so there should actually be no way it could display an image until Nuke loads a new image into the target.
Also Nuke will cancel a previous request. I'm using Nuke myself and don't have these issues. Are you sure your "changed code" isn't the faulty piece here? For me everything seems to be correct and I don't see an issue why it shouldn't work.
One possible issue could be the reload of the whole collection view though. We should always try to provide the best user experience so if you alter the datasource of the collection view, try to use performBatchUpdates to animate the inserts/updates/moves/deletes.
A nice library which automatically can take care of this is Dwifft for example.
I have a picture and some labels inside my cells. If I have more cells than what can fit on the page, scrolling down then back up loads a different image momentarily then loads the original photo. I have read around StackOverflow to see what would work in my case, but so far I can't find anything since my UITableView is inside a ViewController.
Here is how I load my content into my cell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! PostTableViewCell
// Configure the cell...
let post = self.posts[indexPath.row] as! [String: AnyObject]
cell.titleLabel.text = post["title"] as? String
cell.priceLabel.text = post["price"] as? String
if let imageName = post["image"] as? String {
let imageRef = FIRStorage.storage().reference().child("images/\(imageName)")
imageRef.data(withMaxSize: 25 * 1024 * 1024, completion: { (data, error) -> Void in if error == nil {
let image = UIImage(data: data!)
UIView.animate(withDuration: 0.4, animations: {
cell.titleLabel.alpha = 1
cell.postImageView.alpha = 1
cell.priceLabel.alpha = 1
cell.postImageView.image = image
})
} else {
print("Error occured during image download: \(error?.localizedDescription)")
}
})
}
return cell
}
Is there any way I could change tableView.dequeueReusableCell to something different so this doesn't happen?
In your table view cell PostTableViewCell you need to implement the method
override func prepareForReuse() {
super.prepareForReuse()
self.postImageView.image = nil
// Set cell to initial state here, reset or set values
}
The cells are holding on to their old content
I think you run into problems because you update the cell inside the completion block. If the cell scrolls out of view before the completion block is run, it'll be reused for a different row, but you're still setting the image for the previous row.
Try this:
imageRef.data(withMaxSize: 25 * 1024 * 1024, completion: { (data, error) -> Void in if error == nil {
if let cell = self.tableView.cellForRow(at: indexPath) as? PostTableViewCell {
let image = UIImage(data: data!)
UIView.animate(withDuration: 0.4, animations: {
cell.titleLabel.alpha = 1
cell.postImageView.alpha = 1
cell.priceLabel.alpha = 1
cell.postImageView.image = image
}
})
Instead of relying on the cell still being visible, this will try to get it from the table view based on indexPath. If it's not visible any more, cellForRow(at:) will return nil.
I'm trying to show images from XML enclosure to tableViewCell image. Images are show but not in sequence, due to dequeueReusableCellWithIdentifier because when i scroll tableViewCell up and down it change images and not show in sequence according to array index. I've tried different ways but did't get success'
Can anyone please tell me how can show images in sequence, or is there any way that first download all images and then show in cell image??
Or any other quick or easy method instead using dispatch_async.
Thanks
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
downloadFileFromURL(NSURL(string: self.posts.objectAtIndex(indexPath.row).valueForKey("enclosure") as! String)!, completionHandler:{(img) in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.sideImageView.image = img
})
})
return cell
}
UPDATE
Now i tried this
let picURL = self.posts.objectAtIndex(indexPath.row).valueForKey("enclosure") as! String
let url = NSURL(string: picURL)
let data = NSData(contentsOfURL: url!)
cell.sideImageView?.image = UIImage(data: data!)
It show images in sequence but make scrolling hard?
Update2
Now i've tried this
var check = true
var imageArrayNsData : [NSData] = []
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
if check == true{
var indeX = 0
for i in posts.valueForKey("enclosure") as! [NSString]{
let picURL = self.posts.objectAtIndex(indeX).valueForKey("enclosure") as! String
let url = NSURL(string: picURL)
let data = NSData(contentsOfURL: url!)
print("download")
imageArrayNsData.append(data!)
indeX++
print(indeX)
}
check = false
}
if check == false{
cell.sideImageView.image = UIImage(data: imageArrayNsData[indexPath.row])
}
return cell
}
This method only download images one time. And after downloading images it appends in array and next time it show images from array without downloading again. But this method is little bit hard for scrolling. Any one have idea why?
The problem is that the cell object may have been already reused by the time you set the image. You need to add a check to make sure the cell still represents the content you want. That could be as simple as:
if tableView.indexPathForCell(cell) == indexPath {
cell.sideImageView.image = img
}
But might need to be more complex if the index path for a specific item might change in that time (for example, if the user can insert/delete rows).
You could also use a library like AlamofireImage which handles this work (in a different way) for you. With AlamofireImage, your code would look like:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
let URL = NSURL(string: self.posts.objectAtIndex(indexPath.row).valueForKey("enclosure") as! String)!
cell.sideImageView.af_setImageWithURL(URL)
return cell
}
To download asynchronously images and set to UIImageView of your UITableViewCell, you can add an extension to your UIImageView.
extension UIImageView {
func downloadImageFrom(link link:String, contentMode: UIViewContentMode) {
//in my methods, I have a cache to avoid re-downloading my images. Images in cache are identified by its URL
if let _imageData = ImageCache.shareCache.getImageData(link) {
self.image = UIImage(data: _imageData)
return
}
//else, download image
NSURLSession.sharedSession().dataTaskWithURL( NSURL(string:link)!, completionHandler: {
(data, response, error) -> Void in
dispatch_async(dispatch_get_main_queue()) {
self.contentMode = contentMode
if let data = data {
ImageCache.shareCache.cacheImageData(data, imageId: link)
self.image = UIImage(data: data)
}
}
}).resume()
}
}
then, from your call-back cellforrow,
let cell : ImageCell2 = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! ImageCell2
cell.titleLabel.text = posts.objectAtIndex(indexPath.row).valueForKey("title") as! NSString as String
cell.imageView.downloadImageFrom(yourImageUrl)
return cell
I am loading a tableview of images that are being fetched from a public CloudKit database as CKAssets. However, the images are loading out of order about two seconds until the correct image is loaded into the UIImageView of a custom UITableview cell. I know that the issue is that since the cell is reusable the image is still downloaded from CloudKit and displayed in any visible cell while a user is scrolling through the TableView before the correct image is shown in the image view. I am wondering if there is a fix to this in swift so that the image downloaded is only for that of a visible cell and not any previous cells.
Here is the code for cellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! PostsTableViewCell
cell.userInteractionEnabled = false
photoRecord = sharedRecords.fetchedRecords[indexPath.row]
cell.photoTitle.text = photoRecord.objectForKey("photoTitle") as? String
cell.photoImage.backgroundColor = UIColor.blackColor()
cell.photoImage.image = UIImage(named: "stock_image.png")
if let imageFileURL = imageCache.objectForKey(self.photoRecord.recordID) as? NSURL {
cell.photoImage.image = UIImage(data: NSData(contentsOfURL: imageFileURL)!)
cell.userInteractionEnabled = true
print("Image Cached: \(indexPath.row)")
} else {
let container = CKContainer.defaultContainer()
let publicDatabase = container.publicCloudDatabase
let fetchRecordsImageOperation = CKFetchRecordsOperation(recordIDs:[self.photoRecord.recordID])
fetchRecordsImageOperation.desiredKeys = ["photoImage"]
fetchRecordsImageOperation.queuePriority = .VeryHigh
fetchRecordsImageOperation.perRecordCompletionBlock = {(record:CKRecord?, recordID:CKRecordID?, error:NSError?) -> Void in
if let imageRecord = record {
NSOperationQueue.mainQueue().addOperationWithBlock() {
if let imageAsset = imageRecord.objectForKey("photoImage") as? CKAsset{
cell.photoImage.image = UIImage(data: NSData(contentsOfURL: imageAsset.fileURL)!)
self.imageCache.setObject(imageAsset.fileURL, forKey:self.photoRecord.recordID)
cell.userInteractionEnabled = true
}
}
}
}
publicDatabase.addOperation(fetchRecordsImageOperation)
}
return cell
}
Thanks in advance!
There is latency between when your table view appears and when fetchRecordsImageOperation.perRecordCompletionBlock is called. Within that time the user may scroll the table view causing the table view cell to dequeue and requeue with a different indexPath and different data associated with it, if you do not check that the cell's index path is the same as when you constructed fetchRecordsImageOperation.perRecordCompletionBlock, this line: cell.photoImage.image = UIImage(data: NSData(contentsOfURL: imageAsset.fileURL)!) will cause the image to be placed in the cell that is already displaying different data. You can modify your completion block like so to avoid this.
if let imageRecord = record {
NSOperationQueue.mainQueue().addOperationWithBlock() {
if let imageAsset = imageRecord.objectForKey("photoImage") as? CKAsset{
if indexPath == tableView.indexPathForCell(cell){
cell.photoImage.image = UIImage(data: NSData(contentsOfURL: imageAsset.fileURL)!)
}
self.imageCache.setObject(imageAsset.fileURL, forKey:self.photoRecord.recordID)
cell.userInteractionEnabled = true
}
}
}
You find the answer in here I believe, I bias of course cause I wrote it.
How to determine when all images have been downloaded from a set in Swift?
You should setup an image to display while its loading an image and show that so that the user understands what is happening?
I have just started using swift. I am using blocks and NSOperationQueue to download the image in tableViewCell and in the completion handler I am returning the downloaded image. I am trying to update the cell as below.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("itemCell", forIndexPath: indexPath) as! UITableViewCell
var itemImage = cell.viewWithTag(1000) as! UIImageView
var itemName = cell.viewWithTag(1001) as! UILabel
if let item = self.itemArray?[indexPath.row] {
itemImage.image = UIImage(named: "Placeholder.jpg")
getImageForItem(item, withCompletion: { (image) -> () in
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
var imageViewToUpdate = cellToUpdate.viewWithTag(1000) as! UIImageView
imageViewToUpdate.image = image
}
})
itemName.text = item.itemName
}
return cell
}
func getImageForItem(item: item, withCompletion completion:((image: UIImage) -> ())) {
if let image = self.imageCache.objectForKey(item.itemID) as? UIImage {
completion(image: image)
} else {
let request = item.getItemImage(ItemImageSize(rawValue: 2)!, withWidth: 100, shouldFetch: false, block: { (image, tempID) -> Void in
if image != nil {
self.imageCache.setObject(image, forKey: item.itemID)
if item.itemID == tempID {
completion(image: image)
}
}
})
if request != nil {
imageQueue.addOperation(request)
}
}
}
The problem I face is, I am getting the image successfully in the completion block of cellForRowAtIndexPath(), but, I fail to update the cell. For the above code, the downloaded image is applied to all the visible cells in the tableView, but, as I scroll down, I see only the placeholder image. Even I loose the loaded images to placeholder image on scrolling back.
On debugging, I found that
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
var imageViewToUpdate = cellToUpdate.viewWithTag(1000) as! UIImageView
imageViewToUpdate.image = image
}
loop is called for the visible cells only first time. But not called again on scrolling. What am I missing?
I sorted out myself. I added one more argument, indexPath to track it.
getImageForItem(item, indexPath: indexPath, withCompletion: { (image, imageIndexPath) -> () in
if Set<NSIndexPath>(tableView.indexPathsForVisibleRows() as! [NSIndexPath]).contains(imageIndexPath) {
itemImage.image=image
}
})
That gave me the perfect solution