欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS学习——(转)UIResponder详解

程序员文章站 2022-07-08 18:34:13
本文转载自:ios开发 之 UIResponder详解 我们知道UIResponder是所有视图View的基类,在iOS中UIResponder类是专门用来响应用户的操作处理各种事件的,包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Co ......

本文转载自:ios开发 之 UIResponder详解 

  我们知道UIResponder是所有视图View的基类,在iOS中UIResponder类是专门用来响应用户的操作处理各种事件的,包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events)。我们知道UIApplication、UIView、UIViewController这几个类是直接继承自UIResponder,所以这些类都可以响应事件。当然我们自定义的继承自UIView的View以及自定义的继承自UIViewController的控制器都可以响应事件。本文将详细介绍UIResponder类。

一 UIResponder.h文件注释版

  首先,我们对UIResponder.h文件进行了研究和解释,并对各模块进行了注释添加,方便我们在阅读和学习时候的理解。在学习了UIView、NSObject的.h文件之后,我们发现这些基类的.h文件的组织架构基本类似,最初是定义该类中需要用到的一些枚举类型数据,然后对相应的协议进行定义,接着就是对本类进行定义,一些基本的属性和方法的定义,最后就是对本类做各种功能性的分类。

  1 //
  2 //  UIResponder.h
  3 
  4 #import <Foundation/Foundation.h>
  5 #import <UIKit/UIKitDefines.h>
  6 #import <UIKit/UIEvent.h>
  7 
  8 NS_ASSUME_NONNULL_BEGIN
  9 
 10 @class UIPress;
 11 @class UIPressesEvent;
 12 
 13 #pragma mark - UIResponderStandardEditActions协议定义
 14 
 15 @protocol UIResponderStandardEditActions <NSObject>
 16 @optional
 17 /** 剪切事件 */
 18 - (void)cut:(nullable id)sender NS_AVAILABLE_IOS(3_0);
 19 /** 复制事件 */
 20 - (void)copy:(nullable id)sender NS_AVAILABLE_IOS(3_0);
 21 /** 粘贴事件 */
 22 - (void)paste:(nullable id)sender NS_AVAILABLE_IOS(3_0);
 23 /** 选择事件 */
 24 - (void)select:(nullable id)sender NS_AVAILABLE_IOS(3_0);
 25 /** 全选事件 */
 26 - (void)selectAll:(nullable id)sender NS_AVAILABLE_IOS(3_0);
 27 /** 删除事件 */
 28 - (void)delete:(nullable id)sender NS_AVAILABLE_IOS(3_2);
 29 /** 从左到右写入字符串(居左) */
 30 - (void)makeTextWritingDirectionLeftToRight:(nullable id)sender NS_AVAILABLE_IOS(5_0);
 31 /** 从右到左写入字符串(居右) */
 32 - (void)makeTextWritingDirectionRightToLeft:(nullable id)sender NS_AVAILABLE_IOS(5_0);
 33 /** 切换字体为黑体(粗体) */
 34 - (void)toggleBoldface:(nullable id)sender NS_AVAILABLE_IOS(6_0);
 35 /** 切换字体为斜体 */
 36 - (void)toggleItalics:(nullable id)sender NS_AVAILABLE_IOS(6_0);
 37 /** 给文字添加下划线 */
 38 - (void)toggleUnderline:(nullable id)sender NS_AVAILABLE_IOS(6_0);
 39 
 40 /** 增加字体大小 */
 41 - (void)increaseSize:(nullable id)sender NS_AVAILABLE_IOS(7_0);
 42 /** 减小字体大小 */
 43 - (void)decreaseSize:(nullable id)sender NS_AVAILABLE_IOS(7_0);
 44 
 45 @end
 46 
 47 #pragma mark - UIResponder类定义
 48 
 49 NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>
 50 
 51 #pragma mark - 响应者相关方法
 52 
 53 /** 获取下一个响应者 */
 54 #if UIKIT_DEFINE_AS_PROPERTIES
 55 @property(nonatomic, readonly, nullable) UIResponder *nextResponder;
 56 #else
 57 - (nullable UIResponder *)nextResponder;
 58 #endif
 59 
 60 /** 是否允许成为第一响应者。默认返回NO */
 61 #if UIKIT_DEFINE_AS_PROPERTIES
 62 @property(nonatomic, readonly) BOOL canBecomeFirstResponder;
 63 #else
 64 - (BOOL)canBecomeFirstResponder;
 65 #endif
 66 /** 设置成为第一响应者 */
 67 - (BOOL)becomeFirstResponder;
 68 
 69 /** 是否允许放弃第一响应者。默认返回YES */
 70 #if UIKIT_DEFINE_AS_PROPERTIES
 71 @property(nonatomic, readonly) BOOL canResignFirstResponder;
 72 #else
 73 - (BOOL)canResignFirstResponder;
 74 #endif
 75 /** 设置放弃第一响应者 */
 76 - (BOOL)resignFirstResponder;
 77 
 78 /** 判断对象是否是第一响应者 */
 79 #if UIKIT_DEFINE_AS_PROPERTIES
 80 @property(nonatomic, readonly) BOOL isFirstResponder;
 81 #else
 82 - (BOOL)isFirstResponder;
 83 #endif
 84 
 85 #pragma mark - 触摸相关方法,一般用于响应屏幕触摸
 86 /** 手指按下时响应 */
 87 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
 88 /** 手指移动时响应 */
 89 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
 90 /** 手指抬起时响应 */
 91 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
 92 /** 取消(意外中断, 如:电话, 系统警告窗等) */
 93 - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
 94 /** 3DTouch响应(iOS9.1后使用) */
 95 - (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
 96 
 97 #pragma mark - 深按相关方法,一般用于遥控器按键响应
 98 /** 手指按压开始时响应 */
 99 - (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
100 /** 手指按压位置移动时响应 */
101 - (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
102 /** 手指抬起接受按压时响应 */
103 - (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
104 /** 按压取消(意外中断, 如:电话, 系统警告窗等) */
105 - (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
106 
107 #pragma mark - 加速相关方法,一般用于摇一摇、运动事件监听等
108 /** 开始加速 */
109 - (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
110 /** 结束加速 */
111 - (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
112 /** 加速取消(意外中断, 如:电话, 系统警告窗等) */
113 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
114 
115 /** 远程控制事件 */
116 - (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);
117 
118 /** 返回UIMenuController需要显示的控件(如:复制,粘贴等) */
119 - (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(3_0);
120 
121 /** 返回响应的操作目标对象 */
122 - (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(7_0);
123 
124 /** 获取响应链就近共享撤消管理 */
125 @property(nullable, nonatomic,readonly) NSUndoManager *undoManager NS_AVAILABLE_IOS(3_0);
126 
127 @end
128 
129 /** 快捷主键枚举 */
130 typedef NS_OPTIONS(NSInteger, UIKeyModifierFlags) {
131     UIKeyModifierAlphaShift     = 1 << 16,  //!< Alpha+Shift键.
132     UIKeyModifierShift          = 1 << 17,  //!< Shift键.
133     UIKeyModifierControl        = 1 << 18,  //!< Control键.
134     UIKeyModifierAlternate      = 1 << 19,  //!< Alt键.
135     UIKeyModifierCommand        = 1 << 20,  //!< Command键.
136     UIKeyModifierNumericPad     = 1 << 21,  //!< Num键.
137 } NS_ENUM_AVAILABLE_IOS(7_0);
138 
139 #pragma mark - 快捷键对象
140 
141 NS_CLASS_AVAILABLE_IOS(7_0) @interface UIKeyCommand : NSObject <NSCopying, NSSecureCoding>
142 
143 /** 初始化对象 */
144 - (instancetype)init NS_DESIGNATED_INITIALIZER;
145 /** 初始化对象 */
146 - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
147 
148 /** 获取快捷辅键(如快捷命令【Command+A】中的 A 键) */
149 @property (nonatomic,readonly) NSString *input;
150 /** 获取快捷主键(如快捷命令【Command+A】中的 Command 键) */
151 @property (nonatomic,readonly) UIKeyModifierFlags modifierFlags;
152 /** 显示给用户的快捷键标题 */
153 @property (nullable,nonatomic,copy) NSString *discoverabilityTitle NS_AVAILABLE_IOS(9_0);
154 
155 /** 创建一个快捷键命令 */
156 + (UIKeyCommand *)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action;
157 
158 /** 创建一个快捷键命令 */
159 + (UIKeyCommand *)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action discoverabilityTitle:(NSString *)discoverabilityTitle NS_AVAILABLE_IOS(9_0);
160 
161 @end
162 
163 #pragma mark - 响应快捷命令
164 
165 @interface UIResponder (UIResponderKeyCommands)
166 /** 返回快捷键命令数组 */
167 @property (nullable,nonatomic,readonly) NSArray<UIKeyCommand *> *keyCommands NS_AVAILABLE_IOS(7_0);
168 @end
169 
170 @class UIInputViewController;
171 @class UITextInputMode;
172 @class UITextInputAssistantItem;
173 
174 #pragma mark - 输入视图
175 
176 @interface UIResponder (UIResponderInputViewAdditions)
177 
178 /** 键盘输入视图(系统默认的,可以自定义) */
179 @property (nullable, nonatomic, readonly, strong) __kindof UIView *inputView NS_AVAILABLE_IOS(3_2);
180 /** 弹出键盘时附带的视图 */
181 @property (nullable, nonatomic, readonly, strong) __kindof UIView *inputAccessoryView NS_AVAILABLE_IOS(3_2);
182 
183 /** 输入助手配置键盘的快捷方式栏时使用 */
184 @property (nonnull, nonatomic, readonly, strong) UITextInputAssistantItem *inputAssistantItem NS_AVAILABLE_IOS(9_0) __TVOS_PROHIBITED __WATCHOS_PROHIBITED;
185 
186 /** 键盘输入视图控制器 */
187 @property (nullable, nonatomic, readonly, strong) UIInputViewController *inputViewController NS_AVAILABLE_IOS(8_0);
188 /** 弹出键盘时附带的视图的视图控制器 */
189 @property (nullable, nonatomic, readonly, strong) UIInputViewController *inputAccessoryViewController NS_AVAILABLE_IOS(8_0);
190 
191 /** 文本输入模式 */
192 @property (nullable, nonatomic, readonly, strong) UITextInputMode *textInputMode NS_AVAILABLE_IOS(7_0);
193 
194 /** 文本输入模式标识 */
195 @property (nullable, nonatomic, readonly, strong) NSString *textInputContextIdentifier NS_AVAILABLE_IOS(7_0);
196 /** 根据设置的标识清除指定的文本输入模式 */
197 + (void)clearTextInputContextIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(7_0);
198 
199 /** 重新刷新键盘输入视图 */
200 - (void)reloadInputViews NS_AVAILABLE_IOS(3_2);
201 
202 @end
203 
204 /** 特殊快捷辅键定义 */
205 UIKIT_EXTERN NSString *const UIKeyInputUpArrow         NS_AVAILABLE_IOS(7_0); //!< 上按键.
206 UIKIT_EXTERN NSString *const UIKeyInputDownArrow       NS_AVAILABLE_IOS(7_0); //!< 下按键.
207 UIKIT_EXTERN NSString *const UIKeyInputLeftArrow       NS_AVAILABLE_IOS(7_0); //!< 左按键.
208 UIKIT_EXTERN NSString *const UIKeyInputRightArrow      NS_AVAILABLE_IOS(7_0); //!< 右按键
209 UIKIT_EXTERN NSString *const UIKeyInputEscape          NS_AVAILABLE_IOS(7_0); //!< Esc按键.
210 
211 #pragma mark - 响应者活动
212 
213 @interface UIResponder (ActivityContinuation)
214 /** 用户活动 */
215 @property (nullable, nonatomic, strong) NSUserActivity *userActivity NS_AVAILABLE_IOS(8_0);
216 /** 更新用户活动 */
217 - (void)updateUserActivityState:(NSUserActivity *)activity NS_AVAILABLE_IOS(8_0);
218 /** 恢复用户活动 */
219 - (void)restoreUserActivityState:(NSUserActivity *)activity NS_AVAILABLE_IOS(8_0);
220 @end
221 
222 NS_ASSUME_NONNULL_END

 二 UIResponder的使用

2.1 通过响应者链查找视图的视图控制器

   通过响应链查找视图控制器,nextResponder获取下一个响应者,响应者顺序为:

 iOS学习——(转)UIResponder详解

/**
 *  查找视图的视图控制器
 *
 *  @param view 视图
 *
 *  @return 返回视图的控制器
 */
- (UIViewController *)getControllerFromView:(UIView *)view {
    // 遍历响应者链。返回第一个找到视图控制器
    UIResponder *responder = view;
    while ((responder = [responder nextResponder])){
        if ([responder isKindOfClass: [UIViewController class]]){
            return (UIViewController *)responder;
        }
    }
    // 如果没有找到则返回nil
    return nil;
}

2.2 设置与取消第一响应者

  UIView默认不允许设置为第一响应者,因此设置UIView为第一响应者需要重写canBecomeFirstResponder方法并返回YES。 设置为第一响应者后,对象则可以接受远程控制事件进行处理(如耳机线控)。UITextField、UITextView成为第一响应者后会弹出输入键盘,取消第一响应者则会隐藏输入键盘。 

 1 //  ZMFirstResponderView.m
 2 
 3 #import "ZMFirstResponderView.h"
 4 
 5 @implementation ZMFirstResponderView
 6 
 7 /** 演示设置为第一响应者 */
 8 - (void)setBecomeFirstResponder {
 9     // 判断对象是否已经是第一响应者
10     if ([self isFirstResponder]) {
11         return;
12     }
13     // 判断对象是否允许成为第一响应者
14     if ([self canBecomeFirstResponder]) {
15         // 设置成为第一响应者
16         [self becomeFirstResponder];
17     }
18 }
19 
20 /** 演示放弃第一响应者 */
21 - (void)setResignFirstResponder {
22     // 判断对象是否不是第一响应者
23     if (![self isFirstResponder]) {
24         return;
25     }
26     // 判断对象是否允许放弃第一响应者
27     if ([self canResignFirstResponder]) {
28         // 设置放弃第一响应者
29         [self resignFirstResponder];
30     }
31 }
32 
33 /** 重写方法,允许对象成为第一响应者 */
34 - (BOOL)canBecomeFirstResponder {
35     return YES;
36 }
37 
38 @end

2.3 触摸相关方法,一般用于响应屏幕触摸

/** 手指按下时响应 */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    NSLog(@"--->手指按下时响应");
}

/** 手指移动时响应 */
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    NSLog(@"--->手指移动时响应");
}

/** 手指抬起时响应 */
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    NSLog(@"--->手指抬起时响应");
}

/** 触摸取消(意外中断, 如:电话, Home键退出等) */
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    NSLog(@"--->取消触摸响应");
}

2.4 加速相关方法,一般用于摇一摇、运动事件监听等

/** 开始加速 */
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0) {
    [super motionBegan:motion withEvent:event];
    NSLog(@"--->开始加速");
}

/** 结束加速 */
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0) {
    [super motionEnded:motion withEvent:event];
    NSLog(@"--->结束加速");
}

/** 加速取消(意外中断, 如:电话, Home键退出等) */
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0) {
    [super motionCancelled:motion withEvent:event];
    NSLog(@"--->加速取消");
}

2.5 远程控制方法,一般用于耳机线控

耳机线控要注意三点要素:

  1. 启动接受远程事件:[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
  2. 设置成为第一响应者(UIViewController,AppDelegate中不需要设置)
  3. 获取音频的控制权
//  ZMAudioView.m

#import "ZMAudioView.h"
#import <AVFoundation/AVFoundation.h>

@implementation ZMAudioView

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // 1 启动接受远程事件
        [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
        //2  设置成为第一响应者
        [self becomeFirstResponder];
        // 3 播放一段静音文件,使APP获取音频的控制权
        NSURL *audioURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"mute_60s" ofType:@"mp3"]];
        AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:audioURL error:nil];
        [audioPlayer play];
    }
    return self;
}

/** 允许对象成为第一响应者 */
- (BOOL)canBecomeFirstResponder {
    return YES;
}

/** 远程控制事件响应 */
- (void)remoteControlReceivedWithEvent:(UIEvent *)receivedEvent {
    NSLog(@"--->耳机线控响应");
}

- (void)dealloc {
    // 停止接受远程事件
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
    // 放弃第一响应者
    [self resignFirstResponder];
}

@end

2.6 在UILabel中实现长按菜单(复制、粘贴等)

为UILabel添加长按菜单需要注意几点:

  1. 启用用户交互:self.userInteractionEnabled = YES;
  2. 在显示菜单之前设置对象成为第一响应者(UIViewController,AppDelegate中不需要设置)
  3. 返回菜单需要显示的按钮,并重写实现对应方法
  4. 注册长按手势,显示菜单
//  ZMMenuLabel.m

#import "ZMMenuLabel.h"

@implementation ZMMenuLabel

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // 启用用户交互
        self.userInteractionEnabled = YES;
        // 添加长按手势
        UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressMenu:)];
        longPressGesture.minimumPressDuration = 0.2;
        [self addGestureRecognizer:longPressGesture];
    }
    return self;
}

/** 允许对象成为第一响应者 */
- (BOOL)canBecomeFirstResponder {
    return YES;
}

