In my tableView I'm using custom cells with two custom labels. Eventually the text of these labels will change but at the same time, layoutIfNeeded() is called to animate some other things. This is causing the text in the labels to animate as well which I'm trying to prevent.
The code I'm using to change the text:
func tapped(recognizer: UIGestureRecognizer) {
let path = NSIndexPath(forRow: myData.count - 1, inSection: 0)
let cell = tableView.cellForRowAtIndexPath(path) as! CustomCell
cell.infoLabel.text = "Changing text here"
UIView.animateWithDuration(0.5) { self.anotherView.layoutIfNeeded() }
}
To prevent the labels from animating I have tried adding this in CustomLabel and CustomCell, as well as in willDisplayCell:
override func layoutSubviews() {
UIView.performWithoutAnimation { super.layoutSubviews() }
}
Or using another way of preventing it to animate in CustomLabel and CustomCell:
override func layoutSubviews() {
CATransaction.begin()
CATransaction.setDisableActions(true)
super.layoutSubviews()
CATransaction.commit()
}
I've also tried a property observer setting the text and removing all animations at the same time:
var text: String {
didSet {
infoLabel.text = text
infoLabel.layer.removeAllAnimations()
}
}
Nothing seems to work unless I use:
CATransaction.begin()
CATransaction.setDisableActions(true)
cell.infoLabel.text = "Changing text here"
CATransaction.commit()
But then I'd have to use it everywhere I'm changing the text. Is there a place I've overlooked to do this more elegantly?
Related
I'm struggling with UITableView. As you can see in this video in third section of table view third cell isn't display correctly. That happens when I dequeue my cell like that:
let cell = tableView.dequeueReusableCell(withIdentifier: MultipleSelectAnswerSurveyTableViewCellIdentifier, for: indexPath) as! MultipleSelectAnswerSurveyTableViewCell
cell.setup(answer: question.answers?[indexPath.row].value ?? "", isSelected: false, style: style, isLastInSection: indexPath.row == (question.answers?.count ?? 1) - 1)
return cell
Cell's setup() method:
func setup(answer: String, isSelected: Bool, style: Style, isLastInSection: Bool) {
self.isLastInSection = isLastInSection
selectionStyle = .none
backgroundColor = style.survey.singleSelectAnswerTableViewCell.backgroundColor
answerLabel.textColor = style.survey.singleSelectAnswerTableViewCell.answerLabelColor
answerLabel.font = style.survey.singleSelectAnswerTableViewCell.answerLabelFont
answerLabel.text = answer
addSubview(answerLabel)
addSubview(selectionIndicator)
answerLabel.snp.makeConstraints { make in
make.left.equalTo(8)
make.centerY.equalTo(selectionIndicator.snp.centerY)
make.top.equalTo(8)
make.bottom.equalTo(-8)
make.right.equalTo(selectionIndicator.snp.left).offset(-8)
}
selectionIndicator.snp.makeConstraints { make in
make.right.equalTo(-8)
make.top.greaterThanOrEqualTo(8)
make.bottom.lessThanOrEqualTo(-8)
make.width.height.equalTo(26)
}
}
self.isLastInSection variable is used inside layoutSubviews():
override func layoutSubviews() {
super.layoutSubviews()
if isLastInSection {
roundCorners(corners: [.bottomLeft, .bottomRight], radius: 16.0)
}
contentView.layoutIfNeeded()
}
And finally roundCorners():
extension UIView {
func roundCorners(corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
When I dequeue cell with isLastInSection set to false cell is being displayed as expected (related video). So I think the problem is in life cycle of the cell and when the layoutSubview() is being called. I tried many solutions for similar problem found in different threads but none of them helped me. tableView(_:heightForRowAt:) causes the third cell to display correctly, but the first one has rounded bottom corners. Also all of them are fixed in height and that cannot happen.
But what is really weird: when I print the isLastInSection during dequeueing cell which is unexpectedly rounded debugger returns me false:
(lldb) po indexPath.row == (question.answers?.count ?? 1) - 1
false
As you can see in Debug View Hierarchy view text exists so that's why I 've defined the problem as hiding part of content.
You dequeue cell and each time you add subviews you don't check if they are already there which will happen in case of recycled cell. That probably breaks constraints and causes incorrect sizing.
Same problem with rounding - you set rounded corners, but you never revert this behavior when reused cell should not be rounded.
Best way to solve this issue would be to add additional check and create subviews only once:
func setup(answer: String, isSelected: Bool, style: Style, isLastInSection: Bool) {
if self.subviews.count == 0 {
// adding subviews etc.
// code that needs to be performed only once for whole cell's life
}
self.isLastInSection = isLastInSection
// set up content that changes for each cell (like text)
// for example a code depending on parameters of this method
}
alternatively you could keep some property like isInitialized and check that at the beginning.
Also your method layoutSubviews must support both cases:
override func layoutSubviews() {
super.layoutSubviews()
if isLastInSection {
roundCorners(corners: [.bottomLeft, .bottomRight], radius: 16.0)
} else {
layer.mask = nil
}
contentView.layoutIfNeeded()
}
I have one UICollectionView with five custom cells that need to be render on specific conditions, cells are getting generated. Now problem I am going through is consider I have selected one cell at specific index, and now I scroll downwards and now again I long scroll. When UICollectionView stops scrolling, if the index is the same as which we selected, UICollectionView doesn't show that cell as selected. But now if I even try to move the cell a little bit, even a bit, UICollectionView shows that cell as selected cell.
Following is my code, that I have wrote in prefetchItem:
(cell as? PATemplateTypeOneCollectionCell)?.fillCellData(row: indexPath.row,section:indexPath.section, paCategoryQuestions: currentIndexQuestion, paQuestionCollection: currentIndexCollection)
cell!.alpha = 0.4
if self.multipleIndexPathsArray[indexPath.section][0] != []{
collectionView.selectItem(at: self.multipleIndexPathsArray[indexPath.section][0], animated: true, scrollPosition: .right)
}
else{
print("self.multipleIndexPathsArray[indexPath.section][0] is empty")
}
UICollectionViewCell:
override var isSelected: Bool {
didSet {
if isSelected {
self.alpha = 1.0
self.layer.borderWidth = 2.0
self.layer.borderColor = ColorConstants.colorFromHexString(hexString: paCategoryQuestions.selection_color).cgColor
}else {
self.alpha = 0.4
self.layer.borderWidth = 1.0
self.layer.borderColor = UIColor.lightGray.cgColor
}
}
Kindly request you guys to help with this issue.
So after hours of thinking I came across one solution. So as my cellForItem was not rendering selected scroll properly after long scroll, what I tried is recognizing UIScrollView's delegate scrollViewDidEndDecelerating. And recognizing if currently visible indexPath cells were selected previously or not by using my saved values array. Following is the code that worked for me:
fileprivate func checkIfCellIsSelected(){
for eachCell in afterPaymentPACollectionView.visibleCells{
let indexPath = afterPaymentPACollectionView.indexPath(for: eachCell)
for eachIndexSelected in multipleIndexPathsArray{
if eachIndexSelected.contains(indexPath!){
afterPaymentPACollectionView.selectItem(at: indexPath, animated: true, scrollPosition: .right)
}
}
}
}
//MARK: UIScrollViewDelegate
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
checkIfCellIsSelected()
}
I have UItablview with custom cell i need to change a label background colour when select this row, but the label colour is repeated when scroll down
You could subclass your Cell like this (and cellForRow then will not be responsible for updating the color, only for setting default color).
class YourTableViewCellClass: UITableViewCell {
#IBOutlet weak var yourLabel: UILabel!
override func setSelected(_ selected: Bool, animated: Bool) {
if(selected) {
self.contentView.backgroundColor = UIColor.red //or what you want as your cell bg color
self.yourLabel.backgroundColor = UIColor.green //or what you want
} else {
self.contentView.backgroundColor = UIColor.white //or what you want as your cell bg color
self.yourLabel.backgroundColor = UIColor.red //or what you want
}
} }
What I understand is, you put code in didSelectRow method of tableview to change the color, but it shows previous color while scrolling.
So,you need to set condition in cellForRow method also e.g.
if(condition)
{
lbl.textcolor = x
}
else
{
lbl.textcolor = y
}
I have UITableView with 2 UITextField elements in each row placed horizontally.
I want right UITextField automatically enlarge when I append text, but layout only happens when I end editing text.
I tried adding this code to UITableViewCell but it doesn't work
override func awakeFromNib() {
super.awakeFromNib()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(BillPositionCell.textDidChange), name: UITextFieldTextDidChangeNotification, object: self.sumTextField)
}
func textDidChange() {
self.sumTextField.setNeedsLayout()
self.sumTextField.layoutIfNeeded()
self.contentView.setNeedsLayout()
self.contentView.layoutIfNeeded()
}
Try:(width is sumTextField width NSLayoutConstraint)
func textDidChange() {
let newSize = sumTextField.sizeThatFits(CGSizeMake(CGFloat.max, CGFloat.max))
width.constant = newSize.width
layoutIfNeeded()
}
See this answer: textDidChange vs controlTextDidChange
In short, use controlTextDidChange: instead.
I'm trying to implement functionality in my practice app very similar to that of Snapchat, where you drag a UITableViewCell to the right or left, and as you're dragging, an image behind the view is slowly appearing until it reaches a certain point, and after it does, it starts a segue or PageViewController type segue to another view controller that you can use to chat with your friend.
At first, I tried using the screen edge pan gesture recognizer, but that only works if you start swiping from the edge of the screen. I need to be able to swipe from anywhere within the UITableViewCell.
A demonstration of my needed functionality is in Snapchat, where you see it more clearly when you slowly swipe right on one of your friend's table cell's and it slowly shows the image of a messaging icon and eventually leads to another view where you can chat.
So would a pan gesture recognizer be enough for this? If so, what would be the best method to follow to get this done? I've seen tutorials on pan gesture recognizers but I don't see how it could eventually lead to another view controller after swiping a certain distance. I think I could get away with putting the messaging icon behind the displayed table cell content that could appear while swiping right, but how could I implement such smooth functionality?
Learning this would really increase my experience in smooth user experience. Any advice or methods would be greatly appreciated, thanks.
EDIT: Please leave answers in Objective C, please. I don't know Swift.
You can create a custom TableViewCell for this. Code below should do what you're asking. Just need to add the delegate to your ViewController
protocol TableViewCellDelegate {
func something()
}
class TableViewCell: UITableViewCell {
var startSegue = false
var label: UILabel // Or UIView, whatever you want to show
var delegate: TableViewCellDelegate? // Delegate to your ViewController to perform the segue
required init(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
// Utility method for creating the label / view
func createLabel() -> UILabel {
let label = UILabel(frame: CGRect.nullRect)
// Add label customization here
return label
}
// Create "check" & "cross" labels for context cues
label = createLabel() // Create the label
label.text = "\u{2713}" // Add check symbol as text
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(label)
// Add a pan recognizer
var recognizer = UIPanGestureRecognizer(target: self, action: "handleSwipe:")
recognizer.delegate = self
addGestureRecognizer(recognizer)
}
let cueMargin: CGFloat = 10.0, cueWidth: CGFloat = 50.0
override func layoutSubviews() {
// The label starts out of view
label.frame = CGRect(x: bounds.size.width + cueMargin, y: 0, width: cueWidth, height: bounds.size.height)
}
func handleSwipe(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self)
if recognizer.state == .Changed {
center = CGPointMake(center.x + translation.x, center.y)
startSegue = frame.origin.x < -frame.size.width / 2.0 // If swiped over 50%, return true
label.textColor = startSegue ? UIColor.redColor() : UIColor.whiteColor() // If swiped over 50%, become red
recognizer.setTranslation(CGPointZero, inView: self)
}
if recognizer.state == .Ended {
let originalFrame = CGRect(x: 0, y: frame.origin.y, width: bounds.size.width, height: bounds.size.height)
if delegate != nil {
startSegue ? delegate!.something() : UIView.animateWithDuration(0.2) { self.frame = originalFrame }
}
}
}
// Need this to handle the conflict between vertical swipes of tableviewgesture and pangesture
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
let velocity = panGestureRecognizer.velocityInView(superview!)
if fabs(velocity.x) >= fabs(velocity.y) { return true }
return false
}
return false
}
}
I got this from this tutorial
EDIT:
In your ViewController you'll have to 'register' this TableViewCell by using:
yourTableName.registerClass(TableViewCell.self, forCellReuseIdentifier: "cellIdentifier")
And your TableViewDataSource for cellForRowAtIndexPath would look like:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier", forIndexPath: indexPath) as TableViewCell
cell.delegate = self
// Add stuff for your other labels like:
// "cell.name = item.name" etc
return cell
}
If you want to pass data back to the ViewController, just use the delegate.