UITableView с переменной высотой ячейки: работает в IB, но не программно
TL;DR
Мои программно созданные ячейки табличного представления не меняются в соответствии с внутренней высотой содержимого их пользовательских представлений, даже если я использую UITableViewAutomaticDimension
и установка как верхнего, так и нижнего ограничения.
Проблема, вероятно, заключается в моей реализации UITableViewCell
подкласс. См. Код ниже в разделе " Не работает программно"> "Код"> MyCustomCell.swift.
Цель
Я пытаюсь сделать панель предложений для пользовательской монгольской клавиатуры. Монгольский написан вертикально. В Android это выглядит так:
Прогресс
Я узнал, что я должен использовать UITableView
с переменной высотой ячейки, которая доступна начиная с iOS 8. Для этого необходимо использовать автоматическую разметку и указать табличному представлению использовать автоматические размеры для высоты ячейки.
Некоторые вещи, которые мне пришлось выучить на этом пути, представлены в моих недавних SO-вопросах и ответах:
- Как сделать настраиваемую ячейку табличного представления
- Получение переменной высоты для работы в табличном представлении со стандартом
UILabel
- Получение внутреннего размера контента для работы с пользовательским представлением
- Использование программно созданного
UITableViewCell
- Установить ограничения программно
Итак, я пришел к тому, что у меня есть вертикальные метки, которые поддерживают внутренний размер контента. Эти метки помещаются в ячейки моего пользовательского табличного представления. И, как описано в следующем разделе, они работают, когда я делаю это в раскадровке, а не когда я создаю все программно.
Работает в IB
Чтобы изолировать проблему, я создал два основных проекта: один для того, где я использую раскадровку, и другой, где я делаю все программно. Проект раскадровки работает. Как видно на следующем рисунке, каждая ячейка табличного представления изменяется в соответствии с высотой настраиваемой вертикальной метки.
В IB
Я установил ограничения, чтобы закрепить верх и низ, а также центрировать метку.
Код
ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let myStrings: [String] = ["a", "bbbbbbb", "cccc", "dddddddddd", "ee"]
let cellReuseIdentifier = "cell"
@IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
}
// number of rows in table view
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.myStrings.count
}
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
return cell
}
// method to run when table view cell is tapped
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("You tapped cell number \(indexPath.row).")
}
}
MyCustomCell.swift
import UIKit
class MyCustomCell: UITableViewCell {
@IBOutlet weak var myCellLabel: UIMongolSingleLineLabel!
}
Не работает программно
Поскольку я хочу, чтобы панель подсказок была частью окончательной клавиатуры, я должна иметь возможность создавать ее программно. Однако, когда я пытаюсь воссоздать приведенный выше пример проекта программно, он не работает. Я получаю следующий результат.
Высота ячеек не изменяется, а пользовательские вертикальные метки накладываются друг на друга.
Я также получаю следующую ошибку:
Только предупреждение один раз: обнаружен случай, когда ограничения неоднозначно указывают высоту нуля для представления содержимого ячейки табличного представления. Мы рассматриваем развал непреднамеренно и вместо этого используем стандартную высоту.
Эта ошибка неоднократно поднималась при переполнении стека:
- iOS8 - ограничения неоднозначно предполагают высоту нуля
- Обнаружен случай, когда ограничения неоднозначно предполагают высоту ноль
- пользовательская высота ячейки UITableview установлена неправильно
- ios 8 (UITableViewCell): ограничения неоднозначно указывают высоту нуля для представления содержимого ячейки табличного представления
Тем не менее, проблема для большинства из этих людей заключается в том, что они не устанавливали ограничение верхнего и нижнего штифта. Я, или, по крайней мере, я так думаю, как показано в моем коде ниже.
Код
ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let myStrings: [String] = ["a", "bbbbbbb", "cccc", "dddddddddd", "ee"]
let cellReuseIdentifier = "cell"
var tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
// Suggestion bar
tableView.frame = CGRect(x: 0, y: 20, width: view.bounds.width, height: view.bounds.height)
tableView.registerClass(MyCustomCell.self, forCellReuseIdentifier: cellReuseIdentifier)
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
view.addSubview(tableView)
}
// number of rows in table view
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.myStrings.count
}
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
return cell
}
// method to run when table view cell is tapped
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("You tapped cell number \(indexPath.row).")
}
}
MyCustomCell.swift
Я думаю, что проблема, вероятно, здесь, так как это главное отличие от проекта IB.
import UIKit
class MyCustomCell: UITableViewCell {
var myCellLabel = UIMongolSingleLineLabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.myCellLabel.translatesAutoresizingMaskIntoConstraints = false
self.myCellLabel.centerText = false
self.myCellLabel.backgroundColor = UIColor.yellowColor()
self.addSubview(myCellLabel)
// Constraints
// pin top
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
}
override internal class func requiresConstraintBasedLayout() -> Bool {
return true
}
}
Дополнительный код
Я также включу код для пользовательской вертикальной метки, которую я использовал в обоих проектах выше, но поскольку проект IB работает, я не думаю, что главная проблема здесь.
import UIKit
@IBDesignable
class UIMongolSingleLineLabel: UIView {
private let textLayer = LabelTextLayer()
var useMirroredFont = false
// MARK: Primary input value
@IBInspectable var text: String = "A" {
didSet {
textLayer.displayString = text
updateTextLayerFrame()
}
}
@IBInspectable var fontSize: CGFloat = 17 {
didSet {
updateTextLayerFrame()
}
}
@IBInspectable var centerText: Bool = true {
didSet {
updateTextLayerFrame()
}
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
func setup() {
// Text layer
textLayer.backgroundColor = UIColor.yellowColor().CGColor
textLayer.useMirroredFont = useMirroredFont
textLayer.contentsScale = UIScreen.mainScreen().scale
layer.addSublayer(textLayer)
}
override func intrinsicContentSize() -> CGSize {
return textLayer.frame.size
}
func updateTextLayerFrame() {
let myAttribute = [ NSFontAttributeName: UIFont.systemFontOfSize(fontSize) ]
let attrString = NSMutableAttributedString(string: textLayer.displayString, attributes: myAttribute )
let size = dimensionsForAttributedString(attrString)
// This is the frame for the soon-to-be rotated layer
var x: CGFloat = 0
var y: CGFloat = 0
if layer.bounds.width > size.height {
x = (layer.bounds.width - size.height) / 2
}
if centerText {
y = (layer.bounds.height - size.width) / 2
}
textLayer.frame = CGRect(x: x, y: y, width: size.height, height: size.width)
textLayer.string = attrString
invalidateIntrinsicContentSize()
}
func dimensionsForAttributedString(attrString: NSAttributedString) -> CGSize {
var ascent: CGFloat = 0
var descent: CGFloat = 0
var width: CGFloat = 0
let line: CTLineRef = CTLineCreateWithAttributedString(attrString)
width = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, nil))
// make width an even integer for better graphics rendering
width = ceil(width)
if Int(width)%2 == 1 {
width += 1.0
}
return CGSize(width: width, height: ceil(ascent+descent))
}
}
// MARK: - Key Text Layer Class
class LabelTextLayer: CATextLayer {
// set this to false if not using a mirrored font
var useMirroredFont = true
var displayString = ""
override func drawInContext(ctx: CGContext) {
// A frame is passed in, in which the frame size is already rotated at the center but the content is not.
CGContextSaveGState(ctx)
if useMirroredFont {
CGContextRotateCTM(ctx, CGFloat(M_PI_2))
CGContextScaleCTM(ctx, 1.0, -1.0)
} else {
CGContextRotateCTM(ctx, CGFloat(M_PI_2))
CGContextTranslateCTM(ctx, 0, -self.bounds.width)
}
super.drawInContext(ctx)
CGContextRestoreGState(ctx)
}
}
Обновить
Весь код проекта находится здесь, поэтому, если кто-то заинтересован в том, чтобы попробовать его, просто создайте новый проект, вырезайте и вставляйте приведенный выше код в следующие три файла:
- ViewController.swift
- MyCustomCell.swift
- UIMongolSingleLineLabel.swift
5 ответов
Ошибка довольно тривиальна:
Вместо
self.addSubview(myCellLabel)
использование
self.contentView.addSubview(myCellLabel)
Также я бы заменил
// pin top
NSLayoutConstraint(...).active = true
// pin bottom
NSLayoutConstraint(...).active = true
// center horizontal
NSLayoutConstraint(...).active = true
с
let topConstraint = NSLayoutConstraint(...)
let bottomConstraint = NSLayoutConstraint(...)
let centerConstraint = NSLayoutConstraint(...)
self.contentView.addConstraints([topConstraint, bottomConstraint, centerConstraint])
который является более явным (необходимо указать владельца ограничения) и, следовательно, более безопасным.
Проблема в том, что при звонке active = true
на ограничении система макета должна решить, к какому представлению она должна добавить ограничения. В вашем случае, потому что первый общий предок contentView
а также myCellLabel
твой UITableViewCell
они были добавлены в ваш UITableViewCell
таким образом, они на самом деле не ограничивали contentView
(ограничения были между братьями и сестрами, а не между superview-subview).
Ваш код фактически вызвал предупреждение консоли:
Только предупреждение один раз: обнаружен случай, когда ограничения неоднозначно указывают высоту нуля для представления содержимого ячейки табличного представления. Мы рассматриваем развал непреднамеренно и вместо этого используем стандартную высоту.
Это заставило меня сразу же посмотреть, как создаются ограничения для вашего ярлыка.
Я проверил ваш код и обнаружил, что проблема заключалась в настройке ограничений. Используйте приведенную ниже часть кода для установки констант в функции настройки файла () MyCustomCell.swift.
let topConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1, constant: 0)
let centerConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .CenterX, multiplier: 1, constant: 0)
self.addConstraints([centerConstraint, topConstraint, bottomConstraint])
Также установите для клипов привязанное свойство к списку ячеек в "viewcontroller.swift"
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
cell.myCellLabel.clipsToBounds=true
return cell
}
Для вашего удобства я загрузил свой пример кода на образец динамической высоты GitHub.
Выход выглядит вот так
Кажется, проблема связана с вертикальными ограничениями в ячейке. Поместив их относительно self вместо self.contentView в MyCustomCell, вы можете исправить свою проблему
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
полный класс будет:
import UIKit
class MyCustomCell: UITableViewCell {
var myCellLabel = UIMongolSingleLineLabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.myCellLabel.translatesAutoresizingMaskIntoConstraints = false
self.myCellLabel.centerText = false
self.myCellLabel.backgroundColor = UIColor.yellowColor()
self.addSubview(myCellLabel)
// Constraints
// pin top
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
}
override internal class func requiresConstraintBasedLayout() -> Bool {
return true
}
}
То, что вам не хватает, это эта функция:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return heightValue
}
Я не совсем уверен, что именно вам следует делать, но благодаря тому, что вы знаете свои метки, вы сможете вернуть точное значение высоты для каждой ячейки в этом методе.
Я думаю, что вам не хватает, чтобы установить ограничения для tableView с superview. И попробуйте также увеличить предполагаемую высоту строки.