/** 长按响应 */
- (void)longPressMenu:(UILongPressGestureRecognizer *)sender {
    if (sender.state == UIGestureRecognizerStateBegan) {
        // 设置成为第一响应者
        [self becomeFirstResponder];
        // 显示菜单
        UIMenuController *menuCtrl = [UIMenuController sharedMenuController];
        [menuCtrl setTargetRect:self.frame inView:self.superview];
        [menuCtrl setMenuVisible:YES animated:YES];
    }
}

/** 返回需要显示的菜单按钮 */
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    // 只显示复制、粘贴按钮
    if (action == @selector(copy:) || action == @selector(paste:)) {
        return YES;
    }
    return NO;
}

/** 实现复制方法 */
- (void)copy:(id)sender {
    UIPasteboard *paste = [UIPasteboard generalPasteboard];
    paste.string = self.text;
}

/** 实现粘贴方法 */
- (void)paste:(id)sender {
    UIPasteboard *paste = [UIPasteboard generalPasteboard];
    self.text = paste.string;
}

@end

2.7 使用NSUndoManager实现画板撤销/重做功能

实现撤销/重做注意以下几点:

  1. 在调用方法时需要添加注册一个对应的撤销方法
  2. 撤销/ 重做只需要调用undoManager中的相应方法即可
  3. 如果需要多个动作一起撤销则需要标记分组
  1 /** ==============ZMDrawingBoardView.h文件=================== */
  2 
  3 #import <UIKit/UIKit.h>
  4 
  5 /** 画板View */
  6 @interface ZMDrawingBoardView : UIView
  7 
  8 @end
  9 
 10 
 11 /** 划线Model */
 12 @interface ZMLineModel : NSObject
 13 
 14 @property (nonatomic) CGPoint begin;
 15 @property (nonatomic) CGPoint end;
 16 
 17 @end
 18 
 19 
 20 /** ==============ZMDrawingBoardView.m文件=================== */
 21 
 22 #import "ZMDrawingBoardView.h"
 23 
 24 /** 画板View */
 25 @interface ZMDrawingBoardView ()
 26 
 27 @property (nonatomic, strong) ZMLineModel *currentLine;
 28 @property (nonatomic, strong) NSMutableArray<ZMLineModel *> *toucheArray;
 29 
 30 @end
 31 
 32 @implementation ZMDrawingBoardView
 33 
 34 
 35 - (instancetype)initWithFrame:(CGRect)frame
 36 {
 37     self = [super initWithFrame:frame];
 38     if (self) {
 39         [self initSubView];
 40         self.backgroundColor = [UIColor whiteColor];
 41         self.toucheArray = [NSMutableArray array];
 42     }
 43     return self;
 44 }
 45 
 46 /** 绘制画板 */
 47 - (void)drawRect:(CGRect)rect {
 48     // 获得上下文
 49     CGContextRef context = UIGraphicsGetCurrentContext();
 50     // 设置样式
 51     CGContextSetLineCap(context, kCGLineCapSquare);
 52     // 设置宽度
 53     CGContextSetLineWidth(context, 5.0);
 54     // 设置颜色
 55     CGContextSetStrokeColorWithColor(context, [[UIColor redColor] CGColor]);
 56     
 57     for (ZMLineModel *line in self.toucheArray) {
 58         // 开始绘制
 59         CGContextBeginPath(context);
 60         // 移动画笔到起点
 61         CGContextMoveToPoint(context, line.begin.x, line.begin.y);
 62         // 添加下一点
 63         CGContextAddLineToPoint(context, line.end.x, line.end.y);
 64         // 绘制完成
 65         CGContextStrokePath(context);
 66     }
 67 }
 68 
 69 /** 划线开始 */
 70 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
 71 {
 72     // 标记开始撤销分组
 73     [self.undoManager beginUndoGrouping];
 74     
 75     for (UITouch *touch in touches) {
 76         // 记录起始点
 77         CGPoint locTouch = [touch locationInView:self];
 78         _currentLine = [[ZMLineModel alloc] init];
 79         _currentLine.begin = locTouch;
 80         _currentLine.end = locTouch;
 81     }
 82     
 83 }
 84 
 85 /** 划线移动 */
 86 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
 87 {
 88     for (UITouch *touch in touches) {
 89         // 添加线条
 90         CGPoint locTouch = [touch locationInView:self];
 91         _currentLine.end = locTouch;
 92         [self addLine:_currentLine];
 93         // 当前线条
 94         _currentLine = [[ZMLineModel alloc] init];
 95         _currentLine.begin = locTouch;
 96         _currentLine.end = locTouch;
 97     }
 98 }
 99 
