Swift 3.0 - Vertical lines on TableViewCell for every CellIndentationLevel - swift

I have an app that has a bunch of collapsible comments.
Each cell that holds a comment has an indentation cell and I would like to draw a vertical line the height of the cell for every indentation level. The end goal is to look something like this: Taken from Reddit
I tried to add a rectangle shaped view for every indent level in CellForRowAt but it would just keep adding onto itself whenever I scrolled out of view and back into it.
Currently I have it working in the "willDisplay cell" function but it only loads it when the ENTIRE cell is visible, not partially and it still has some issues of overlapping content with lines.
Here's my current code:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
for cell in commentTable.visibleCells {
if cell.indentationLevel > 0 {
for i in 1...cell.indentationLevel {
let rect = UIView(frame: CGRect(x: i * 15, y: 0, width: 1, height: Int(cell.bounds.height)))
cell.addSubview(rect)
}
}
}
}
How can I do this preferably in cellForRowAt function, or the simplest and most efficient way to get this done?

You can do this in cellForRowAtIndexPath. Note that you should not just add views to your cells without tracking them because the cells are reused (thats why its called dequeResuableCell). Make a property called indentationViews and a function called setIndentationLevel. In cellForRowAtIndexPath you call cell.setIndentationLevel (note that I have removed your if because for 0..
func setIndentationLevel(indentationLevel: Int) {
for i in 0..<cell.indentationLevel {
let view = UIView(frame: CGRect(x: i * 15 + 15, y: 0, width: 1, height: Int(cell.bounds.height)))
cell.addSubview(view)
indentationViews.append(view)
}
}
You also need to implement prepare for reuse to kill all of the views you just added before the cell gets used again:
override func prepareForReuse() {
indentationViews.forEach{$0.removeFromSuperview()}
indentationViews.removeAll()
}

Related

Does iOS 14 have some breaking changes that disable vertical scroll for Collection View? [duplicate]

I have a search form that uses a tableview. After updating Xcode 12 today the UISwitch, UITextField, UISlider no longer work when nested inside a UITableViewCell. Is there a property that has changed that I need to set to make this work again?
To be sure it wasn't just my project, I created a new project and nestled a UITextField inside of it and it doesn't work either.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let textField = UITextField(frame: CGRect(x: 5, y: 5, width: 400.0, height: 25.0))
textField.delegate = self
textField.backgroundColor = .blue
cell.addSubview(textField)
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("this will get called even when selecting the UITextField")
}
func textFieldDidBeginEditing(_ textField: UITextField) {
print("this is never called")
}
Your code was always wrong:
cell.addSubview(textField)
You must never add a subview to a cell. Add the subview to the cell's contentView.
The same happened to me since I upgraded to iOS 14.
This has worked for me when I add the subViews directly to the cell,
cell.contentView.isUserInteractionEnabled = true
Had similar issue, and been going on around for it... what was issue with my code is that under UITableViewCell I was doing this:
didSet {
if contentView.backgroundColor == backgroundColor { return }
contentView.backgroundColor = backgroundColor
for v in otherView.subview { v.backgroundColor = backgroundColor }
}
Removing this line here contentView.backgroundColor = backgroundColor did the trick. Cell is now visible and there is no duplicated contentView
Maybe this will help someone, since I found only answers regarding adding subviews directly to cell instead to cell.contentView
EDIT 1:
Okay, just wanted to update you on situation, issue was that my subviews where of type UIStackView and I had used subview where I actually should have used arrangedSubviews
Hope this will help someone

NSCollectionView interitem gap indicator has wrong height

I'm implementing an application for macOS in which I use an NSCollectionView as a sort of timeline. For this I use a custom subclass of NSCollectionViewFlowLayout for the following reasons:
I want the items to be scrollable horizontally only without wrapping to the next line
Only NSCollectionViewFlowLayout seems to be capable of telling NSCollectionView to not scroll vertically (inspiration from here)
All of this works smoothly, however: I'm now trying to implement drag & drop reordering of the items. This also works, but I noticed that the inter item gap indicator (blue line) is not being displayed properly, even though I do return proper sizes. I calculate them as follows:
override func layoutAttributesForInterItemGap(before indexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
{
var result: NSCollectionViewLayoutAttributes? = nil
// The itemLayoutAttributes dictionary is created in prepare() and contains
// the layout attributes for every item in the collection view
if indexPath.item < itemLayoutAttributes.count
{
if let itemLayout = itemLayoutAttributes[indexPath]
{
result = NSCollectionViewLayoutAttributes(forInterItemGapBefore: indexPath)
result?.frame = NSRect(x: itemLayout.frame.origin.x - 4, y: itemLayout.frame.origin.y, width: 3, height: itemLayout.size.height)
}
}
else
{
if let itemLayout = itemLayoutAttributes.reversed().first
{
result = NSCollectionViewLayoutAttributes(forInterItemGapBefore: indexPath)
result?.frame = NSRect(x: itemLayout.value.frame.origin.x + itemLayout.value.frame.size.width + 4, y: itemLayout.value.frame.origin.y, width: 3, height: itemLayout.value.size.height)
}
}
return result
}
Per documentation the indexPath passed to the method can be between 0 and the number of items in the collection view including if we're trying to drop after the last item. As you can see I return a rectangle for the inter item gap indicator that should be 3 pixels wide and the same height as an item is.
While the indicator is displayed in the correct x-position with the correct width, it is only 2 pixels high, no matter what height I pass in.
How do I fix this?
NB: If I temporarily change the layout to NSCollectionViewFlowLayout the indicator displays correctly.
I'm overriding the following methods/properties in NSCollectionViewFlowLayout:
prepare()
collectionViewContentSize
layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
layoutAttributesForItem(at indexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
shouldInvalidateLayout(forBoundsChange newBounds: NSRect) -> Bool
layoutAttributesForInterItemGap(before indexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
You may also need to override
layoutAttributesForDropTarget(at pointInCollectionView: NSPoint) -> NSCollectionViewLayoutAttributes.
The documentation mentions that the frame of the attributes returned by this method provides a bounding box for the gap, that will be used to position and size a drop target indicator (view).
You can set a breakpoint in your drag and drop code, after the drop indicator is drawn (e.g. collectionView(:acceptDrop:indexPath:dropOperation:) and then when the breakpoint is hit during a drag-and-drop session, run Xcode view debugger to examine the drop indicator view after it's added to the collection view. This way you can be sure the view hierarchy is correct and any issues are visually apparent.

How to update UITableview Header height while scrolling programatically

In My application I have top navigation bar and a tableview below the navigation bar. I have CollectionViewCell with two rows which added inside the UITableViewHeader programmatically. When ever I scroll the the TableView to top, i want the header to stop just below the navigation bar, and update the TableView Header height so I can show only one row. I just want to do an animation (like Shrinked)when the TableViewHeader sticks to the navigationbar the two collectionview rows should turn into one row by decreasing the Header Height. How can I do it programmatically
Below is my code for showing CustomHeaderView
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView.init(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 183))
let headerCell = tableView.dequeueReusableCell(withIdentifier: kLastPlayedidentifier) as! LastPlayedTVC
headerCell.frame = headerView.frame
headerCell.category = lastPlayedData
headerView.addSubview(headerCell)
return headerView
}
Also i'm checking for the scroll position to set the tableview header height progmmatically which isn't successful for me.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset)
if scrollView.contentOffset.y > 237 //This value is to check when the header reached the top position {
//Condition to check and animate the headerview height to make collectionview cell two rows into one rows.
}
How can I achieve the TableViewHeader height update when header sticks on top while scrolling.
Any help is appreciated.
What you are looking for is "sticky header"
and you want to change the header as well.
Sticky part is built in automatically I think if you just use UITableViewController(style: .plain), if that doesn't work for you, you can just google sticky header and there are lots of answers.
the part about changing the height or animating it. you are doing it right, just do something like:
// update your viewForHeader method to account for headerRows variable above
// update your viewForHeader method to account for headerRows variable above
// default 2, you modify this in your scroll
var headerRows = 2
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let height = headerRows == 2 ? 183 : 91
let headerView = UIView.init(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: height))
let headerCell = tableView.dequeueReusableCell(withIdentifier: kLastPlayedidentifier) as! LastPlayedTVC
headerCell.frame = headerView.frame
headerCell.category = lastPlayedData
headerView.addSubview(headerCell)
return headerView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset)
if scrollView.contentOffset.y > 237 {
updatedHeader.frame.size.height = 40
self.tableviewObj.tableHeaderView = updatedHeader
headerRows = 1 } else {
headerRows = 2
}
self.tableView.reloadSectionHeaders()
}
If you want to do some animating instead, what you would do is store reference to your headerView in a variable of your view controller and inside your scrollViewDidScroll animate it using UIView.animate{...}
hope this helps man.

UITableviewCell showing old data and new data on reload

