问题
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.
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?
回答1:
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/)
回答2:
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.
回答3:
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.
回答4:
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)
}
```
来源:https://stackoverflow.com/questions/37242334/where-to-initialize-target-in-a-uibutton-particularly-caring-for-ibdesignable