J'ai une classe de contrôle générique qui doit définir l'achèvement du bouton en fonction du contrôleur de vue. En raison de cette fonction setLeftButtonActionWithClosure doit prendre comme paramètre une fermeture qui devrait être définie comme action sur un unbutton.Comment serait-il possible dans Swift puisque nous devons passer le nom de la fonction sous forme de chaîne à action: paramètre.
func setLeftButtonActionWithClosure(completion: () -> Void)
{
self.leftButton.addTarget(<#target: AnyObject?#>, action: <#Selector#>, forControlEvents: <#UIControlEvents#>)
}
NOTE: Comme @ EthanHuang a déclaré "Cette solution ne fonctionne pas si vous avez plus de deux instances. Toutes les actions seront écrasées par la dernière affectation." N'oubliez pas ceci lorsque vous développerez, je publierai une autre solution bientôt.
Si vous souhaitez ajouter une fermeture en tant que cible à une UIButton
, vous devez ajouter une fonction à la classe UIButton
à l'aide de extension
.
import UIKit
extension UIButton {
private func actionHandleBlock(action:(() -> Void)? = nil) {
struct __ {
static var action :(() -> Void)?
}
if action != nil {
__.action = action
} else {
__.action?()
}
}
@objc private func triggerActionHandleBlock() {
self.actionHandleBlock()
}
func actionHandle(controlEvents control :UIControlEvents, ForAction action:() -> Void) {
self.actionHandleBlock(action)
self.addTarget(self, action: "triggerActionHandleBlock", forControlEvents: control)
}
}
et l'appel:
let button = UIButton()
button.actionHandle(controlEvents: UIControlEvents.TouchUpInside,
ForAction:{() -> Void in
print("Touch")
})
Solution similaire à celles déjà répertoriées, mais peut-être plus légère:
class ClosureSleeve {
let closure: ()->()
init (_ closure: @escaping ()->()) {
self.closure = closure
}
@objc func invoke () {
closure()
}
}
extension UIControl {
func addAction(for controlEvents: UIControl.Event, _ closure: @escaping ()->()) {
let sleeve = ClosureSleeve(closure)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
objc_setAssociatedObject(self, String(format: "[%d]", arc4random()), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
Usage:
button.addAction(for: .touchUpInside) {
print("Hello, Closure!")
}
Ou si vous évitez de conserver des boucles:
self.button.addAction(for: .touchUpInside) { [weak self] in
self?.doStuff()
}
L'utilisateur MH175 mentionne qu'il y aura une exception d'exécution s'il utilise la propriété "allTargets du contrôle: static Set_unnconditionalBridgeFromObjectiveC (_ :) -". Faire en sorte que ClosureSleeve soit étendu à partir de NSObject résoudra le problème:
@objc class ClosureSleeve: NSObject {
let closure: ()->()
init (_ closure: @escaping ()->()) {
self.closure = closure
super.init()
}
@objc func invoke () {
closure()
}
}
Vous pouvez y parvenir efficacement en sous-classant 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)
}
}
Utilisation:
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")
}
C'est fondamentalement la réponse d'Armanoide , ci-dessus, mais avec quelques légères modifications qui me sont utiles:
UIButton
, vous permettant de passer self
les fonctions et les arguments sont renommés de manière à clarifier ce qui se passe, par exemple, en distinguant une fermeture Swift d'une action UIButton
.
private func setOrTriggerClosure(closure:((button:UIButton) -> Void)? = nil) {
//struct to keep track of current closure
struct __ {
static var closure :((button:UIButton) -> Void)?
}
//if closure has been passed in, set the struct to use it
if closure != nil {
__.closure = closure
} else {
//otherwise trigger the closure
__. closure?(button: self)
}
}
@objc private func triggerActionClosure() {
self.setOrTriggerClosure()
}
func setActionTo(closure:(UIButton) -> Void, forEvents :UIControlEvents) {
self.setOrTriggerClosure(closure)
self.addTarget(self, action:
#selector(UIButton.triggerActionClosure),
forControlEvents: forEvents)
}
Beaucoup d'accessoires à Armanoide cependant pour une certaine magie robuste ici.
J'ai commencé à utiliser la réponse de Armanoide sans tenir compte du fait qu'elle sera annulée par la deuxième mission, principalement parce qu'au début, j'en avais besoin d'un endroit précis qui importait peu. Mais il a commencé à s'effondrer.
J'ai proposé une nouvelle implémentation en utilisant AssicatedObjects qui n'a pas cette limitation, je pense une syntaxe plus intelligente, mais ce n'est pas une solution complète:
C'est ici:
typealias ButtonAction = () -> Void
fileprivate struct AssociatedKeys {
static var touchUp = "touchUp"
}
fileprivate class ClosureWrapper {
var closure: ButtonAction?
init(_ closure: ButtonAction?) {
self.closure = closure
}
}
extension UIControl {
@objc private func performTouchUp() {
guard let action = touchUp else {
return
}
action()
}
var touchUp: ButtonAction? {
get {
let closure = objc_getAssociatedObject(self, &AssociatedKeys.touchUp)
guard let action = closure as? ClosureWrapper else{
return nil
}
return action.closure
}
set {
if let action = newValue {
let closure = ClosureWrapper(action)
objc_setAssociatedObject(
self,
&AssociatedKeys.touchUp,
closure as ClosureWrapper,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
self.addTarget(self, action: #selector(performTouchUp), for: .touchUpInside)
} else {
self.removeTarget(self, action: #selector(performTouchUp), for: .touchUpInside)
}
}
}
}
Comme vous pouvez le constater, j’ai décidé de créer un dossier dédié à touchUpInside
. Je sais que les contrôles ont plus d'événements que celui-ci, mais de qui rigolons-nous? Avons-nous besoin d'actions pour chacun d'entre eux?! C'est beaucoup plus simple de cette façon.
Exemple d'utilisation:
okBtn.touchUp = {
print("OK")
}
Dans tous les cas, si vous souhaitez étendre cette réponse, vous pouvez créer une Set
actions pour tous les types d'événement ou ajouter d'autres propriétés d'événement pour d'autres événements. C'est relativement simple.
Salutations, M.
Solution similaire à celles déjà répertoriées, mais peut-être plus légère et ne repose pas sur le hasard pour générer des identifiants uniques:
class ClosureSleeve {
let closure: ()->()
init (_ closure: @escaping ()->()) {
self.closure = closure
}
@objc func invoke () {
closure()
}
}
extension UIControl {
func add (for controlEvents: UIControlEvents, _ closure: @escaping ()->()) {
let sleeve = ClosureSleeve(closure)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
objc_setAssociatedObject(self, String(ObjectIdentifier(self).hashValue) + String(controlEvents.rawValue), sleeve,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
Usage:
button.add(for: .touchUpInside) {
print("Hello, Closure!")
}
Rapide
Après avoir essayé toutes les solutions, celle-ci a fonctionné pour moi dans tous les cas, même lorsque le bouton de la cellule de vue tableau réutilisable
import UIKit
typealias UIButtonTargetClosure = UIButton -> ()
class ClosureWrapper: NSObject {
let closure: UIButtonTargetClosure
init(_ closure: UIButtonTargetClosure) {
self.closure = closure
}
}
extension UIButton {
private struct AssociatedKeys {
static var targetClosure = "targetClosure"
}
private var targetClosure: UIButtonTargetClosure? {
get {
guard let closureWrapper = objc_getAssociatedObject(self, &AssociatedKeys.targetClosure) as? ClosureWrapper else { return nil }
return closureWrapper.closure
}
set(newValue) {
guard let newValue = newValue else { return }
objc_setAssociatedObject(self, &AssociatedKeys.targetClosure, ClosureWrapper(newValue), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func addTargetClosure(closure: UIButtonTargetClosure) {
targetClosure = closure
addTarget(self, action: #selector(UIButton.closureAction), forControlEvents: .TouchUpInside)
}
func closureAction() {
guard let targetClosure = targetClosure else { return }
targetClosure(self)
}
}
Et ensuite vous l'appelez comme ceci:
loginButton.addTargetClosure { _ in
// login logics
}
Ressource: https://medium.com/@jackywangdeveloper/Swift-the-right-way-to-add-target-in-uibutton-in-using-closures-877557ed9455 }
Swift 4.2 pour UIControl et UIGestureRecognizer, et supprimer des cibles via le paradigme de propriété stockée d’extension Swift.
Classe wrapper pour le sélecteur
class Target {
private let t: () -> ()
init(target t: @escaping () -> ()) { self.t = t }
@objc private func s() { t() }
public var action: Selector {
return #selector(s)
}
}
Protocoles avec associatedtype
s afin que nous puissions masquer le code objc_
protocol PropertyProvider {
associatedtype PropertyType: Any
static var property: PropertyType { get set }
}
protocol ExtensionPropertyStorable: class {
associatedtype Property: PropertyProvider
}
Extension pour rendre la propriété par défaut et disponible
extension ExtensionPropertyStorable {
typealias Storable = Property.PropertyType
var property: Storable {
get { return objc_getAssociatedObject(self, String(describing: type(of: Storable.self))) as? Storable ?? Property.property }
set { return objc_setAssociatedObject(self, String(describing: type(of: Storable.self)), newValue, .OBJC_ASSOCIATION_RETAIN) }
}
}
Laissez-nous appliquer la magie
extension UIControl: ExtensionPropertyStorable {
class Property: PropertyProvider {
static var property = [String: Target]()
}
func addTarget(for controlEvent: UIControl.Event = .touchUpInside, target: @escaping () ->()) {
let key = String(describing: controlEvent)
let target = Target(target: target)
addTarget(target, action: target.action, for: controlEvent)
property[key] = target
}
func removeTarget(for controlEvent: UIControl.Event = .touchUpInside) {
let key = String(describing: controlEvent)
let target = property[key]
removeTarget(target, action: target?.action, for: controlEvent)
property[key] = nil
}
}
Et aux gestes
extension UIGestureRecognizer: ExtensionPropertyStorable {
class Property: PropertyProvider {
static var property: Target?
}
func addTarget(target: @escaping () -> ()) {
let target = Target(target: target)
addTarget(target, action: target.action)
property = target
}
func removeTarget() {
let target = property
removeTarget(target, action: target?.action)
property = nil
}
}
Exemple d'utilisation:
button.addTarget {
print("touch up inside")
}
button.addTarget { [weak self] in
print("this will only happen once")
self?.button.removeTarget()
}
button.addTarget(for: .touchDown) {
print("touch down")
}
slider.addTarget(for: .valueChanged) {
print("value changed")
}
textView.addTarget(for: .allEditingEvents) { [weak self] in
self?.editingEvent()
}
gesture.addTarget { [weak self] in
self?.gestureEvent()
self?.otherGestureEvent()
self?.gesture.removeTarget()
}
Une optimisation supplémentaire (utile si vous l’utilisez dans de nombreux endroits et que vous ne voulez pas dupliquer l’appel à objc_setAssociatedObject
). Cela nous permet de ne pas nous soucier d'une partie sale de objc_setAssociatedObject
et de la conserver dans le constructeur de ClosureSleeve
:
class ClosureSleeve {
let closure: () -> Void
init(
for object: AnyObject,
_ closure: @escaping () -> Void
) {
self.closure = closure
objc_setAssociatedObject(
object,
String(format: "[%d]", arc4random()),
self,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN
)
}
@objc func invoke () {
closure()
}
}
Donc, votre extension aura l’air un peu plus propre:
extension UIControl {
func add(
for controlEvents: UIControlEvents,
_ closure: @escaping ()->()
) {
let sleeve = ClosureSleeve(
for: self,
closure
)
addTarget(
sleeve,
action: #selector(ClosureSleeve.invoke),
for: controlEvents
)
}
}
Ma solution.
typealias UIAction = () -> Void;
class Button: UIButton {
public var touchUp :UIAction? {
didSet {
self.setup()
}
}
func setup() -> Void {
self.addTarget(self, action: #selector(touchInside), for: .touchUpInside)
}
@objc private func touchInside() -> Void {
self.touchUp!()
}
}
class ViewController : UIViewController {
var aButton: UIButton!
var assignedClosure: (() -> Void)? = nil
override func loadView() {
let view = UIView()
view.backgroundColor = .white
aButton = UIButton()
aButton.frame = CGRect(x: 95, y: 200, width: 200, height: 20)
aButton.backgroundColor = UIColor.red
aButton.addTarget(self, action: .buttonTapped, for: .touchUpInside)
view.addSubview(aButton)
self.view = view
}
func fizzleButtonOn(events: UIControlEvents, with: @escaping (() -> Void)) {
assignedClosure = with
aButton.removeTarget(self, action: .buttonTapped, for: .allEvents)
aButton.addTarget(self, action: .buttonTapped, for: events)
}
@objc func buttonTapped() {
guard let closure = assignedClosure else {
debugPrint("original tap")
return
}
closure()
}
}
fileprivate extension Selector {
static let buttonTapped = #selector(ViewController.buttonTapped)
}
Ensuite, à un moment donné du cycle de vie de votre application, vous modifiez la fermeture des instances. Voici un exemple
fizzleButtonOn(events: .touchUpInside, with: { debugPrint("a new tap action") })