I have a UITableViewCell and I am adding xib in cellForRowIndexPath method. It works fine until I update the model and call reloadData on UITableView. The cell is showing new data on top of the old data, I can see the label on the old label text.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let customView:CustomView = UIView.fromNib()
userView.frame = CGRect(x: 10, y: y, width: Int(self.tableView.bounds.size.width-50), height: 50)
userView.label.text = data[indexPath.row]
cell.addSubview(customView:)
Any guesses why this could happen?
The quick answer is this: since cells are reused when dequeued, you are adding a new CustomView to a cell that already had a CustomView added when dequeued previously.
One way you could handle this is to remove any existing CustomView from the hierarchy before creating a new one and adding it. To do this you could add a recognizable tag to the view each time, and then look for a view with that same tag to remove during your dequeue process, like this:
//Remove existing view, if it exists
if let existingView = cell.viewWithTag(999) {
//A view was found - so remove it.
existingView.removeFromSuperview()
}
let customView: CustomView = UIView.fromNib()
//Set a tag so it can be removed in the future
customView.tag = 999
customView.frame = CGRect(x: 10, y: y, width: Int(self.tableView.bounds.size.width-50), height: 50)
customView.label.text = data[indexPath.row]
cell.addSubview(customView)
To me this feels like overkill, as it seems you should just add your customView to a custom UICollectionViewCell so you aren't essentially creating a custom cell on the fly, but that is just me. If you did this you could simply dequeue your custom cell and set the text on the label without having to add more views to the hierarchy all the time.

How to increase the UITableView separator height?

