Пока идут праздники написал расширение для UIView и UIButton, которое позволяет функцию-event поместить в замыкание. Легче показать, чем рассказать))
Допустим, у нас есть кнопка, настроенная через Interface Builder:
class ViewController: UIViewController {
@IBOutlet weak var myButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
myButton.actionClick = ActionClick { _ in
print("hello")
}
}
}
Как видите, мы избавлены от необходимости настраивать отдельный Action outlet.
Хотя польза от расширения более очевидна, если мы создаем кнопку через код:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let btn = UIButton.init(frame: CGRect.init(x: (UIScreen.main.bounds.width-120)/2, y: (UIScreen.main.bounds.height-44)/2, width: 120, height: 44))
self.view.addSubview(btn)
btn.setTitle("My button", for: .normal)
btn.backgroundColor = UIColor.blue
btn.layer.cornerRadius = 44/2
btn.actionClick = ActionClick { sender in
print (sender.titleLabel?.text)
}
}
}
Нам больше не нужно писать
btn.addTarget(self, action: #selector(btn_Clicked(sender:)), for: .touchUpInside)
и где-то далее
@objc private func btn_Clicked(sender: UIButton) {
print(sender.titleLabel?.text ?? "")
}
То есть мы избавляемся от метания по коду туда-сюда.
Многие создают элементы в коде так:
let myButton: UIButton = {
let rect = CGRect.init(x: (UIScreen.main.bounds.width-120)/2, y: (UIScreen.main.bounds.height-44)/2, width: 120, height: 44)
let btn = UIButton(frame: rect)
self.view.addSubview(btn)
btn.setTitle("My button", for: .normal)
btn.backgroundColor = UIColor.blue
btn.layer.cornerRadius = 44/2
btn.actionClick = ActionClick { sender in
print (sender.titleLabel?.text ?? "")
}
return btn
}(); let _ = myButton
И в этом есть большие плюсы: 1) значительно облегчается последующий копи-паст кнопок, 2) все настройки помещаются в тело замыкания, что делает код более аккуратным, а самое главное позволяет внутри замыкания создавать локальные переменные. Это полезно, если, например, надо создать локально сложную attibuted string, чтобы затем присвоить ее надписи кнопки. Теперь и action мы тоже можем поместить внутрь замыкания.
Я предпочитаю более короткую запись с использование протокола Then (https://github.com/devxoul/Then):
let myButton = UIButton(frame: CGRect.init(x: (UIScreen.main.bounds.width-120)/2, y: (UIScreen.main.bounds.height-44)/2, width: 120, height: 44)).then {
self.view.addSubview($0)
$0.setTitle("My button", for: .normal)
$0.backgroundColor = UIColor.blue
$0.layer.cornerRadius = 44/2
$0.actionClick = ActionClick { sender in
print (sender.titleLabel?.text ?? "")
}
}; let _ = myButton
Для view можно аналогичным образом писать что-то вроде:
$0.actionTap = ActionTap { _ in
print("view tapped")
}
Тогда к view будет добавлен UITapGestureRecognizer.
И вот весь код расширения:
// Actions.swift
import UIKit
final class ActionTap: NSObject {
private var _tap: (_ sender: UITapGestureRecognizer) -> ()
init (block: @escaping (_ sender: UITapGestureRecognizer) -> ()) {
_tap = block
super.init()
}
@objc func tap(sender: UITapGestureRecognizer) {
_tap(sender)
}
}
final class ActionClick: NSObject {
private var _click: (_ sender: UIButton) -> ()
init (block: @escaping (_ sender: UIButton) -> ()) {
_click = block
super.init()
}
@objc func click(sender: UIButton) {
_click(sender)
}
}
protocol Actionable: class { }
extension UIView: Actionable {
fileprivate struct CustomProperties {
static var _actionTap: ActionTap?
static var _actionClick: ActionClick?
}
fileprivate func _getAssociatedObject<T>(_ key: UnsafeRawPointer!, defaultValue: T) -> T {
guard let value = objc_getAssociatedObject(self, key) as? T else {
return defaultValue
}
return value
}
}
extension Actionable where Self: UIView {
var actionTap: ActionTap? {
get {
return _getAssociatedObject(&CustomProperties._actionTap, defaultValue: CustomProperties._actionTap)
}
set {
objc_setAssociatedObject(self, &CustomProperties._actionTap, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
let tap = UITapGestureRecognizer.init(target: self.actionTap, action: #selector(self.actionTap?.tap(sender:)))
self.addGestureRecognizer(tap)
self.isUserInteractionEnabled = true
}
}
func tap(_ selector: Selector) {
guard let target = self.parentViewController else {return}
if self.gestureRecognizers?.first(where: {$0 is UITapGestureRecognizer}) == nil {
let tap = UITapGestureRecognizer(target: target, action: selector)
self.addGestureRecognizer(tap)
self.isUserInteractionEnabled = true
}
}
}
extension Actionable where Self: UIButton {
var actionClick: ActionClick? {
get {
return _getAssociatedObject(&CustomProperties._actionClick, defaultValue: CustomProperties._actionClick)
}
set {
objc_setAssociatedObject(self, &CustomProperties._actionClick, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
self.addTarget(self.actionClick, action: #selector(self.actionClick?.click(sender:)), for: .touchUpInside)
}
}
func click(_ selector: Selector) {
guard let target = self.parentViewController else {return}
if self.actions(forTarget: target, forControlEvent: .touchUpInside) == nil {
self.addTarget(target, action: selector, for: .touchUpInside)
}
}
}
В проекте еще не пробовал применять, поэтому не знаю, удобно ли на самом деле. Если кто-нибудь попробует, расскажите.
P.S. View в коде лучше закреплять констрейнтами, конечно. Но это другая тема.