适用于iOS

时间:2016-03-01 00:52:41

标签: ios uitableview uiview

问题定义

我正在尝试构建一个与UILabel类似的自定义控件。我应该能够将这样的控件放在自定义表格单元格中,它应该:

  • 包装它的内容(如带有numberOfLines = 0的UILabel)
  • 自动扩展自定义单元格高度
  • 处理设备轮换
  • 在UITableCellView或ViewControll中不需要任何特殊代码来实现此功能(UILabel不需要任何特殊代码)。

研究

我做的第一件事很简单。我决定观察UILabel的工作原理。我做了以下事情:

  • 创建了一个包含自定义单元格的表格
  • 创建了一个自定义单元格,将UILabel(其中包含numberOfLines=0)
  • 放入其中
  • 创建约束以确保UILabel占用整个单元格
  • 子类UILabel并覆盖了一堆方法来查看它的行为方式

我检查过以下内容

  • 以纵向运行(标签在多行上正确显示)并且单元格高度正确
  • 旋转它。桌面宽度和高度已更新,它们也是正确的。

我观察到它表现良好。它不需要任何特殊代码,我看到系统执行的(某些)调用的顺序。

部分解决方案

@Wingzero在下面写了一个部分解决方案。它创建了正确大小的单元格。

然而,他的解决方案有两个问题:

  • 它使用“self.superview.bounds.size.width”。如果您的控件占据整个单元格,则可以使用此方法。但是,如果单元格中有其他任何使用约束的内容,则此类代码将无法正确计算宽度。

  • 根本不处理旋转。我很确定它不会处理其他调整大小的事件(有很多不太常见的调整大小事件 - 比如状态栏在电话呼叫时越来越大等)。

你知道如何解决这个问题吗? 我找到了一些文章,讨论构建更多静态自定义控件以及在自定义单元格中使用预构建控件。

但是,我还没有找到任何可以解决这两个问题的解决方案。

4 个答案:

答案 0 :(得分:6)

我必须使用答案部分发布我的想法并继续前进,虽然这可能不是你的答案,因为我不完全理解阻止你的是什么,因为我认为你已经知道内在的大小和是的。

根据评论,我尝试使用text属性创建一个视图并覆盖内在:

头文件,后来我发现maxPreferredWidth没有完全使用,所以请忽略它:

@interface LabelView : UIView

IB_DESIGNABLE
@property (nonatomic, copy) IBInspectable NSString *text;
@property (nonatomic, assign) IBInspectable CGFloat maxPreferredWidth;

@end

.m文件:

#import "LabelView.h"

@implementation LabelView

-(void)setText:(NSString *)text {
    if (![_text isEqualToString:text]) {
        _text = text;
        [self invalidateIntrinsicContentSize];
    }
}

-(CGSize)intrinsicContentSize {
    CGRect boundingRect = [self.text boundingRectWithSize:CGSizeMake(self.superview.bounds.size.width, CGFLOAT_MAX)
                                           options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading
                                        attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]}
                                           context:nil];
    return boundingRect.size;
}

@end

和带有xib的UITableViewCell:

头文件:

@interface LabelCell : UITableViewCell

@property (weak, nonatomic) IBOutlet LabelView *labelView;

@end

.m文件:

@implementation LabelCell

- (void)awakeFromNib {
    [super awakeFromNib];
}

@end

xib,它简单,只是顶部,底部,前导,尾随约束: enter image description here

所以运行它,基于文本的边界矩形,单元格的高度是不同的,在我的情况下,我有两个文本循环:1。"哈哈", 2." asdf" {重复多次创建一个长字符串}

所以奇数单元格高度为19,偶数单元格高度为58高度: enter image description here

这是你在寻找什么?

我的想法:

UITableView的单元格宽度始终与表格视图相同,因此宽度为。 UICollectionView可能会出现更多问题,但重点是我们将计算它并返回它就足够了。

演示项目:https://github.com/liuxuan30/StackOverflow-DynamicSize
(我根据我的旧项目进行了更改,其中包含一些图像,忽略这些图像。)

答案 1 :(得分:3)

这是一个满足您要求的解决方案,也是IBDesignable,因此它可以在Interface Builder中实时预览。该类将布置一系列正方形(总数等于IBInspectable B属性)。默认情况下,它只会将它们全部放在一条长行中。但是如果将count IBInspectable属性设置为wrap,它将包装正方形并根据其约束宽度增加其高度(如带有numberOfLines == 0的UILabel)。在自定义表格视图单元格中,这将具有推出顶部和底部以适应自定义视图的包装内在大小的效果。

代码:

On

