SwiftUI 开发实战:构建一个 macOS 剪贴板意图识别工具(附完整源码)
一、项目背景在日常开发中复制粘贴是最频繁的操作之一。但从不同来源复制的内容往往带有格式差异网页富文本、CJK 字符空格、多余的换行符。更麻烦的是复制一个 URL、邮箱或颜色值后我们通常需要手动执行下一步操作——打开浏览器、写邮件、查看颜色预览。PurePaste 是一个 macOS 菜单栏应用解决了上述两个问题纯文本模式自动剥离格式清理多余空白PasteFlow 模式识别复制内容的类型弹出对应操作项目使用 SwiftUI AppKit 原生开发零第三方依赖。完整开源MIT 协议。二、架构总览项目包含 12 个核心源文件按职责分层PurePaste/ ├── PurePasteApp.swift # MenuBarExtra 入口 ├── PurePasteViewModel.swift # 剪贴板轮询 状态管理 ├── ContentDetector.swift # 11 种类型识别引擎 ├── ActionExecutor.swift # 操作分发执行 ├── FloatingPanelView.swift # 浮动面板 UI ├── FloatingPanelController.swift # NSWindow 管理器 ├── TextProcessor.swift # 文本处理 / HTML↔Markdown ├── HistoryEntry.swift # 历史数据模型 ├── HistoryStore.swift # JSON 存储 筛选 ├── HistoryView.swift # 历史窗口 UI ├── HistoryWindowController.swift # 历史窗口管理 └── Localization.swift # 中英双语支持三、核心技术实现3.1 剪贴板监听与防死循环基于NSPasteboard.changeCount轮询每 0.5 秒检测变化MainActorfinalclassPurePasteViewModel:ObservableObject{privatevarmonitorTimer:Timer?privatevarlastChangeCount:Int0privatevarinternalWriteFlag:BoolfalsefuncstartMonitoring(){monitorTimerTimer.scheduledTimer(withTimeInterval:0.5,repeats:true){[weakself]_inTask{MainActor[weakself]inself?.checkClipboard()}}RunLoop.current.add(monitorTimer!,forMode:.common)}privatefunccheckClipboard(){// 双重防死循环guard!internalWriteFlagelse{return}letpasteboardNSPasteboard.generalletcurrentChangeCountpasteboard.changeCountguardcurrentChangeCount!lastChangeCountelse{return}lastChangeCountcurrentChangeCount// 读、处理、写...}}关键设计internalWriteFlag标记自写入 lastChangeCount跳过自触发。两者缺一不可。3.2 内容识别引擎ContentDetector采用优先级链设计从精确度最高的匹配器开始依次检测staticfuncdetect(_text:String,hasHTML:Bool,htmlData:Data?)-DetectedContent?{ifletresultdetectColor(trimmed){returnresult}// #RGBifletresultdetectIP(trimmed){returnresult}// IPv4ifletresultdetectEmail(trimmed){returnresult}// xx.xifletresultdetectImageURL(trimmed){returnresult}// .png.jpgifletresultdetectURL(trimmed){returnresult}// http(s)ifletresultdetectPhone(trimmed){returnresult}// 1xx...ifletresultdetectTracking(trimmed){returnresult}// SF/YT...ifletresultdetectMathExpression(trimmed){returnresult}ifletresultdetectGeoCoordinate(trimmed){returnresult}ifletresultdetectDatetime(trimmed){returnresult}ifhasHTML{return.richHTML(...)}// 二次检测ASCII clean 重试处理网页不可见字符ifletresultdetectMathExpression(asciiClean){returnresult}ifletresultdetectAddress(trimmed){returnresult}returnnil}每种类型都是独立的检测函数返回DetectedContent枚举携带解析后的结构化数据。3.3 递归下降数学解析器业界常见做法是用NSExpression做数学求值但它会误解%为格式字符串。笔者手写了一个完整的递归下降解析器// 语法结构Expr→Term((|-)Term)*Term→Power((*|/|%)Power)*Power→Unary(^Unary)?Unary→(|-)?AtomAtom→Number|(Expr)支持 - * / % ^ ()及一元负号运算符优先级正确。完整的 tokenizer parser 代码约 150 行在 ContentDetector.swift 中。3.4 浮动操作面板使用NSWindow的.nonactivatingPanel样式实现letwindowNSWindow(contentRect:NSRect(x:0,y:0,width:280,height:200),styleMask:[.borderless,.nonactivatingPanel],backing:.buffered,defer:false)window.level.floating window.collectionBehavior[.canJoinAllSpaces,.ignoresCycle]window.contentViewNSHostingView(rootView:FloatingPanelView(...))// 定位到鼠标旁letpanelFramecalculatePanelFrame(screenPoint:NSEvent.mouseLocation,panelSize:window.frame.size)window.setFrame(panelFrame,display:true)面板在鼠标右下弹出不抢夺焦点ESC 或点击外部关闭。3.5 意图历史存储数据模型structHistoryEntry:Codable,Identifiable{letid:UUIDlettext:Stringlettimestamp:Dateletmode:String// plainText / pasteFlowletdetectedType:String?// 识别类型名letaction:String?// 用户执行的操作letisIntentFulfilled:Bool}存储到~/Library/Application Support/PurePaste/history.json最大 5000 条FIFO 淘汰。内存常驻用于实时筛选。3.6 中英双语通过L10n枚举管理所有文案自动检测系统语言也支持手动切换enumL10n{staticvarisChinese:Bool{switchLanguage(rawValue:preferredLanguage)??.auto{case.chinese:returntruecase.english:returnfalsecase.auto:returnLocale.preferredLanguages.first?.hasPrefix(zh)??false}}}四、运行效果PasteFlow 智能识别复制 URL鼠标旁弹出「浏览器打开」复制颜色值面板内展示大色块 HEX/RGB复制数学表达式自动计算意图历史按类型、模式、关键词实时筛选五、踩坑记录NSApp在App.init()中为 nil改用NSApplicationDelegateAdaptor在applicationDidFinishLaunching中初始化SMAppService 在 Debug 模式下报错加canManageLoginItem守卫检查 App 是否在/Applications/下NSExpression的%陷阱NSExpression(format: 10%3)会把%3当作格式字符串 → 手写递归下降解析器com.apple.linkd.autoShortcut错误macOS 26 beta 系统级 daemon 问题对 App 无影响Off-by-onetokenizer 末尾.end哨兵导致pos tokens.count总是 false数学检测全程返回 nil六、编译与安装gitclone https://github.com/xiaoyunchengzhu/PurePaste.gitcdPurePasteopenPurePaste.xcodeproj# CmdR 运行macOS 14.0Xcode 15.0Swift 5.0。七、总结PurePaste 是一个麻雀虽小五脏俱全的 macOS 开发范例涵盖了MenuBarExtra 生命周期管理NSPasteboard 底层操作正则匹配 递归下降解析器NSWindow 自定义样式SwiftUI AppKit 混合编程JSON 持久化 内存过滤多语言本地化源码完整开源注释中文适合 SwiftUI 开发者学习参考。 GitHub: github.com/xiaoyunchengzhu/PurePaste如果觉得有帮助给个 Star ⭐