7

I have a

@IBDesignable
class Fancy:UIButton

I want to

addTarget(self, action:#selector(blah),
   forControlEvents: UIControlEvents.TouchUpInside)

So where in UIButton should that be done?

Where is the best place for addTarget ?

1 - I have seen layoutSubviews suggested - is that right?

Note - experimentation shows that a problem with layoutSubviews is that, of course, it can be called often, whenever things move around. It would be a bad idea to "addTarget" more than once.

2 - didMoveToSuperview is another suggestion.

3 - Somewhere in (one of) the Inits?

Note - experimentation shows a fascinating problem if you do it inside Init. During Init, IBInspectable variables are not yet actually set! (So for example, I was branching depending on the "style" of control set by an IBInspectable; it plain doesn't work as @IBInspectable: won't work when running!)

4 - Somewhere else???

I tried to do it in Init, and it worked well. But it breaks designables from working in the Editor.

enter image description here

By thrashing around, I came up with this (for some reason both must be included?)

@IBDesignable
class DotButton:UIButton
    {
    @IBInspectable var mainColor ... etc.
    
    required init?(coder decoder: NSCoder)
        {
        super.init(coder: decoder)
        addTarget(self, action:#selector(blah),
            forControlEvents: UIControlEvents.TouchUpInside)
        }
    override init(frame:CGRect)
        {
        super.init(frame:frame)
        }

I don't know why that works, and I don't understand why there would be two different init routines.

What's the correct way to include addTarget in a UIButton?

Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • Are you adding the target to the subclassed UIButton itself? Is not clear where the code comes from – Andrea May 15 '16 at 18:55

4 Answers4

4

tl;dr

    override func endTrackingWithTouch(touch: UITouch?, withEvent event: UIEvent?) {
        super.endTrackingWithTouch(touch, withEvent: event)
        if let touchNotNil = touch {
            if self.pointInside(touchNotNil.locationInView(self), withEvent: event) {
                print("it works")
            }
        }
    }

Why not use addTarget

addTarget method is part of action-target interface which is considered 'public'. Anything with reference to your button can, say, remove all of its actions, effectively breaking it. It is preffered to use some of 'protected' means, for instance endTrackingWithTouch which is accessible only to be overriden, not called directly. This way it will not interfere with any external objects using action-target mechanism.

(I know there is no strict 'public' or 'protected' in ObjC/UIKit, but the concept remains)

Your way

If you want to do it exactly your way then your example is all good, just copy addTarget call to init(frame:CGRect).

Or you can put addTarget in awakeFromNib (don't forget super) instead of init?(coder decoder: NSCoder), but you will be forced to implement init with coder anyway, so...

layoutSubviews and didMoveToSuperView both are terrible ideas. Both may happen more than once resulting in blah target-action added again. Then blah will be called multiple times for a single click.

By the way

The Apple way

By the Cocoa MVC (which is enforced by UIKit classes implmentation) you should assign that action to the object controlling that button, animations or not. Most often that object will be Cocoa MVC 'Controller' - UIViewController.

If you create button programmatically UIViewController should assign target to itself in overridden loadView or viewDidLoad. When button is loaded from nib the preffered way is to assign target action in xib itself.

Old Good MVC

As mentioned here in real MVC views do not send actions to themselves. The closest thing to real MVC Controller in UIKit is UIGestureRecognizer.

Be warned that it's pretty difficult to pull of real MVC with UIKit class set.

Community
  • 1
  • 1
gp-v
  • 114
  • 1
  • 7
  • gp-v, your point about *not* using addTarget, **since, it is publicly exposed**, is a superb point. thanks for putting it in as an answer. you're absolutely right, I'm aware of that danger, and I don't like that there's no "internal access" targets. `endTrackingWithTouch` for example is indeed protected as you explain (but, they don't match the logical situations found in UIControlEvents, unfortunately, as far as I'm aware). – Fattie May 25 '16 at 18:55
  • note though that it would be great to have an answer to the question at hand - which is straightforward; where to do that sort of initialization in UIButton. TomS's answer seems IMO to be best so far, `awakeFromNib` (noting though that it is a storyboard-only solution). – Fattie May 25 '16 at 18:57
  • NB. regarding your "By the way" comment. (I also addressed this under Andrea's answer above). I disagree, IMO MVC very much has conceptual layers. You perhaps agree that *the entire app plus iPhone* as a concept can very much be "just a V" viz-a-viz the overall system of (say) some MMP cloud game. And down at the level of animations etc on the button: note that, indeed, Apple themselves *within* the UIButton do precisely what I'm asking about. – Fattie May 25 '16 at 19:00
  • In my opinion _Cocoa MVC_ is flawed. – gp-v May 25 '16 at 19:08
  • awakeFromNib answer is nice one indeed. just do not forget to also `addTarget` in `init(frame:)`. I edited my answer for consistency but honor goes to TomSwift. – gp-v May 25 '16 at 19:10
  • OK.. interesting. awakeFromNib. say, there's a problem in Init ! notice the italics in my question ...... – Fattie May 25 '16 at 19:15
  • For the UIControlEvents you are right, it doesn't provide that. If you want to persue `endTrackingWithTouch` option for some reason you can implement `beginTrackingWithTouch` etc. methods to differentiate between events. Requires more work thought. Sorry for triple posting. – gp-v May 25 '16 at 19:15
  • Right solution is to always set the same action and branch in its code. Wrong-but-working solution is to set @IBInspectable variable to default value in `init` and then implement `didSet` or `set()` for that variable where you will remove/re-add action-target depending on new value. – gp-v May 25 '16 at 19:22
  • "set the same action and branch in its code" true, that's an excellent idea. – Fattie May 25 '16 at 19:50
  • "where you will remove/re-add" ... right, I wouldn't do that. Just TBC, I simply mean the case of: imagine a "FancyButton" I am writing for the team. it adds EXPLOSIONS (!) when you click. great. you can imagine my init code addTarget:RedExplosion. Say the guys ask me "oh, that is great, make two *varieties* of FancyButton please, RedExplosion and RedFireworks". They choose which "variety" using an IBInspector toggle. so in init it would be either addTarget:RedExplosion or addTarget:AmazingFlames (I mean during object instantiation); targets would never change or anything. – Fattie May 25 '16 at 19:54
  • {Of course, that sucker could be changed at runtime; you're right. But if apple wants to make Xcode a game engine, they're gonna get people using it that way you know! :) } – Fattie May 25 '16 at 19:55
  • 'update targets-actions' is bad solution. but for the sake of the argument: it will still work. when loading from nib it's just that setter will be called twice: first time from init and second time when loading xib itself overwritting default value. ofcourse one should plan for such possibility and manage `addTarget` calls accordingly. but it's still pretty bad solution altogether. – gp-v May 25 '16 at 20:04
  • i lost track of what you mean :) but it's all good - cheers! – Fattie May 25 '16 at 20:13
  • An interesting question is, simply: ***What's the best place to do ANY initialization in UIButton?*** (Let's say one is NOT setting `addTarget` - just any ordinary initialization.) What the heck is the answer? The `Init` calls are no good because the IBInspectable values have not been set yet, and you may (of course) need them in initialization code. – Fattie May 25 '16 at 20:15
  • In short: in `init(frame)` call your own `commonInit()` then `UIInit()`. In `init(coder)` call `commonInit()`. In `awakeFromNib` call `UIInit()`. This is one (rather good) option of many and leaves opening for few _very rare_ cases. Let's not chat in comments any more. Start a chat with me or post a question if you have more. I can feel moderating incoming:) – gp-v May 25 '16 at 21:10
  • Don't worry about the moderators, they're all drunks. Cool guys don't chat ;) Good one l8r.... – Fattie May 26 '16 at 12:17