在我的示例应用程序中,我设置表视图以使每行包含此自定义视图的单元格出列,并将自定义视图的import Foundation import UIKit @IBDesignable class WrappingView : UIView { private class InnerWrappingView : UIView { private var lastPoint:CGPoint = CGPointZero private var wrap = false private var count:Int = 100 private var size:Int = 8 private var spacing:Int = 3 private func calculatedSize() -> CGSize { lastPoint = CGPoint(x:-(size + spacing), y: 0) for _ in 0..<count { var nextPoint:CGPoint! if wrap { nextPoint = lastPoint.x + CGFloat(size + spacing + size) <= bounds.width ? CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y) : CGPoint(x: 0, y: lastPoint.y + CGFloat(size + spacing)) } else { nextPoint = CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y) } lastPoint = nextPoint } return CGSize(width: wrap ? bounds.width : lastPoint.x + CGFloat(size), height: lastPoint.y + CGFloat(size)) } override func layoutSubviews() { super.layoutSubviews() guard bounds.size != calculatedSize() || subviews.count == 0 else { return } for subview in subviews { subview.removeFromSuperview() } lastPoint = CGPoint(x:-(size + spacing), y: 0) for _ in 0..<count { let square = createSquareView() var nextPoint:CGPoint! if wrap { nextPoint = lastPoint.x + CGFloat(size + spacing + size) <= bounds.width ? CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y) : CGPoint(x: 0, y: lastPoint.y + CGFloat(size + spacing)) } else { nextPoint = CGPoint(x: lastPoint.x + CGFloat(size + spacing), y: lastPoint.y) } square.frame = CGRect(origin: nextPoint, size: square.bounds.size) addSubview(square) lastPoint = nextPoint } let newframe = CGRect(origin: frame.origin, size: calculatedSize()) frame = newframe invalidateIntrinsicContentSize() setNeedsLayout() } private func createSquareView() -> UIView { let square = UIView(frame: CGRect(x: 0, y: 0, width: size, height: size)) square.backgroundColor = UIColor.blueColor() return square } override func intrinsicContentSize() -> CGSize { return calculatedSize() } } @IBInspectable var count:Int = 500 { didSet { innerView.count = count layoutSubviews() } } @IBInspectable var size:Int = 8 { didSet { innerView.size = size layoutSubviews() } } @IBInspectable var spacing:Int = 3 { didSet { innerView.spacing = spacing layoutSubviews() } } @IBInspectable var wrap:Bool = false { didSet { innerView.wrap = wrap layoutSubviews() } } private var _innerView:InnerWrappingView! { didSet { clipsToBounds = true addSubview(_innerView) _innerView.clipsToBounds = true _innerView.frame = bounds _innerView.wrap = wrap _innerView.translatesAutoresizingMaskIntoConstraints = false _innerView.leftAnchor.constraintEqualToAnchor(leftAnchor).active = true _innerView.rightAnchor.constraintEqualToAnchor(rightAnchor).active = true _innerView.topAnchor.constraintEqualToAnchor(topAnchor).active = true _innerView.bottomAnchor.constraintEqualToAnchor(bottomAnchor).active = true _innerView.setContentCompressionResistancePriority(750, forAxis: .Vertical) _innerView.setContentHuggingPriority(251, forAxis: .Vertical) } } private var innerView:InnerWrappingView! { if _innerView == nil { _innerView = InnerWrappingView() } return _innerView } override func layoutSubviews() { super.layoutSubviews() if innerView.bounds.width != bounds.width { innerView.frame = CGRect(origin: CGPointZero, size: CGSize(width: bounds.width, height: 0)) } innerView.layoutSubviews() if innerView.bounds.height != bounds.height { invalidateIntrinsicContentSize() superview?.layoutIfNeeded() } } override func intrinsicContentSize() -> CGSize { return innerView.calculatedSize() } } 属性设置为20 * indexPath的行。自定义视图约束为单元格宽度的50%,因此在横向和纵向之间移动时,其宽度将自动更改。因为每个连续的表格单元格包裹更长更长的正方形,所以每个单元格的大小都会自动变得更高更高。

运行时,它看起来像这样(包括旋转演示):

custom view in self-sizing table cells

答案 2 :(得分:1)

基于@Wingzero的另一个答案,布局是一个复杂的问题...

提到的maxPreferredWidth很重要,与preferredMaxLayoutWidth的{​​{1}}有关。这个属性的要点是告诉标签不要只是一个长行,而是在宽度达到该值时更喜欢换行。因此,在计算内在大小时,您将使用UILabel(如果设置)的最小值或视图宽度作为最大宽度。

另一个关键方面是preferredMaxLayoutWidth,当事情发生变化时,视图应该调用它,并且意味着需要新的布局。

invalidateIntrinsicContentSize无法处理轮换 - 它并不了解它。视图控制器负责检测和处理旋转,通常是在触发新布局运行之前使布局无效并更新视图大小。标签(和其他视图)就在那里处理生成的布局。作为旋转的一部分,您(即视图控制器)可能会更改UILabel,因为例如在横向布局中允许更多宽度是有意义的。

答案 3 :(得分:-2)

enter image description here

你在寻找像这样的东西^^?单元格具有动态高度以便于UILabel的内容,并且没有任何代码可以计算尺寸/宽度/高度 - 只是一些约束。

基本上,左侧的标签具有单元格的顶部,底部和前缘,以及右侧标签的尾随边距,该标签具有单元格的尾随边距。只需要一个标签?然后忽略右侧标签,并将左侧标签配置为对单元格的尾随约束。

如果您需要标签为多行,请配置。将numberOfLines设置为2,3或0,由您决定。

您无需计算表格视图单元格的高度,自动布局将为您计算;但是你需要告诉它使用自动维度:self.tableView.rowHeight = UITableViewAutomaticDimension,或者在tableView:heightForRowAtIndexPath:中返回它。您还可以在tableView:estimatedHeightForRowAtIndexPath:中告诉表格视图“粗略”估算,以获得更好的性能。

仍然无法使用?将内容压缩阻力优先级 - 垂直设置为1000 /所需的UILabel,这样标签的内容将尽力“抵抗压缩”,并且numberOfLines配置将被完全确认。

它旋转了吗?尝试观察旋转(有方向更改通知),然后更新布局(setNeedsLayout)。

仍然无法使用?更多内容请点击此处:Using Auto Layout in UITableView for dynamic cell layouts & variable row heights