I got a ViewController, with two UITableView in it.
They both get data from the same object, but from different arrays inside that object.
My second Tableview doesn't show me all the items, only 5.
The TableViews are both dynamic and size themself.
Here is my cellForRowAt function:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == tableViewSteps, //This is the table that doesn't show right
let cell = tableView.dequeueReusableCell(withIdentifier: "stepsCell", for: indexPath) as? StepsTableViewCell {
let text = detailItem?.itemStepAr[indexPath.row] as! String
cell.textAreaOutlet.text = "Schritt \(indexPath.row + 1) \n\(text)"
let textAsNSString = cell.textAreaOutlet.text as NSString
let lineBreakRange = textAsNSString.range(of: "\n")
let newAttributedText = NSMutableAttributedString(attributedString: cell.textAreaOutlet.attributedText)
let boldRange: NSRange
if lineBreakRange.location < textAsNSString.length {
boldRange = NSRange(location: 0, length: lineBreakRange.location)
} else {
boldRange = NSRange(location: 0, length: textAsNSString.length)
}
newAttributedText.addAttribute(NSAttributedString.Key.font, value: UIFont.preferredFont(forTextStyle: UIFont.TextStyle.headline), range: boldRange)
cell.textAreaOutlet.attributedText = newAttributedText
let fixedWidth = cell.textAreaOutlet.frame.size.width
let newSize = cell.textAreaOutlet.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
cell.textAreaOutlet.frame.size = CGSize(width: max(newSize.width, fixedWidth), height: newSize.height)
cell.textAreaOutlet.isScrollEnabled = false
tableStepsHeight = tableStepsHeight + Int(newSize.height)
cell.textAreaOutlet.textColor = UIColor.lightGray
tableViewSteps.frame = CGRect(x:0, y: currentViewHeight + 20, width: 375, height: tableStepsHeight)
changeScrollHeight()
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "ingredsCell")! //1.
let text = detailItem?.itemIngredAr[indexPath.row]
cell.textLabel?.text = text as? String
cell.textLabel?.font = UIFont(name: "Futura-Medium", size: 17)
cell.textLabel?.textColor = UIColor.lightGray
return cell
}
}
as commented, the first tableview is the one, that doesn't work properly.
It only runs 5 times, when I watch it with the debugger, but there are more Items in the Array.
Edit:
Here is my numberOfRowsInSection function
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (tableView == tableViewSteps) {
return detailItem?.itemStepAr.count ?? 0
}
else {
return detailItem?.itemIngredAr.count ?? 0
}
}
Edit 2:
When I enable scrolling in my TableView and then try to scroll, all the items appear. But the scrolling should be disabled, because it automatic resizes (which works)
Final edit:
I added like some more height to the tableViewand enabled scroll (but disabled the bounce, so u don't see anything from the scroll)
Now it loads properly and everything works fine
A UITableView works with recycling UITableViewCells. This means that a tableview will only load the visible cells first, and only when you scroll, will it load more cells (by recycling the old cells).
The issue I can see is that you may try to get the height of the tableview, to update a height constraint (so the tableview shows in its entirety). But that may be an incorrect calculation (as said before, it only instantiates the first x cells, so it can't know the height of the remaining cells). Also, this calculation should never be performed from cellForRowAt, as that method should never do things outside the cell (unless you want to know when a cell has become visible).
You can fix this by making your cells all have a predefined height, and then multiplying that by the number of cells.
If you have self-sizing cells, it can't be done (read: it can be done, but it's too unwieldy and inefficient to be practical).
You could try to look into taking all other views that should scroll above and below the tableview, into tableviewcells. Then you can just use a regular tableview, no fancy height constraints and calculations needed.
Related
In a section of my UITableView, there are 5 cells, three of which have been configured to expand/collapse to provide a more detailed view when selected. One of these cells shows a diagram of a number of small squares, which displays perfectly, until another cell is expanded, like this:
When the cell is collapsed, however, the subviews in the cell display in different cells, in different sections, like this:
and this:
To create the subviews in the cell, this is my code in the cellForRow method, which just uses an array of UIViews:
for vote in vote_array {
cell.contentView.addSubview(vote as? UIView ?? UIView.init(frame: CGRect.init(x: 0, y: 0, width: 0, height: 0)))
}
I tried removing all the subviews before I added them by doing this, but it doesn't change anything:
for subview in cell.contentView.subviews {
subview.removeFromSuperview()
}
Edit: This is inside a switch statement, but here is the relevant cell/case cellForRow:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.contentView.clipsToBounds = true
cell.clipsToBounds = true
let vote_array = getVoteArray()
for subview in cell.contentView.subviews {
subview.removeFromSuperview()
}
for case let vote as UIView in vote_array {
cell.contentView.addSubview(vote)
}
Edit:
The core of getVoteArray:
func getVoteArray() -> NSMutableArray {
var i = 0
var x = 20
var y = 4
let blockViews : NSMutableArray = []
for color in blocks {
let block = UIView.init(frame: CGRect.init(x: x, y: y, width: 20, height: 20))
block.backgroundColor = color as? UIColor
blockViews.add(block)
x = x + 24
i = i + 1
if i == num_blocks_per_row { i = 0; y = y + 24; x = 20 }
}
diagramHeight = y + 24
return blockViews
}
I can't seem to figure out why the subviews are generating randomly all over the tableView.
Ended up adding
for case let cell as UITableViewCell in tableView.subviews {
for subview in cell.contentView.subviews {
if subview.tag == 115 {
subview.removeFromSuperview()
}
}
}
to my didSelectRowAt method, after adding the tag when each view is created. I'm still not sure why the views were being added to different cells, but this got rid of them at least.
Try to implement unique ReuseIdentifiers for collapsed and expanded states.
If the cell is collapsed then don't load all those views in it by dequeuing a collapsedCell where the height of all those UIViews is either 0 or they are not added to subview.
If the cell is expanded than deque a expandedCell where the views are layed out as in the first screenshot.
After expanding and or collapsing call tableview.reloadData()
It used to be a long long time ago that UIViews clipped their children, but that hasn't been true for a very long time. If you want clipping on you need to either change UIView.clipsToBounds to true or use the underlying CALayer property maskToBounds.
cell.contentView.clipsToBounds = true
Or you can check the box in the storyboard/nib.
I have a tableview with buttons which I resize with the below code to fit their labels:
class ResizableButtons: UIButton {
override var intrinsicContentSize: CGSize {
let labelSize = titleLabel?.sizeThatFits(CGSize(width: frame.width, height: .greatestFiniteMagnitude)) ?? .zero
let desiredButtonSize = CGSize(width: labelSize.width + titleEdgeInsets.left + titleEdgeInsets.right, height: labelSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom)
return desiredButtonSize
}
}
It works pretty well.
The problem occurs when I try to scroll tableview down and up.
As you can see from the snapshots, buttons get resized. And it seems to happen only after I click on some button and start to scroll. If I scroll without clicking the sizes remains ok.
I believe the problem lays in how I set titles to the buttons. I do it with the following code :
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//Every second row has a button.
if indexPath.row % 2 == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "ReusableCell") as! ReusableCell
cell.button.setTitle(textAndButtonsArray[indexPath.row], for: .normal)
cell.backgroundColor = UIColor.clear
//Checking if button was pressed before
if pressedButtons.contains(indexPath.row) {
print("Text for selected button at indexpath \(indexPath.row) is \(cell.button.titleLabel?.text)")
}
}
So if I print into the console indexPath and textLabel for pressed buttons, it gives me correct indexPath. But the textLabel displayed in the console is from a different button. From the one that is 10 rows later than the actual pressed one.
Any ideas how I could fix it?
P.S. Don't pay attention to text and labels on the buttons, I wrote whatever came to my mind first :)
Try to resize your cell or buttons inside willDisplayCell and reset the button size in prepareForReuse. Call inside intrinsicContentSize button.layoutIfNeeded().
It seems I've found a solution. In the part where I resize the buttons, I need to change titleLabel to currentTitle. So it looks like this:
class ResizableButtons: UIButton {
override var intrinsicContentSize: CGSize {
let label = currentTitle
titleLabel?.text = label
let labelSize = titleLabel?.sizeThatFits(CGSize(width: frame.width, height: .greatestFiniteMagnitude)) ?? .zero
let desiredButtonSize = CGSize(width: labelSize.width + titleEdgeInsets.left + titleEdgeInsets.right, height: labelSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom)
return desiredButtonSize
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
}
}
Not sure why is that. But it seems that XCode is updating labels on the buttons with some sort of delay. And button.titleLabel doesn't always match with button.currentTitle
Also last few lines with layoutSubviews() seems to matter.
I have a grid of cells in a UICollectionViewController:
Pretty standard 3 column grid (based on approximate cell size, wider layouts have more cells)
The overall idea of the app is that a user taps a cell which counts down the specified time, and once the timer is complete it will unlock the next time. The first time is always unlocked:
After the first 3 buttons are tapped and have counted down
Tapping the same cell twice in succession removes all cells except the first and the one which was tapped twice, to allow the user to loop around.
Doing this should also update the collectionViewLayout to be only 2 columns of equal width which fill the screen, but at the moment the first and second cells are not resizing:
[After a button is tapped twice in succession](http:// i.imgur.com/cLvaApG.png)
The layout is definitely updated / invalidated because rotating the device updates the cells as expected:
[Rotated to landscape mode](http:// i.imgur.com/rngwzre.png)
[And rotated back to portrait](http:// i.imgur.com/ajwPr1J.png)
I'm sizing the cells as follows:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
if repeating {
let target = collectionView.frame.width
let targetFraction = target / 2
return CGSize(width: targetFraction, height: targetFraction)
}
let target = CGFloat(125)
let frameRef = collectionView.frame.width
let frameWidthRemainder = frameRef % target
let frameWidthRounded = frameRef - frameWidthRemainder
let columnCount = frameWidthRounded / target
let columnWidth = frameRef / columnCount
return CGSize(width: columnWidth, height: columnWidth)
}
Repeating is a bool which is set / reset by tapping on the cells as part of an action:
#IBAction func circleProgressButtonTapped(sender: CircleProgressButton) {
...
if previouslySelectedButton == sender && !repeating {
repeating = true
collectionViewLayout.invalidateLayout()
buttons = [buttons[0], buttons[row]]
collectionView?.reloadData()
for button in buttons {
button.circleProgressButton.setNeedsDisplay()
}
}
...
}
When the device rotates, I'm resetting the layout with:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
for button in buttons {
button.circleProgressButton.setNeedsDisplay()
}
collectionViewLayout.invalidateLayout()
}
Buttons is an array of the cells, which is populated / reset with:
func refreshView() {
if let min = defaults.valueForKey(userDefaultsKeys.min.rawValue) as? Int {
self.min = min
}
if let max = defaults.valueForKey(userDefaultsKeys.max.rawValue) as? Int {
self.max = max
}
buttons = []
collectionView?.reloadData()
for i in 0..<buttonCount {
guard let collectionView = collectionView else { break }
let indexPath = NSIndexPath(forRow: i, inSection: 0)
let multiplier = i + 1
let time = min * multiplier
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CircleProgressButtonCollectionViewCell
cell.circleProgressButton.delegate = self
cell.circleProgressButton.value = time
cell.circleProgressButton.resetText()
cell.circleProgressButton.indexPath = indexPath
cell.circleProgressButton.progress = 0
cell.circleProgressButton.activeProgressColour = theme
cell.circleProgressButton.tintColor = theme
cell.circleProgressButton.setNeedsDisplay()
if i > 0 {
cell.circleProgressButton.enabled = false
}
buttons.append(cell)
}
}
I know this is probably not the best way to be making the cells, but any other way caused problems with the cell re-use. I.e. the progress in a cell would also be re-used.
I'm not even sure if the issue is collectionViewLayout.invalidateLayout() is not invalidating properly or what. It could just be the collectionView needs to be updated in some other way. I don't even know if it's the collectionView or the cell I should be focusing on. It's just weird that the first two cells are the only ones not updating, especially as I'm not doing anything different to them.
I would list everything I've tried but I'd be here a while. Among other things, I've tried:
if previouslySelectedButton == sender && !repeating {
...
collectionViewLayout.prepareLayout()
if let collectionView = collectionView {
collectionView.setNeedsDisplay()
collectionView.setNeedsLayout()
}
if let collectionViewLayoutCollectionView = collectionViewLayout.collectionView {
collectionViewLayoutCollectionView.setNeedsDisplay()
collectionViewLayoutCollectionView.setNeedsLayout()
}
collectionView?.setNeedsDisplay()
collectionViewLayout.collectionView?.setNeedsDisplay()
collectionView?.setNeedsLayout()
collectionViewLayout.collectionView?.setNeedsLayout()
collectionView?.reloadData()
buttons = [buttons[0], buttons[row]]
for button in buttons {
button.setNeedsLayout()
button.setNeedsDisplay()
}
I've tried setting the buttons array to empty, reloading, then populating it (generating entirely new cells) and reloading it again but even that doesn't work.
If I have to radically alter my entire approach, so be it.
So I have a TableView with 2 prototype cells. Here is my code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! ItemDetailsCell
cell.centerImage.image = mainImage
cell.heading.text = detailsHeadings[indexPath.row]
let headingString = detailsHeadings[indexPath.row]
cell.body.text = details[headingString]
tableView.rowHeight = cell.labelBlack.frame.height + 40
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell2", forIndexPath: indexPath) as! ItemDetailsCell
cell.heading.text = detailsHeadings[indexPath.row]
let headingString = detailsHeadings[indexPath.row]
cell.body.text = details[headingString]
let bodyString = details[headingString]
let labelWidth = Int(cell.body.frame.width)
println(labelWidth)
let label = UILabel(frame: CGRect(x: 0, y: 0, width: labelWidth, height: 10000))
label.text = bodyString
label.numberOfLines = 100
label.font = UIFont(name: "OpenSans-Light", size: 12.0)
label.sizeToFit()
tableView.rowHeight = label.frame.height + 3
return cell
}
}
So the second prototype cell has just two labels with the values being assigned from a Dictionary. The cell size needs to expand or contract based upon how many lines of text there are. In auto layout I have the number of lines set to 0 so it will pick however many lines are needed. This works fine except when you scroll within the app it will snap the view up as users scroll back up from the bottom. Is there a way to avoid this?
Thanks in advance for any help.
I found this actually after spending some more time looking:
http://candycode.io/automatically-resizing-uitableviewcells-with-dynamic-text-height-using-auto-layout/
It gave me what I needed. I removed the parts of my code that set the rowHeight and then used the viewDidLoad method as well as auto layout to constrain my cell sizes and after a bit of trial and error it is working without snapping to place when you scroll.
You are changing the rowHeight of the tableview. That's most likely very wrong. The rowHeight of the tableView is the default for all rows that don't have their own height, so you are effectively changing the height of all cells.
You should use the delegate method tableView:heightForRowAtIndexPath:
I'm trying to implement a table by code, so far so good but when I scroll down and up in my table my rows went crazy, I did a little research and I think its because of the way I reuse my cells, but the examples I found were all in Obj- C, So could you please help me to understand the problem? Here are my function where I implement the cells:
override func tableView(tableView: (UITableView!), cellForRowAtIndexPath indexPath: (NSIndexPath!)) -> UITableViewCell{
let sectionA = seccionesDiccionario[indexPath.section]
let sectionName = tableDataSwiftDictionary[sectionA]!
var cell: UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("CellId") as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "CellId")
cell!.backgroundColor = UIColor.clearColor()
cell!.textLabel?.textColor = UIColor.darkTextColor()
let selectedView:UIView = UIView(frame:CGRect(x: 0, y: 0, width: cell!.frame.size.width, height: cell!.frame.size.height))
selectedView.backgroundColor = UIColor.orangeColor().colorWithAlphaComponent(0.3)
cell!.selectedBackgroundView = selectedView
cell!.textLabel?.text = sectionName[indexPath.row]
cell!.textLabel?.numberOfLines = 0
cell!.textLabel?.lineBreakMode = NSLineBreakMode.ByWordWrapping
cell!.textLabel?.sizeToFit()
}
return cell!
}
Thank you so much.
when I scroll down and up in my table my rows went crazy
what do you mean by 'went crazy'.
One thing that I can see in the above code: You should move the text assignment out of the if statement. You want the 'textLabel' to show the String in the 'sectionName' array at the given indexPath.row. Currently, you are creating some cells and then - when you start scrolling your tableView - the cells are reused but the textLabel's text is not set, so it will always show its initial value.
Move this line
cell!.textLabel?.text = sectionName[indexPath.row]
out of the if{} block. Maybe that's all you need to do here.
EDIT
btw: since you're calling sizeToFit on the textLabel, I assume you want the cell to be high enough to display all the text. Note that sizeToFit will not be enough in order to achieve that. You'll have to either use Auto-sizing cells using AutoLayout (> iOS8) or implement the tableView:heightForRowAtIndexPath: delegate method and return the calculated cell height there.