Automatic scrolling animation of the text in a UITableViewCell - ios

I have a long string:
"A very long string"
that won't be displayed fully in a UITableView, i.e. it gets truncated to:
"A very long s..."
Is it possible to have this text in a UITableViewCell automatically scroll horizontally and indefinitely so that it loops around? Perhaps this can be achieved using an attributed string?
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath)
cell.textLabel?.text = "A very long string"
cell.textLabel?.textAlignment = .center
Thanks in advance for any help with this!
EDIT (my attempt at a solution proposed)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath)
let frame = CGRect(x: 0.0, y: 0.0, width: cell.bounds.width * 2.0, height: cell.bounds.height)
let scrollView = UIScrollView(frame: frame)
cell.addSubview(scrollView)
let label = UILabel(frame: scrollView.bounds)
scrollView.addSubview(label)
label.text = "some very long text that doesn't quite fit"
label.textAlignment = .center
let newFrame = CGRect(x: label.frame.width, y: 0, width: label.frame.width, height: scrollView.frame.height)
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.repeat, .autoreverse], animations:( { void in
scrollView.scrollRectToVisible(newFrame, animated: true)}),
completion: ({ completed in }))
}

Scrollview inside a table view is a bad idea. Instead you can enable the automaticDimension and estimated row height and tableView will manage the cell height automatically.
self.tableView.estimatedRowHeight = 44
self.tableView.allowsMultipleSelection = true
So as per the content each cell will manage its own height.
Hope this would be helpful!

You would have to make a custom tableview cell, which has a custom scrollview which handles UILabel scrolling. You would need to assign the text and measure the width. If it is wider than the containing view then add a second duplicate label to its right. You then need to animate the scrollview to the left by the width of the label. On complete, jump back to offset 0,0 and animate again.

Usually, to have special effect as such will require customization of an UILabel. To make my life easier, I will always try to search if there is already a working library out there for me to consume.
A simple Google search brings me MarqueeLabel, hope it helps!

Related

UILabel not breaking lines in UITableViewCell

I'm building an app with a messenger like interface. I use a tableView to accomplish this. Each cell contains a UIView - the message bubble and a UILabel - the message that is nested in the UIView.
It works great on texts of small sizes but for some reason when the UILabel is supposed to break lines it doesn't and it all is in one line. The amount of lines is set to zero.
This is my message handling class:
func commonInit() {
print(MessageView.frame.height)
MessageView.clipsToBounds = true
MessageView.layer.cornerRadius = 15
myCellLabel.numberOfLines = 0
let bubbleSize = CGSize(width: self.myCellLabel.frame.width + 28, height: self.myCellLabel.frame.height + 20)
print(bubbleSize.height)
MessageView.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: bubbleSize.width, height: bubbleSize.height)
if reuseIdentifier! == "Request" {
MessageView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMinXMaxYCorner]
MessageView.backgroundColor = UIColor(red: 0, green: 122/255, blue: 1.0, alpha: 1.0)
} else {
MessageView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner]
MessageView.backgroundColor = UIColor.lightGray
}
}
Cell calling function:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if queryCounter % 2 == 0 && indexPath.row % 2 == 0{
cellReuseIdentifier = "Answer"
} else {
cellReuseIdentifier = "Request"
}
let cell:MessageCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! MessageCell
cell.myCellLabel.textColor = UIColor.white
cell.myCellLabel.text = self.messages[indexPath.row]
let height = cell.myCellLabel.text!.height(withConstrainedWidth: cell.myCellLabel.frame.width, font: cell.myCellLabel.font)
print(height)
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
return cell
}
The height variable is calculated based on text size. It shows that the text size is calculated normally - accounting for line break.
I was unable to modify the cell height based on this calculation - nothing I tried works.
I think it might be a constraints issue.
My Constraints:
How do I make the lines break? Please help.
EDIT: I just notice that the MessageView.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: bubbleSize.width, height: bubbleSize.height) has no affect what so ever on the message bubbles.
Setting the frame while using autolayout won't work.
I can't say what exactly happens here without the entire context, but some common pitfalls when reusing cells and autolayout are:
Forgetting to set automatic height for your cells (expanding cell in a storyboard manually will override this setting)
Tableview also needs estimatedHeight sometimes to work properly
Sometimes you need to call setNeedsLayout after you add content to
the cell
Check the console and if there are some warnings about breaking constraints, you can easily find issues there.
Try to find the label height based on label width and text font and then set your label height constraint to that.
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil)
return ceil(boundingBox.height)
}
}
something like this:
let textHeight = yourtext.height(withConstrainedWidth: yourlabel.frame.width, font: font)
yourLabel.heightAnchor.constraint(equalToConstant: textHeight).isActive = true
After multiple hours of trying everything I managed to fix it. The problem was with the constraints.
1. As you can see the in this old layout. The UIView was constrained everywhere except the left -> that's where the text goes.
The commonInit() method of the UITableViewCell was called before any text was initialized. That's not good because all of the cell resizing is based on text which was not yet passed to the cell -> Move the method after cell initialization.
let cell:MessageCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! MessageCell
cell.myCellLabel.text = self.messages[indexPath.row]
//Before calling commonInit() we need to adjust the cell height.
let height = cell.myCellLabel.text!.heightForView(text: cell.myCellLabel.text!, font: cell.myCellLabel.font, width: self.view.frame.width / 2)
// Then we set the width of the UILabel for it to break lines at 26 characters
if cell.myCellLabel.text!.count > 25 {
tableView.rowHeight = height + 20
cell.myCellLabel.widthAnchor.constraint(equalToConstant: cell.frame.width / 2).isActive = true
cell.updateConstraints()
}
// Calling commonInit() after adjustments
cell.commonInit()
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
return cell
Then we need to update the constraints so that the UIView and UILabel resize with the cell height.
Done. Now it works as needed. Thank you for all of the suggestions!

