Nib-file loaded UIView in UITableViewCell does not

2020-04-10 02:50发布

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

3条回答
\"骚年 ilove
2楼-- · 2020-04-10 03:30

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
}
查看更多
放荡不羁爱自由
3楼-- · 2020-04-10 03:40

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!

查看更多
兄弟一词,经得起流年.
4楼-- · 2020-04-10 03:50

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
    }
}
查看更多
登录 后发表回答