SWIFT 2 - UICollectionView - slow scrolling - ios

I have setup a uicollectionview in my project that get data from a JSON file. Everything works good however, the scrolling is very slow and when the view is scrolling the coming cell, for few moments shows the content of the cell before.
I have tried using dispatch_async but it still very slow and jumpy.
any Idea what am I doing wrong?
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let videoCell = collectionView.dequeueReusableCellWithReuseIdentifier("VideoCell", forIndexPath: indexPath) as UICollectionViewCell
let communityViewController = storyboard?.instantiateViewControllerWithIdentifier("community_id")
videoCell.frame.size.width = (communityViewController?.view.frame.size.width)!
videoCell.center.x = (communityViewController?.view.center.x)!
videoCell.layer.borderColor = UIColor.lightGrayColor().CGColor
videoCell.layer.borderWidth = 2
let fileURL = NSURL(string:self.UserVideosInfo[indexPath.row][2])
let asset = AVAsset(URL: fileURL!)
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
assetImgGenerate.appliesPreferredTrackTransform = true
let time = CMTimeMake(asset.duration.value / 3, asset.duration.timescale)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
//self.showIndicator()
let NameLabelString = self.UserVideosInfo[indexPath.row][0]
let CommentLabelString = self.UserVideosInfo[indexPath.row][1]
let DateLabelString = self.UserVideosInfo[indexPath.row][3]
let buttonPlayUserVideo = videoCell.viewWithTag(1) as! UIButton
let nameLabel = videoCell.viewWithTag(2) as! UILabel
let commentUserVideo = videoCell.viewWithTag(3) as! UILabel
let dateUserVideo = videoCell.viewWithTag(4) as! UILabel
let thumbUserVideo = videoCell.viewWithTag(5) as! UIImageView
let deleteUserVideo = videoCell.viewWithTag(6) as! UIButton
buttonPlayUserVideo.layer.setValue(indexPath.row, forKey: "indexPlayBtn")
deleteUserVideo.layer.setValue(indexPath.row, forKey: "indexDeleteBtn")
dispatch_async(dispatch_get_main_queue()) {
nameLabel.text = NameLabelString
commentUserVideo.text = CommentLabelString
dateUserVideo.text = DateLabelString
self.shadowText(nameLabel)
self.shadowText(commentUserVideo)
self.shadowText(dateUserVideo)
if let cgImage = try? assetImgGenerate.copyCGImageAtTime(time, actualTime: nil) {
thumbUserVideo.image = UIImage(CGImage: cgImage)
}
}
}
//THIS IS VERY IMPORTANT
videoCell.layer.shouldRasterize = true
videoCell.layer.rasterizationScale = UIScreen.mainScreen().scale
return videoCell
}

At first - you are working with UI objects from global queue and seems like without any purpose. That is forbidden - or behavior will be undefined.
Secondary, the mostly heavy operation is creation of thumbnail which you perform on main queue.
Consider using of the AVAssetImageGenerator's method
public func generateCGImagesAsynchronouslyForTimes(requestedTimes: [NSValue], completionHandler handler: AVAssetImageGeneratorCompletionHandler)
instead of your own asyncs.
At third, viewWithTag is pretty heavy operation causing enumeration on subviews. Consider to declare properties in the cell for views which you need.
UPD: to declare properties in a cell, create subclass of UICollectionViewCell with appropriate properties as IBOutlets. Then, in your view controller viewDidLoad implementation, call
collecionView.registerClass(<YourCellSubclass>.dynamicType, forCellWithReuseIdentifier:"VideoCell")
Or, if your collection view cell is configured in the Storyboard, specify the class of the cell and connect its subviews to class' outlets directly in the cell's settings window in Interface Builder.
At fourth, your cells are being reused by a collection view. Each time your cell is going out of visible area, it is removed from collection view and is put to reuse queue. When you scroll back to the cell, your view controller is asked again to provide a cell. And you're fetching the thumbnail for the video again for each newly appeared cell. Consider caching of already fetched thumbnails by storing them in some array by collectionView's indexPath.item index.

Related

Fullscreen view for a collectionview

