UITextView inside tableview not resizing according to content - ios

I have a tableView and a UITextView inside it. I am trying to add data from JSON to the table view but it is not shrinking/expanding according to size. I have tried a lot of forums and methods.
The main problem is - if it starts expanding/shrinking with height, it stops shrinking/expanding and vice versa. Bot height and width are not working.
This is because, when I set the trailing and leading constraints of the UITextView to the cell, it starts working with height but stops working with width. When I remove the leading/trailing constraints, the content of the UITextView goes beyond the screen and does not come as multiline i.e. does not expand with height. I have tried -
How do I size a UITextView to its content?
How to resize table cell based on textview?
and a lot many like these.
A little of my code-
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Message") as! TextViewCell
cell.customTextView.textColor = UIColor.white
// Give a source to table view cell's label
cell.customTextView.text = requestResponseArr[indexPath.row]
cell.translatesAutoresizingMaskIntoConstraints = true
cell.sizeToFit()
if (indexPath.row % 2 == 0) {
cell.customTextView.backgroundColor = ConstantsChatBot.Colors.iMessageGreen
cell.customTextView.textAlignment = .right
} else {
cell.customTextView.backgroundColor = ConstantsChatBot.Colors.ButtonBlueColor
cell.customTextView.textAlignment = .left
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
And in viewDidLoad()-
override func viewDidLoad() {
// RandomEstimatedRowHeight
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 300
}
I do not want the textview to be editable. Please reply with swift as I am not familiar with Objective C. Thanks
EDIT: Custom cell code
class TextViewCell: UITableViewCell {
#IBOutlet weak var customTextView: UITextView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
customTextView.isScrollEnabled = false
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}

I have tried with UITextView inside UITableViewCell.
Constraints
UITextView, top, bottom and right as 2, 2 and 5, Width as 50. Font Size as 13, Alignment as Center. Give Outlet connection for Width constraints as txtVwWidthConst
UIViewController
#IBOutlet weak var tblView: UITableView!
var textWidthHeightDict = [Int : CGSize]()
override func viewDidAppear(_ animated: Bool) {
stringValue = [0 : "qwer\nasdasfasdf", 1 : "qwe", 2 : "123123\nasdas\nwqe", 3 : "q\n3\n4", 4 : "klsdfjlsdhfjkhdjkshfjadhskfjhdjksfhkdjsahfjksdhfkhsdfhjksdfjkasklsdfjlsdhfjkhdjkshfjadhskfjhdjksfhkdjsahfjksdhfkhsdfhjksdfjkasklsdfjlsdhfjkhdjkshfjadhskfjhdjksfhkdjsahfjksdhfkhsdfhjksdfjkas"]
for i in 0..<stringValue.count
{
GettingTextViewSize(getStr: stringValue[i]!, fontSize: 14, loopValue: i)
}
tblView.reloadData()
}
func GettingTextViewSize(getStr : String, fontSize: CGFloat, loopValue : Int)
{
var textSize = (getStr as! NSString).size(withAttributes: [NSAttributedStringKey.font : UIFont.systemFont(ofSize: fontSize)])
if textSize.width > self.tblView.frame.width
{
// IF STRING WIDTH GREATER THAN TABLEVIEW WIDTH
let multipleValue = textSize.width / self.tblView.frame.width
textSize.height = (textSize.height * (multipleValue + 1.0))
textSize.width = self.tblView.frame.width
textWidthHeightDict[loopValue] = textSize
}
else
{
textSize.height = textSize.height + 10 //ADDING EXTRA SPACE
textSize.width = textSize.width + 10 //ADDING EXTRA SPACE
textWidthHeightDict[loopValue] = textSize
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stringValue.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "table", for: indexPath) as! TblTableViewCell
cell.backgroundColor = cellBGColr[indexPath.row]
cell.txtVw.inputAccessoryView = toolBar
cell.txtVw.text = stringValue[indexPath.row]
cell.txtVw.tag = indexPath.row
cell.txtVwWidthConst.constant = (textWidthHeightDict[indexPath.row]?.width)!
cell.selectionStyle = .none
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
var heightVal = (textWidthHeightDict[indexPath.row])
return heightVal!.height + 8 //ADDING EXTRA SPACE
}
UITableViewCell
class TblTableViewCell: UITableViewCell {
#IBOutlet weak var txtVw: UITextView!
#IBOutlet weak var txtVwWidthConst: NSLayoutConstraint!
// TEXTVIEW WIDTH CONSTRAINTS
override func awakeFromNib() {
super.awakeFromNib()
}
}
Output
Note
UITextView only, we have to calculate String size. But, in UILabel, this concept is very simple. Let me know, if you have any queries.

Disable UITextView isScrollEnabled is false inside cellForRowAt
Demo Example

You need to disable UItextView Scrolling for that.
textView.isScrollingEnabled = false
and in the cell add top, bottom, right and left constraints of textview.
And add height constraint >= 10

You need to set all four constraints for textview i.e. leading, trailing, top and bottom lets say all are set to 8 from margin.
It will look something like below:
check demo on GitHub here

Related

UITableView scroll to wrong position when keyboard shows

Although UITableViewController can automatically adjust table view when keyboard shows, it's not flexible enough. I try to use a UIViewController and UITableView to build my UI.
There are many cells in the table view. Among all the cells, there is a cell with a UITextField. When I tap that text field, the keyboard shows and the table view does not do anything even if the cell is overlaid by the keyboard. It's OK because this is the expected result.
The strange thing comes. If I give the table view a large contentInset.bottom, e.g. contentInset.bottom = 600, the table view will automatically scroll when keyboard shows.
I try to aviod using tableView.contentInsetAdjustmentBehavior = .never.
The following code shows this strange behavior. It can be reproduced on iOS 14.5, iPhone 12 mini Simulator.
class TestViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let tableView = UITableView()
override func loadView() {
view = tableView
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
tableView.delegate = self
tableView.dataSource = self
tableView.contentInset.bottom = 600
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("scrolling contentOffset-Y: \(scrollView.contentOffset.y)")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Ugly code, only for showing the problem.
let cell = UITableViewCell()
if indexPath.row == 9 {
let textField = UITextField()
cell.contentView.addSubview(textField)
textField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textField.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor),
textField.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor),
textField.topAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.topAnchor),
textField.bottomAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.bottomAnchor),
])
} else {
cell.textLabel?.text = "\(indexPath)"
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
}
You should add UITextfield delegate to the textfield in the tableview cell to check if keyboard has been launched and let it check for you as shown by the example below:
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
var pointInTable:CGPoint = textField.superview!.convertPoint(textField.frame.origin, toView:_tableview)
var contentOffset:CGPoint = _tableview.contentOffset
contentOffset.y = pointInTable.y
if let accessoryView = textField.inputAccessoryView {
contentOffset.y -= accessoryView.frame.size.height
}
_tableview.contentOffset = contentOffset
return true;
}
Hope that one helps.