3

You should not add as target the same object that produces the action.
The target and its callback should be another object, usually a view controller.
There are 2 inits methods because the button can be instantiated by calling init or by the process of deserializion (NSCoder) from a nib/xib. Since you probably added the button to a storyboard the init method called is init?(_: NSCoder).
[UPDATE]
I agree about what you say in the comment, but I think that the action-target pattern should be used for communicating with other objects, I'm using conditional, because as far as I know I never seen something like what you wrote in Apple code or some other library. If you want to intercept and make some actions inside the button you should probably override some of the methods exposed in UIControl.
About designable, you are, again, correct. init(frame) is called if you are creating a button programmatically, init(coder) if the button comes from a xib.
The method init(frame) is also called during designable process. At this point I think that the best option is to debug directly your view.

  • Place some breakpoints inside you UIButton subclass
  • Select the view in your storyboard
  • Go to the Editor -> Debug selected views

Now you should be able to understand where the problem is.

Andrea
  • 26,120
  • 10
  • 85
  • 131
  • Hi Andrea - ahh, could it be the case then that `init?(_: NSCoder)` is used in the running app, but, the **other one** (as far as I know it is `override init(frame:CGRect)` ) **is used when running the storyboard designables**?? Maybe that's the explanation? Could there be a problem with the way I'm using "required", is that wrong in some way? – Fattie May 15 '16 at 19:21
  • 2
    { *"You should not add as target the same object that produces the action."* In the MVC model, views (nonsense like animations etc) must take care of themselves. Nontrivial buttons will know about when they are clicked (and perhaps moved over - whatever). That "view-user interface bells and whistles crap" is handled locally in controls. Just as you say, of course the actual "target" of "the button" as a logical unit goes to a Controller. } – Fattie May 15 '16 at 19:24
  • Say Andrea, thanks, *"probably override some of the methods exposed in UIControl...."* which methods do you mean there? – Fattie May 16 '16 at 11:01
  • Well depends on what you want to do, sendAction:to:forEvent: is used `to observe or modify the dispatching of action methods to the control’s associated targets`.. What are you trying to achieve? what the "blah" selector should do? – Andrea May 16 '16 at 11:58
  • (ah, the calls like `sendAction:to:forEvent` are used for actually making a new type of UIControl. so, the user perhaps clicks on something and that's how you send the message, just as a normal UIButton normally does.) As I said just above, in the MVC model "V" views will look after themselves in terms of "interface bells and whistles crap". A UI button uses addTarget to itself to achieve any "user interface bells and whistles crap". For example it's very common in a game (say) that every time a button is clicked the button spins around, gives off a smoke bomb, and plays a tune.... – Fattie May 16 '16 at 12:08
  • ... so all that "V" material is handled internally in the UIButton. Your colleague who is programming the "C" doesn't even know about it; that person just drags the click target as normal to the controller, where the "button as a whole" does something like "Login" or whatever. inside writing the actual button (I mean the "V" - animations and crap) of course you need to know when clicking, mouseover, etc etc has happened; you use `addTarget` as in my example. My question is where to run addTarget. (As I mention, many suggest `layoutSubviews` - that seems like a bad idea to me; I don't know.) – Fattie May 16 '16 at 12:12
  • I don't think `layoutSubviews` is a good idea, it can be called a lot of times. I'm wondering if `- didMoveToSuperview` will suit your requirements. This method is called once your view is added/removed to another view (removed is when the superview is nil) here you can add and remove the target. Probably is worth to check it out – Andrea May 16 '16 at 13:17
  • `didMoveToSuperview` could be a great suggestion. unfortunately I just don't know. – Fattie May 16 '16 at 13:18