This is more of an approach question. Suppose I have a collection view where each cell is occupying the entire screen, which have images in them and some other data(eg: title, info etc). What I want is for the user to tap on the cell and the image to go to fullscreen mode. I am able to achieve this by initializing a separate view and scaling the image to fit the screen with this code.
let newImageView = UIImageView(image: imageView.image)
newImageView.frame = UIScreen.main.bounds
newImageView.backgroundColor = .black
newImageView.contentMode = .scaleAspectFit
and then dismissing it by removing the view like so:
self.navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
sender.view?.removeFromSuperview()
}
Q1. Is there a way I can manipulate all the collectionview cells to transform into the fullscreen view on tap so i can use the default swiping action of the collection view to scroll through the images horizontally ?
Q2. If not, I use this library INSPhotoGallery! to add the effect on tap, which gives me the desired effect but due to heavy loading of the images from the PHAsset library my app crashes.
This is how i initialized my phassets to pass into this library:
lazy var photos: [INSPhotoViewable] = {
var allPhotos: [INSPhoto] = Array()
fetchResult.enumerateObjects({ (asset, index, stop) in
let image = self.requestImageForPHAsset(asset: asset)
allPhotos.append(INSPhoto(image: image, thumbnailImage: nil))
})
return allPhotos
}()
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// handle tap events
print("You selected cell #\(indexPath.item)!")
let cell = collectionView.cellForItem(at: indexPath) as! DetailedCollectionViewCell
let currentPhoto = photos[indexPath.row]
let galleryPreview = INSPhotosViewController(photos: photos, initialPhoto: currentPhoto, referenceView: cell)
galleryPreview.overlayView.photosViewController?.singleTapGestureRecognizer.isEnabled = false
galleryPreview.referenceViewForPhotoWhenDismissingHandler = { [weak self] photo in
if let index = self?.photos.index(where: {$0 === photo}) {
let indexPath = IndexPath(item: index, section: 0)
return collectionView.cellForItem(at: indexPath) as? DetailedCollectionViewCell
}
return nil
}
present(galleryPreview, animated: true, completion: nil)
}
Question being how can I prevent my app from crashing. I know this has something to do with caching the images and loading asynchronously. Do you know of a library that integrates well with PHAssets if nothing else? Thanks!
You also can use page view controller to achieve the same functionality. It's valuable if you have less then 5 cells.
You can display image in a full screen modally. Just create separate view controller and implement animators(transition delegates) for this modal screen. It will be more difficult but also more proper way. The AppStore working in the same way

Data From Label in CollectionViewCell Sometimes Refreshes on Reload other times it Doesn't

