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.
Related
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
}
I tried to copy only the necessary code to show my problem. I have a tableview with dynamic content. I created a prototype cell and it has a user name and 10 stars (it's a rating page). People in the group are allowed to rate other people. Everything is working ok, but I have a problem when I scroll down. If I rate my first user with 8 stars, when I scroll down then some user that was in the bottom area of the tableview, appears with the rate that I gave to my first user. I know that tableview reuse cells. I tried many things but with no success. Hope someone can help me on that.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let model = users[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier("RatingCell") as! RatingTableViewCell
cell.tag = indexPath.row
cell.playerLabel.text = model.name
cell.averageView.layer.borderWidth = 1
cell.averageView.layer.borderColor = Color.Gray1.CGColor
cell.averageView.layer.cornerRadius = 5
cell.starsView.userInteractionEnabled = true
cell.averageLabel.text = "\(user.grade)"
for i in 0...9 {
let star = cell.starsView.subviews[i] as! UIImageView
star.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(starTap)))
star.userInteractionEnabled = true
star.tag = i
star.image = UIImage(named: (i + 1 <= grade ? "star-selected" : "star-empty"))
}
return cell
}
func changeRating(sender: UIImageView) {
let selectedStarIndex = sender.tag
let cell = sender.superview?.superview?.superview as! RatingTableViewCell
let model = users[cell.tag]
let stars = sender.superview?.subviews as! [UIImageView]
cell.averageLabel.text = "\(selectedStarIndex + 1)"
for i in 0...9 {
let imgName = i <= selectedStarIndex ? "star-selected" : "star-empty"
stars[i].image = UIImage(named: imgName)
}
}
func starTap(gesture: UITapGestureRecognizer) {
changeRating(gesture.view as! UIImageView)
}
The way to solve this problem is by updating the model that holds all the information for the uitableviewcell. Whenever a rating is updated fora particular cell, make sure you reflect that update in the respective object / dictionary in an array. Furthermore, if you have a customuitableviewcell, it might be a good idea to reset the stars in the "prepareForUse" function, so that way when a cell is reused it doesn't use old data.
In your comments, you said that you have an array with selected rates.But you did not show that in your code.
In my opinion, you need record indexPath too, because indexPath.row is binding with your rate data(may be grade?).The best way to do so is that #Jay described up.And you should not write the code of configuring cell data and cell's logic in your view controller.If your business logic is complex, you will find that it is a nightmare.^=^
I know this question has been asked many times.
I'm using a UICollectionView with custom cells which have a few properties, most important being an array of UISwitch's and and array of UILabel's. As you can guess, when I scroll, the labels overlap and the switches change state. I have implemented the method of UICollectionViewCell prepareForReuse in which I empty these arrays and reset the main label text.
I have tried to combine solutions from different answers and I have reached a point where my labels are preserved, but the state of my switches in the cells isn't. My next step was to create an array to preserve the state before removing the switches and then set the on property of a newly created switch to a value of this array at an index. This works, until after I scroll very fast and switches in cells which were not selected previously become selected(or unselected). This is creating a huge problem for me.
This is my collectionView cellForItemAtIndexPath method:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("pitanjeCell", forIndexPath: indexPath) as! PitanjeCell
// remove views previously created and create array to preserve the state of switches
var selectedSwitches: [Bool] = []
for item: UIView in cell.contentView.subviews {
if (item.isKindOfClass(UILabel) && !item.isEqual(cell.tekstPitanjaLabel)){
item.removeFromSuperview()
}
if (item.isKindOfClass(UISwitch)){
selectedSwitches.append((item as! UISwitch).on)
item.removeFromSuperview()
}
}
// get relevant data needed to place cells programmatically
cell.tekstPitanjaLabel.text = _pitanja[indexPath.row].getText()
let numberOfLines: CGFloat = CGFloat(cell.tekstPitanjaLabel.numberOfLines)
cell.setOdgovori(_pitanja[indexPath.row].getOdgovori() as! [Odgovor])
var currIndex: CGFloat = 1
let floatCount: CGFloat = CGFloat(_pitanja.count)
let switchConstant: CGFloat = 0.8
let switchWidth: CGFloat = cell.frame.size.width * 0.18
let heightConstant: CGFloat = (cell.frame.size.height / (floatCount + 2) + (numberOfLines * 4))
let labelWidth: CGFloat = cell.frame.size.width * 0.9
for item in _pitanja[indexPath.row].getOdgovori() {
// create a switch
let odgovorSwitch: UISwitch = UISwitch(frame: CGRectMake((self.view.frame.size.width - (switchWidth * 2)), currIndex * heightConstant , switchWidth, 10))
odgovorSwitch.transform = CGAffineTransformMakeScale(switchConstant, switchConstant)
let switchValue: Bool = selectedSwitches.count > 0 ? selectedSwitches[Int(currIndex) - 1] : false
odgovorSwitch.setOn(switchValue, animated: false)
// cast current item to relevant class
let obj: Odgovor = item as! Odgovor
// create a label
let odgovorLabel: UILabel = UILabel(frame: CGRectMake((self.view.frame.size.width / 12), currIndex * heightConstant , labelWidth, 20))
odgovorLabel.text = obj.getText();
odgovorLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping
odgovorLabel.font = UIFont(name: (odgovorLabel.font?.fontName)!, size: 15)
// add to cell
cell.addSwitch(odgovorSwitch)
cell.addLabel(odgovorLabel)
currIndex++
}
return cell
}
My custom cell also implements methods addSwitch and addLabel which add the element to the contentView as a subview.
Is there any way I can consistently preserve the state of switches when scrolling?
EDIT: As per #Victor Sigler suggestion, I created a bydimensional array like this:
var _switchStates: [[Bool]]!
I initialized it like this:
let odgovori: Int = _pitanja[0].getOdgovori().count
_switchStates = [[Bool]](count: _pitanja.count, repeatedValue: [Bool](count: odgovori, repeatedValue: false))
And I changed my method like this:
for item: UIView in cell.contentView.subviews {
if (item.isKindOfClass(UILabel) && !item.isEqual(cell.tekstPitanjaLabel)){
item.removeFromSuperview()
}
if (item.isKindOfClass(UISwitch)){
_switchStates[indexPath.row][current] = (item as! UISwitch).on
current++
selectedSwitches.append((item as! UISwitch).on)
item.removeFromSuperview()
}
}
And in the end:
let switchValue: Bool = _switchStates.count > 0 ? _switchStates[indexPath.row][Int(currIndex) - 1] : false
First of all as you said in your question regarding the default behavior of the cell in the UICollectionView you need to save the state of each cell, in your case with the UISwitch, but you need to preserve the state in a local property, not in the cellForRowAtIndexPath method because this method is calles every time a cell is going to be reused.
So first declare the array where you going to save the state of the UISwitch's outside this function, something like this:
var selectedSwitches: [Bool] = []
Or you can declare it and then in your viewDidLoad instantiate it, it's up to you, I recommend you instantiate it in your viewDidLoad and only declare it as a property like this:
var selectedSwitches: [Bool]!
Then you can do whatever you want with the state of your UISwitch's always of course preserving when change to on or off.
I hope this help you.
I have done it!
In the end I implemented this:
func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
var current: Int = 0
var cell = cell as! PitanjeCell
for item: UISwitch in cell.getOdgovoriButtons() {
if(current == _switchStates[0].count) { break;}
_switchStates[indexPath.row][current] = (item).on
current++
}
}
In this function I saved the state.
This in combo with prepareForReuse:
public override func prepareForReuse() {
self.tekstPitanjaLabel.text = nil
self._odgovoriButtons.removeAll()
self._odgovoriLabels.removeAll()
self._odgovori.removeAll()
super.prepareForReuse()
}
Finally did it!
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.
Allright, I think I've read all the issues which seemed to be the same as I did.
I have a UITextView in a UITableViewCell, the cell itself contains an imageView, a UILabel as title, the UITextView and another UILabel.
Screenshot:
I believe the constraints to be correct. Currently the view does an estimate which is correct in most cases, but in my case the content is larger than the UITableViewCell's height, this is probably due to the delayed rendering of the UITableViewCell, causing other list items to draw behind this one (it only appears on top).
The code which estimates the height:
func heightForCellAtIndexPath(indexPath:NSIndexPath) -> CGFloat {
if (calculatorCell[cellIdentifier] == nil) {
calculatorCell[cellIdentifier] = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? UITableViewCell
}
if let cell = calculatorCell[cellIdentifier] {
populateCell(cell, indexPath: indexPath)
cell.setNeedsLayout()
cell.layoutIfNeeded()
return cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
}
ErrorClass.log("Could not instantiate cell? returning 100 float value instead for listView")
return 100.0
}
This is a generic function as you see since it's handling a UITableViewCell not a custom, so the populateCell in this case, the first index returns the cell with the issue, the rest are comments which are working fine, I'm just sharing it all since people will ask for it:
if (indexPath.row == 0) {
let cellUnwrapped = cell as! ChallengeHeader
cellUnwrapped.setContent(
challenge!.content,
date: StrFormat.deadLineFromDate(challenge!.endAt),
likes: Language.countValue("challenge_count_like", count: challenge!.totalLikes),
likeController: self,
commentCount: Language.countValue("global_count_comment", count: challenge!.totalComments)
)
cellUnwrapped.like(userLiked)
cellUnwrapped.loadAttachments(challenge!.attachments)
return cellUnwrapped
} else {
let cellUnwrapped = cell as! ListItemComment
let challengeComment = items[(indexPath.row - 1)] as! ChallengeComment
var username = "Anonymous"
if let user = challengeComment.author as User? {
cellUnwrapped.iconLoad(user.avatar)
username = user.displayName
}
cellUnwrapped.setContentValues(
StrFormat.cleanHtml(challengeComment.text),
date: StrFormat.dateToShort(challengeComment.createdAt),
username: username
)
return cellUnwrapped
}
The cell's setContentvalues which parses the html string to NSAttributedString, it's wrapped in an if to prevent reloads to re-render the NSAttributedString. It won't change but it does take a while to render:
func setContent(content:String, date:String, likes:String, likeController : LikeController, commentCount:String) {
if (content != txtContent) {
self.contentLabel.text = nil
self.contentLabel.font = nil
self.contentLabel.textColor = nil
self.contentLabel.textAlignment = NSTextAlignment.Left
contentLabel.attributedText = StrFormat.fromHtml(content)
txtContent = content
}
deadlineLabel.text = date
likesLabel.text = likes
self.likeController = likeController
likeButton.addTarget(self, action: "likeClicked:", forControlEvents: .TouchUpInside)
totalComments.text = commentCount
totalComments.textColor = StyleClass.getColor("primaryColor")
doLikeLayout()
}
So TL;DR; I have a UITextView inside a UITableViewCell scaling sizes using autolayout and containing an NSAttributedString to parse html and html links. How come it does not render well, and how to fix it?