Nib-file loaded UIView in UITableViewCell does not stretch
Asked Answered
D

4

3

I have a UIView which is reusable via a nib/xib-file. I want to load this and fill a UITableViewCell which is to be used in a self-resizing UITableView. All with auto-layout.

Most works good, but It seems as the loaded UIView uses the added constraints around it to shrink the UITableViewCell's contentView. This is good for the height, but I don't want this for the width.

How the UITableViewCell looks Ignore the grey cell below, this is just a selected cell.

public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
    let cellId = "RowView0002"
    var cell = tableView.dequeueReusableCell(withIdentifier: cellId)
    if cell == nil {
        cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: cellId)

        let subView = RowView(frame: cell!.frame)

        cell!.contentView.attachViewWithConstraints(subView)
        let _ = subView.viewLoadedFromNibAttached(name: cellId)

    }

    return cell!
}

override public func viewDidLoad() {
    super.viewDidLoad()

    tableView.delegate = self
    tableView.dataSource = self
    tableView.estimatedRowHeight = 40.0
    tableView.rowHeight = UITableViewAutomaticDimension
}

extension UIView
{
    public func attachViewWithConstraints(_ view:UIView)
    {
        addSubview(view)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.layoutAttachAll(to: self)
    }

    @discardableResult
    public func viewLoadedFromNibAttached<T : UIView>(name:String) -> T? {
        guard let view = Bundle.main.loadNibNamed(name, owner: self, options: nil)?[0] as? T else {
            return nil
        }
        attachViewWithConstraints(view)
        return view
    }

    public func layoutAttachAll(to childView:UIView)
    {
        var constraints = [NSLayoutConstraint]()

        childView.translatesAutoresizingMaskIntoConstraints = false
        constraints.append(NSLayoutConstraint(item: childView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0))
        constraints.append(NSLayoutConstraint(item: childView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0))
        constraints.append(NSLayoutConstraint(item: childView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0))
        constraints.append(NSLayoutConstraint(item: childView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0))

        childView.addConstraints(constraints)
    }

In RowView0002.xib I have set the rootviews background to red, added a UILabel with 4 constraints to it's sides with some margin as you can see. I both tried to set the rootView to the class RowView as well as it's File's Owner. Both "works".

Any idea how to get the contentView to match the UITableView?

*Edit 1: The green is the background of the UILabel. The red is the background of the nib-file. After running the app the View heirarcy is: UITableViewCell > ContentView > RowView > NibFileView (red) > UILabel (green)

Inspecting the view hierarchy shows that all constraints is setup as expected. However the UITableViewContentView have constraints that match the total size seen (wrong):

self.width = 156.5 @ 1000
Dishwasher answered 25/1, 2017 at 12:40 Comment(8)
Why the TVC separator line are at a different height than the end of your views? It seems that the height occupied by your view is three times the height of the cell. Are you sure that is not overlapping? try to check dimensions by using the view debuggerLatham
Ignore the tableview to the left. It's another UITableView.Dishwasher
There are some stuff that I do not understand, is the green your view from xib? is the red the TVC contentView?Latham
The green is the background of the UILabel. The red is the background of the nib-file. After running the app the View heirarcy is: UITableViewCell > ContentView > RowView > NibFileView (red) > UILabel (green)Dishwasher
Hugging and resistance priorities of the label are set to the default values?Latham
Yes think to. The UILabel has hugging/compression 251/750. The other UIViews all have 250/750.Dishwasher
Hey sunkas.? thanks for posting the question I'm a newbie who is also trying to achieve what you did. How are you registering the cell, for the tableview?Bibulous
Here's the question asked by me, that would be really awesome if you suggest something: #57355646Bibulous
D
1

I solved it by also adding a width constraint matching the tableViews width. This is code from CustomTableViewCell:

public override func layoutSubviews() {
    super.layoutSubviews()

    if let width = tableView()?.frame.width, !haveAddedWidthConstraint {
        haveAddedWidthConstraint = true
        rowView.addWidthConstraint(width: width)
    }
}

UIViewExtension:

public func addWidthConstraint(width: CGFloat) {
    let widthConstraint = NSLayoutConstraint(item: self, attribute: .width, relatedBy: .greaterThanOrEqual, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: width)
    widthConstraint.priority = 1000
    addConstraint(widthConstraint)
}

UITableViewCellExtension:

func tableView() -> UITableView? {
    var currentView: UIView = self
    while let superView = currentView.superview {
        if superView is UITableView {
            return (superView as! UITableView)
        }
        currentView = superView
    }
    return nil
}
Dishwasher answered 21/2, 2017 at 14:20 Comment(0)
S
5

A full implementation of layoutAttachAll is below.

Some usage examples first:

 // pin all edge to superview
 myView.layoutAttachAll()

 // pin all edges (to superview) with margin:
 myView.layoutAttachAll(margin: 8.0)

 // for child views: pin leading edge to superview's leading edge:
 myView.layoutAttachLeading()

 // for sibling views: pin leading edge to siblingView's trailing edge:
 myView.layoutAttachLeading(to: siblingView)

 // for sibling views: pin top edge to siblingView's bottom edge:
 myView.layoutAttachTop(to: siblingView)

Note: myView must be added as a subview before attaching to superview using these methods. Also, all participating views must be set with translatesAutoresizingMaskIntoConstraints = false.

The full implementation:

import UIKit

extension UIView {

    /// attaches all sides of the receiver to its parent view
    func layoutAttachAll(margin : CGFloat = 0.0) {
        let view = superview
        layoutAttachTop(to: view, margin: margin)
        layoutAttachBottom(to: view, margin: margin)
        layoutAttachLeading(to: view, margin: margin)
        layoutAttachTrailing(to: view, margin: margin)
    }

    /// attaches the top of the current view to the given view's top if it's a superview of the current view, or to it's bottom if it's not (assuming this is then a sibling view).
    /// if view is not provided, the current view's super view is used
    @discardableResult
    func layoutAttachTop(to: UIView? = nil, margin : CGFloat = 0.0) -> NSLayoutConstraint {

        let view: UIView? = to ?? superview
        let isSuperview = view == superview
        let constraint = NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: view, attribute: isSuperview ? .top : .bottom, multiplier: 1.0, constant: margin)
        superview?.addConstraint(constraint)

        return constraint
    }

    /// attaches the bottom of the current view to the given view
    @discardableResult
    func layoutAttachBottom(to: UIView? = nil, margin : CGFloat = 0.0, priority: UILayoutPriority? = nil) -> NSLayoutConstraint {

        let view: UIView? = to ?? superview
        let isSuperview = (view == superview) || false
        let constraint = NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: isSuperview ? .bottom : .top, multiplier: 1.0, constant: -margin)
        if let priority = priority {
            constraint.priority = priority
        }
        superview?.addConstraint(constraint)

        return constraint
    }

    /// attaches the leading edge of the current view to the given view
    @discardableResult
    func layoutAttachLeading(to: UIView? = nil, margin : CGFloat = 0.0) -> NSLayoutConstraint {

        let view: UIView? = to ?? superview
        let isSuperview = (view == superview) || false
        let constraint = NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: view, attribute: isSuperview ? .leading : .trailing, multiplier: 1.0, constant: margin)
        superview?.addConstraint(constraint)

        return constraint
    }

    /// attaches the trailing edge of the current view to the given view
    @discardableResult
    func layoutAttachTrailing(to: UIView? = nil, margin : CGFloat = 0.0, priority: UILayoutPriority? = nil) -> NSLayoutConstraint {

        let view: UIView? = to ?? superview
        let isSuperview = (view == superview) || false
        let constraint = NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: isSuperview ? .trailing : .leading, multiplier: 1.0, constant: -margin)
        if let priority = priority {
            constraint.priority = priority
        }
        superview?.addConstraint(constraint)

        return constraint
    }
}
Statuette answered 18/9, 2017 at 12:35 Comment(1)
These are just help-methods to set top, bottom, leading and trailing constraints. My layoutAttachAll are already doing this. The problem was as the accepted answer describes a missing width-constraint.Dishwasher
D
1

