Solución universal para UITableViewRowAction y UIContextualAction



Muchos se han encontrado con el problema de implementar una hermosa asignación de botones para UITableViewCell cuando se desplazan hacia la izquierda. Algunos usaron la funcionalidad estándar "fuera de la caja", otros se molestaron con su propia implementación, mientras que otros lo hicieron con caracteres Unicode. En este artículo, le diré cómo lograr la máxima personalización de UITableViewRowAction.

Primero, una pequeña teoría: UITableViewRowAction es una clase que permite al usuario realizar ciertas acciones con una celda determinada (como “eliminar”, “cambiar”, etc.) al mover una celda hacia la izquierda. Cada instancia de esta clase representa una acción para ejecutar e incluye texto, estilo y un controlador de clics. El SDK de iOS nos permite personalizar UITableViewRowAction de la siguiente manera:

public convenience init(style: UITableViewRowActionStyle, title: String?, handler: @escaping (UITableViewRowAction, IndexPath) -> Swift.Void) 

También puede establecer backgroundColor.

Métodos existentes


Considere los ejemplos de implementación existentes, o lo que encontré en Internet.
Método número 1 . Utilice la funcionalidad "fuera de la caja". Esto es suficiente para usar botones comunes con texto y fondo. Se ve demasiado simple y aburrido:


Método número 2 . Establezca backgroundColor usando UIColor (patternImage: UIImage) . Esto logrará belleza, pero privará la versatilidad, agregará una serie de restricciones y dificultades. Dado que el ancho de UITableViewRowAction se calcula por el tamaño del texto, debe especificar el título como un conjunto de caracteres de espacio en blanco de cierto ancho. No podremos utilizar este método para celdas con alturas dinámicas. Si la celda aumenta de altura, se verá así:



Método número 3 . Utiliza caracteres Unicode. No es necesario calcular el tamaño del texto con espacios, y este método es adecuado para celdas con altura dinámica. Se ve bien:



Pero si queremos hacer el texto debajo del ícono, este método no nos conviene.

Método número 4 . Use CoreGraphics para representar los componentes de la interfaz de usuario. La idea es dibujar manualmente una imagen con la disposición de elementos deseada (texto e imagen). En este caso, debe calcular los tamaños y las coordenadas para la ubicación de cada elemento. Con este método, podemos hacer tanto texto como una imagen, organizándolos según lo necesitemos. Pero este método es difícil de implementar. Queremos algo más simple.

Otras implementaciones . Hay algunas soluciones más preparadas. Usan su propia implementación de la celda con la adición de ScrollView y otra lógica. Por ejemplo, SwipeCellKit o MGSwipeTableCell . Pero estas soluciones no permiten lograr un comportamiento estándar de iOS; cubren solo una celda, a diferencia de UITableViewRowAction, que se crea en el delegado de la tabla. Por ejemplo, usando una de estas soluciones, si abre acciones para el desplazamiento a la izquierda para varias celdas, tendrá que cerrar manualmente estas acciones en cada una de ellas, lo cual es extremadamente inconveniente. Para hacer esto, debe agregar su propia lógica para controlar las acciones abiertas en un deslizamiento. Queremos crear una solución simple que no requiera costos adicionales, como si estuviéramos utilizando la implementación estándar de UITableViewRowAction.

Empecemos


Intentemos crear una solución universal que le permita hacer UITableViewRowAction con cualquier diseño que necesitemos.

Cree nuestra clase descendiente de UITableViewRowAction y defina el método de creación de la instancia principal:

 typealias RowActionTapHandler = (RowAction, IndexPath) -> Void class RowAction: UITableViewRowAction { static func rowAction(with actionHandler: @escaping RowActionTapHandler) -> Self { let rowAction = self.init(style: .default, title: nil) { action, indexPath in guard let action = action as? RowAction else { return } actionHandler(action, indexPath) } return rowAction } } 

Dado que UITableViewRowAction no tiene un inicializador primario, por conveniencia podemos crear un método estático en el que creamos un UITableViewRowAction de manera estándar y arrojamos un actionHandler para manejar el clic.

Ahora intentemos lograr la personalización universal del botón UITableViewRowAction. La idea es obtener el aspecto del botón desde la vista final de UIView (que se puede configurar en un xib normal). Por lo tanto, podemos hacer un botón de cualquier tipo, en función de las preferencias individuales (cualquier fuente del texto, tamaño y disposición de los elementos, etc.).

Tenemos dos cosas que podemos cambiar en UITableViewRowAction: backgroundColor y title . Por el momento, la única forma de cambiar el aspecto del botón UITableViewRowAction es cambiar la propiedad backgroundColor. Por lo tanto, nuestro objetivo es transformar: UIView -> UIImage -> UIColor. Podemos obtener el color usando la imagen de la siguiente manera: UIColor (patternImage: UIImage) . Y podemos obtener la imagen de UIView al representarla con el marco estándar CoreGraphics.

Pero primero, decidiremos cómo establecer el tamaño del botón: "listo para usar" no existe esta funcionalidad, y el tamaño en UITableViewRowAction se calcula por el tamaño del texto del campo de título.

