Skip to content

Instantly share code, notes, and snippets.

@dterekhov
Created November 13, 2025 09:13
Show Gist options
  • Select an option

  • Save dterekhov/b7b3c8761fc45ce9bad9e864989f6cd0 to your computer and use it in GitHub Desktop.

Select an option

Save dterekhov/b7b3c8761fc45ce9bad9e864989f6cd0 to your computer and use it in GitHub Desktop.
UITableViewCell+ClearButton: Adds clear data button to the cell #uikit #swift-api #real-project
import UIKit
extension UITableViewCell {
// MARK: - Public API
public var fw_onClearButtonTap: (() -> Void)? {
get { return objc_getAssociatedObject(self, AssociatedKeys.onClearButtonTap) as? () -> Void }
set {
objc_setAssociatedObject(self,
AssociatedKeys.onClearButtonTap,
newValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
@objc dynamic public var fw_isClearButtonHidden: Bool {
get { return fw_clearButton.superview == nil }
set { setClearButtonHidden(newValue) }
}
// MARK: - Private API
private enum AssociatedKeys {
// Use stable, immutable keys to avoid concurrency warnings
@MainActor
static let clearButton: UnsafeRawPointer = {
UnsafeRawPointer(bitPattern: "fw_clearButton".hashValue)!
}()
@MainActor
static let onClearButtonTap: UnsafeRawPointer = {
UnsafeRawPointer(bitPattern: "fw_onClearButtonTap".hashValue)!
}()
}
private enum Constants {
static let imageInset: CGFloat = 10
static let sideMeasure: CGFloat = 40
static let clearImage = #imageLiteral(resourceName: "ico_cross_circle").withRenderingMode(.alwaysTemplate)
}
private var fw_clearButton: UIButton {
if let readonlyButton = objc_getAssociatedObject(self, AssociatedKeys.clearButton) as? UIButton {
return readonlyButton
}
let button = createClearButton()
objc_setAssociatedObject(self,
AssociatedKeys.clearButton,
button,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return button
}
private func createClearButton() -> UIButton {
let button = UIButton(type: .custom)
if #available(iOS 15.0, *) {
var config = UIButton.Configuration.plain()
// Make the button square and let Auto Layout size it via constraints later
config.contentInsets = NSDirectionalEdgeInsets(
top: Constants.imageInset,
leading: Constants.imageInset,
bottom: Constants.imageInset,
trailing: Constants.imageInset
)
config.image = Constants.clearImage
config.baseForegroundColor = UIColor(named: "clr_fieldworkGreen") ?? .systemGreen
// Keep aspect fit-like appearance
config.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(scale: .medium)
button.configuration = config
// Ensure template rendering uses tintColor
button.tintColor = UIColor(named: "clr_fieldworkGreen") ?? .systemGreen
} else {
button.setImage(Constants.clearImage, for: .normal)
button.imageView?.tintColor = UIColor(named: "clr_fieldworkGreen") ?? .systemGreen
// Deprecated on iOS 15+, but still valid before that
button.imageEdgeInsets = UIEdgeInsets(top: Constants.imageInset,
left: Constants.imageInset,
bottom: Constants.imageInset,
right: Constants.imageInset)
button.contentMode = .scaleAspectFit
button.tintColor = UIColor(named: "clr_fieldworkGreen") ?? .systemGreen
}
button.addTarget(self, action: #selector(clearButtonTap), for: .touchUpInside)
return button
}
private func setClearButtonHidden(_ isHidden: Bool) {
let constraints: [NSLayoutConstraint] = [
fw_clearButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
fw_clearButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
fw_clearButton.widthAnchor.constraint(equalToConstant: Constants.sideMeasure),
fw_clearButton.heightAnchor.constraint(equalToConstant: Constants.sideMeasure)
]
fw_clearButton.translatesAutoresizingMaskIntoConstraints = false
if isHidden {
fw_clearButton.removeFromSuperview()
NSLayoutConstraint.deactivate(constraints)
} else {
if fw_clearButton.superview == nil {
contentView.addSubview(fw_clearButton)
NSLayoutConstraint.activate(constraints)
}
}
}
@objc private func clearButtonTap() { fw_onClearButtonTap?() }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment