Tap & Click для UIView и UIButton


#1

Пока идут праздники написал расширение для 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 в коде лучше закреплять констрейнтами, конечно. Но это другая тема.


Обновление UIImageView после applicationWillEnterForeground
#2

Зачем вы пишите везде .init?

Везде self зачем?

let myButton: UIButton = { 
       
}(); let _ = myButton

Это что бы Xcode не гневался? :smile:

Это откуда?

Серьезно? Три строки тащите через cocoapods?

Я бы сделал так:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(.new {
            $0.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
            $0.backgroundColor = .red
        })
    }
}
extension UIView: New {}
protocol New: class {
    init()
}
extension New {
    static func new(closure: (Self) -> ()) -> Self {
        let object = Self()
        closure(object)
        return object
    }
}

Но практика показала что это бесполезная фигня и проще писать по старинке :slight_smile:


#3

init пишу, потому что Xcode не всегда подсказку почему-то показывает для инициализаторов. Не знаю, в чем проблема. В общем как-то так приспособился.

self.actions
self.addTarget

Ну здесь self можно опустить, скопипастил из другого кода.

let _ = myButton

Это да, раздражает Xcode своими варнингами, поэтому временно ставлю такую строчку, потом удаляю.

Через cocoapods я ничего не тащу, просто скопировал весь код в файл Then.swift и таскаю его туда-сюда.

guard let target = self.parentViewController else {return}

Забыл про extension.

extension UIView {
    var parentViewController: UIViewController? {
        var parentResponder: UIResponder? = self
        while parentResponder != nil {
            parentResponder = parentResponder!.next
            if let viewController = parentResponder as? UIViewController {
                return viewController
            }
        }
        return nil
    }
}

#4

Лучше так тогда :slight_smile:

extension UIView {
    var parentViewController: UIViewController? {
        var responder: UIResponder? = self
        while let next = responder?.next {
            guard let vc = next as? UIViewController else { responder = next; continue }
            return vc
        }
        return nil
    }
}

#5

Не вижу особой разницы))


#6

Больше на свифт похоже.


#7

С этим соглашусь. Надо изживать в себе С ))