100 /** 划线结束 */
101 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
102 {
103     // 结束标记撤销分组
104     [self.undoManager endUndoGrouping];
105 }
106 
107 /** 添加划线 */
108 - (void)addLine:(ZMLineModel *)line
109 {
110     // 添加划线并重绘画板
111     [self.toucheArray addObject:line];
112     [self setNeedsDisplay];
113     // 注册撤销方法
114     [[self.undoManager prepareWithInvocationTarget:self] removeLine:line];
115 }
116 
117 /** 移除划线 */
118 - (void)removeLine:(ZMLineModel *)line
119 {
120     if ([self.toucheArray containsObject:line]) {
121         // 移除划线并重绘画板
122         [self.toucheArray removeObject:line];
123         [self setNeedsDisplay];
124         // 注册撤销方法
125         [[self.undoManager prepareWithInvocationTarget:self] addLine:line];
126     }
127 }
128 
129 /** 撤销按钮点击响应 */
130 - (void)undoButtonAction:(id)sender {
131     if ([self.undoManager canUndo]) {
132         [self.undoManager undo];
133     }
134 }
135 
136 /** 重做按钮点击响应 */
137 - (void)redoButtonAction:(id)sender {
138     if ([self.undoManager canRedo]) {
139         [self.undoManager redo];
140     }
141 }
142 
143 /** 初始化子控件 */
144 - (void)initSubView {
145     // 撤销按钮
146     UIButton *undoButton = [UIButton buttonWithType:UIButtonTypeSystem];
147     undoButton.frame = CGRectMake(0, 64, 70, 50);
148     [undoButton setTitle:@"undo撤销" forState:UIControlStateNormal];
149     [undoButton sizeToFit];
150     [undoButton addTarget:self action:@selector(undoButtonAction:) forControlEvents:UIControlEventTouchUpInside];
151     [self addSubview:undoButton];
152     // 重做按钮
153     UIButton *redoButton = [UIButton buttonWithType:UIButtonTypeSystem];
154     redoButton.frame = CGRectMake(CGRectGetWidth(self.frame)-70, 64, 70, 50);
155     [redoButton setTitle:@"redo重做" forState:UIControlStateNormal];
156     [redoButton sizeToFit];
157     [redoButton addTarget:self action:@selector(redoButtonAction:) forControlEvents:UIControlEventTouchUpInside];
158     [self addSubview:redoButton];
159 }
160 
161 @end