Dynamic section header height on runtime

I have UITableView in view controller with a section header and in the header, I have one UITextView with scroll disabled and pinned all UITextView edges to its super view.
Here is the code for automatic height change
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "CreatePostHeaderView") as? CreatePostHeaderView else {
return nil
}
return view
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableView.automaticDimension
}
And also set the estimated Height with this code
tableView.estimatedSectionHeaderHeight = 75
But on runtime when the text of UITextView exceeds height of 75, it doesn't change after that regardless of UITextView content. So, Do I need to add anything to make sure the table section header height changed according to UITextView content? Am I missing anything here?
When performing some action that changes the height of a cell (including header / footer cells), you have to inform the table view that the height has changed.
This is commonly done with either:
tableView.beginUpdates()
tableView.endUpdates()
or:
tableView.performBatchUpdates(_:completion:)
In this case, you want to call this when the text in your text view changes - easily done with a "callback" closure.
Here is an example of using a UITextView in a reusable UITableViewHeaderFooterView.
This will apply to loading a complex view from a XIB, but since this view is simple (only contains a UITextView), we'll do it all from code. This example uses 3 sections, each with 12 rows (default table view cells).
First, the table view controller class - no #IBOutlet or #IBAction connections, so just create a new UITableViewController and set its custom class to MyTestSectionHeaderTableViewController:
class MyTestSectionHeaderTableViewController: UITableViewController {
var myHeaderData: [String] = [
"Section 0",
"Section 1",
"Section 2",
]
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = 50
tableView.keyboardDismissMode = .onDrag
tableView.sectionHeaderHeight = UITableView.automaticDimension
tableView.estimatedSectionHeaderHeight = 75
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "defCell")
tableView.register(MySectionHeaderView.self, forHeaderFooterViewReuseIdentifier: MySectionHeaderView.reuseIdentifier)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return myHeaderData.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 12
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "defCell", for: indexPath)
c.textLabel?.text = "\(indexPath)"
return c
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: MySectionHeaderView.reuseIdentifier) as! MySectionHeaderView
v.myTextView.text = myHeaderData[section]
v.textChangedCallback = { txt in
self.myHeaderData[section] = txt
tableView.performBatchUpdates(nil, completion: nil)
}
return v
}
}
and this is the UITableViewHeaderFooterView class. Note that it needs to conform to UITextViewDelegate so we can tell the controller the text has changed (so it can update the height when needed), and we pass back the newly edited text to update our data source:
class MySectionHeaderView: UITableViewHeaderFooterView, UITextViewDelegate {
static let reuseIdentifier: String = String(describing: self)
var myTextView: UITextView = {
let v = UITextView()
v.isScrollEnabled = false
return v
}()
var textChangedCallback: ((String) -> ())?
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(myTextView)
myTextView.translatesAutoresizingMaskIntoConstraints = false
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
myTextView.topAnchor.constraint(equalTo: g.topAnchor),
myTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
myTextView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
myTextView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
])
myTextView.delegate = self
}
func textViewDidChange(_ textView: UITextView) {
guard let str = textView.text else {
return
}
textChangedCallback?(str)
}
}
The result:

