Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITableViewCell hide separator using separatorInset fails in iOS 11

This is the code I used to hide the separator for a single UITableViewCell prior to iOS 11:

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {

    if (indexPath.row == 0) {


        // Remove separator inset
        if ([cell respondsToSelector:@selector(setSeparatorInset:)]) {
            [cell setSeparatorInset:UIEdgeInsetsMake(0, tableView.frame.size.width, 0, 0)];
        }

        // Prevent the cell from inheriting the Table View's margin settings
        if ([cell respondsToSelector:@selector(setPreservesSuperviewLayoutMargins:)]) {
            [cell setPreservesSuperviewLayoutMargins:NO];
        }

        // Explictly set your cell's layout margins
        if ([cell respondsToSelector:@selector(setLayoutMargins:)]) {
            [cell setLayoutMargins:UIEdgeInsetsMake(0, tableView.frame.size.width, 0, 0)];
        }
    }
}

In this example, the separator is hidden for the first row in every section. I don't want to get rid of the separators completely - only for certain rows.

In iOS 11, the above code does not work. The content of the cell is pushed completely to the right.

Is there a way to accomplish the task of hiding the separator for a single UITableViewCell in iOS 11?

Let me clarify in advance that I do know that I can hide the separator for the entire UITableView with the following code (to hopefully avoid answers instructing me to do this):

self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;

EDIT: Also to clarify after a comment below, the code does exactly the same thing if I include the setSeparatorInset line at all. So even with only that one line, the content of the cell is pushed all the way to the right.

like image 728
SAHM Avatar asked Oct 16 '17 22:10

SAHM


3 Answers

If you are not keen on adding a custom separator to your UITableViewCell I can show you yet another workaround to consider.

How it works

Because the color of the separator is defined on the UITableView level there is no clear way to change it per UITableViewCell instance. It was not intended by Apple and the only thing you can do is to hack it.

The first thing you need is to get access to the separator view. You can do it with this small extension.

extension UITableViewCell {
    var separatorView: UIView? {
        return subviews .min { $0.frame.size.height < $1.frame.size.height }
    }
}

When you have an access to the separator view, you have to configure your UITableView appropriately. First, set the global color of all separators to .clear (but don't disable them!)

override func viewDidLoad() {
    super.viewDidLoad()
    tableView.separatorColor = .clear
}

Next, set the separator color for each cell. You can set a different color for each of them, depends on you.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "SeparatorCell", for: indexPath)
    cell.separatorView?.backgroundColor = .red

    return cell
}

Finally, for every first row in the section, set the separator color to .clear.

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if indexPath.row == 0 {
        cell.separatorView?.backgroundColor = .clear
    }
}

Why it works

First, let's consider the structure of the UITableViewCell. If you print out the subviews of your cell you will see the following output.

<UITableViewCellContentView: 0x7ff77e604f50; frame = (0 0; 328 43.6667); opaque = NO; gestureRecognizers = <NSArray: 0x608000058d50>; layer = <CALayer: 0x60400022a660>>
<_UITableViewCellSeparatorView: 0x7ff77e4010c0; frame = (15 43.5; 360 0.5); layer = <CALayer: 0x608000223740>>
<UIButton: 0x7ff77e403b80; frame = (0 0; 22 22); opaque = NO; layer = <CALayer: 0x608000222500>>

As you can see there is a view which holds the content, the separator, and the accessory button. From this perspective, you only need to access the separator view and modify it's background. Unfortunately, it's not so easy.

Let's take a look at the same UITableViewCell in the view debugger. As you can see, there are two separator views. You need to access the bottom one which is not present when the willDisplay: is called. This is where the second hacky part comes to play.

structorue of the table view cell

When you will inspect these two elements, you will see that the first (from the top) has a background color set to nil and the second has a background color set to the value you have specified for entire UITableView. In this case, the separator with the color covers the separator without the color.

To solve the issue we have to "reverse" the situation. We can set the color of all separators to .clear which will uncover the one we have an access to. Finally, we can set the background color of the accessible separator to what is desired.

like image 105
Kamil Szostakowski Avatar answered Oct 16 '22 16:10

Kamil Szostakowski


Begin by hiding all separators via tableView.separatorStyle = .none. Then modify your UITableViewCell subclass to something as follows:

class Cell: UITableViewCell {
    var separatorLine: UIView?
    ...
}

Add the following to the method body of tableView(_:cellForRowAt:):

if cell.separatorLine == nil {
    // Create the line.
    let singleLine = UIView()
    singleLine.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
    singleLine.translatesAutoresizingMaskIntoConstraints = false

    // Add the line to the cell's content view.
    cell.contentView.addSubview(singleLine)

    let singleLineConstraints = [singleLine.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 8),
                                 singleLine.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor),
                                 singleLine.topAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -1),
                                 singleLine.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: 0)]
    cell.contentView.addConstraints(singleLineConstraints)

    cell.separatorLine = singleLine
}

cell.separatorLine?.isHidden = [Boolean which determines if separator should be displayed]

This code is in Swift, so do as you must for the Objective-C translation and make sure to continue your version checking. In my tests I don't need to use the tableView(_:willDisplayCell:forRowAt:) at all, instead everything is in the cellForRowAtIndexPath: method.

like image 44
DaveAMoore Avatar answered Oct 16 '22 17:10

DaveAMoore


Best way IMO is just to add a simple UIView with 1pt height. I wrote the following protocol which enables you to use it in any UITableViewCell you like:

// Base protocol requirements
protocol SeperatorTableViewCellProtocol: class {
    var seperatorView: UIView! {get set}
    var hideSeperator: Bool! { get set }
    func configureSeperator()
}

// Specify the separator is of a UITableViewCell type and default separator configuration method
extension SeperatorTableViewCellProtocol where Self: UITableViewCell {
    func configureSeperator() {
        hideSeperator = true
        seperatorView = UIView()
        seperatorView.backgroundColor = UIColor(named: .WhiteThree)
        contentView.insertSubview(seperatorView, at: 0)

        // Just constraint seperatorView to contentView
        seperatorView.setConstant(edge: .height, value: 1.0)
        seperatorView.layoutToSuperview(.bottom)
        seperatorView.layoutToSuperview(axis: .horizontally)

        seperatorView.isHidden = hideSeperator
    }
}

You use it like this:

// Implement the protocol with custom cell
class CustomTableViewCell: UITableViewCell, SeperatorTableViewCellProtocol {

    // MARK: SeperatorTableViewCellProtocol impl'
    var seperatorView: UIView!
    var hideSeperator: Bool! {
        didSet {
            guard let seperatorView = seperatorView else {
                return
            }
            seperatorView.isHidden = hideSeperator
        }
    }


    override func awakeFromNib() {
        super.awakeFromNib()
        configureSeperator()
        hideSeperator = false
    }
}

And that's all. You are able to customize any UITableViewCell subclass to use a separator.

Set separator visibility from tableView:willDisplayCell:forRowAtIndexPath by:

cell.hideSeperator = false / true
like image 1
DanielH Avatar answered Oct 16 '22 15:10

DanielH