I solved it by also adding a width constraint matching the tableViews width. This is code from CustomTableViewCell:

public override func layoutSubviews() {
    super.layoutSubviews()

    if let width = tableView()?.frame.width, !haveAddedWidthConstraint {
        haveAddedWidthConstraint = true
        rowView.addWidthConstraint(width: width)
    }
}

UIViewExtension:

public func addWidthConstraint(width: CGFloat) {
    let widthConstraint = NSLayoutConstraint(item: self, attribute: .width, relatedBy: .greaterThanOrEqual, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: width)
    widthConstraint.priority = 1000
    addConstraint(widthConstraint)
}

UITableViewCellExtension:

func tableView() -> UITableView? {
    var currentView: UIView = self
    while let superView = currentView.superview {
        if superView is UITableView {
            return (superView as! UITableView)
        }
        currentView = superView
    }
    return nil
}
Dishwasher answered 21/2, 2017 at 14:20 Comment(0)
R
0

It looks like the problem is that the cell created with UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: cellId) has no information about the width of the table view, so it is sizing itself based on the width of the label. Apparently the table view / cell don't force the cell's content view to take its width.

You probably want to rework some of this cell-handling code.

If you want to load your cell from a xib, you can skip everything with constraints. Just implement:

 override func viewDidLoad() {
    //...
    let nib = UINib(nibName: "RowView0002", bundle: NSBundle.main)
    tableView.reigsterNib(nib, forCellReuseIdentifier: "RowView0002")
 }

Very important: The first top level item in the .xib file must be a UITableViewCell. Nibs are UIViews by default, delete the view in IB and drag a UITableViewCell from the object library in the lower-right of IB. Then if necessary set its subclass to a UITableViewCell subclass that you created. (You might also need to set the reuseIdentifier in IB.)

Then in tableView(_:cellForRowAt IndexPath:):

guard let cell = tableView.dequeueResuableCell(withIdentifier: "RowView0002", for: indexPath) as? TheNibUITableViewSubclass else { //something went wrong, probably crash } 
cell.label.text = //...
return cell 

You probably will want to put that "RowView0002" in a constant somewhere.

If the "RowView0002" and the RowView class both need to be views, you should probably create a subclass of UITableViewCell. Override just init(style:resueIdentifier:) and after callingsuper` add your subviews in the code above. Hope this helps!

Radley answered 4/2, 2017 at 0:37 Comment(1)
The issue is that I need to load the nib-file in other places where it is not used in a UITableViewCell. So I cannot use your suggestion of having a UITableViewCell as a root view in my nib-file. I however solved it by having a additional width constraint that stretches the view when placed in a UITableViewCell.Dishwasher
S
-1

I solved it by exposing a method that sets the anchor constraint of UITableViewCell in my custom UITableViewCell class and call it from parent UITableView cellForRowAtIndexPath method.

Here is the code in Objective C.

(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{

    YourTableViewCellType *cell = [tableView dequeueReusableCellWithIdentifier:cellId forIndexPath:indexPath];
    **[cell expandToParentSize];** // Self-sizing magic!
    
    return cell;
}

// Write below code in "YourTableViewCellType" class. "containerStack" is my UIStackView that holds all controls in the nib UITableViewCell

(void)expandToParentSize 
{    
    [NSLayoutConstraint activateConstraints:@[
        [self.containerStack.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor],
        [self.containerStack.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor],
        [self.containerStack.topAnchor constraintEqualToAnchor:self.contentView.topAnchor],
        [self.containerStack.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor]
     ]];
}
Sublease answered 21/10, 2020 at 23:59 Comment(5)
This code does not contain any width constraints. Is does the same as my original questions. My (accepted) answer already mentions that I solved it by setting the width constraint.Dishwasher
Its a typo, I should have said anchor constraints. That doesn't mean the answer is wrong my friend, it worked for me.Sublease
But do you load UIViews from nib/xib files? If you provide the same code as the question you're not really helping I'm afraid.Dishwasher
Yes, I do load nib file for my viewSublease
Yes, but do you load a UIView (not UITableViewCell) from a nib file?Dishwasher

© 2022 - 2024 — McMap. All rights reserved.