Layout and sizing issues with TableViewCells that change height based on content inside - Swift 3

I'm building a simple messenger app that uses a tableview to display the messages. Each cell contains text and a stretchable background image. When messages are added to the tableview, they do change height to accommodate the text. However, whenever a single-line message is entered, the table view cell appears to be way too long for just a single line of text.
I think it has to do with the initial height and width of the tableviewcell, but I am not sure. How can I fix this to ensure the text bubble image encompasses the text but does not expand too much over it?
Screenshot of single and multi-lined texts:
Screenshot of long single-lined text for comparison:
I am using auto layout, if it helps.
ViewController code:
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
var texts: [String] = ["Hey, how are you?", "Good, you?", "Great!"]
var user: [Int] = [1, 0, 1]
let screenSize = UIScreen.main.bounds
#IBOutlet weak var tableView: UITableView!
#IBAction func sendMessage(_ sender: Any) {
if textBox.text != ""
{
let str:String = textBox.text
let retstr:String = insert(seperator: "\n", afterEveryXChars: 27, intoString: str)
let rand:UInt32 = arc4random_uniform(2)
addText(text: String(retstr), user: Int(rand))
}
}
#IBOutlet weak var textBox: UITextView!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.texts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (self.user[indexPath.row]==1)
{
let cell:CustomCell = self.tableView.dequeueReusableCell(withIdentifier: "cell") as! CustomCell
cell.myCellLabel.text = self.texts[indexPath.row]
cell.myCellLabel.textAlignment = NSTextAlignment.left
cell.myCellLabel.sizeToFit()
cell.myBackgroundImage.image = UIImage(named: "bubbleReversed")?.resizableImage(withCapInsets: UIEdgeInsetsMake(60, 50, 60, 50))
return cell
}
else
{
let cell:CustomCellOther = self.tableView.dequeueReusableCell(withIdentifier: "cell2") as! CustomCellOther
cell.myCellLabel.text = self.texts[indexPath.row]
cell.myCellLabel.textAlignment = NSTextAlignment.right
cell.myCellLabel.sizeToFit()
cell.myBackgroundImage.image = UIImage(named: "bubble")?.resizableImage(withCapInsets: UIEdgeInsetsMake(60, 50, 60, 50))
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("You tapped cell number \(indexPath.row).")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = 10.0
tableView.rowHeight = UITableViewAutomaticDimension
tableView.reloadData()
}
func addText(text:String, user:Int)
{
if (self.texts.count > 20)
{
self.texts.remove(at: 0)
self.user.remove(at: 0)
}
self.texts.append(text)
self.user.append(user)
tableView.reloadData()
let indexPath = NSIndexPath(row: self.texts.count-1, section: 0)
tableView.scrollToRow(at: indexPath as IndexPath, at: .top, animated: true)
}
func insert(seperator: String, afterEveryXChars: Int, intoString: String) -> String {
var output = ""
intoString.characters.enumerated().forEach { index, c in
if index % afterEveryXChars == 0 && index > 0 {
output += seperator
}
output.append(c)
}
return output
}
}
My tableviewcell classes just contain a UIImageView and a label.

Gap appears between header and cell when resize tableview header height in scrollViewDidScroll

