You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 08:02:49