I'm currently in the process of building a social networking app that displays the user's posts in a table. Each post will be displayed in one cell of the table. Because I don't know how much the user will type for one post, I need to set the text view size according to the amount of text typed by the user. The problem is that my current implementation is not working and behaving strangely.
Here is my table code:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let cell : UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("HomeCell") as? UITableViewCell
let post : Post = tblData[indexPath.row]
//Get all custom cell components
let userLbl = cell?.viewWithTag(101) as UILabel
let timeLbl = cell?.viewWithTag(102) as UILabel
let postLbl = cell?.viewWithTag(103) as UITextView
let profilePic = cell?.viewWithTag(100) as UIImageView
let postPic = cell?.viewWithTag(104) as UIImageView
//Post lbl properties
let textHeight = textViewDidChange(postLbl)
postLbl.frame.size.height = CGFloat(textHeight)
//postLbl.selectable = false
//Individual height variables
let postInfoHeight = 66 as CGFloat
var postHeight = 8 + CGFloat(textHeight)
var imgHeight = 8 + postPic.frame.height as CGFloat
if post.postInformation == "" {
postLbl.removeFromSuperview()
postHeight = 0
}
if post.img == nil {
postPic.removeFromSuperview()
imgHeight = 0
}
//Change the autolayout constraints so it works properly
return postInfoHeight + postHeight + imgHeight
}
and here is the code that calculates the height of the text view:
func textViewDidChange(textView : UITextView) -> Float {
let content : NSString = textView.text
let oneLineSize = content.sizeWithAttributes(["NSFontSizeAttribute": UIFont.systemFontOfSize(14.0)])
let contentSize = CGSizeMake(textView.frame.width, oneLineSize.height * round(oneLineSize.width/textView.frame.width))
return Float(contentSize.height)
}
I can't understand why this isn't giving me the correct results. If anyone can spot an error or suggest how to solve this issue I'd really appreciate it. Thanks in advance!
Thanks to #jrisberg for the link to the other question. I decided to abandon using the text view and am now using a UILabel instead. The code in the other question is extremely old and uses a lot of deprecated methods, so I'll post some updated code that works on iOS 8 here. I deleted the textViewDidChange(textView : UITextView) method and changed my tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) method to this:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let cell : UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("HomeCell") as? UITableViewCell
let post : Post = tblData[indexPath.row]
//Get custom cell components
let postLbl = cell?.viewWithTag(103) as UILabel
let postPic = cell?.viewWithTag(104) as UIImageView
//Post lbl properties
let PADDING : Float = 8
let pString = post.postInformation as NSString?
let textRect = pString?.boundingRectWithSize(CGSizeMake(CGFloat(self.tableView.frame.size.width - CGFloat(PADDING * 3.0)), CGFloat(1000)), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName : UIFont.systemFontOfSize(14.0)], context: nil)
//Individual height variables
let postInfoHeight = 66 as CGFloat
var postHeight = textRect?.size.height
postHeight? += CGFloat(PADDING * 3)
var imgHeight = 8 + postPic.frame.height as CGFloat
if post.img == nil {
postPic.removeFromSuperview()
imgHeight = 0
}
//Change the autolayout constraints so it works properly
return postInfoHeight + postHeight! + imgHeight
}
Related
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
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!
I am trying to build a UICollectionViewas tag flow layout according to this topic: http://codentrick.com/create-a-tag-flow-layout-with-uicollectionview/
It worked until the button action but there is one issue. As you can see at the picture below, custom UICollectionViewCell's don't align correctly as like as at the topic. I did everything same but FlowLayout.swift
I want to make equal gaps between these cells. But they are aligning like this.
You can find FlowLayout.swift below.
Can someone tell me how can I fix it?
import Foundation
import UIKit
class FlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesForElementsInRect = super.layoutAttributesForElementsInRect(rect)
var newAttributesForElementsInRect = [UICollectionViewLayoutAttributes]()
var leftMargin : CGFloat = 0.0
for attributes in attributesForElementsInRect! {
let refAttributes = attributes
// assign value if next row
if (refAttributes.frame.origin.x == self.sectionInset.left) {
leftMargin = self.sectionInset.left
} else {
// set x position of attributes to current margin
var newLeftAlignedFrame = refAttributes.frame
newLeftAlignedFrame.origin.x = leftMargin
refAttributes.frame = newLeftAlignedFrame
}
// calculate new value for current Fmargin
leftMargin += refAttributes.frame.size.width + 8
newAttributesForElementsInRect.append(refAttributes)
}
return newAttributesForElementsInRect
}
}
I know this is an old thread but the problem still persists. The code here helped me, to solve my problem but I created a more swifty and robust version of it. Already tested with Swift 5.1 and iOS 13.
// A collection view flow layout in which all items get left aligned
class LeftAlignedFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let originalAttributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
var leftMargin: CGFloat = 0.0
var lastY: Int = 0
return originalAttributes.map {
let changedAttribute = $0
// Check if start of a new row.
// Center Y should be equal for all items on the same row
if Int(changedAttribute.center.y.rounded()) != lastY {
leftMargin = sectionInset.left
}
changedAttribute.frame.origin.x = leftMargin
lastY = Int(changedAttribute.center.y.rounded())
leftMargin += changedAttribute.frame.width + minimumInteritemSpacing
return changedAttribute
}
}
}
Solved.
I changed the UICollectionView's layout flow to custom and set FlowLayout.swift as UICollectionView's class.
Allright, I think I've read all the issues which seemed to be the same as I did.
I have a UITextView in a UITableViewCell, the cell itself contains an imageView, a UILabel as title, the UITextView and another UILabel.
Screenshot:
I believe the constraints to be correct. Currently the view does an estimate which is correct in most cases, but in my case the content is larger than the UITableViewCell's height, this is probably due to the delayed rendering of the UITableViewCell, causing other list items to draw behind this one (it only appears on top).
The code which estimates the height:
func heightForCellAtIndexPath(indexPath:NSIndexPath) -> CGFloat {
if (calculatorCell[cellIdentifier] == nil) {
calculatorCell[cellIdentifier] = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? UITableViewCell
}
if let cell = calculatorCell[cellIdentifier] {
populateCell(cell, indexPath: indexPath)
cell.setNeedsLayout()
cell.layoutIfNeeded()
return cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
}
ErrorClass.log("Could not instantiate cell? returning 100 float value instead for listView")
return 100.0
}
This is a generic function as you see since it's handling a UITableViewCell not a custom, so the populateCell in this case, the first index returns the cell with the issue, the rest are comments which are working fine, I'm just sharing it all since people will ask for it:
if (indexPath.row == 0) {
let cellUnwrapped = cell as! ChallengeHeader
cellUnwrapped.setContent(
challenge!.content,
date: StrFormat.deadLineFromDate(challenge!.endAt),
likes: Language.countValue("challenge_count_like", count: challenge!.totalLikes),
likeController: self,
commentCount: Language.countValue("global_count_comment", count: challenge!.totalComments)
)
cellUnwrapped.like(userLiked)
cellUnwrapped.loadAttachments(challenge!.attachments)
return cellUnwrapped
} else {
let cellUnwrapped = cell as! ListItemComment
let challengeComment = items[(indexPath.row - 1)] as! ChallengeComment
var username = "Anonymous"
if let user = challengeComment.author as User? {
cellUnwrapped.iconLoad(user.avatar)
username = user.displayName
}
cellUnwrapped.setContentValues(
StrFormat.cleanHtml(challengeComment.text),
date: StrFormat.dateToShort(challengeComment.createdAt),
username: username
)
return cellUnwrapped
}
The cell's setContentvalues which parses the html string to NSAttributedString, it's wrapped in an if to prevent reloads to re-render the NSAttributedString. It won't change but it does take a while to render:
func setContent(content:String, date:String, likes:String, likeController : LikeController, commentCount:String) {
if (content != txtContent) {
self.contentLabel.text = nil
self.contentLabel.font = nil
self.contentLabel.textColor = nil
self.contentLabel.textAlignment = NSTextAlignment.Left
contentLabel.attributedText = StrFormat.fromHtml(content)
txtContent = content
}
deadlineLabel.text = date
likesLabel.text = likes
self.likeController = likeController
likeButton.addTarget(self, action: "likeClicked:", forControlEvents: .TouchUpInside)
totalComments.text = commentCount
totalComments.textColor = StyleClass.getColor("primaryColor")
doLikeLayout()
}
So TL;DR; I have a UITextView inside a UITableViewCell scaling sizes using autolayout and containing an NSAttributedString to parse html and html links. How come it does not render well, and how to fix it?
I am having a problem creating UILabels to insert into my tableview cells. They insert fine and display fine, the problem comes in when I go to scroll. The ordering of the row's goes haywire and looses it's initial order. For instance, at the beginning, the idLabel's cell text, goes from 0 > 14, in sequential order. After scrolling down and back up, the order could be anything, 5, 0, 10, 3 etc.
Any ideas on why this is happening? I am presuming my attempts to update the label's after cell creation is wrong.
var idArr: NSMutableArray!
// In init function
self.idArr = NSMutableArray()
for ind in 0...14 {
self.idArr[ind] = ind
}
// Create the tableview cells
internal func tableView( tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath ) -> UITableViewCell {
if cell == nil {
let id = self.idArr[indexPath.row] as Int
cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell" )
// Id
let idLabel: UILabel = UILabel( frame: CGRectZero )
idLabel.text = String( id )
idLabel.textColor = UIColor.blackColor()
idLabel.tag = indexPath.row
idLabel.sizeToFit()
idLabel.layer.anchorPoint = CGPointMake( 0, 0 )
idLabel.layer.position.x = 15
idLabel.layer.position.y = 10
idLabel.textAlignment = NSTextAlignment.Center
cell?.addSubview( idLabel )
}
// Update any labels text string
for obj: AnyObject in cell!.subviews {
if var view = obj as? UILabel {
if view.isKindOfClass( UILabel ) {
if view.tag == indexPath.row {
view.text = String( id )
}
}
}
}
// Return the cell
return cell!
}
Thanks for any assistance, I can't see the wood for the trees anymore.
The logic in your cell updating looks off. You are checking that a tag you set at initialization is equal to the index path's row but because the cells should be reused this will not always be the case. If you just remove that tag checking line in the for loop it should work fine.
Also, I'm not sure why you are checking ifKindOfClass after you have optionally cast the view as a label anyway.
To prevent accidentally updating Apple's views you would be better off adding a constant tag for the label and pulling the label by that tag instead of looping through all of the subviews.
This is clearly not the actual code you are running since the id variable is not in the proper scope and there is no definition of cell. I've added proper dequeueing and move the id variable to something that works. I'm assuming you are doing those in your actual code.
internal func tableView( tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath ) -> UITableViewCell {
let labelTag = 2500
let id = self.idArr[indexPath.row] as Int
var cell = tableView.dequeueReusableCellWithIdentifier("cell")
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell" )
// Id
let idLabel: UILabel = UILabel( frame: CGRectZero )
idLabel.text = String( id )
idLabel.textColor = UIColor.blackColor()
idLabel.tag = labelTag
idLabel.sizeToFit()
idLabel.layer.anchorPoint = CGPointMake( 0, 0 )
idLabel.layer.position.x = 15
idLabel.layer.position.y = 10
idLabel.textAlignment = NSTextAlignment.Center
cell?.addSubview( idLabel )
}
// Update any labels text string
if let view = cell?.viewWithTag(labelTag) as? UILabel {
view.text = String( id )
}
// Return the cell
return cell!
}