Originally the headerView's size is the same as the screen bound's size, and the first cell's top aligns with the headerView's bottom. I tried to resize the UITableView headerView height in the scrollViewDidScorll method, but there was a gap appearing when I scrolled. It seems there are some default constraints between the headerView and the cell.
I have changed the background color of my header view to blue, and print out the height after I scroll down. It seems the header view's height is updated correctly, but the gap enlarged as my headerView height becomes smaller.
Here is my code snippet
class ProfileTableViewController: UITableViewController {
#IBOutlet weak var headerView: UIView!
#IBOutlet weak var backgroundContainerView: UIView!
#IBOutlet weak var backgroundImageView: UIImageView!
#IBOutlet weak var overlayView: UIView!
#IBOutlet weak var backgroundContainerViewTopSpace: NSLayoutConstraint!
var backgroundImageViewOriginHeight: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = self.tableView.rowHeight
self.tableView.rowHeight = UITableViewAutomaticDimension
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.headerView.frame.size = UIScreen.main.bounds.size
self.backgroundImageViewOriginHeight = self.headerView.frame.size.height
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0{
self.headerView.frame.origin.y = scrollView.contentOffset.y
self.headerView.frame.size.height = self.backgroundImageViewOriginHeight - scrollView.contentOffset.y
}else{
self.headerView.frame.size.height = self.backgroundImageViewOriginHeight - scrollView.contentOffset.y
print(self.headerView.frame.size.height)
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIden, for: indexPath) as! Mycell
cell.setNeedsLayout()
cell.layoutIfNeeded()
return cell
}
}
The header height update from the console
574.5
572.5
571.5
569.0
568.0
567.0
565.5
564.5
563.5
562.5
561.5
560.0
Any help and suggestion is welcome
In your scrollViewDidScroll, under the else, you are not setting the self.headerView.frame.origin.y to scrollView.contentOffset.y. If you are only changing the height of the view, the views Y will remain the same, but as the contentView of the scrollview is moving, it will move up.
You will need to set self.headerView.frame.origin.y to scrollView.contentOffset.y. This will cause the view to move with the scrolling at the top of the window.
Add the line
self.headerView.frame.origin.y = scrollView.contentOffset.y
in the else to fix this problem.

When I swipe a UITableViewCell, the header view moves as well

So, I have a few Swipe actions like delete, block, etc in my UITableView. I wanted to add headers to separate my two sections. So, I added a prototype cell, named it HeaderCell and then went to the view. I added one label, named headerLabe. My problem is that when I swipe for the actions, the header cells were moving as well, which looked bad. I researched, and found a solution to just return the contentView of the cell. However, when I do this, the label has not shown up. I have tried a dozen different solutions, and nothing has worked, so I have turned to SO. Can anyone help me?
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell : CustomHeaderTableViewCell = tableView.dequeueReusableCellWithIdentifier("HeaderCell") as! CustomHeaderTableViewCell
if section == 0 {
headerCell.headerLabel.text = "Thank You's"
} else if section == 1 {
headerCell.headerLabel.text = "Conversations"
}
return headerCell.contentView
}
Thanks so much.
You can use a section Header as #ozgur suggest.If you still want to use a cell.
Refer to this datasource method
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
if indexPath = YourHeaderCellIndexPath{
return false
}
return true
}
check the following methods
In your UIViewController use the following
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 60
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCellWithIdentifier("HeaderCell") as! WishListHeaderCell
headerCell.lblTitle.text = cartsData.stores_Brand_Name
let imgVw = UIImageView()
imgVw.frame = CGRectMake(8, 18, 25, 25)
imgVw.image = UIImage(named: "location.png")
let title = UILabel()
title.frame = CGRectMake(41, 10, headerCell.viwContent.frame.width - 49, 41)
title.text = cartsData.stores_Brand_Name
title.textColor = UIColor.whiteColor()
headerCell.viwContent.addSubview(imgVw)
headerCell.viwContent.addSubview(title)
return headerCell.viwContent
}
In your UITableViewCell use the following
import UIKit
class HeaderCell: UITableViewCell {
#IBOutlet weak var viwContent: UIView!
#IBOutlet weak var imgIcn: UIImageView!
#IBOutlet weak var lblTitle: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
self.viwContent.backgroundColor = UIColor.grayColor()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//UITableViewCell
let headerCell = tableView.dequeueReusableCellWithIdentifier("headerCell") as! SecJobCCHeaderTableViewCell
// Cell Rect
var cellRect : CGRect = headerCell.frame
cellRect.size.width = screenBounds.width
// Header Footer View
let headerFooterView = UITableViewHeaderFooterView(frame : cellRect)
//Adding Gesture
let swipeGestRight = UISwipeGestureRecognizer(target: self, action:#selector(AddSecJobCostCentreViewController.draggedViewRight(_:)))
swipeGestRight.enabled = true
swipeGestRight.direction = UISwipeGestureRecognizerDirection.Right
headerFooterView.addGestureRecognizer(swipeGestRight)
// Update Cell Rect
headerCell.frame = cellRect
// Add Cell As Subview
headerCell.tag = 1000
headerFooterView.addSubview(headerCell)
// Return Header Footer View
return headerFooterView
}
func draggedViewRight(sender:UISwipeGestureRecognizer) {
// Swipe Gesture Action
let currentHeaderView = sender.view?.viewWithTag(1000) as! SecJobCCHeaderTableViewCell
}

Resources