Подведение UIButton к закрытию? (Swift, целевое действие)
Я хочу подключить UIButton к фрагменту кода - из того, что я нашел, предпочтительный метод для этого в Swift по-прежнему использует функцию addTarget(target: AnyObject?, action: Selector, forControlEvents: UIControlEvents)
. Это использует конструкцию Selector
, предположительно, для обратной совместимости с библиотеками Obj-C. Я думаю, что я понимаю причину @selector
в Obj-C - возможность ссылаться на метод, поскольку в Obj-C-методах не являются первоклассными значениями.
Однако в Swift функции являются первоклассными значениями. Есть ли способ подключить UIButton к закрытию, что-то похожее на это:
// -- Some code here that sets up an object X
let buttonForObjectX = UIButton()
// -- configure properties here of the button in regards to object
// -- for example title
buttonForObjectX.addAction(action: {() in
// this button is bound to object X, so do stuff relevant to X
}, forControlEvents: UIControlEvents.TouchUpOutside)
Насколько мне известно, вышеизложенное в настоящее время невозможно. Учитывая, что Swift выглядит так, чтобы быть достаточно функциональным, почему? Эти два варианта могут явно сосуществовать для обратной совместимости. Почему это не похоже на onClick() в JS? Кажется, что единственный способ подключить UIButton к паре целевого действия - использовать что-то, что существует исключительно для соображений обратной совместимости (Selector
).
Моим вариантом использования является создание UIButtons в цикле для разных объектов, а затем привязка каждого из них до закрытия. (Установка тега/поиск в словаре/подклассе UIButton - грязные полурешения, но мне интересно, как это сделать, например, этот подход закрытия)
Ответы
Ответ 1
Вы можете заменить target-action замыканием, добавив вспомогательную оболочку замыкания (ClosureSleeve) и добавив ее в качестве связанного объекта в элемент управления, чтобы сохранить его.
Это похоже на решение в ответе n13. Но я нахожу это проще и элегантнее. Закрытие вызывается более напрямую, и оболочка автоматически сохраняется (добавляется как связанный объект).
Свифт 3 и 4
class ClosureSleeve {
let closure: () -> ()
init(attachTo: AnyObject, closure: @escaping () -> ()) {
self.closure = closure
objc_setAssociatedObject(attachTo, "[\(arc4random())]", self, .OBJC_ASSOCIATION_RETAIN)
}
@objc func invoke() {
closure()
}
}
extension UIControl {
func addAction(for controlEvents: UIControlEvents = .primaryActionTriggered, action: @escaping () -> ()) {
let sleeve = ClosureSleeve(attachTo: self, closure: action)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
}
}
Использование:
button.addAction {
print("Hello")
}
Он автоматически .primaryActionTriggered
событию .touchUpInside
равному .touchUpInside
для UIButton.
Ответ 2
Общий подход ко всему, что вы думаете, должен быть в библиотеках, но не есть: Напишите категорию. Там много этого конкретного в GitHub, но не нашел его в Swift, поэтому я написал свой собственный:
=== Поместите это в свой собственный файл, например UIButton + Block.swift ===
import ObjectiveC
var ActionBlockKey: UInt8 = 0
// a type for our action block closure
typealias BlockButtonActionBlock = (sender: UIButton) -> Void
class ActionBlockWrapper : NSObject {
var block : BlockButtonActionBlock
init(block: BlockButtonActionBlock) {
self.block = block
}
}
extension UIButton {
func block_setAction(block: BlockButtonActionBlock) {
objc_setAssociatedObject(self, &ActionBlockKey, ActionBlockWrapper(block: block), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
addTarget(self, action: "block_handleAction:", forControlEvents: .TouchUpInside)
}
func block_handleAction(sender: UIButton) {
let wrapper = objc_getAssociatedObject(self, &ActionBlockKey) as! ActionBlockWrapper
wrapper.block(sender: sender)
}
}
Затем вызовите его следующим образом:
myButton.block_setAction { sender in
// if you're referencing self, use [unowned self] above to prevent
// a retain cycle
// your code here
}
Ясно, что это можно было бы улучшить, могли быть варианты для различных видов событий (а не только для прикосновения внутри) и т.д. Но это сработало для меня.
Это немного сложнее, чем чистая версия ObjC из-за необходимости обертки для блока. Компилятор Swift не позволяет хранить блок как "AnyObject". Поэтому я просто завернул его.
Ответ 3
Это не обязательно "перехват", но вы можете эффективно достичь этого поведения путем подкласса UIButton:
class ActionButton: UIButton {
var touchDown: ((button: UIButton) -> ())?
var touchExit: ((button: UIButton) -> ())?
var touchUp: ((button: UIButton) -> ())?
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:)") }
override init(frame: CGRect) {
super.init(frame: frame)
setupButton()
}
func setupButton() {
//this is my most common setup, but you can customize to your liking
addTarget(self, action: #selector(touchDown(_:)), forControlEvents: [.TouchDown, .TouchDragEnter])
addTarget(self, action: #selector(touchExit(_:)), forControlEvents: [.TouchCancel, .TouchDragExit])
addTarget(self, action: #selector(touchUp(_:)), forControlEvents: [.TouchUpInside])
}
//actions
func touchDown(sender: UIButton) {
touchDown?(button: sender)
}
func touchExit(sender: UIButton) {
touchExit?(button: sender)
}
func touchUp(sender: UIButton) {
touchUp?(button: sender)
}
}
Использование:
let button = ActionButton(frame: buttonRect)
button.touchDown = { button in
print("Touch Down")
}
button.touchExit = { button in
print("Touch Exit")
}
button.touchUp = { button in
print("Touch Up")
}
Ответ 4
Это легко решается с помощью RxSwift
import RxSwift
import RxCocoa
...
@IBOutlet weak var button:UIButton!
...
let taps = button.rx.tap.asDriver()
taps.drive(onNext: {
// handle tap
})
Редактировать:
Я хотел признать, что RxSwift/RxCocoa является довольно тяжелой зависимостью, которую можно добавить в проект только для того, чтобы решить это одно требование. Могут быть доступны более легкие решения или просто придерживаться схемы "цель/действие".
В любом случае, если вам нравится идея декларативного подхода общего назначения к обработке пользовательских и пользовательских событий, определенно обратите внимание на RxSwift. Это бомба.
Ответ 5
Согласно n13 решение, я сделал версию swift3.
Надеюсь, что это поможет некоторым людям вроде меня.
import Foundation
import UIKit
import ObjectiveC
var ActionBlockKey: UInt8 = 0
// a type for our action block closure
typealias BlockButtonActionBlock = (_ sender: UIButton) -> Void
class ActionBlockWrapper : NSObject {
var block : BlockButtonActionBlock
init(block: @escaping BlockButtonActionBlock) {
self.block = block
}
}
extension UIButton {
func block_setAction(block: @escaping BlockButtonActionBlock, for control: UIControlEvents) {
objc_setAssociatedObject(self, &ActionBlockKey, ActionBlockWrapper(block: block), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
self.addTarget(self, action: #selector(UIButton.block_handleAction), for: .touchUpInside)
}
func block_handleAction(sender: UIButton, for control:UIControlEvents) {
let wrapper = objc_getAssociatedObject(self, &ActionBlockKey) as! ActionBlockWrapper
wrapper.block(sender)
}
}
Ответ 6
UIButton наследует от UIControl, который обрабатывает получение ввода и пересылки для выбора. Согласно документам, действие "Селектор, определяющий сообщение о действии, не может быть NULL". Селектор - это строго указатель на метод.
Я бы подумал, что, учитывая акценты, которые Swift, кажется, размещает на Closures, это было бы возможно, но это, похоже, не так.
Ответ 7
Вы можете приблизиться к этому с помощью прокси-класса, который маршрутизирует события через механизм target/action (селектор) до закрытия вашего создания. Я сделал это для распознавателей жестов, но для элементов управления должен использоваться тот же шаблон.
Вы можете сделать что-то вроде этого:
import UIKit
@objc class ClosureDispatch {
init(f:()->()) { self.action = f }
func execute() -> () { action() }
let action: () -> ()
}
var redBlueGreen:[String] = ["Red", "Blue", "Green"]
let buttons:[UIButton] = map(0..<redBlueGreen.count) { i in
let text = redBlueGreen[i]
var btn = UIButton(frame: CGRect(x: i * 50, y: 0, width: 100, height: 44))
btn.setTitle(text, forState: .Normal)
btn.setTitleColor(UIColor.redColor(), forState: .Normal)
btn.backgroundColor = UIColor.lightGrayColor()
return btn
}
let functors:[ClosureDispatch] = map(buttons) { btn in
let functor = ClosureDispatch(f:{ [unowned btn] in
println("Hello from \(btn.titleLabel!.text!)") })
btn.addTarget(functor, action: "execute", forControlEvents: .TouchUpInside)
return functor
}
Одно из предостережений в этом состоит в том, что поскольку addTarget:... не сохраняет цель, вам нужно удержать объекты отправки (как это делается с массивом функторов). Конечно, вам не нужно строго придерживаться кнопок, поскольку вы можете сделать это с помощью захваченной ссылки в закрытии, но, вероятно, вам понадобятся явные ссылки.
PS. Я попытался проверить это на игровой площадке, но не смог заставить sendActionsForControlEvents работать. Однако я использовал этот подход для распознавателей жестов.
Ответ 8
СвязанныйObject, обертывание и указатели и импорт ObjectiveC не нужны, по крайней мере, в Swift 3. Это отлично работает и намного быстрее Swift-y. Не стесняйтесь бросать титалиас там для () -> ()
, если вы считаете его более читаемым, бит мне легче читать подпись блока напрямую.
import UIKit
class BlockButton: UIButton {
fileprivate var onAction: (() -> ())?
func addClosure(_ closure: @escaping () -> (), for control: UIControlEvents) {
self.addTarget(self, action: #selector(actionHandler), for: control)
self.onAction = closure
}
dynamic fileprivate func actionHandler() {
onAction?()
}
}