UIControl.h
UIControlEvents控制事件的类型
typedef NS_OPTIONS(NSUInteger, UIControlEvents) {
UIControlEventTouchDown = 1 << 0, // on all touch downs
UIControlEventTouchDownRepeat = 1 << 1, // on multiple touchdowns (tap count > 1)
UIControlEventTouchDragInside = 1 << 2,
UIControlEventTouchDragOutside = 1 << 3,
UIControlEventTouchDragEnter = 1 << 4,
UIControlEventTouchDragExit = 1 << 5,
UIControlEventTouchUpInside = 1 << 6,
UIControlEventTouchUpOutside = 1 << 7,
UIControlEventTouchCancel = 1 << 8,
UIControlEventValueChanged = 1 << 12, // sliders, etc.
UIControlEventPrimaryActionTriggered API_AVAILABLE(ios(9.0)) = 1 << 13, // semantic action: for buttons, etc.
UIControlEventMenuActionTriggered API_AVAILABLE(ios(14.0)) = 1 << 14, // triggered when the menu gesture fires but before the menu presents
UIControlEventEditingDidBegin = 1 << 16, // UITextField
UIControlEventEditingChanged = 1 << 17,
UIControlEventEditingDidEnd = 1 << 18,
UIControlEventEditingDidEndOnExit = 1 << 19, // 'return key' ending editing
UIControlEventAllTouchEvents = 0x00000FFF, // for touch events
UIControlEventAllEditingEvents = 0x000F0000, // for UITextField
UIControlEventApplicationReserved = 0x0F000000, // range available for application use
UIControlEventSystemReserved = 0xF0000000, // range reserved for internal framework use
UIControlEventAllEvents = 0xFFFFFFFF
};
UIControl.h文件
UIControlEvents属性是由内部判断出来的具体是哪种事件
@interface UIControl : UIView
- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
/// Initializes the control and adds primaryAction for the UIControlEventPrimaryActionTriggered control event. Subclasses of UIControl may alter or add behaviors around the usage of primaryAction, see subclass documentation of this initializer for additional information.
- (instancetype)initWithFrame:(CGRect)frame primaryAction:(nullable UIAction *)primaryAction API_AVAILABLE(ios(14.0));
@property(nonatomic,getter=isEnabled) BOOL enabled; // default is YES. if NO, ignores touch events and subclasses may draw differently
@property(nonatomic,getter=isSelected) BOOL selected; // default is NO may be used by some subclasses or by application
@property(nonatomic,getter=isHighlighted) BOOL highlighted; // default is NO. this gets set/cleared automatically when touch enters/exits during tracking and cleared on up
@property(nonatomic) UIControlContentVerticalAlignment contentVerticalAlignment; // how to position content vertically inside control. default is center
@property(nonatomic) UIControlContentHorizontalAlignment contentHorizontalAlignment; // how to position content horizontally inside control. default is center
@property(nonatomic, readonly) UIControlContentHorizontalAlignment effectiveContentHorizontalAlignment; // how to position content horizontally inside control, guaranteed to return 'left' or 'right' for any 'leading' or 'trailing'
@property(nonatomic,readonly) UIControlState state; // could be more than one state (e.g. disabled|selected). synthesized from other flags.
@property(nonatomic,readonly,getter=isTracking) BOOL tracking;
@property(nonatomic,readonly,getter=isTouchInside) BOOL touchInside; // valid during tracking only
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event; // touch is sometimes nil if cancelTracking calls through to this.
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event; // event may be nil if cancelled for non-event reasons, e.g. removed from window
// add target/action for particular event. you can call this multiple times and you can specify multiple target/actions for a particular event.
// passing in nil as the target goes up the responder chain. The action may optionally include the sender and the event in that order
// the action cannot be NULL. Note that the target is not retained.
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
// remove the target/action for a set of events. pass in NULL for the action to remove all actions for that target
- (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(UIControlEvents)controlEvents;
/// Adds the UIAction to a given event. UIActions are uniqued based on their identifier, and subsequent actions with the same identifier replace previously added actions. You may add multiple UIActions for corresponding controlEvents, and you may add the same action to multiple controlEvents.
- (void)addAction:(UIAction *)action forControlEvents:(UIControlEvents)controlEvents API_AVAILABLE(ios(14.0));
/// Removes the action from the set of passed control events.
- (void)removeAction:(UIAction *)action forControlEvents:(UIControlEvents)controlEvents API_AVAILABLE(ios(14.0));
/// Removes the action with the provided identifier from the set of passed control events.
- (void)removeActionForIdentifier:(UIActionIdentifier)actionIdentifier forControlEvents:(UIControlEvents)controlEvents API_AVAILABLE(ios(14.0));
// get info about target & actions. this makes it possible to enumerate all target/actions by checking for each event kind
@property(nonatomic,readonly) NSSet *allTargets; // set may include NSNull to indicate at least one nil target
@property(nonatomic,readonly) UIControlEvents allControlEvents; // list of all events that have at least one action
- (nullable NSArray<NSString *> *)actionsForTarget:(nullable id)target forControlEvent:(UIControlEvents)controlEvent; // single event. returns NSArray of NSString selector names. returns nil if none
/// Iterate over the event handlers installed on this control at the time this method is called. For each call, either actionHandler or action will be non-nil. controlEvents is always non-zero. Setting *stop to YES will terminate the enumeration early. It is legal to manipulate the control's event handlers within the block.
- (void)enumerateEventHandlers:(void (NS_NOESCAPE ^)(UIAction * _Nullable actionHandler, id _Nullable target, SEL _Nullable action, UIControlEvents controlEvents, BOOL *stop))iterator API_AVAILABLE(ios(14.0));
/// Dispatch the target-action pair. This method is called repeatedly by -sendActionsForControlEvents: and is a point at which you can observe or override behavior.
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
/// Like -sendAction:to:forEvent:, this method is called by -sendActionsForControlEvents:. You may override this method to observe or modify behavior. If you override this method, you should call super precisely once to dispatch the action, or not call super to suppress sending that action.
- (void)sendAction:(UIAction *)action API_AVAILABLE(ios(14.0));
/// send all actions associated with the given control events
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents;
/// Returns a UIContextMenuInteraction with this control set as its delegate. Before constructing the UIContextMenuInteraction, UIControl verifies 'self' is a viable delegate. See 'Implementing UIControl Menus' below for more details.
@property (nonatomic, readonly, strong, nullable) UIContextMenuInteraction *contextMenuInteraction API_AVAILABLE(ios(14.0)) API_UNAVAILABLE(watchos, tvos);
/// Specifies if the context menu interaction is enabled. NO by default.
@property (nonatomic, readwrite, assign, getter = isContextMenuInteractionEnabled) BOOL contextMenuInteractionEnabled API_AVAILABLE(ios(14.0)) API_UNAVAILABLE(watchos, tvos);
/// If the contextMenuInteraction is the primary action of the control, invoked on touch-down. NO by default.
@property (nonatomic, readwrite, assign) BOOL showsMenuAsPrimaryAction API_AVAILABLE(ios(14.0)) API_UNAVAILABLE(watchos, tvos);
/// Return a point in this control's coordinate space to which to attach the given configuration's menu.
- (CGPoint)menuAttachmentPointForConfiguration:(UIContextMenuConfiguration *)configuration API_AVAILABLE(ios(14.0)) API_UNAVAILABLE(watchos, tvos);
/// Assigning a value to this property causes the tool tip to be displayed for the view. Setting the property to nil cancels the display of the tool tip for the view.
@property (nonatomic, copy, nullable) NSString *toolTip API_AVAILABLE(ios(15.0)) API_UNAVAILABLE(watchos, tvos);
/// Returns the control's default UIToolTipInteraction.
@property (nonatomic, readonly, strong, nullable) UIToolTipInteraction *toolTipInteraction API_AVAILABLE(ios(15.0)) API_UNAVAILABLE(watchos, tvos);
@end
UIControl.m文件
#import "UIControl+UIPrivate.h"
#import "UIEvent.h"
#import "UITouch.h"
#import "UIApplication.h"
#import "UIControlAction.h"
@implementation UIControl {
NSMutableArray *_registeredActions;
}
- (id)initWithFrame:(CGRect)frame
{
if ((self=[super initWithFrame:frame])) {
_registeredActions = [[NSMutableArray alloc] init];
self.enabled = YES;
self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
self.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
}
return self;
}
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
{
UIControlAction *controlAction = [[UIControlAction alloc] init];
controlAction.target = target;
controlAction.action = action;
controlAction.controlEvents = controlEvents;
[_registeredActions addObject:controlAction];
}
- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
{
NSMutableArray *discard = [[NSMutableArray alloc] init];
for (UIControlAction *controlAction in _registeredActions) {
if (controlAction.target == target && (action == NULL || controlAction.controlEvents == controlEvents)) {
[discard addObject:controlAction];
}
}
[_registeredActions removeObjectsInArray:discard];
}
- (NSArray *)actionsForTarget:(id)target forControlEvent:(UIControlEvents)controlEvent
{
NSMutableArray *actions = [[NSMutableArray alloc] init];
for (UIControlAction *controlAction in _registeredActions) {
if ((target == nil || controlAction.target == target) && (controlAction.controlEvents & controlEvent) ) {
[actions addObject:NSStringFromSelector(controlAction.action)];
}
}
if ([actions count] == 0) {
return nil;
} else {
return actions;
}
}
- (NSSet *)allTargets
{
return [NSSet setWithArray:[_registeredActions valueForKey:@"target"]];
}
- (UIControlEvents)allControlEvents
{
UIControlEvents allEvents = 0;
for (UIControlAction *controlAction in _registeredActions) {
allEvents |= controlAction.controlEvents;
}
return allEvents;
}
- (void)_sendActionsForControlEvents:(UIControlEvents)controlEvents withEvent:(UIEvent *)event
{
for (UIControlAction *controlAction in _registeredActions) {
if (controlAction.controlEvents & controlEvents) {
[self sendAction:controlAction.action to:controlAction.target forEvent:event];
}
}
}
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents
{
[self _sendActionsForControlEvents:controlEvents withEvent:nil];
}
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
[[UIApplication sharedApplication] sendAction:action to:target from:self forEvent:event];
}
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
return YES;
}
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
}
- (void)cancelTrackingWithEvent:(UIEvent *)event
{
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
_touchInside = YES;
_tracking = [self beginTrackingWithTouch:touch withEvent:event];
self.highlighted = YES;
if (_tracking) {
UIControlEvents currentEvents = UIControlEventTouchDown;
if (touch.tapCount > 1) {
currentEvents |= UIControlEventTouchDownRepeat;
}
[self _sendActionsForControlEvents:currentEvents withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
const BOOL wasTouchInside = _touchInside;
_touchInside = [self pointInside:[touch locationInView:self] withEvent:event];
self.highlighted = _touchInside;
if (_tracking) {
_tracking = [self continueTrackingWithTouch:touch withEvent:event];
if (_tracking) {
UIControlEvents currentEvents = ((_touchInside)? UIControlEventTouchDragInside : UIControlEventTouchDragOutside);
if (!wasTouchInside && _touchInside) {
currentEvents |= UIControlEventTouchDragEnter;
} else if (wasTouchInside && !_touchInside) {
currentEvents |= UIControlEventTouchDragExit;
}
[self _sendActionsForControlEvents:currentEvents withEvent:event];
}
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
_touchInside = [self pointInside:[touch locationInView:self] withEvent:event];
self.highlighted = NO;
if (_tracking) {
[self endTrackingWithTouch:touch withEvent:event];
[self _sendActionsForControlEvents:((_touchInside)? UIControlEventTouchUpInside : UIControlEventTouchUpOutside) withEvent:event];
}
_tracking = NO;
_touchInside = NO;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
self.highlighted = NO;
if (_tracking) {
[self cancelTrackingWithEvent:event];
[self _sendActionsForControlEvents:UIControlEventTouchCancel withEvent:event];
}
_touchInside = NO;
_tracking = NO;
}
- (void)_stateDidChange
{
[self setNeedsDisplay];
[self setNeedsLayout];
}
- (void)setEnabled:(BOOL)newEnabled
{
if (newEnabled != _enabled) {
_enabled = newEnabled;
[self _stateDidChange];
self.userInteractionEnabled = _enabled;
}
}
- (void)setHighlighted:(BOOL)newHighlighted
{
if (newHighlighted != _highlighted) {
_highlighted = newHighlighted;
[self _stateDidChange];
}
}
- (void)setSelected:(BOOL)newSelected
{
if (newSelected != _selected) {
_selected = newSelected;
[self _stateDidChange];
}
}
- (UIControlState)state
{
UIControlState state = UIControlStateNormal;
if (_highlighted) state |= UIControlStateHighlighted;
if (!_enabled) state |= UIControlStateDisabled;
if (_selected) state |= UIControlStateSelected;
return state;
}
@end
UIControl和UIResponder关系
UIControl继承UIView,而UIView继承UIResponder,UIResponder里最重要的四个方法是:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
而UIControl里面又有四个和这四个方法很类似的方法,它们是touches阶段方法内部调用的。
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)cancelTrackingWithEvent:(UIEvent *)event;
看UIControl内部的touches方法如何调用上述tracking方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
_touchInside = YES;
// 调用beginTrackingWithTouch方法
_tracking = [self beginTrackingWithTouch:touch withEvent:event];
self.highlighted = YES;
if (_tracking) {
UIControlEvents currentEvents = UIControlEventTouchDown;
if (touch.tapCount > 1) {
currentEvents |= UIControlEventTouchDownRepeat;
}
[self _sendActionsForControlEvents:currentEvents withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
const BOOL wasTouchInside = _touchInside;
_touchInside = [self pointInside:[touch locationInView:self] withEvent:event];
self.highlighted = _touchInside;
if (_tracking) {
_tracking = [self continueTrackingWithTouch:touch withEvent:event];
if (_tracking) {
UIControlEvents currentEvents = ((_touchInside)? UIControlEventTouchDragInside : UIControlEventTouchDragOutside);
if (!wasTouchInside && _touchInside) {
currentEvents |= UIControlEventTouchDragEnter;
} else if (wasTouchInside && !_touchInside) {
currentEvents |= UIControlEventTouchDragExit;
}
[self _sendActionsForControlEvents:currentEvents withEvent:event];
}
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
_touchInside = [self pointInside:[touch locationInView:self] withEvent:event];
self.highlighted = NO;
if (_tracking) {
// 调用endTrackingWithTouch方法
[self endTrackingWithTouch:touch withEvent:event];
[self _sendActionsForControlEvents:((_touchInside)? UIControlEventTouchUpInside : UIControlEventTouchUpOutside) withEvent:event];
}
_tracking = NO;
_touchInside = NO;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
self.highlighted = NO;
if (_tracking) {
// 调用cancelTrackingWithEvent方法
[self cancelTrackingWithEvent:event];
[self _sendActionsForControlEvents:UIControlEventTouchCancel withEvent:event];
}
_touchInside = NO;
_tracking = NO;
}
Target-Action
addTarget:action:forControlEvents:方法的实现如下,是把target和action,controlEvents绑定在一起,生成一个UIControlAction类,UIControl里面由一个UIControlAction数组,专门存放这些类。
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
{
UIControlAction *controlAction = [[UIControlAction alloc] init];
controlAction.target = target;
controlAction.action = action;
controlAction.controlEvents = controlEvents;
[_registeredActions addObject:controlAction];
}
那UIControl如何识别到Action,又是如何将事件发送给target的呢?
先看如何识别UIControlEvent类型的事件的,这个不是UIEvent,他是UIEvent更高级的事件,从UIEvent+Touches方法以及触摸位置信息里判断出来的。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
_touchInside = YES;
_tracking = [self beginTrackingWithTouch:touch withEvent:event];
self.highlighted = YES;
if (_tracking) {
// touchesBegan里判断出UIControlEventTouchDown
UIControlEvents currentEvents = UIControlEventTouchDown;
if (touch.tapCount > 1) {
currentEvents |= UIControlEventTouchDownRepeat;
}
// 判断出来后,给Actions发送事件
[self _sendActionsForControlEvents:currentEvents withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
const BOOL wasTouchInside = _touchInside;
_touchInside = [self pointInside:[touch locationInView:self] withEvent:event];
self.highlighted = _touchInside;
if (_tracking) {
_tracking = [self continueTrackingWithTouch:touch withEvent:event];
if (_tracking) {
// 判断出来是UIControlEventTouchDragInside还是UIControlEventTouchDragOutside事件,或者UIControlEventTouchDragEnter,或者UIControlEventTouchDragExit事件
UIControlEvents currentEvents = ((_touchInside)? UIControlEventTouchDragInside : UIControlEventTouchDragOutside);
if (!wasTouchInside && _touchInside) {
currentEvents |= UIControlEventTouchDragEnter;
} else if (wasTouchInside && !_touchInside) {
currentEvents |= UIControlEventTouchDragExit;
}
// 给actions发送事件
[self _sendActionsForControlEvents:currentEvents withEvent:event];
}
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
_touchInside = [self pointInside:[touch locationInView:self] withEvent:event];
self.highlighted = NO;
if (_tracking) {
[self endTrackingWithTouch:touch withEvent:event];
// 判断出来是UIControlEventTouchUpInside还是UIControlEventTouchUpOutside事件,并且发送给Actions
[self _sendActionsForControlEvents:((_touchInside)? UIControlEventTouchUpInside : UIControlEventTouchUpOutside) withEvent:event];
}
_tracking = NO;
_touchInside = NO;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
self.highlighted = NO;
if (_tracking) {
[self cancelTrackingWithEvent:event];
[self _sendActionsForControlEvents:UIControlEventTouchCancel withEvent:event];
}
_touchInside = NO;
_tracking = NO;
}
然后看看如何给target-action发送事件的,
sendActionsForControlEvents方法的实现
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents
{
[self _sendActionsForControlEvents:controlEvents withEvent:nil];
}
- (void)_sendActionsForControlEvents:(UIControlEvents)controlEvents withEvent:(UIEvent *)event
{
// 这里对acion里事件的过滤,只发送target关注的UICtrolEvent事件对应的UIEvent,这样UIEvent就被过滤成UICtrolEvent了
for (UIControlAction *controlAction in _registeredActions) {
if (controlAction.controlEvents & controlEvents) {
[self sendAction:controlAction.action to:controlAction.target forEvent:event];
}
}
}
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
[[UIApplication sharedApplication] sendAction:action to:target from:self forEvent:event];
}
其最终是通过UIApplication去分发UIControlEvent事件
UIApplication的方法sendAction:to:to:forEvent
- (BOOL)sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event
{
if (!target) {
// The docs say this method will start with the first responder if target==nil. Initially I thought this meant that there was always a given
// or set first responder (attached to the window, probably). However it doesn't appear that is the case. Instead it seems UIKit is perfectly
// happy to function without ever having any UIResponder having had a becomeFirstResponder sent to it. This method seems to work by starting
// with sender and traveling down the responder chain from there if target==nil. The first object that responds to the given action is sent
// the message. (or no one is)
// My confusion comes from the fact that motion events and keyboard events are supposed to start with the first responder - but what is that
// if none was ever set? Apparently the answer is, if none were set, the message doesn't get delivered. If you expicitly set a UIResponder
// using becomeFirstResponder, then it will receive keyboard/motion events but it does not receive any other messages from other views that
// happen to end up calling this method with a nil target. So that's a seperate mechanism and I think it's confused a bit in the docs.
// It seems that the reality of message delivery to "first responder" is that it depends a bit on the source. If the source is an external
// event like motion or keyboard, then there has to have been an explicitly set first responder (by way of becomeFirstResponder) in order for
// those events to even get delivered at all. If there is no responder defined, the action is simply never sent and thus never received.
// This is entirely independent of what "first responder" means in the context of a UIControl. Instead, for a UIControl, the first responder
// is the first UIResponder (including the UIControl itself) that responds to the action. It starts with the UIControl (sender) and not with
// whatever UIResponder may have been set with becomeFirstResponder.
id responder = sender;
while (responder) {
if ([responder respondsToSelector:action]) {
target = responder;
break;
} else if ([responder respondsToSelector:@selector(nextResponder)]) {
responder = [responder nextResponder];
} else {
responder = nil;
}
}
}
if (target) {
typedef void(*EventActionMethod)(id, SEL, id, UIEvent *);
EventActionMethod method = (EventActionMethod)[target methodForSelector:action];
//直接让target执行action方法,传参数是sender和UIEvent
method(target, action, sender, event);
return YES;
}
return NO;
}
网友评论