I am programmatically creating cells and adding a delete button to each one of them. The problem is that I'd like to toggle their .hidden state. The idea is to have an edit button that toggles all of the button's state at the same time. Maybe I am going about this the wrong way?
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("verticalCell", forIndexPath: indexPath) as! RACollectionViewCell
let slide = panelSlides[indexPath.row]
cell.slideData = slide
cell.slideImageView.setImageWithUrl(NSURL(string: IMAGE_URL + slide.imageName + ".jpg")!)
cell.setNeedsLayout()
let image = UIImage(named: "ic_close") as UIImage?
var deleteButton = UIButton(type: UIButtonType.Custom) as UIButton
deleteButton.frame = CGRectMake(-25, -25, 100, 100)
deleteButton.setImage(image, forState: .Normal)
deleteButton.addTarget(self,action:#selector(deleteCell), forControlEvents:.TouchUpInside)
deleteButton.hidden = editOn
cell.addSubview(deleteButton)
return cell
}
#IBAction func EditButtonTap(sender: AnyObject) {
editOn = !editOn
sidePanelCollectionView.reloadData()
}
I think what you want to do is iterate over all of your data by index and then call cellForItemAtIndexPath: on your UICollectionView for each index. Then you can take that existing cell, cast it to your specific type as? RACollectionViewCell an then set the button hidden values this way.
Example (apologies i'm not in xcode to verify this precisely right now but this is the gist):
for (index, data) in myDataArray.enumerated() {
let cell = collectionView.cellForRowAtIndexPath(NSIndexPath(row: index, section: 0)) as? RACollectionViewCell
cell?.deleteButton.hidden = false
}
You probably also need some sort of isEditing Boolean variable in your view controller that keeps track of the fact that you are in an editing state so that as you scroll, newly configured cells continue to display with/without the button. You are going to need your existing code above as well to make sure it continues to work as scrolling occurs. Instead of creating a new delete button every time, you should put the button in your storyboard and set up a reference too and then you can just use something like cell.deleteButton.hidden = !isEditing
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
}
Here is my code in cell for row method:-
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeue(.journalListingCell, for: indexPath) as! JournalListViewCell
cell.delegate = self
//fetching the category model as per the category id
if let category = categories?.first(where: { $0.categoryID == dreamJournalArray[indexPath.row].categoryID }) {
cell.configureCell(dreamJournalArray[indexPath.row], category)
}
return cell
}
This code is just matching a category id from a array of categories to the category id in the main data model and passing along that category to the configure cell model along with the data model.
The configure cell method is as follows:-
typealias Model = DreamJournalModel
func configureCell(_ model: Model, _ category: CategoryModel) {
//setting the gradient colors
verticalView.backgroundColor = category.categoryGradientColor(style: .topToBottom, frame: verticalView.frame, colors: [UIColor.init(hexString: category.initialGradientColor)!, UIColor.init(hexString: category.lastGradientColor)!])
horizontalVIew.backgroundColor = category.categoryGradientColor(style: .leftToRight, frame: self.frame, colors: [ UIColor.init(hexString: category.lastGradientColor)!, UIColor.init(hexString: category.initialGradientColor)!])
timelineCircleView.backgroundColor = category.categoryGradientColor(style: .topToBottom, frame: timelineCircleView.frame, colors: [UIColor.init(hexString: category.initialGradientColor)!, UIColor.init(hexString: category.lastGradientColor)!])
// setting the intention matching image as per the rating
intentionImageView.image = UIImage.init(named: "Match\(model.intentionMatchRating)")
// setting up the date of the journal recorded
let date = Date(unixTimestamp: model.createdAt!)
dateMonthLabel.text = (date.monthName(ofStyle: .threeLetters)).uppercased()
dateNumberLabel.text = String(date.day)
//setting the titles and labels
dreamTitleLabel.text = model.title
dreamTagsLabel.text = model.tags
// setting the lucid icon
gotLucidImageview.isHidden = !(model.isLucid!)
//setting the buttons text or image
if model.recordingPath != nil {
addRecordButton.setTitle(nil, for: .normal)
addRecordButton.backgroundColor = UIColor.clear
addRecordButton.layer.cornerRadius = 0
addRecordButton.setImage(LRAsset.cardRecording.image, for: .normal)
}
else{
addRecordButton.setTitle(" + RECORD ", for: .normal)
addRecordButton.backgroundColor = UIColor.blackColorWithOpacity()
addRecordButton.layer.cornerRadius = addRecordButton.bounds.height/2
addRecordButton.setImage(nil, for: .normal)
}
if model.note != nil {
addNoteButton.setTitle(nil, for: .normal)
addNoteButton.backgroundColor = UIColor.clear
addNoteButton.layer.cornerRadius = 0
addNoteButton.setImage(LRAsset.cardNote.image, for: .normal)
}
else{
addNoteButton.setTitle(" + NOTE ", for: .normal)
addNoteButton.backgroundColor = UIColor.blackColorWithOpacity()
addNoteButton.layer.cornerRadius = addRecordButton.bounds.height/2
addNoteButton.setImage(nil, for: .normal)
}
if model.sketchPath != nil {
addSketchButton.setTitle(nil, for: .normal)
addSketchButton.backgroundColor = UIColor.clear
addSketchButton.layer.cornerRadius = 0
addSketchButton.setImage(LRAsset.CardSketch.image, for: .normal)
}
else{
addSketchButton.setTitle(" + SKETCH ", for: .normal)
addSketchButton.backgroundColor = UIColor.blackColorWithOpacity()
addSketchButton.layer.cornerRadius = addSketchButton.bounds.height/2
addSketchButton.setImage(nil, for: .normal)
}
}
In this method I am setting the gradient colors in the cell as per the category. And then I am setting the button states at the bottom according to the mentioned conditions.
Here's what my tableview looks like:-
The 3 buttons at the bottom of the cell are in a Stackview and their appearance is changing dynamically as per the conditions.
The tableview is not scrolling smooth and the lag is very visible to the naked eye. The height for the cell is fixed at 205.0 and is defined in the height for the row index method.
I don't know where my fetching and feeding the data to the cell logic is mistaken.
Please guide if the scrolling can be improved here.
Thanks in advance.
You are writing too much code inside func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell which should not be there Examples can be setting corner radius, background colors, gradients. These are called each time. You can implement these when the cell is created inside awakeFromNib method.
But this is not that much of a reason your tableview is having scrolling issues. It is because of images that you are adding inside that method. You can use iOS 10 new tableView(_:prefetchRowsAt:) method which will be a better place to load images or preparing data for your cells before being displayed taking things like images there which is called prior to dequeue method.
Here is the link form Apple documentation:
https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching/1771764-tableview
The table view calls this method as the user scrolls, providing the index paths for cells it is likely to display in the near future. Your implementation of this method is responsible for starting any expensive data loading processes. The data loading must be performed asynchronously, and the results made available to the tableView(_:cellForRowAt:) method on the table view’s data source.The collection view does not call this method for cells it requires immediately, so your code must not rely on this method to load data. The order of the index paths provided represents the priority.
https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching
In my swift app I have a UITableView with one static cell and many dynamic cells.
Static cell contains different fields, such as labels, map (taken from MapKit) and a button, that indicates whether user voted up or not.
Now, when user presses the button, I want to change its color, possibly without refreshing anything else.
So far my code looks like this:
var currentUserVote:Int = 0
func tableView(_ tview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath as NSIndexPath).row == 0 {
let cell = tview.dequeueReusableCell(withIdentifier: "cellStatic") as! VideoDetailsCell
fetchScore(cell.score)
let voteUpImage = UIImage(named: "voteUp");
let tintedVoteUpImage = voteUpImage?.withRenderingMode(UIImageRenderingMode.alwaysTemplate)
cell.voteUpButton.setImage(tintedVoteUpImage, for: UIControlState())
checkUsersVote() { responseObject in
if(responseObject == 1) {
cell.voteUpButton.tintColor = orangeColor
} else if (responseObject == -1){
cell.voteUpButton.tintColor = greyColor
} else {
cell.voteUpButton.tintColor = greyColor
}
self.currentUserVote = responseObject
}
//map handling:
let regionRadius: CLLocationDistance = 1000
let initialLocation = CLLocation(latitude: latitude, longitude: longitude)
centerMapOnLocation(initialLocation, map: cell.mapView, radius: regionRadius)
//cell.mapView.isScrollEnabled = false
cell.mapView.delegate = self
.
.
.
return cell
} else {
//handle dynamic cells
}
}
So in the method above I'm checking if user voted already and based on that I'm setting different color on the button. I'm also centering the map on a specific point.
Now, since it's a static cell, I connected IBAction outlet to that button:
#IBAction func voteUpButtonAction(_ sender: AnyObject) {
if(currentUserVote == 1) {
self.vote(0)
}else if (currentUserVote == -1){
self.vote(1)
} else {
self.vote(1)
}
}
and the vote method works as follows:
func vote(_ vote: Int){
let indexPath = IndexPath(row: 0, section: 0)
let cell = tview.dequeueReusableCell(withIdentifier: "cellStatic") as! VideoDetailsCell
switch(vote) {
case 1:
cell.voteUpButton.tintColor = orangeColor
case 0:
cell.voteUpButton.tintColor = greyColor
case -1:
cell.voteUpButton.tintColor = greyColor
default:
cell.voteUpButton.tintColor = greyColor
}
tview.beginUpdates()
tview.reloadRows(at: [indexPath], with: .automatic)
tview.endUpdates()
currentUserVote = vote
//sending vote to my backend
}
My problem is, that when user taps the button, he invokes the method vote, then - based on the vote, the button changes color, but immediately after that method cellForRow is called and it changes the color of the button again. Also, it refreshes the map that's inside of it.
What I want to achieve is that when user taps the button, it should immediately change its color and that's it. Map stays untouched and the button is not changed again from cellForRow.
Is there a way of refreshing only that particular button without calling again cellForRow?
First of all, you confuse static and dynamic cells. You can use static cells only in the UITableViewController and you can't use static and dynamic cell at the same time.
I strongly recommend you not to use cell for storing map and button. All elements from the cell will be released after scrolling it beyond the screen.
I can advise you use TableViewHeaderView for this task. In this case you will be able set button and map view as #IBOutlet.
(See more about adding tableview headerView. You can also set it from interface builder.)
Another way is change tableView.contentInset and set your view with map and button as subview to tableView. This method is used when you need create Stretchy Headers.
It should be quite easy, simply do it in your button handler. The sender argument there is the button object that caused the action. When you were connecting it from IB there was a dropdown to select sender type, you may have missed it and the whole thing would have been obvious with UIButton type there.
Simply change your handler like this :
#IBAction func voteUpButtonAction(_ sender: UIButton) {
if(currentUserVote == 1) {
self.vote(0)
}else if (currentUserVote == -1){
self.vote(1)
} else {
self.vote(1)
}
sender.backgroundColor = yourFavouriteColor
}
Another approach would be to create an IBOutlet for your button, since its from a static cell, and then you would be able to reference it from any place in your view controller.
In this call:
func tableView(_ tview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
I see it calls checkUsersVote() which I'm guessing should get the updated value set in the vote() call. Could the problem be that you aren't doing this
currentUserVote = vote
until after reloadRows() is called?
Each row of a table has a position control implemented as a segmented control. When the table is initialized, the selectedSegment is unknown. I want to update the selectedSegment from a different function.
Currently the code creates an array of segmentControls which can be accessed by a function. The program runs OK, but the segmentControl does not updated.
Here is the code to create the table cells:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) as UITableViewCell
cell.textLabel?.text = self.shades[indexPath.row]
let shadePositionControl = UISegmentedControl (items: [#imageLiteral(resourceName: "Icon-Closed").withRenderingMode(.alwaysOriginal),
#imageLiteral(resourceName: "Icon-Half").withRenderingMode(.alwaysOriginal),
#imageLiteral(resourceName: "Icon-Open").withRenderingMode(.alwaysOriginal)])
shadePositionControl.frame = CGRect(x: cell.bounds.origin.x + 200, y: cell.bounds.origin.y + 6, width: 90, height: 30)
shadePositionControl.tag = indexPath.row
shadePositionControl.addTarget(self, action: #selector(self.shadePositionControlAction), for: .valueChanged)
shadePositionControls.append(shadePositionControl)
cell.contentView.addSubview(shadePositionControl)
return cell
}
and here is the function to update the selectedSegment:
func statusPoll() {
myShade.getShadow() //gets the state from AWS-IOT
if myShade.gotState() {
let shadeIndex = 0 //hard-coded during proof-of-concept testing
let position = myShade.getDesiredState(key: "position") as! String
switch position {
case "closed": shadePositionControls[shadeIndex].selectedSegmentIndex = 0
case "half": shadePositionControls[shadeIndex].selectedSegmentIndex = 1
case "open": shadePositionControls[shadeIndex].selectedSegmentIndex = 2
default: shadePositionControls[shadeIndex].selectedSegmentIndex = -1
}
}
}
What am I missing to have the segmentedControl get updated, and have the view refreshed to reflect the update? Is there a better way to update the per-cell segmentControl than creating an array of them? I tried using shadeTableView.reloadData() to refresh the display of the segmentedControl, but it had no effect.
This method of updating controls in a table cell works.
The reason it didn't appear to work was there was a bug in instantiating the shadePositionControls array. It was instantiated as follows, which added an unwanted segmentedControl at the zero position of the array.
var shadePositionControls = [UISegmentedControl()]
Hence, the code in statusPoll was updating the bogus segmentedControl, not the intended one.
The fix was change the array creation to:
var shadePositionControls:[UISegmentedControl] = []
I have a tableView with custom tableViewCell. Each cell has an image on the left (see screen 1). The custom tableViewCell has been subclassed. Every row has a different image which depends on indexPath.row
If I click on the image(s) the screen, the images should change to tick boxes so user can multiselect (see screen 2). If I click back on the image with a tick, it should revert back to old image on screen 1.
Issue :
I know how to change the image to tick mark but don't know how to go back to old image (pls remember every row has a different image - so the "else" part in my below code will not make sense).
Below is my code to change and revert the image
#IBAction func imageTap(sender: AnyObject) {
let imageView : UIImageView! = sender.view! as! UIImageView
if imageView.image != UIImage(named: "Oval 5"){
imageView.image = UIImage(named: "Oval 5")
awardBadge.hidden = false
}
else // Need to figure out how to go back to same image
{
imageView.image = UIImage(named: "Oval 1")
awardBadge.hidden = true
}
}
Add a boolean named "isTicked", or something like that, to your cell and set it to the right value at selecting / deselecting. Based on this value, you can set the right image. Let the cell do the rest.
Comparing images makes it unnecessarily complex and less performing.
You should track selected items. The easiest way would be to define an array of booleans in the view controller's subclass.
Define the delegate for you UITableViewCell's subclass like:
protocol MYCustomCellDelegate {
func didSelectedCell(cell: MYCustomCell)
}
And handle it in your controller:
func didSelectedCell(cell: MYCustomCell)
{
let indexPath = tableView.indexPath(cell: cell)
selectedItems[indexPath.row] = !selectedItems[indexPath.row]
tableView.reloadItemsAtIndexPaths([indexPath])
}
In the cellForItemAtIndexPath method set the state of you cell according to the selectedItems[indexPath.row]. If it is selected show the checkbox, if it is not - image:
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath) as! MYCustomCell
cell.setTitle(titles[indexPath.row])
cell.setSelected(selectedItems[indexPath.row])
cell.delegate = self
return cell
}
i think you approach would be different, why don't you use the custom image button with 2 different images which represents selected and normal state. available states are normal/highlighted/selected/disabled. when we have custom image button, since you are using the static images, you would use your images for default state and tick image for selected state. all you have to do is just use the inbuilt property selected.