
Muitos encontraram o problema de implementar um mapeamento de botão bonito para um UITableViewCell quando deslocado para a esquerda. Alguns usavam a funcionalidade padrão "pronta para uso", outros se preocupavam com sua própria implementação, enquanto outros gerenciavam com caracteres Unicode. Neste artigo, mostrarei como obter a personalização máxima do UITableViewRowAction.
Primeiro, um pouco de teoria:
UITableViewRowAction é uma classe que permite ao usuário executar determinadas ações com essa célula ao mover uma célula para a esquerda (como excluir, alterar etc.). Cada instância desta classe representa uma ação a ser executada e inclui texto, estilo e um manipulador de cliques. O iOS SDK nos permite personalizar o UITableViewRowAction da seguinte maneira:
public convenience init(style: UITableViewRowActionStyle, title: String?, handler: @escaping (UITableViewRowAction, IndexPath) -> Swift.Void)
Você também pode definir backgroundColor.
Métodos existentes.
Considere os exemplos de implementação existentes, ou o que encontrei na Internet.
Método número 1 . Use a funcionalidade "pronto para uso". Isso é suficiente para usar botões comuns com texto e plano de fundo. Parece muito simples e chato:
Método número 2 . Defina backgroundColor usando
UIColor (patternImage: UIImage) . Isso alcançará a beleza, mas privará a versatilidade, acrescentará uma série de restrições e dificuldades. Como a largura da UITableViewRowAction é calculada pelo tamanho do texto, é necessário especificar o
título como um conjunto de caracteres de espaço em branco de uma determinada largura. Não poderemos usar esse método para células com alturas dinâmicas. Se a célula aumentar de altura, será algo como isto:
Método número 3 . Use caracteres Unicode. Não há necessidade de calcular o tamanho do texto com espaços, e esse método é adequado para células com altura dinâmica. Parece bom:

Mas se quisermos colocar o texto embaixo do ícone, esse método não será adequado para nós.
Método número 4 . Use CoreGraphics para renderizar componentes da interface do usuário. A idéia é desenhar manualmente uma imagem com a disposição desejada dos elementos (texto e imagem). Nesse caso, você precisa calcular os tamanhos e coordenadas para a localização de cada elemento. Usando esse método, podemos criar texto e imagem, organizando-os conforme necessário. Mas esse método é difícil de implementar. Queremos algo mais simples.
Outras implementações . Existem mais algumas soluções prontas. Eles usam sua própria implementação da célula com a adição de ScrollView e outra lógica. Por exemplo,
SwipeCellKit ou
MGSwipeTableCell . Mas essas soluções não permitem alcançar o comportamento padrão do iOS; elas cobrem apenas uma célula, ao contrário do UITableViewRowAction, criado no delegado da tabela. Por exemplo, usando uma dessas soluções, se você abrir ações para o turno esquerdo para várias células, precisará fechar manualmente essas ações em cada uma delas, o que é extremamente inconveniente. Para fazer isso, você precisa adicionar sua própria lógica para controlar as ações abertas em um furto. Queremos criar uma solução simples que não exija custos adicionais, como se estivéssemos usando a implementação padrão de UITableViewRowAction.
Vamos começar
Vamos tentar criar uma solução universal que permita criar UITableViewRowAction com qualquer design necessário.
Crie nossa classe descendente de UITableViewRowAction e defina o método de criação da instância 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 } }
Como UITableViewRowAction não possui um inicializador primário, por conveniência, podemos criar um método estático em que, por dentro, criamos um UITableViewRowAction de maneira padrão e lançamos um actionHandler para manipular o clique.
Agora vamos tentar obter uma personalização universal do botão UITableViewRowAction. A idéia é obter a aparência do botão no UIView finalizado (que pode ser configurado em um xib regular). Assim, podemos criar um botão de qualquer tipo, com base nas preferências individuais (qualquer fonte do texto, tamanho e disposição dos elementos, etc.).
Temos duas coisas que podemos alterar em UITableViewRowAction:
backgroundColor e
title . No momento, a única maneira de alterar a aparência do botão UITableViewRowAction é alterar a propriedade backgroundColor. Portanto, nosso objetivo é transformar: UIView -> UIImage -> UIColor. Podemos obter a cor usando a imagem da seguinte maneira:
UIColor (patternImage: UIImage) . E podemos obter a imagem do UIView renderizando-a usando a estrutura padrão CoreGraphics.
Mas primeiro vamos decidir como definir o tamanho do botão: "pronto para uso" não existe essa funcionalidade, e o tamanho em UITableViewRowAction é calculado pelo tamanho do texto do campo de título.
Após realizar experimentos (medindo a largura calculada e real do botão), encontrei um caractere Unicode em branco com a menor largura diferente de zero:
U + 200AVamos usá-lo e criar uma constante:
private enum Constants { static let placeholderSymbol = "\u{200A}" }
Escreveremos uma função para obter uma sequência de caracteres de espaço em branco correspondente ao tamanho que precisamos:
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) }
A variável
placeholderSymbolWidth obtém a largura do nosso caractere Unicode e a função
emptyTitle (para tamanho: CGSize) -> String calcula o número de caracteres de espaço em branco correspondente ao tamanho do
tamanho e retorna uma sequência desses caracteres. A fórmula acima usa a constante
shiftFactor . Este é o coeficiente de desvio da largura do valor nominal. O fato é que, à medida que a largura da UITableViewRowAction aumenta, um pequeno erro aumenta (ou seja, a largura real do botão difere da largura calculada pela fórmula usual). Para evitar isso, usamos essa constante, derivada experimentalmente.
O UITableViewRowAction também possui uma largura mínima de 30 (por exemplo, se você definir title = ""). Essa largura é o mínimo necessário para o recuo à esquerda e à direita do título.
Atualize nossas constantes:
private enum Constants { static let placeholderSymbol = "\u{200A}" static let minimalActionWidth: CGFloat = 30 static let shiftFactor: CGFloat = 1.1 }
Agora vamos obter a imagem do UIView. Vamos escrever a seguinte função:
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 função converte nossa
visão em uma imagem. Nesse caso, o tamanho da imagem corresponde ao tamanho da
visualização .
A última etapa permanece, onde implementamos o método para definir 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) }
Aqui, usamos as funções escritas anteriormente para configurar nosso botão e definir o
backgroundColor usando a imagem resultante.
Só isso. Temos uma solução universal para UITableViewRowAction e agora podemos personalizá-la de qualquer forma usando o UIView.

A partir do iOS 11, a Apple adicionou uma nova classe para ação de furto para células na tabela - UIContextualAction. Ele permite que você especifique imediatamente a imagem que será exibida na ação durante o furto (é isso que perdemos durante todos esses anos de uso de UITableViewRowAction). Ele deveria facilitar nossa vida, mas, além de uma nova oportunidade, ele criou problemas adicionais. Essa classe impõe restrições ao tamanho da imagem, não permite o uso de texto e imagem ao mesmo tempo e também deve ter um formato especial (imagem do modelo). Portanto, finalizei minha solução para oferecer suporte a ações de furto no iOS 11. Agora, para criar uma UITableViewRowAction ou UIContextualAction, primeiro você precisa criar a fábrica RowActionFactory, configurá-la usando nossa view (como fizemos acima no
método setupForCell (com view: UIView)) e retorne a entidade que precisamos: UITableViewRowAction ou UIContextualAction (usando os
métodos rowAction () ou
contextualAction (para indexPath: IndexPath), respectivamente). A seguir, é apresentado um exemplo do 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 }
A solução final pode ser encontrada aqui:
TCSCustomRowActionFactory (github.com) .