Después de realizar experimentos (midiendo el ancho calculado y real del botón), encontré un carácter Unicode en blanco que tiene el menor ancho distinto de cero: U + 200A

Lo usaremos y crearemos una constante:

 private enum Constants { static let placeholderSymbol = "\u{200A}" } 

Escribiremos una función para obtener una cadena de caracteres de espacio en blanco correspondiente al tamaño que necesitamos:

 private static let placeholderSymbolWidth: CGFloat = { let flt_max = CGFloat.greatestFiniteMagnitude let maxSize = CGSize(width: flt_max, height: flt_max) let attributes = [NSFontAttributeName : UIFont.systemFont(ofSize: UIFont.systemFontSize)] let boundingRect = Constants.placeholderSymbol.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: attributes, context: nil) return boundingRect.width }() private func emptyTitle(for size: CGSize) -> String { var usefulWidth = size.width - Constants.minimalActionWidth usefulWidth = usefulWidth < 0 ? 0 : usefulWidth let countOfSymbols = Int(floor(usefulWidth * Constants.shiftFactor / RowAction.placeholderSymbolWidth)) return String(repeating: Constants.placeholderSymbol, count: countOfSymbols) } 

La variable placeholderSymbolWidth obtiene el ancho de nuestro carácter Unicode, y la función emptyTitle (for size: CGSize) -> String calcula el número de caracteres de espacios en blanco correspondientes al tamaño del tamaño y devuelve una cadena de estos caracteres. La fórmula anterior usa la constante shiftFactor . Este es el coeficiente de desviación del ancho del valor nominal. El hecho es que a medida que aumenta el ancho de UITableViewRowAction, aumenta un pequeño error (es decir, el ancho real del botón difiere del ancho calculado por la fórmula habitual). Para evitar esto, usamos esta constante, derivada experimentalmente.

Además, UITableViewRowAction tiene un ancho mínimo de 30 (por ejemplo, si establece title = ""). Este ancho es el mínimo requerido para la sangría a la izquierda y derecha del título.
Actualiza nuestras constantes:

 private enum Constants { static let placeholderSymbol = "\u{200A}" static let minimalActionWidth: CGFloat = 30 static let shiftFactor: CGFloat = 1.1 } 

Ahora obtengamos la imagen del UIView. Escribiremos la siguiente función:

 private func image(from view: UIView) -> UIImage? { UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, UIScreen.main.scale) guard let context = UIGraphicsGetCurrentContext() else { assertionFailure("Something wrong with CoreGraphics image context"); return nil } context.setFillColor(UIColor.white.cgColor) context.fill(CGRect(origin: .zero, size: view.bounds.size)) view.layer.render(in: context) let img = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return img } 

Esta función convierte nuestra vista en una imagen. En este caso, el tamaño de la imagen corresponde al tamaño de la vista .

El último paso permanece, donde implementamos el método para configurar UITableViewRowAction usando UIView:

 func setupForCell(with view: UIView) { guard let img = image(from: view) else { return } self.title = emptyTitle(for: img.size) self.backgroundColor = UIColor(patternImage: img) } 

Aquí usamos las funciones escritas anteriormente para configurar nuestro botón y establecer el color de fondo usando la imagen resultante.

Eso es todo Tenemos una solución universal para UITableViewRowAction y ahora podemos personalizarla de cualquier manera usando UIView.



A partir de iOS 11, Apple ha agregado una nueva clase para la acción de deslizar las celdas de la tabla: UIContextualAction. Le permite especificar de inmediato la imagen que se mostrará en la acción en el deslizamiento (esto es lo que nos hemos perdido durante todos estos años de uso de UITableViewRowAction). Se suponía que debía hacernos la vida más fácil, pero además de una nueva oportunidad, creó problemas adicionales. Esta clase impone restricciones en el tamaño de la imagen, no permite el uso de texto e imagen, y además, la imagen debe tener un formato especial (imagen de plantilla). Por lo tanto, finalicé mi solución para admitir acciones de deslizamiento en iOS 11. Ahora, para crear un UITableViewRowAction o UIContextualAction, primero debe crear la fábrica RowActionFactory, configurarla utilizando nuestra vista (como lo hicimos anteriormente en el método setupForCell (con vista: UIView)) y devolver la entidad que necesitamos: UITableViewRowAction o UIContextualAction (utilizando los métodos rowAction () o contextualAction (para indexPath: IndexPath), respectivamente). El siguiente es un ejemplo del uso de RowActionFactory:

 @available(iOS 11.0, *) func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { var contextualActions: [UIContextualAction] = [] for actionType in rowActionTypes { // actionType -     let rowActionFactory = RowActionFactory { (indexPath) in tableView.setEditing(false, animated: true) } let rowActionView = <...> //  UIView     rowActionFactory.setupForCell(with: rowActionView) if let contextualAction = rowActionFactory.contextualAction(for: indexPath) { contextualActions.append(contextualAction) } } let swipeActionsConfiguration = UISwipeActionsConfiguration(actions: contextualActions) swipeActionsConfiguration.performsFirstActionWithFullSwipe = false return swipeActionsConfiguration } 

La solución final se puede encontrar aquí: TCSCustomRowActionFactory (github.com) .

Source: https://habr.com/ru/post/es414019/


All Articles