How to update a UISegmentedControl which is inside a UITableViewCell - ios

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] = []

Related

Tableview scrolling is a bit fidgety. How can I make it buttery smooth

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

How can I add random content (UIKit element) in collection view cell depending on incoming data with autoresizing?

In this function I made switch on incoming data type and I invoke function that add subview to my cell. At this moment I haven't data and I inserted stubs.
Method doesn't work, result is empty space after textview( haven't picture) ,and I dont know why, I add subview to my cell, in theory it must work. Now I don't use switch. I have cell height 350 , label height 50 and textview height 50 so for image/video I keep 250.
Please help me with advice.
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionNews",for: indexPath) as! NewsCell
cell.personImage.image = UIImage(named: "welder.png")
cell.textView.layer.cornerRadius = 5
cell.addSubview(addImage(image : UIImage(named : "error.png")!, for: cell, at : indexPath ))
//switch (contentArray[indexPath.row]){
//case let image where image is String :
//cell.addSubview(addImage(image : UIImage(named : "error.png")!, for: cell, at : indexPath ))
//case let video where video is String :
//break
//default:
//break
// }
return cell
}
func addImage(for cell : NewsCell, at index : IndexPath)->UIImageView{
let testImg = UIImage(named: "error.png")
let imageForCell = UIImageView(image: testImg)
imageForCell.autoresizingMask = [UIViewAutoresizing.flexibleWidth , UIViewAutoresizing.flexibleHeight]
imageForCell.frame = CGRect(x: cell.textView.bounds.minX, y: cell.textView.bounds.maxY + 5, width: imageForCell.frame.origin.x, height: imageForCell.frame.origin.y)
imageForCell.layer.cornerRadius = 5
imageForCell.layer.masksToBounds = true
return imageForCell
}
func addVideo () {
}
First: I see that function addImage is not declaring a UIImage as a parameter
Second: You're setting frame property for imageForCell wrong. You should do something like: (Supposing that you want your image below the textView)
var newFrame = cell.textView.frame
newFrame.y += 5
imageForCell.frame = newFrame

Hide button in UICollectionView cell

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

UITableView mess with the rows content

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.^=^

UICollectionView cell reuse causing problems even after taking steps

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!

Resources