2

Your initialize method is not correct, this will work:

```swift

override init(frame: CGRect) {
    super.init(frame: frame)
    self.loadNib() 
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    self.loadNib()
}

private func loadNib() {
    let nibView = NSBundle(forClass: self.classForCoder).loadNibNamed("yourView", owner: self, options: nil).first as! UIView
    nibView.frame = self.bounds
    nibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
    self.button.addTarget(self, action: #selector(action), forControlEvents: .TouchUpInside)
    self.addSubview(nibView)
}

```

Sayaka
  • 21
  • 2
2

How about implementing awakeFromNib and doing it there?

https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Protocols/NSNibAwaking_Protocol/#//apple_ref/occ/instm/NSObject/awakeFromNib

You can also conditionally run (or not run) code when it is being run in the context of Interface Builder:

#if !TARGET_INTERFACE_BUILDER
    // this code will run in the app itself
#else
    // this code will execute only in IB
#endif

(see http://nshipster.com/ibinspectable-ibdesignable/)

TomSwift
  • 39,369
  • 12
  • 121
  • 149
  • If your button is ever instantiated via code (not the nib loader) then awakeFromNib won't be called. Plan accordingly. – TomSwift May 25 '16 at 18:44
  • Correct, nib loader == from storyboard. Thanks for calling out the typo in my profile; fixed now. – TomSwift May 25 '16 at 18:49