2.8 自定义快捷键

自定义快捷键需要注意两点:

  1. 设置对象成为第一响应者(UIViewController,AppDelegate中不需要设置)
  2. 重写keyCommands返回快捷命令组合
//  ZMKeyCommandView.m

#import "ZMKeyCommandView.h"

@implementation ZMKeyCommandView

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // 设置成为第一响应者
        [self becomeFirstResponder];
    }
    return self;
}

/** 允许对象成为第一响应者 */
- (BOOL)canBecomeFirstResponder {
    return YES;
}

/** 返回快捷命令数组 */
-(NSArray<UIKeyCommand *> *)keyCommands {
    return @[
             [UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:UIKeyModifierShift action:@selector(pressedShiftAndEscapeKey:) discoverabilityTitle:@"自定义[Shift+Esc]快捷键"],
             [UIKeyCommand keyCommandWithInput:@"a" modifierFlags:UIKeyModifierShift action:@selector(pressedShiftAndAKey:) discoverabilityTitle:@"自定义[Shift+A]快捷键"]
             ];
}

/** Shift+Esc快捷命令响应 */
-(void)pressedShiftAndEscapeKey:(UIKeyCommand *)keyCommand {
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:keyCommand.discoverabilityTitle message:[NSString stringWithFormat:@"按下快捷辅键:[%@]", keyCommand.input] delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];
    [alertView show];
}