Swift: Continuous resizing of tableViewCell while user types in a UITextView

Introduction
Context:
I'm creating one of my first apps but I've ran into an issue I cannot figure out.
I have tableView with cells packed with quite a few UIElements. All constraints are done using the EasyPeasy library which basically just sets auto layout constraints ( I have tried setting them manually also). The UITextView in question is constrained by various numbers to the left, right, top and bottom, I have no constraints on it for height or width.
in cellForRowAt indexPath: I set the textView delegate for each cells textView to self, using a delegate property declared within the cells custom class. I also tag every textView with its cells indexPath.row (gives textView.tag integer in textViewDidChange method).
Issue/acknowledgments:
After browsing SO a lot I've found a few questions alike this but I have not been able to make them work for me, I have implemented parts of them that felt logic to my case. I believe the problem differencing my situation from those questions lies in that for my cell design to work the cells has to have a height of itemHeight or higher.
I have noticed that as I type into the textview the textview itself increases in height (even below the cells border but its not visible as it reaches that point), however the cell itself doesn't resize.
I've tried with a cell that only contains a textView so the problem must lie in the textViewDidchange or heightForRowAt indexPath methods.
Question:
What am I doing wrong here? Why doesn't the cells height change dynamically as I type in the textView?
Code:
func textViewDidChange(_ textView: UITextView) {
var newframe = textView.frame
newframe.size.height = textView.contentSize.height - textView.frame.size.height + itemHeight[textView.tag]
textView.frame = newframe
let ndxPath = IndexPath(row: textView.tag, section: 0)
let cell = tableView.cellForRow(at: ndxPath) as! EventsCell
cell.frame = CGRect(x: cell.frame.origin.x, y: cell.frame.origin.y, width: cell.frame.width, height: textView.frame.height)
tableView.beginUpdates()
tableView.setNeedsLayout() //have tried without this line
tableView.layoutIfNeeded() //have tried without this line
tableView.endUpdates()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if UITableViewAutomaticDimension > itemHeight[indexPath.row] {
return UITableViewAutomaticDimension
} else {
return itemHeight[indexPath.row]
}
}
TextView constraints:
let containerView : UIView = {
let cv = UIView(frame: .zero)
cv.backgroundColor = .white
cv.translatesAutoresizingMaskIntoConstraints = false
cv.layer.cornerRadius = 7
return cv
}()
let eventText : GrowingTextView = { // GrowingTextView is a extension to a regular UITextView
let tv = GrowingTextView()
tv.allowsEditingTextAttributes = true
tv.isScrollEnabled = false
var delegate: UITextViewDelegate?
tv.textContainerInset = UIEdgeInsetsMake(1, 1, 0, 1)
tv.autoresizingMask = .flexibleHeight
tv.translatesAutoresizingMaskIntoConstraints = false
return tv
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
containerView.addSubview(eventText)
contentView.addSubview(containerView)
containerView .easy.layout([Height(CGFloat(95 * itemCount)), Left(8), Right(8)])
eventText .easy.layout([Left(77), Right(5), Top(90), Bottom(4)])
}
Thanks for reading my post.
The constraints that determine the height should be laid out in such a way that the textView is attached directly to the top and bottom of the contentView or to views which are connected to the top and bottom of the contentView, so that autolayout can make out the height by connecting the constraints.
Make sure that you do not mention a height for the textView and disable scrolling. Let automatic dimension take care of all that.
Now all you need to do is call tableView.beginUpdates() and tableView.endUpdates() on textViewDidChange
Here is my repo which demonstrates the same.
OP Edit:
You should store the additional height that you add in a variable in the cell class so the cells can reload an appropriate height when the tableVIew is reloaded.
You should also change textViewDidChange method
cell.frame = CGRect(x: cell.frame.origin.x, y: cell.frame.origin.y, width: cell.frame.width, height: textView.frame.height)
to
let newFrame = ”originalCellHeight” - ”originalTextViewHeight” + textView.contentSize.height
cell.frame = CGRect(x: cell.frame.origin.x, y: cell.frame.origin.y, width: cell.frame.width, height: newFrame )`

Animated table-view cell when the user picks up with his finger (Swift 3)

I'm trying to make this animation
so i want that when the user picks up with his finger the cell get smaller and then when it stops pressing come back on the original size, important is that i'm not using a custom cell but the default UITableViewCell, here is my code with whom i tried to do the animation (after watching old question and tutorial)
var originalCellSize: CGRect!
func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) {
print("I'm Highlighted at \(indexPath.section)")
guard let cell = tableView.cellForRow(at: indexPath) else {
return
}
originalCellSize = cell.frame
UIView.animate(withDuration: 0.1, animations: {
cell.frame = CGRect(x: cell.frame.origin.x + 20, y: cell.frame.origin.y, width: cell.frame.width - 40, height: cell.frame.height)
})
}
func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) else {
return
}
UIView.animate(withDuration: 0.1, animations: {
cell.frame = CGRect(x: 0, y: self.originalCellSize.origin.y, width: self.view.bounds.width, height: 180)
})
originalCellSize = .zero
}
i don't know why but something is wrong and the animation do not work, can someone help me (it also happens that after pushing on the cell this changes position in a definitive way)
Don't manipulate UITableViewCell's frame directly.
Use it's transform property.
Likewise:
Scale down the cell upon highlight / selection.
UIView.animate(withDuration: 0.3) {
cell.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
cell.layoutIfNeeded()
}
Scale back to normal:
UIView.animate(withDuration: 0.3) {
cell.transform = CGAffineTransform.identity
cell.layoutIfNeeded()
}
Don't forget to call cell.layoutIfNeeded() before the animation block and inside (right after assigning the CGAffineTransform).
You can approach the problem in a different way. You can resize a view inside the Table view cell.
Like you can keep a view with white color and cell background as grey color. In highlight delegate method, you can change the frame of UIView inside the cell and on unhighlight method, you can resize to back to cell size.
WhenEver You want to change layout of cell you must need to call cell.layoutIfNeeded().
Set flag that cell should change size.
Add flag check for heightForRowAtIndexPath
On highlighting use:
table.beginUpdates()
table.reloadRowsAt(...)
table.endUpdates()
Should work just fine.
I'm not sure you can just change frame of the cell. So you can also try try to change cell's content view size. Or your custom view frame itself.

UICollectionView Refresh Issue

Using UICollectionView I add a Label to a cell as a subView with the scroll direction set to horizontal. Inside the Label I add a button whose background is an image. For some odd reason if I scroll the view in one direction and then come back the buttons image seems to either have left remnants to the right of the button. Either that or their is another button slightly shifted to the right under the initial buttons. I have realized the more I played around with the buttons the further right they shift. Any assistance would be appreciated
The issue does not seem to occur if the label is shorter
var padding = String(count: 2, repeatedValue: (" " as Character))
let newLabel = UILabel(frame: CGRectZero)
newLabel.autoresizingMask = .FlexibleHeight
newLabel.backgroundColor = UIColor(red: 56/255, green: 143/255, blue: 212/255, alpha: 1)
newLabel.text = "\(padding)\(title)\(padding)"
newLabel.textColor = UIColor.whiteColor()
let fontName: CFStringRef = "Superclarendon-Regular"
newLabel.font = CTFontCreateWithName(fontName, 15, nil)
newLabel.adjustsFontSizeToFitWidth = true
newLabel.clipsToBounds = true
newLabel.layer.cornerRadius = 4
//Fit TO Text
newLabel.numberOfLines = 1
newLabel.sizeToFit()
//Add Button
if let image = UIImage(named: "Nav_Button_X"){//?//.CGImage
let button = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
var imageWidth = image.size.width
var imageHeight = image.size.height
//Resize label for button
var oldLabelFrame = newLabel.frame
var buttonHeight = self.frame.height - self.sectionInsets.top - self.sectionInsets.bottom
var buttonWidth = newLabel.frame.width + imageWidth
//newLabel.frame = CGRect(x: oldLabelFrame.origin.x, y: oldLabelFrame.origin.y, width: oldLabelFrame.width, height: bh)
newLabel.frame.size = CGSize(width: buttonWidth, height: buttonHeight)
button.frame = CGRectMake(newLabel.frame.width - imageWidth, 0, imageWidth, newLabel.frame.height)
button.setBackgroundImage(image, forState: UIControlState.Normal)
newLabel.addSubview(button)
}
return newLabel
}
EDIT:
I have tried recreating the view using only a button and NSMutableAttributedString for styling, which may or may not be a better solution shrugs, and the issue persists so it may not be an issue of how I construct the button. Are there suggestions?
I subclass and set up the UICollectionView like so
let flowLayout:UICollectionViewFlowLayout = UICollectionViewFlowLayout();
flowLayout.scrollDirection = UICollectionViewScrollDirection.Horizontal
super.init(frame: CGRectMake(0, 0, width, height), collectionViewLayout: flowLayout);
self.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier);
self.autoresizingMask = UIViewAutoresizing.FlexibleWidth
self.backgroundColor = UIColor.whiteColor()
self.bounces = true
self.layer.cornerRadius = 5
self.scrollEnabled = true
self.delegate = self;
self.dataSource = self;
self.backgroundColor = UIColor.whiteColor();
and
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell:UICollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! UICollectionViewCell;
cell.backgroundColor = UIColor.clearColor();
let cellItem = subCategories[indexPath.row]
cell.contentView.addSubview(cellItem)
// Configure the cell
return cell
}
Another interesting trait is that I removed the button from odd numbered labels and found that when I perform the action to get the issue the buttons appear on all of the labels
This looks like your cells are being reused and they show views from old cells.
When you call dequeueReusableCellWithReuseIdentifier, the method returns a cell that is not visible anymore in the UICollectionView, but it doesn't clear it's content.
Let's say you have cells A, B and C visible, you add subviews to all of them (with cell.contentView.addSubview(cellItem)). When you scroll until cell A isn't visible anymore, and you call dequeueReusableCellWithReuseIdentifier to get a new cell (call it D), you'll reuse cell A, so it'll have the subviews you added before to cell A, and the subviews you add now to cell D.
To get rid of this, you have two options:
If you're using a custom UICollectionViewCell, you should override prepareForReuse method. There you should remove all contentView's subviews.
If you're using a UICollectionViewCell, you should remove all contentView's subviews before calling cell.contentView.addSubview(cellItem)
If you want to see if you're adding the button multiple times to reused cells, you can reproduce the issue and use the View Hierarchy inspector:
I created a small project to illustrate the issue of "dirty" cells: https://github.com/lucaslt89/DirtyUICollectionViewCellsExample
In my case, I created the view hierarchy in the init method of the cell.
When scrolling slowly, the cells along with the multiline labels would appear perfectly. However, when scrolling too quickly, labels would have the size of the reused cell's label, thus appearing very bad.
I was setting preferredMaxLayoutWidth correctly, but nevertheless the cells would have issues on fast scrolling.
My problem was solved by calling setNeedsLayout inside prepareForReuse:
override func prepareForReuse() {
setNeedsLayout()
super.prepareForReuse()
}

change width of tableviewcell swift

I have a tableView using IB with custom cells and prototype cells.
I'm trying to make the cells a little shorter in width than the tableView.frame to leave a little space between the left and right corners.
var cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as TableViewCell
cell.bounds = CGRectMake(10, self.tableView.frame.origin.y, 30, self.tableView.frame.size.height)
cell.layer.bounds = CGRectMake(10, self.tableView.frame.origin.y, 30, self.tableView.frame.size.height)
cell.textLabel?.bounds = CGRectMake(10, self.tableView.frame.origin.y, 30, self.tableView.frame.size.height)
Update: here is a good example explaining how to add a subView to your tableView.
http://natashatherobot.com/ios-frame-vs-bounds-resize-basic-uitableview-cell/
Update 2: Looks like there isn't an easy way to do this. There are 3 ways of achieving this as far as I know:
Add a rounded and a shorter image to your cell that has the same exact color and matches your background.
You could subclass tableViewCell and then play with the layoutSubviews, this way you can make it shorter before it draws the cell. I've done it but the scrolling performance sucks.
The best way is to ditch the tableView altogether and re-do it with a collectionView.
The cells in the tableview are supposed to be as wide as their container.
If you need your cells to have a different width than the table view, I would suggest adding a view as subview to cell.contentView and make that view as wide as you need while making sure the contentView has clear background and no separator and all (so that it appears it is not there).
Another solution would be to have the tableView not as wide as it's superview by adding the left/right padding to it. But the you would have the issue that on the left and right side, where the padding is, you won't be able to scroll the tableView
I consider the cleanest solution to use a collectionView. It is not that much different than a tableView and you can configure the entire size of the cell, not just the height.
Hope this helps you fix your problem. Let me know if you need more help.
Swift 3:
For people still looking for a good solution, there's an easier and more effective alternative to using layoutSubviews or re-doing the whole thing with collectionView.
If you have already subclassed the tableViewCell, then in your TableViewController class you can add this to add a plain white border to each side of the table view cell.
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// make sure in storyboard that your cell has the identifier "cell" and that your cell is subclassed in "TableViewCell.swift"
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! TableViewCell
// the following code increases cell border on all sides of the cell
cell.layer.borderWidth = 15.0
cell.layer.borderColor = UIColor.white.cgColor
return cell;
}
If you want to add different sized borders to each side of the cell, you can also do this:
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! TableViewCell
// the following code increases cell border only on specified borders
let bottom_border = CALayer()
let bottom_padding = CGFloat(10.0)
bottom_border.borderColor = UIColor.white.cgColor
bottom_border.frame = CGRect(x: 0, y: cell.frame.size.height - bottom_padding, width: cell.frame.size.width, height: cell.frame.size.height)
bottom_border.borderWidth = bottom_padding
let right_border = CALayer()
let right_padding = CGFloat(15.0)
right_border.borderColor = UIColor.white.cgColor
right_border.frame = CGRect(x: cell.frame.size.width - right_padding, y: 0, width: right_padding, height: cell.frame.size.height)
right_border.borderWidth = right_padding
let left_border = CALayer()
let left_padding = CGFloat(15.0)
left_border.borderColor = UIColor.white.cgColor
left_border.frame = CGRect(x: 0, y: 0, width: left_padding, height: cell.frame.size.height)
left_border.borderWidth = left_padding
let top_border = CALayer()
let top_padding = CGFloat(10.0)
top_border.borderColor = UIColor.white.cgColor
top_border.frame = CGRect(x: 0, y: 0, width: cell.frame.size.width, height: top_padding)
top_border.borderWidth = top_padding
cell.layer.addSublayer(bottom_border)
cell.layer.addSublayer(right_border)
cell.layer.addSublayer(left_border)
cell.layer.addSublayer(top_border)
return cell;
}
Hope this helps.
I found the other answers unhelpful/incorrect, but found what I needed in one of the answers here:
How to set the width of a cell in a UITableView in grouped style
The key is, if you have a custom cell (which OP has, and most apps would have anyway), then you can override the setFrame method to whatever width you need. No need to redesign your app or do anything tricky.

Resources