First let me say this seems to be a common question on SO and I've read through every post I could find from Swift to Obj-C. I tried a bunch of different things over the last 9 hrs but my problem still exists.
I have a vc (vc1) with a collectionView in it. Inside the collectionView I have a custom cell with a label and an imageView inside of it. Inside cellForItem I have a property that is also inside the the custom cell and when the property gets set from datasource[indePath.item] there is a property observer inside the cell that sets data for the label and imageView.
There is a button in vc1 that pushes on vc2, if a user chooses something from vc2 it gets passed back to vc1 via a delegate. vc2 gets popped.
The correct data always gets passed back (I checked multiple times in the debugger).
The problem is if vc1 has an existing cell in it, when the new data is added to the data source, after I reload the collectionView, the label data from that first cell now shows on the label in new cell and the data from the new cell now shows on the label from old cell.
I've tried everything from prepareToReuse to removing the label but for some reason only the cell's label data gets confused. The odd thing is sometimes the label updates correctly and other times it doesn't? The imageView ALWAYS shows the correct image and I never have any problems even when the label data is incorrect. The 2 model objects that are inside the datasource are always in their correct index position with the correct information.
What could be the problem?
vc1: UIViewController, CollectionViewDataSource & Delegate {
var datasource = [MyModel]() // has 1 item in it from viewDidLoad
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell
cell.priceLabel.text = ""
cell.cleanUpElements()
cell.myModel = dataSource[indexPath.item]
return cell
}
// delegate method from vc2
func appendNewDataFromVC2(myModel: MyModel) {
// show spinner
datasource.append(myModel) // now has 2 items in it
// now that new data is added I have to make a dip to fb for some additional information
firebaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
if let dict = snapshot.value as? [String: Any] else { }
for myModel in self.datasource {
myModel.someValue = dict["someValue"] as? String
}
// I added the gcd timer just to give the loop time to finish just to see if it made a difference
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.datasource.sort { return $0.postDate > $1.postDate } // Even though this sorts correctly I also tried commenting this out but no difference
self.collectionView.reloadData()
// I also tried to update the layout
self.collectionView.layoutIfNeeded()
// remove spinner
}
})
}
}
CustomCell Below. This is a much more simplified version of what's inside the myModel property observer. The data that shows in the label is dependent on other data and there are a few conditionals that determine it. Adding all of that inside cellForItem would create a bunch of code that's why I didn't update the data it in there (or add it here) and choose to do it inside the cell instead. But as I said earlier, when I check the data it is always 100% correct. The property observer always works correctly.
CustomCell: UICollectionViewCell {
let imageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
let priceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var someBoolProperty = false
var myModel: MyModel? {
didSet {
someBoolProperty = true
// I read an answer that said try to update the label on the main thread but no difference. I tried with and without the DispatchQueue
DispatchQueue.main.async { [weak self] in
self?.priceLabel.text = myModel.price!
self?.priceLabel.layoutIfNeeded() // tried with and without this
}
let url = URL(string: myModel.urlStr!)
imageView.sd_setImage(with: url!, placeholderImage: UIImage(named: "placeholder"))
// set imageView and priceLabel anchors
addSubview(imageView)
addSubview(priceLabel)
self.layoutIfNeeded() // tried with and without this
}
}
override func prepareForReuse() {
super.prepareForReuse()
// even though Apple recommends not to clean up ui elements in here, I still tried it to no success
priceLabel.text = ""
priceLabel.layoutIfNeeded() // tried with and without this
self.layoutIfNeeded() // tried with and without this
// I also tried removing the label with and without the 3 lines above
for view in self.subviews {
if view.isKind(of: UILabel.self) {
view.removeFromSuperview()
}
}
}
func cleanUpElements() {
priceLabel.text = ""
imageView.image = nil
}
}
I added 1 breakpoint for everywhere I added priceLabel.text = "" (3 total) and once the collectionView reloads the break points always get hit 6 times (3 times for the 2 objects in the datasource).The 1st time in prepareForReuse, the 2nd time in cellForItem, and the 3rd time in cleanUpElements()
Turns out I had to reset a property inside the cell. Even though the cells were being reused and the priceLabel.text was getting cleared, the property was still maintaining it's old bool value. Once I reset it via cellForItem the problem went away.
10 hrs for that, smh
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell
cell.someBoolProperty = false
cell.priceLabel.text = ""
cell.cleanUpElements()
cell.myModel = dataSource[indexPath.item]
return cell
}

UITableViewCell button tags return desired IndexPath in UISearchController, not FetchedResultsController