/** Shift+A快捷命令响应 */
-(void)pressedShiftAndAKey:(UIKeyCommand *)keyCommand {
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:keyCommand.discoverabilityTitle message:[NSString stringWithFormat:@"按下快捷辅键:[%@]", keyCommand.input] delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];
    [alertView show];
}

@end

2.9 自定义UITextField输入键盘

 1 //  ZMCustomInputView.m
 2 
 3 #import "ZMCustomInputView.h"
 4 
 5 #define MAIN_SCREEN_WIDTH [[UIScreen mainScreen] bounds].size.width   //!< 屏幕的Width
 6 
 7 @interface ZMCustomInputView ()
 8 
 9 @property (nonatomic, strong) UITextField *textField;
10 @property (nonatomic, strong) UIView *customInputView;
11 @property (nonatomic, strong) UIToolbar *customAccessoryView;
12 
13 @end
14 
15 @implementation ZMCustomInputView
16 
17 - (instancetype)initWithFrame:(CGRect)frame
18 {
19     self = [super initWithFrame:frame];
20     if (self) {
21         // 添加TextField
22         [self addSubview:self.textField];
23     }
24     return self;
25 }
26 
27 /** 懒加载textField */
28 - (UITextField *)textField {
29     if (!_textField) {
30         // 初始化textField
31         _textField = [[UITextField alloc]initWithFrame:CGRectMake(50, 100, MAIN_SCREEN_WIDTH - 100, 30)];
32         _textField.borderStyle = UITextBorderStyleRoundedRect;
33         _textField.placeholder = @"测试";
34         // 设置自定义键盘View
35         _textField.inputView = self.customInputView;
36         _textField.inputAccessoryView = self.customAccessoryView;
37     }
38     return _textField;
39 }
40 
41 /** 懒加载customInputView */
42 - (UIView *)customInputView {
43     if (!_customInputView) {
44         _customInputView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, MAIN_SCREEN_WIDTH, 220)];
45         _customInputView.backgroundColor = [UIColor lightGrayColor];
46         UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(0, 100, MAIN_SCREEN_WIDTH, 40)];
47         label.textAlignment = NSTextAlignmentCenter;
48         label.text = @"自定义inputView";
49         [_customInputView addSubview:label];
50     }
51     return _customInputView;
52 }
53 
54 /** 懒加载customAccessoryView */
55 - (UIToolbar *)customAccessoryView {
56     if (!_customAccessoryView) {
57         _customAccessoryView = [[UIToolbar alloc]initWithFrame:CGRectMake(0, 0, MAIN_SCREEN_WIDTH, 40)];
58         _customAccessoryView.barTintColor = [UIColor orangeColor];
59         UIBarButtonItem *space = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
60         UIBarButtonItem *done = [[UIBarButtonItem alloc]initWithTitle:@"完成" style:UIBarButtonItemStyleDone target:self action:@selector(done)];
61         [_customAccessoryView setItems:@[space, space, done]];
62     }
63     return _customAccessoryView;
64 }
65 
66 /** 响应完成按钮 */
67 - (void)done {
68     [self.textField resignFirstResponder];
69 }
70 
71 
72 @end