Good afternoon.
I've struggled with this "feature" for my iOS app for some hours now and I'm in need of some help.
Question: How should I implement so that when a user types in the UITextView
increases in size (only the bottom margin) and the cell increases its height to fit the UITextView
dynamically meanwhile? I can't figure out how to tackle it.
Research: After some searching I came upon Dynamically change cell's height while typing text, and reload the containing tableview for resize as well as Can you animate a height change on a UITableViewCell when selected? I've also read https://developer.apple.com/documentation/uikit/uicollectionview but I feel my swift knowledge so far is inferior to whats expected in these posts.
In other words I cant figure out how to implement it for my UICollectionView.
My code at moment looks like this (I have more UI elements in the EventsCell but cut them out to save space, thus its only the bottom margin of the UITextView
I want to change). I have set the RootCell as delegate
for my UITextView
and then i intend to grab the textView height as it is edited in order to get the height I want, but it isn't working.
Cell class:
class EventsCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
setupViews()
}
let textView: GrowingTextView = { // GrowingTextView is a cocoapod extending UITextView
let rootCellDelegate = RootCell()
let tv = GrowingTextView()
tv.backgroundColor = .clear
tv.allowsEditingTextAttributes = true
tv.isScrollEnabled = false
tv.font = UIFont(name: "Didot", size: 16)
tv.textColor = .black
tv.textContainerInset = UIEdgeInsetsMake(4, 4, 4, 6)
tv.placeholder = "Write your event text here"
tv.placeholderColor = UIColor.lightGray
tv.autoresizingMask = .flexibleHeight
tv.isUserInteractionEnabled = false
tv.delegate = rootCellDelegate
return tv
}()
let eventPlaceholderMarkImage: UIButton = {
let iv = UIButton()
let image = UIImage(named: "placeHolderEventTitleMark")
iv.setImage(image, for: .normal)
return iv
}()
func setupViews() {
//MARK: - Constraints for EventsCell
//horizontal
addConstraintsWithFormat(format: "H:|-16-[v0(\(frame.width - 32))]-16-|", views: textView)
addConstraintsWithFormat(format: "V:|-47-[v0]-16-|", views: textView)
addConstraint(NSLayoutConstraint(item: eventPlaceholderMarkImage, attribute: .bottom, relatedBy: .equal, toItem: textView, attribute: .top, multiplier: 1, constant: -7))
addConstraint(NSLayoutConstraint(item: eventPlaceholderMarkImage, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 0, constant: 20))
addConstraintsWithFormat(format: "H:|-12-[v0(27)]-\(cellWidth - 39)-|", views: eventPlaceholderMarkImage)
}
}
UICollectionView class:
class RootCell: BaseCell, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var textViewHeight : CGFloat?
func textViewDidChange(_ textView: UITextView) {
textViewHeight = textView.textContainer.size.height + 63
collectionView.reloadInputViews()
collectionView.reloadData()
collectionView.layoutSubviews()
}
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor.white
cv.delegate = self
cv.dataSource = self
return cv
}()
override func setupViews() {
super.setupViews()
addSubview(collectionView)
addConstraintsWithFormat(format: "H:|[v0]|", views: collectionView)
addConstraintsWithFormat(format: "V:|[v0]|", views: collectionView)
collectionView.register(EventsCell.self, forCellWithReuseIdentifier: EventCellID)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: EventCellID, for: indexPath)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let height = textViewHeight ?? frame.width * 1/3
let width = frame.width - 8
return CGSize(width: width, height: height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
//MARK: - Design selected cells background
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? EventsCell {
cell.textView.isUserInteractionEnabled = true
cell.eventTitleLabel.isUserInteractionEnabled = true
cell.layer.borderColor = UIColor.flatBlack.cgColor
cell.layer.borderWidth = 2
cell.layer.cornerRadius = 7
cell.backgroundColor = GradientColor(.topToBottom, frame: cell.frame, colors: [UIColor.flatRed.withAlphaComponent(0.2), UIColor.white])
} else {return}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) {
cell.layer.borderColor = UIColor.clear.cgColor
cell.layer.borderWidth = 0
cell.layer.cornerRadius = 0
cell.backgroundColor = .white
} else {return}
}
}
Attempt after first answer:
class RootCell: BaseCell, UITextViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var textViewText : String?
func textViewDidChange(_ textView: UITextView) {
textViewText = textView.text
DispatchQueue.main.async {
self.collectionView.reloadInputViews()
self.collectionView.reloadData()
}
}
func heightWithConstrainedWidth(width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
guard let boundingBox = textViewText?.boundingRect(with: constraintRect, options: [NSStringDrawingOptions.usesLineFragmentOrigin], attributes: [NSAttributedStringKey.font: font], context: nil) else {return frame.width * 1/3}
return boundingBox.height
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = frame.width - 8
let height = heightWithConstrainedWidth(width: width, font: UIFont(name: "Didot", size: 16)!)
return CGSize(width: width, height: height)
}
Thank you for reading my post!
Reloading the collection view or the individual cell will flicker the cells and the user would lose focus on the TextView as they type. If the user loses focus, they have to click on the UITextView again to keep on typing.
When you detect that your UITextView needs resizing, you can invalidate your collectionview's layout - which will cause your cells to be resized if your auto-layout parameters are all correct. See example below:
I have a UICollectionCell with only UITextView inside of it, and scrolling on the UITextView is disabled. UITextView is properly constrained to all edges of the cell using AutoLayout. Finally, the cell is also the UITextView's delegate as such:
class MyCollectionCell: UICollectionViewCell {
@IBOutlet private weak var myTextView: UITextView!
//this local variable will be used to determine if the intrinsic content size has changed or not
private var textViewHeight: CGFloat = .zero
...
...
override func awakeFromNib() {
super.awakeFromNib()
myTextView.delegate = self
}
...
...
}
Conform this cell to UITextViewDelegate
extension MyCollectionCell: UITextViewDelegate {
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
//this assumes that collection view already correctly laid out the cell
//to the correct height for the contents of the UITextView
//textViewHeight simply needs to catch up to it before user starts typing
let fittingSize = textView.sizeThatFits(CGSize(width: myTextView.frame.width, height: CGFloat.greatestFiniteMagnitude))
textViewHeight = fittingSize.height
return true
}
func textViewDidChange(_ textView: UITextView) {
//flag to determine whether textview's size is changing
var shouldResize = false
//calculate fitting size after the content has changed
let fittingSize = textView.sizeThatFits(CGSize(width: myTextView.frame.width, height: CGFloat.greatestFiniteMagnitude))
//if the current height is not equal to the fitting height
//save the new fitting height to our local variable and inform the delegate
//that collection view needs resizing
if textViewHeight != fittingSize.height {
shouldResize = true
//save the new height
textViewHeight = fittingSize.height
}
//notify the cell's delegate (most likely a UIViewController)
//that UITextView's intrinsic content size has changed
//perhaps with a protocol such as this:
delegate?.textViewDidChange(newText: textView.text, alsoRequiresResize: shouldResize)
}
Then when the delegate receives the notification, perhaps saves the updated text and updates the layout such as this:
myCollectionView.collectionViewLayout.invalidateLayout()
The best part of this solution is that you do not have to invoke collectionView(_:layout:sizeForItemAt:).
Cheers!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With