OS X Objective-C实现NSTextField/NSTextView文本拖拽拆分与排序
在OS X上用Objective-C实现文本拖拽排序与拆分拖拽功能
我经常处理这类OS X上的文本交互需求,这两个功能其实都可以基于NSTextView来实现(毕竟NSTextField的自定义能力有限,推荐用NSTextView更灵活),下面给你详细的实现方案:
一、文本框内拖拽文本调整顺序
这个功能核心是让NSTextView同时作为拖拽源和拖拽目标,实现选中文本的拖拽移动。
步骤1:配置拖拽支持
在你的ViewController的viewDidLoad里,先给NSTextView注册拖拽类型,并设置代理(或者让ViewController实现对应的协议):
- (void)viewDidLoad { [super viewDidLoad]; // 注册支持的拖拽类型 [self.textView registerForDraggedTypes:@[NSStringPboardType]]; // 设置拖拽源和目标的代理 self.textView.draggingSourceDelegate = self; self.textView.draggingDestinationDelegate = self; }
步骤2:实现拖拽源协议(NSDraggingSource)
主要是提供拖拽的数据,以及处理拖拽开始/结束的状态:
// 定义两个属性存储拖拽的文本和范围 @property (nonatomic, assign) NSRange draggedRange; @property (nonatomic, copy) NSString *draggedText; // 提供拖拽操作类型 - (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context { return NSDragOperationMove; } // 拖拽开始时记录选中内容 - (void)draggedImage:(NSImage *)image beganAt:(NSPoint)screenPoint { self.draggedRange = self.textView.selectedRange; self.draggedText = [self.textView.string substringWithRange:self.draggedRange]; }
步骤3:实现拖拽目标协议(NSDraggingDestination)
处理拖拽进入、放下的逻辑,把文本插入到新位置:
- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)sender { // 验证拖拽内容是文本类型 return [[sender draggingPasteboard] containsType:NSStringPboardType]; } - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender { NSPasteboard *pasteboard = [sender draggingPasteboard]; NSString *draggedText = [pasteboard stringForType:NSStringPboardType]; // 获取拖拽放下位置对应的文本索引 NSPoint mousePoint = [self.textView convertPoint:[sender draggingLocation] fromView:nil]; NSInteger insertionPoint = [self.textView characterIndexForPoint:mousePoint]; // 避免拖拽到原选中范围内部,防止重复插入 if (NSLocationInRange(insertionPoint, self.draggedRange)) { return YES; } // 构建新文本:先删除原选中内容,再插入到新位置 NSMutableString *newString = [self.textView.string mutableCopy]; [newString deleteCharactersInRange:self.draggedRange]; // 调整插入点(如果插入点在原范围之后,要减去原文本长度) if (insertionPoint > self.draggedRange.location) { insertionPoint -= self.draggedRange.length; } [newString insertString:draggedText atIndex:insertionPoint]; self.textView.string = newString; // 选中刚插入的文本,提升交互体验 self.textView.selectedRange = NSMakeRange(insertionPoint, draggedText.length); return YES; }
二、文本拆分及拖拽功能(参考系统日期设置示例)
这个功能是把文本拆分成多个独立的可拖拽单元(比如日期格式里的yyyy、MM、dd),每个单元可以单独拖拽调整顺序,核心是识别文本中的可拖拽单元,然后实现单元级的拖拽。
步骤1:定义可拖拽单元的规则
首先你需要定义哪些文本片段是可拖拽的单元,比如用正则表达式匹配日期格式的单元:
- (NSArray<NSValue *> *)parseDraggableUnitsFromString:(NSString *)string { NSMutableArray *units = [NSMutableArray array]; // 匹配字母单元(如yyyy、MM)和非字母分隔符(如/、-) NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"([\\w]+)|([^\\w]+)" options:0 error:nil]; NSArray *matches = [regex matchesInString:string options:0 range:NSMakeRange(0, string.length)]; for (NSTextCheckingResult *match in matches) { NSRange range = match.range; if (range.length > 0) { [units addObject:[NSValue valueWithRange:range]]; } } return units; }
步骤2:自定义NSTextView实现单元拖拽
创建一个自定义的DraggableUnitTextView,继承自NSTextView,重写拖拽相关方法,同时在绘制时高亮可拖拽单元:
高亮可拖拽单元
- (void)drawRect:(NSRect)dirtyRect { [super drawRect:dirtyRect]; // 给每个可拖拽单元绘制浅灰色背景 NSArray *unitRanges = [self parseDraggableUnitsFromString:self.string]; for (NSValue *rangeValue in unitRanges) { NSRange range = rangeValue.rangeValue; NSRect unitRect = [self rectForCharacterRange:range]; [[NSColor colorWithCalibratedWhite:0.9 alpha:1.0] setFill]; NSRectFill(unitRect); } }
处理单元点击与拖拽源逻辑
- (void)mouseDown:(NSEvent *)event { NSPoint point = [self convertPoint:event.locationInWindow fromView:nil]; NSInteger charIndex = [self characterIndexForPoint:point]; // 点击时自动选中对应的单元 NSArray *unitRanges = [self parseDraggableUnitsFromString:self.string]; for (NSValue *rangeValue in unitRanges) { NSRange range = rangeValue.rangeValue; if (NSLocationInRange(charIndex, range)) { self.selectedRange = range; break; } } [super mouseDown:event]; } // 只允许拖拽选中的单元 - (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context { return self.selectedRange.length > 0 ? NSDragOperationMove : NSDragOperationNone; } // 拖拽开始时传递单元文本和原范围 - (void)draggedImage:(NSImage *)image beganAt:(NSPoint)screenPoint { NSPasteboard *pasteboard = [NSPasteboard pasteboardWithName:NSDragPboard]; [pasteboard declareTypes:@[NSStringPboardType, @"OriginalUnitRange"] owner:self]; [pasteboard setString:[self.string substringWithRange:self.selectedRange] forType:NSStringPboardType]; [pasteboard setPropertyList:[NSValue valueWithRange:self.selectedRange] forType:@"OriginalUnitRange"]; }
处理拖拽放下逻辑
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender { NSPasteboard *pasteboard = [sender draggingPasteboard]; NSString *unitText = [pasteboard stringForType:NSStringPboardType]; NSRange originalRange = [[pasteboard propertyListForType:@"OriginalUnitRange"] rangeValue]; // 获取插入位置对应的单元索引 NSPoint mousePoint = [self convertPoint:[sender draggingLocation] fromView:nil]; NSInteger insertionIndex = [self characterIndexForPoint:mousePoint]; NSArray *unitRanges = [self parseDraggableUnitsFromString:self.string]; NSInteger targetUnitIndex = NSNotFound; for (NSInteger i = 0; i < unitRanges.count; i++) { NSRange range = [unitRanges[i] rangeValue]; if (insertionIndex <= range.location + range.length) { targetUnitIndex = i; break; } } if (targetUnitIndex == NSNotFound) { targetUnitIndex = unitRanges.count; } // 重新排列单元数组 NSMutableArray *unitTexts = [NSMutableArray array]; for (NSValue *rangeValue in unitRanges) { NSRange range = rangeValue.rangeValue; [unitTexts addObject:[self.string substringWithRange:range]]; } // 移除原单元,插入到新位置 NSInteger originalUnitIndex = [unitRanges indexOfObjectPassingTest:^BOOL(NSValue * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { return NSEqualRanges(obj.rangeValue, originalRange); }]; [unitTexts removeObjectAtIndex:originalUnitIndex]; [unitTexts insertObject:unitText atIndex:targetUnitIndex]; // 拼接成新字符串并更新 self.string = [unitTexts componentsJoinedByString:@""]; return YES; }
小贴士
- 如果一定要用
NSTextField,可以把它的cellClass设置为自定义的NSTextFieldCell,但灵活性远不如NSTextView,更推荐用NSTextView。 - 拖拽时的预览效果可以通过自定义
NSImage优化,让用户更直观看到拖拽的内容。
内容的提问来源于stack exchange,提问作者Shiv Pogra
相关产品推荐
相关产品推荐

