Подтвердить что ты не робот

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

4b9b3361

Ответ 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?()
    }
}