I implemented an NSFetchedResultsController on a UITableView in a Core Data project in Swift 2.0. Additionally, I have a UISearchController implemented. Everything works perfectly with the exception of the behavior I'm encountering on my custom UITableViewCell buttons.
When UISearchController is active, the customTableViewCell's buttons work as they should. If I click the same button when the fetchedResultsController is displaying its results, the method thinks Index 0 is the sender, regardless of which button I click.
func playMP3File(sender: AnyObject) {
if resultsSearchController.active {
// ** THIS WORKS **
// get a hold of my song
// (self.filteredSounds is an Array)
let soundToPlay = self.filteredSounds[sender.tag]
// grab an attribute
let soundFilename = soundToPlay.soundFilename as String
// feed the attribute to an initializer of another class
mp3Player = MP3Player(fileName: soundFilename)
mp3Player.play()
} else {
// ** THIS ALWAYS GETS THE OBJECT AT INDEX 0 **
let soundToPlay = fetchedResultsController.objectAtIndexPath(NSIndexPath(forRow: sender.tag, inSection: (view.superview?.tag)!)) as! Sound
// OTHER THINGS I'VE TRIED
// let soundToPlay = fetchedResultsController.objectAtIndexPath(NSIndexPath(forRow: sender.indexPath.row, inSection: (view.superview?.tag)!)) as! Sound
// let soundToPlay: Sound = fetchedResultsController.objectAtIndexPath(NSIndexPath(index: sender.indexPath.row)) as! Sound
let soundFilename = soundToPlay.soundFilename as String
mp3Player = MP3Player(fileName: soundFilename)
mp3Player.play()
}
}
Here's an abbreviated version of my cellForRowAtIndexPath to show I'm setting up the cells' buttons:
let customCell: SoundTableViewCell = tableView.dequeueReusableCellWithIdentifier("customCell", forIndexPath: indexPath) as! SoundTableViewCell
if resultsSearchController.active {
let sound = soundArray[indexPath.row]
customCell.playButton.tag = indexPath.row
} else {
let sound = fetchedResultsController.objectAtIndexPath(indexPath) as! Sound
customCell.playButton.tag = indexPath.row
}
// add target actions for cells
customCell.playButton.addTarget(self, action: "playMP3file:", forControlEvents: UIControlEvents.TouchUpInside)
I've tried a few other approaches I've found here, such as translating CGPoints to IndexPaths, etc. without much luck. Everything that looked promising in the compiler crashed when I clicked the button in the simulator.
Thank you for reading.
Update
Installed Xcode 7.1, rebooted, cleaned caches, nuked derived data, did a cold boot.
Solution
Tags will get the job done in many cases (such as getting the location in an Array) and get lots of votes here, but as I've learned, they don't work all the time. Thank you to Mundi for pointing me towards a more robust solution.
// this gets the correct indexPath when resultsSearchController is not active
let button = sender as! UIButton
let view = button.superview
let cell = view?.superview as! SoundTableViewCell
let indexPath: NSIndexPath = self.tableView.indexPathForCell(cell)!
let soundToPlay = fetchedResultsController.objectAtIndexPath(indexPath) as! Sound
I've tried a few other approaches I've found here, such as translating CGPoints to IndexPaths, etc. without much luck.
Translating points is indeed the most robust solution. This answer contains the correct code.

swift indexPath.row==0 works weirdly for UITableViewCell

I am trying to make a table wherein first cell has a differernt layout than the rest. I want to put the image as background for first cell i.e it shd look something like this:
and here is the code for my implementation
func imageCellAtIndexPath(indexPath:NSIndexPath) -> MainTableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier(imageCellIdentifier) as MainTableViewCell
let object = self.fetchedResultsController.objectAtIndexPath(indexPath) as NSManagedObject
let eTitle:NSString = object.valueForKey("title")!.description
let deTitle = eTitle.stringByDecodingHTMLEntities()
cell.artTitle.text = deTitle
var full_url = object.valueForKey("thumbnailURL")!.description
var url = NSURL(string: full_url)
var image: UIImage?
var request: NSURLRequest = NSURLRequest(URL: url!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in
image = UIImage(data: data)
if((indexPath.row)==0) {
var imageView = UIImageView(frame: CGRectMake(10, 10, cell.frame.width - 10, cell.frame.height - 10))
imageView.image = image
cell.backgroundView = UIView()
cell.backgroundView?.addSubview(imageView)
}
else{
cell.thumb.image = image
}
})
return cell
}
bt the problem is.. when i scroll down and scroll up back again, the background image starts repeating and the thumbnails also get overlapped as shown:
If i scroll up and down again.. this is wht happens:
i might have done some silly mistake bt m not able to figure out what it is. pls help
In table views cells are reused and you should reset parts of the cell that are not relevant the specific version of the cell style you are after. Something like:
if((indexPath.row)==0) {
let frame = CGRectMake(10, 10,
cell.frame.width - 10, cell.frame.height - 10)
var imageView = UIImageView(frame: frame)
imageView.image = image
cell.backgroundView = UIView()
cell.backgroundView?.addSubview(imageView)
// Reset
cell.thumb.image = nil
} else{
cell.thumb.image = image
// Reset
cell.backgroundView = nil
}
Even better and more idiomatic idea is to use separate UITableViewCell designs for these two cell types, each with different reuse identifiers. This way you don't need to care about resetting.
P.S. You should use dequeueReusableCellWithIdentifier:forIndexPath: instead of older dequeueReusableCellWithIdentifier: as it guarantees that a cell is returned.
The problem is that the cell is being reused by the tableView for efficiency purposes but it is never being reset.
You need to clear / remove the imageView from the background view if the cell is not at indexPath 0.

