本文轉載自: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獲取下一個響應者,響應者順序為:
/** * 查找視圖的視圖控制器 * * @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 遠程式控制制方法,一般用於耳機線控
耳機線控要註意三點要素:
- 啟動接受遠程事件:[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
- 設置成為第一響應者(UIViewController,AppDelegate中不需要設置)
- 獲取音頻的控制權
// 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添加長按菜單需要註意幾點:
- 啟用用戶交互:self.userInteractionEnabled = YES;
- 在顯示菜單之前設置對象成為第一響應者(UIViewController,AppDelegate中不需要設置)
- 返回菜單需要顯示的按鈕,並重寫實現對應方法
- 註冊長按手勢,顯示菜單
// 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實現畫板撤銷/重做功能
實現撤銷/重做註意以下幾點:
- 在調用方法時需要添加註冊一個對應的撤銷方法
- 撤銷/ 重做只需要調用undoManager中的相應方法即可
- 如果需要多個動作一起撤銷則需要標記分組
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