ios - UITableView con altura de celda variable: trabajando en IB pero no mediante programación
swift mongolian-vertical-script (5)
Creo que falta para establecer restricciones para tableView con superview. Y tratar de aumentar la altura estimada de la fila también.
TL; DR
Las celdas de vista de tabla creadas de manera programática no cambian de tamaño según la altura del contenido intrínseco de sus vistas personalizadas, aunque estoy utilizando UITableViewAutomaticDimension
y configurando las restricciones superior e inferior.
El problema probablemente radica en mi implementación de la subclase UITableViewCell
. Vea el código a continuación en No funciona mediante programación> Código> MyCustomCell.swift .
Gol
Estoy tratando de hacer una barra de sugerencias para un teclado mongol personalizado. El mongol se escribe verticalmente. En Android se ve así:
Progreso
Aprendí que debería usar un UITableView
con alturas de celda variables, que está disponible a partir de iOS 8. Esto requiere usar el diseño automático y decirle a la vista de tabla que use dimensiones automáticas para las alturas de celda.
Algunas de las cosas que he tenido que aprender en el camino están representadas en mis recientes preguntas y respuestas de SO:
- Cómo hacer una celda de vista de tabla personalizada
- Obtención de la altura variable para trabajar en una vista de tabla con un
UILabel
estándar - Conseguir que el tamaño del contenido intrínseco funcione para una vista personalizada
- Usando un
UITableViewCell
creadoUITableViewCell
- Establecer restricciones programáticamente
Así que he llegado al punto en que tengo las etiquetas verticales que admiten el tamaño del contenido intrínseco. Estas etiquetas van en mis celdas de vista de tabla personalizadas. Y como se describe en la siguiente sección, funcionan cuando lo hago en el guión gráfico, pero no cuando creo todo programáticamente.
Trabaja en IB
Para aislar el problema, creé dos proyectos básicos: uno para el que utilizo el guión gráfico y otro donde hago todo programáticamente. El proyecto de guión gráfico funciona. Como se puede ver en la siguiente imagen, cada celda de vista de tabla cambia de tamaño para coincidir con la altura de la etiqueta vertical personalizada.
En IB
Establecí restricciones para fijar la parte superior e inferior, así como centrar la etiqueta.
Código
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!
}
No funciona programáticamente
Como quiero que la barra de sugerencias forme parte del teclado final, necesito poder crearla mediante programación. Sin embargo, cuando intento recrear el proyecto de ejemplo anterior mediante programación, no funciona. Me sale el siguiente resultado.
Las alturas de las celdas no cambian de tamaño y las etiquetas verticales personalizadas se solapan entre sí.
También me sale el siguiente error:
Solo advertencia una vez: detectó un caso en el que las restricciones sugieren de manera ambigua una altura de cero para la vista de contenido de una celda de vista de tabla. Estamos considerando el colapso involuntario y en su lugar estamos usando una altura estándar.
Este error se ha detectado varias veces en Desbordamiento de pila:
- iOS8 - las restricciones sugieren de forma ambigua una altura de cero
- Se detectó un caso en el que las restricciones sugieren ambiguamente una altura de cero.
- La altura de celda de UITableview personalizada no se ha configurado correctamente
- ios 8 (UITableViewCell): las restricciones sugieren de manera ambigua una altura de cero para la vista de contenido de una celda de vista de tabla
Sin embargo, el problema para la mayoría de esas personas es que no estaban configurando una restricción de pasador superior e inferior. Soy, o al menos creo que lo soy, como se muestra en mi código a continuación.
Código
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
Creo que el problema está probablemente aquí, ya que esta es la principal diferencia con el proyecto 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
}
}
Código suplementario
También incluiré el código para la etiqueta vertical personalizada que utilicé en los dos proyectos anteriores, pero como el proyecto IB funciona, no creo que el problema principal esté aquí.
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)
}
}
Actualizar
El código completo para el proyecto está aquí, por lo que si alguien está lo suficientemente interesado como para probarlo, simplemente cree un nuevo proyecto y corte y pegue el código anterior en los siguientes tres archivos:
- ViewController.swift
- MyCustomCell.swift
- UIMongolSingleLineLabel.swift
El error es bastante trivial:
En lugar de
self.addSubview(myCellLabel)
utilizar
self.contentView.addSubview(myCellLabel)
Además, me gustaría reemplazar
// pin top
NSLayoutConstraint(...).active = true
// pin bottom
NSLayoutConstraint(...).active = true
// center horizontal
NSLayoutConstraint(...).active = true
con
let topConstraint = NSLayoutConstraint(...)
let bottomConstraint = NSLayoutConstraint(...)
let centerConstraint = NSLayoutConstraint(...)
self.contentView.addConstraints([topConstraint, bottomConstraint, centerConstraint])
que es más explícito (tiene que especificar el propietario de la restricción) y, por lo tanto, más seguro.
El problema es que cuando se llama a active = true
en una restricción, el sistema de diseño debe decidir a qué vista debe agregar las restricciones. En su caso, debido a que el primer antecesor común de contentView
y myCellLabel
es su UITableViewCell
, se agregaron a su UITableViewCell
, por lo que en realidad no restringieron el contentView
(las restricciones fueron entre hermanos y no entre supervisión y subvista).
Su código realmente activó una advertencia de consola:
Solo advertencia una vez: detectó un caso en el que las restricciones sugieren de manera ambigua una altura de cero para la vista de contenido de una celda de vista de tabla. Estamos considerando el colapso involuntario y en su lugar estamos usando una altura estándar.
Lo que me hizo ver de inmediato la forma en que se crean las restricciones para su etiqueta.
El problema parece provenir de las restricciones verticales en la celda. Poniéndolos en relación con uno mismo en lugar de uno mismo. Contenido en MyCustomCell puede solucionar su problema
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
La clase completa sería:
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
}
}
He probado su código y descubrí que el problema estaba en la configuración de restricciones. Por favor use la parte del código a continuación para configurar las constantes en su función de configuración de archivo "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])
También establezca clips a la propiedad vinculada a la etiqueta de su celda en "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
}
Para su facilidad, he cargado mi código de muestra en GitHub github.com/tarunseera/DynamicHeightCell
La salida se ve así ahora
Lo que te falta es esta función:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return heightValue
}
No estoy tan seguro de lo que debe hacer exactamente, pero por el hecho de que conoce sus etiquetas, debería poder devolver un valor de altura exacto para cada celda en este método