Change Element Value outside TableView Scope

i have a TableView with a customCell. I set the values of the customCell Elements inside cellForRowAtIndexPath:. No problem. But i want to change some customCell Element Values outside of the cellForRowAtIndexPath: Scope. For example after a Swipe i want to change the Value of a cell element inside my swipe function
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell:customCell = self.tableView?.dequeueReusableCellWithIdentifier("customCell")! as customCell
let rowData: NSDictionary = self.tableData[indexPath.row] as NSDictionary
let imageSwipeLeft = UISwipeGestureRecognizer(target: self, action: "imageSwiped:")
imageSwipeLeft.direction = .Left
let urlString: NSString = rowData["testImage"] as NSString
self.indexPathArray += [indexPath]
cell.testLabel.text = "Test Label"
cell.testImage.image = image
cell.testImage.tag = indexPath.row
cell.testImage.addGestureRecognizer(imageSwipeLeft)
cell.testImage.userInteractionEnabled = true
ImageLoader.sharedLoader.imageForUrl(urlString, completionHandler:{(image: UIImage?, url: String) in
cell.testImage.image = image
cell.testImage.layer.borderWidth = 6;
cell.testImage.layer.borderColor = UIColor.whiteColor().CGColor
cell.testImage.clipsToBounds = true
cell.placeholderLoading.stopAnimating()
})
return cell
}
func imageSwiped(recognizer: UISwipeGestureRecognizer) {
let testData: NSDictionary = self.tableData[recognizer.view.tag] as NSDictionary
let imageSlide = recognizer.view as UIImageView
var imageURL = testData["image"] as String
ImageLoader.sharedLoader.imageForUrl(imageURL, completionHandler:{(image: UIImage?, url: String) in
UIView.transitionWithView(imageSlide,
duration:0.44,
options: .TransitionCrossDissolve,
animations: { imageSlide.image = image },
completion: nil)
})
let indexPath = self.indexPathArray[recognizer.view.tag] as NSIndexPath
let cell = self.tableView?.cellForRowAtIndexPath(indexPath)as customCell
cell.testLabel.text = "Test"
}
UITableView follows the Model-View-Controller pattern. According to this pattern, all your changes need to be done to the model - in other words, to the data structure that stores the information from which you populate your cell data. Once you made the change to the model, you tell the view that the data has changed, which would then show the new data.
Let's say that your cellForRowAtIndexPath function reads from an array. Your imageSwiped function should then locate the item that has been swiped, modify its entry in the array, and call either reloadData or reloadRowsAtIndexPaths.
That's it! Once you notify the table view of the reload, it would go back to the array, find the modified data, and call your cellForRowAtIndexPath to display it.
Specifically, in your code add an array called swipedCells to the same class where you declared tableData array:
var swipedCells = Boolean[](count:self.tableData.count, repeatedValue: false)
Now replace
cell.testLabel.text = "Test Label"
line with
if self.swipedCells[indexPath.row] {
cell.testLabel.text = "Swiped!"
} else {
cell.testLabel.text = "Test Label"
}
Finally, change the imageSwiped as follows:
let indexPathRow = self.indexPathArray[recognizer.view.tag] as Int
self.swipedCells[indexPathRow] = true
self.tableView?.reloadData()
This way the cells that you have swiped would continue to have a label "Swiped!" even after you scroll.
Ok after a huge amount of hours :D i solved my problem.
let indexPath = self.priceObjects[recognizer.view.tag] as NSIndexPath
let cell = self.tableView?.cellForRowAtIndexPath(indexPath)as customCell
i call this inside my imageSwipe function. Inside the cellForRowAtIndexPath function where i set the initial cell element values i store the indexPath inside an array. An i tag the Images to get a Index if the Swipe Events happens. I think it is far away from the ideal way...but works for me. I hope someday someone post the good way ;) cause this is very quick n' dirty.

Resources