I want more space(10px) between each cell. How can I do this?
And I have added this code
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
The best way for me, just add this in cellForRowAtIndexPath or in willDisplayCell
CGRect sizeRect = [UIScreen mainScreen].applicationFrame;
NSInteger separatorHeight = 3;
UIView * additionalSeparator = [[UIView alloc] initWithFrame:CGRectMake(0,cell.frame.size.height-separatorHeight,sizeRect.size.width,separatorHeight)];
additionalSeparator.backgroundColor = [UIColor grayColor];
[cell addSubview:additionalSeparator];
For Swift 3.0:
let screenSize = UIScreen.main.bounds
let separatorHeight = CGFloat(3.0)
let additionalSeparator = UIView.init(frame: CGRect(x: 0, y: self.frame.size.height-separatorHeight, width: screenSize.width, height: separatorHeight))
additionalSeparator.backgroundColor = UIColor.gray
self.addSubview(additionalSeparator)
You should add this to cell's method awakeFromNib() to avoid re-creation.
I have seen many clunky solutions like subclassing UITableView with hidden cells, and other less optimal ones incl. in this thread.
When initializing the UITableView, Set the rowHeight property of UITableView to a height that equals = cell height + desired separator/space height.
Do not use standard UITableViewCell class though, instead, subclass the UITableViewCell class and override its layoutSubviews method. There, after calling super (don't forget that), set the height of the cell itself to desired height.
BONUS UPDATE 18/OCT/2015:
You can be a bit smart about this. The solution above basically puts the "separator" at the bottom of the cell. What really happens is, the row height is managed by the TableViewController but the cell is resized to be a bit lower. This results in the separator/empty space being at the bottom. But you can also centre all the subviews vertically so that you leave the same space at the top and the bottom. For example 1pt and 1pt.
You can also create isFirst, isLast convenience properties on your cell subclass. You would set these to yes in the cellForRowAtIndexPath.
This way you can handle the edge cases for top and bottom separators inside the layoutSubviews method as this would have access to these properties.
This way you can handle the edge cases for top or bottom - because sometimes the design department wants N+1 separators while the number of cells is only N. So you have to either deal with the top one or the boot one in a special way. But it's best do this inside cells instead tableViewHeader or TableViewFooter.
I don't think it's possible using standard API. I guess you would need to subclass the UITableViewCell and add a view that simulates a separator at the bottom of the cell.
You may want to check this question, it seems related and has some sample code:
iPhone + UITableView + place an image for separator
In Swift
The easiest and shortest way for me was to add the snippet below in cellForRowAtIndexPath or in willDisplayCell:
override func tableView(tableView: UITableView,
willDisplayCell cell: UITableViewCell,
forRowAtIndexPath indexPath: NSIndexPath)
{
let additionalSeparatorThickness = CGFloat(3)
let additionalSeparator = UIView(frame: CGRectMake(0,
cell.frame.size.height - additionalSeparatorThickness,
cell.frame.size.width,
additionalSeparatorThickness))
additionalSeparator.backgroundColor = UIColor.redColor()
cell.addSubview(additionalSeparator)
}
this is quite old. Nevertheless I will post my approach.
Simply increase your cell height a bit and assign a mask layer to the cell, like that:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "...", for: indexPath)
// Configure the cell...
let maskLayer = CAShapeLayer()
let bounds = cell.bounds
maskLayer.path = UIBezierPath(roundedRect: CGRect(x: 2, y: 2, width: bounds.width-4, height: bounds.height-4), cornerRadius: 5).cgPath
cell.layer.mask = maskLayer
return cell
}
So in this example my seperator height will be 4.
Have fun!
You can do this entirely in the storyboard. Here is how:
go to the storyboard and select the tableview
Show the Size Inspector and from there set row height to say 140.
then show the Attributes Inspector and from there set your separator to Single Line and Style Plain and choose a color
then in the storyboard (or in Document Outline) select the cell
and again in the Size Inspector, under the Table View Cell, set custom Row Height to say 120.
That’s all. Your separator will be 20 units tall.
Kinda old thread, but since I only found hacky solutions in this thread,
here the solution that worked best for me (without additional UIView in every cell)
Swift 3:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//configure your cell...
cell.layer.shadowColor = UIColor.red.cgColor
cell.layer.shadowOffset = CGSize(width: 0, height: 1)
cell.layer.shadowOpacity = 1
cell.layer.shadowRadius = 0
cell.layer.masksToBounds = false
return cell
}
EDIT: Unfortunately this does not work if you scroll up in a table. I leave the answer here anyway, since it might be a solution if your table has limited content.
See Shadow on a UITableViewCell disappears when scrolling for more info.
For a table cell with height of 50 and a space of 5 pix between the rows. Width is 320.
Define the background of the cells to be clear:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
cell.backgroundColor = [UIColor clearColor];
}
Set the height of the cells, this is the size of the row PLUS the delimiter:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 55;
}
And define in cellForRowAtIndexPath a box, with the size of the row (MINUS delimiter) to draw in the background color:
UILabel *headerBackgroundLabel = [[UILabel alloc] initWithFrame:CGRectMake(0,0,320,50)];
backgroundBox.backgroundColor = [UIColor grayColor];
[cell addSubview:backgroundBox];
I do it a much simpler and more flexible way. Some may call it a hack. I call it pragmatic.
I hide the standard UITableViewSeparator. I then add a subview to my cell, using auto layout pin it to the top. Fix the height to what I desire. Pin it to the edges with a margin either side. Change it's background colour. I have a plain separator with the height i desire.
You may question how efficient this is having another UIView in the cell hierarchy. Is it really going to make a noticeable difference? Probably not - you've just taken the standard separator out of the table hierarchy anyway.
Swift 4
It's not possible to make the default separator higher. Instead you need to add a subview that will look as a separator to each cell (and optionally make the cell higher). You can do it for example in cellForRowAtIndexPath or in a UITableViewCell subclass.
In case you allow to select the cell, you need to add the subview for selected state as well, otherwise the separator would disappear when the cell is selected. That's why selectedBackgroundView is also configured.
Add this into your UITableViewController subclass:
override func viewDidLoad() {
super.viewDidLoad()
tableView.separatorStyle = .none
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.backgroundView = UIView(backgroundColor: .white)
cell.backgroundView?.addSeparator()
cell.selectedBackgroundView = UIView(backgroundColor: .blue)
cell.selectedBackgroundView?.addSeparator()
// configure the cell
return cell
}
Add this extensions into the same file at the bottom:
private extension UIView {
convenience init(backgroundColor: UIColor) {
self.init()
self.backgroundColor = backgroundColor
}
func addSeparator() {
let separatorHeight: CGFloat = 2
let frame = CGRect(x: 0, y: bounds.height - separatorHeight, width: bounds.width, height: separatorHeight)
let separator = UIView(frame: frame)
separator.backgroundColor = .gray
separator.autoresizingMask = [.flexibleTopMargin, .flexibleWidth]
addSubview(separator)
}
}
Here's an option that might work for some people
cell.layer.borderColor = UIColor.white.cgColor
cell.layer.borderWidth = 4.0
cell.layer.masksToBounds = true
The easier and safest solution to this problem is to turn off the table separator and use a UITableViewCell as a separator of variable height. Sure, you'll have to do some index math to figure out where items are, but really it's odd / even.
It won't break and you get the benefit of recyclable cells (no extraneous views to clean up).
First make tableview separator none from the storyboard. Then add UILabel/UIView at bottom of cell of height(you needed) using storyboard or Xib
For Swift 4
Implemented King-Wizard's solution to Swift 4:
public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let additionalSeparatorThickness = CGFloat(4)
let additionalSeparator = UIView(frame: CGRect(x: 0,
y: cell.frame.size.height - additionalSeparatorThickness, width: cell.frame.size.width, height: additionalSeparatorThickness))
additionalSeparator.backgroundColor = UIColor.groupTableViewBackground
cell.addSubview(additionalSeparator)
}
This is the easiest solution I've found:
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
" "
}
then just set the height to whatever you want:
tableView.sectionHeaderHeight = 30.0
I came across a way that has allowed me to effectively change the gap between cells.
In Interface builder I set the row height to be 46.
In the cellForRowAtIndexPath method of my TableView delegate I set the frame of the cell to be a smaller value.
cell.frame=CGRectMake(44,0,tableView.bounds.size.width,44)
This gives me a cell with a height of 44 that fits the width of the tableView but the space provided for the row will be 46 as defined in IB.
I was filling the cell programmatically anyway so this suited me fine.
You should implement
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
delegate method. and return 100.0 there.