如果一个工程里几十个文件的开头都自动带着“Created by 某用户 on 2024-xx-xx”和“Copyright (c) 2024 CompanyName”这种默认注释而团队其实想换成统一格式的公司版权头这个需求可能经常被当成“小事一桩”。我自己折腾过一次之后发现这背后涉及Xcode模板引擎的占位符机制、系统模板目录权限、升级覆盖问题还有历史文件的批量替换。这篇文章就把整个思路和实操完整盘一遍适合正在搭团队规范、想统一代码头注释的开发者参考。1. 源头Xcode新建文件时的默认注释到底从哪来的1.1 你看到的这段注释是模板引擎自动拼接的不少人以为默认注释是Xcode写死在程序里的其实不是。Xcode在新建一个类文件时会调用底层的文件模板引擎从一个模板目录里读取文件骨架再把各种占位符替换成当前信息最终呈现到编辑器里。这套机制跟IDE的“文件向导”是一体的所以它不只是生成Swift文件还包括Objective-C的.h/.m、XIB、Core Data模型等一堆模板。默认注释长这样// // ViewController.swift // Demo // // Created by zhangsan on 2024/6/1. // Copyright (c) 2024 CompanyName. All rights reserved. //这里每一行都不是凭空生成的。文件名来自___FILEBASENAME___工程名来自___PROJECTNAME___用户名来自___USERNAME___日期来自___DATE___版权声明来自___COPYRIGHT___或者由___ORGANIZATIONNAME___组合而来。模板文件里写了哪个占位符Xcode就把它展开成对应内容。1.2 占位符展开之后的长什么样先把常用的占位符列个表后面改模板的时候你会反复用到这些名字占位符展开结果说明___FILEBASENAME___LoginViewController当前类名不带扩展名___PROJECTNAME___Demo当前Xcode工程名___USERNAME___zhangsan当前系统的登录用户名___FULLUSERNAME___Zhang San当前系统用户全名___DATE___2024/6/1创建日期___YEAR___2024当前年份___ORGANIZATIONNAME___CompanyName在Xcode偏好设置里填的组织名___COPYRIGHT___Copyright (c) 2024 CompanyName版权组合串这些占位符在不同的Xcode版本里可能会有细微差异但主流版本基本都覆盖了。你不需要全部记住只要知道模板文件里出现双下划线包裹的大写单词就是一个待替换变量。1.3 动手前先搞清楚自己的Xcode版本不同Xcode版本里模板文件的组织方式不太一样所以我建议拿到电脑后先看一眼实际目录别按网上老教程的路径盲目操作。打开终端进入Xcode应用包里的模板目录cd /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File\ Templates/Source ls -la你会看到一堆.xctemplate目录比如Cocoa Touch Class.xctemplate、Cocoa Touch File.xctemplate等。新建ViewController用到的就是Cocoa Touch Class.xctemplate这是最需要我们改的那一个。进入目录后常见结构是Swift和Objective-C两个子文件夹里面放着对应语言的模板文件比如___FILEBASENAME___.swift、___FILEBASENAME___.h、___FILEBASENAME___.m外层还有一个TemplateInfo.plist控制整个模板的选项和定义。2. 定制模板的完整实操两种路径的不同适用场景2.1 方案一修改系统模板简单直接但会被升级覆盖这个方案的优点是路径固定Xcode新建文件时直接用不需要额外配置。缺点是直接改/Applications下面的文件需要管理员权限而且每次Xcode更新都可能把模板目录重置回默认。操作思路先备份再修改最后验证。先在桌面备份一份cd /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File\ Templates/Source cp -R Cocoa Touch Class.xctemplate ~/Desktop/CocoaTouchClassBackup.xctemplate然后打开模板里的Swift类模板文件nano Cocoa Touch Class.xctemplate/Swift/___FILEBASENAME___.swift如果你想避开nano也可以用Xcode或VS Code打开这个纯文本文件。文件内容大致是// // ___FILEBASENAME___ // ___PROJECTNAME___ // // Created by ___USERNAME___ on ___DATE___. // Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. //把第一块连续注释改成你想要的团队文案比如// // ___FILEBASENAME___ // ___PROJECTNAME___ // // 负责模块基础组件 // 创建人移动端架构组 // Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. //保存后关掉Xcode重新打开新建一个类文件观察头部注释有没有变化。正常情况下占位符会被自动展开成当前项目的信息。2.2 方案二用户级自定义模板团队推荐的路径系统模板的痛点在于升级覆盖和权限问题所以很多团队选择把自定义模板放到用户级目录。这个目录同样会被Xcode识别而且出现在新建文件向导的“Custom”分组里。创建自定义模板需要自己维护一套最小文件结构。在终端执行mkdir -p ~/Library/Developer/Xcode/Templates/File\ Templates/Source/TeamBaseClass.xctemplate/Swift然后在Swift子目录里创建模板文件___FILEBASENAME___.swift// // ___FILEBASENAME___ // ___PROJECTNAME___ // // 负责人移动端基础组 // 创建日期___DATE___ // Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. //同时在这一层还需要一个TemplateInfo.plistXcode靠它识别模板名称和选项。一个最简的plist可以是?xml version1.0 encodingUTF-8? !DOCTYPE plist PUBLIC -//Apple//DTD PLIST 1.0//EN http://www.apple.com/DTDs/PropertyList-1.0.dtd plist version1.0 dict keyKind/key stringXcode.IDEFoundation.TextSubstitutionFileTemplateKind/string keyDescription/key string团队基础类模板/string keyOptions/key array dict keyIdentifier/key stringproductName/string keyDescription/key string类名/string keyName/key string类名/string keyRequired/key true/ keyType/key stringtext/string keyNotPersisted/key true/ /dict /array /dict /plist把整个TeamBaseClass.xctemplate放到~/Library/Developer/Xcode/Templates/File Templates/Source/下重启XcodeCmdN新建文件时就能看到它出现在Custom分组里。好处是Xcode升级不会碰用户目录换机器也容易同步团队可以直接把这个模板目录塞进Git仓库。2.3 顺带一提Xcode设置里的Organization也能缓解问题如果只是嫌“CompanyName”这一串太丑可以打开Xcode的设置页在“Text Editing”里找到“Organization”把它改成团队名。这样___ORGANIZATIONNAME___展开时就会用新名字。这个方法只能解决版权行里的组织名解决不了“Created by 用户名”这种个人签名所以作为临时救急可以长期规范还是得靠模板。3. 我把默认头改成团队版权模板的过程3.1 先看自己的plist长什么样真正动刀之前先看看自己机器上的模板文件和网上教程是不是一致。打开TemplateInfo.plistplutil -p Cocoa Touch Class.xctemplate/TemplateInfo.plist你会看到整个模板的选项结构包括Description、Options、Nodes等字段。搜索关键字FILEBASENAME、USERNAME确认当前版本用的占位符名称。有些版本的头部注释不是写在模板文件里而是写在plist的某个字段拼接里所以先搜一遍再改。3.2 把三个核心占位符替换成团队文案我当时的目标其实很朴素去掉个人用户名保留日期和版权加上一条团队署名。于是把模板文件里的// Created by ___USERNAME___ on ___DATE___. // Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved.改成了// Created by 移动端架构组 on ___DATE___. // Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved.这里有一个关键点___USERNAME___本身是系统动态取得的如果只是把它从模板里删掉新建文件时就不会再有“Created by 某个人”。但如果你希望保持日期等动态信息就保留___DATE___、___YEAR___这些占位符。3.3 同时调好Objective-C与Swift模板很多工程还在用Objective-C或者混合开发所以只改Swift模板不够。进入同一个.xctemplate目录下的Objective-C子文件夹里面也有___FILEBASENAME___.h和___FILEBASENAME___.m。把开头的注释块改成和Swift模板一致的格式否则新建的OC类文件还是老的个人签名。还有一个容易忽略的点XIB或Storyboard模板有时候也会带作者信息。如果有团队合规需求可以把XIB.xctemplate等目录一并处理。具体哪些目录需要改取决于你们团队是否允许手写XIB。3.4 验证CmdN建个文件看效果改完模板之后一定要实测不能只看文件改了就觉得完事。关掉Xcode重新打开一个Demo工程新建一个TestViewControllertouch /tmp/demo.sh或者直接在Xcode里操作File New File选择Cocoa Touch Class输入类名确认创建。看编辑器顶部的注释是不是团队模板用户名是不是没了日期是不是自动填了。如果没生效不一定是改错了也可能是Xcode缓存了旧的模板。后面专门讲缓存问题。4. 改注释的坑升级被覆盖、权限、缓存、同步问题4.1 Xcode升级把自定义模板重置了怎么办直接改系统模板最大的敌人是Xcode升级。每升一个版本/Applications/Xcode.app都会被新内容覆盖你精心改过的TemplateInfo.plist和模板文件很可能全部还原。所以我在团队里提了一个约定系统级模板只用来临时验证正式统一规范一律走用户级自定义模板。用户级模板目录不受Xcode升级影响因为它不在应用包内部而是位于~/Library/Developer/Xcode/Templates/。这么一改之后至少不用每次Xcode升级完再熬夜改一遍。4.2 修改系统目录时的权限问题直接改/Applications/Xcode.app下面的文件通常需要管理员权限。如果你用图形界面的文本编辑器打开文件并保存可能被系统拒绝。终端里用sudo可以绕过但也要注意别改坏了文件权限导致Xcode读取失败。更稳妥的做法是把要改的文件复制到桌面改好之后再复制回去cp /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File Templates/Source/Cocoa Touch Class.xctemplate/Swift/___FILEBASENAME___.swift ~/Desktop/___FILEBASENAME___.swift # 桌面改完后 sudo cp ~/Desktop/___FILEBASENAME___.swift /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File Templates/Source/Cocoa Touch Class.xctemplate/Swift/___FILEBASENAME___.swift4.3 新建文件没变化检查缓存与进程改了模板重启Xcode之后发现没什么变化先别急着怀疑路径错了。常见原因有三个第一打开文件时Xcode还持有旧的模板缓存。可以退出Xcode再用命令行重新打开killall Xcode 2/dev/null; open -a Xcode第二改错目录。有的模板目录结构和网上教程不完全一致可能你改了.xctemplate里面的TemplateInfo.plist但真正生效的是另一个同名目录里的文件。建议用find全局搜一下find /Applications/Xcode.app -name *Cocoa Touch Class.xctemplate* -maxdepth 10第三新建文件时选的是“Empty File”而不是“Cocoa Touch Class”它会走完全不同的模板路径。注意你说的“源代码顶部注释”通常指的是类模板空文件模板不会自动生成头部注释块。4.4 团队机器怎么保持模板一致如果只是你自己改那很简单。但如果整个团队都要统一代码头注释就得解决“模板怎么分发给每个人”的问题。我的建议是把自定义模板放到项目的Tools/XcodeTemplates目录然后在项目README里写一段安装脚本让每个成员拉完代码后执行一次mkdir -p ~/Library/Developer/Xcode/Templates/File\ Templates/Source cp -R Tools/XcodeTemplates/TeamBaseClass.xctemplate ~/Library/Developer/Xcode/Templates/File\ Templates/Source/如果你连复制都懒得做也可以直接创建软链接保证项目更新即同步ln -s $(pwd)/Tools/XcodeTemplates ~/Library/Developer/Xcode/Templates/File\ Templates/Source注意这条命令依赖当前工作目录不同成员clone项目的路径不同用绝对路径写死更安全。5. 已经生成的历史文件用脚本批量清洗注释头5.1 新文件解决了老文件还在那里模板只能管住“以后新建的文件”项目里已经存在的几百个.swift和.m文件头部还是旧的个人签名。这时候需要一个批量替换脚本。思路很简单遍历目标目录下所有源码文件把文件开头的连续//注释块整体替换成新的模板内容。直接上Python脚本不引入第三方依赖#!/usr/bin/env python3 import pathlib import re import sys NEW_HEADER // // {filename} // {project} // // Created by 移动端架构组 on {date}. // Copyright (c) 2024 YourCompany. All rights reserved. // def replace_header(path): text path.read_text(encodingutf-8) # 匹配文件开头的连续 // 注释行 pattern re.compile(r\A(?://[^\n]*\n)) if not pattern.match(text): return False # 这里只做占位符简单展开实际项目中可动态传入 new_header NEW_HEADER.format(filenamepath.stem, projectYourProject, date2024/1/1) new_text pattern.sub(new_header \n, text, count1) path.write_text(new_text, encodingutf-8) return True def main(): root pathlib.Path(sys.argv[1] if len(sys.argv) 1 else .) suffixes {.swift, .m, .mm, .h} count 0 for p in root.rglob(*): if p.suffix in suffixes and p.is_file(): try: if replace_header(p): count 1 print(fupdated: {p}) except UnicodeDecodeError: print(fskip due to encoding: {p}) print(ftotal: {count}) if __name__ __main__: main()这个脚本有几个粗糙的地方但对付大部分项目够用。执行方式python3 clean_header.py /path/to/your/project/Sources5.2 正则匹配多行注释时的注意事项文件头注释不一定全是//开头也有可能是/* ... */块注释。如果是后者上面的正则会失效。可以额外加一个分支匹配/\*.*?\*/这种块注释但跨行匹配时得注意re.S标志否则匹配不到换行。还有一个经典坑有些文件开头是#import、import UIKit、interface根本没有注释块脚本会自动跳过这没问题。但有些文件的头部注释后面紧接着的是// MARK: -这种说明性注释如果整体替换会把有用的MARK注释也干掉。所以在写脚本时我直接把匹配范围限制在文件“第一个空行之前”的注释块遇到// MARK:另做保护或者干脆用人工抽查代替全自动替换。5.3 处理不同文件类型swift、h、m、mm脚本里用suffixes覆盖了常见源码后缀但不同后缀的头部注释风格其实略有差异。比如.h文件的开头通常还有文档注释///或者#import替换时要保留这些有效信息。稳妥的做法是只替换到#import或import关键字之前不要把后续导入语句一起吞掉。我在实际项目里是按目录分批跑的先跑Sources/UI下的20个Swift文件git diff看一遍再跑下一批。批量替换这种事最重要的不是脚本有多完美而是“可回滚”。脚本跑之前在Git里打好tag出问题一条命令退回。5.4 批量修改前必须做的备份与抽查给所有文件来一刀之前先做两件事第一确认当前工作区是干净的或者已经提交了一次完整备份。如果在Git仓库里操作直接确保git status能看到改动前的版本就够了如果没Git就把目录打个压缩包。第二用--dry-run的思路抽查。我习惯在脚本里加一个开关先打印所有会被匹配的文件不实际写入DRY_RUN True跑完看列表把不符合预期的文件去掉再执行真正的写入。这个过程很枯燥但能避免很多灾难。比如公司有几个文件是历史遗留的加密脚本生成的编码不一定是UTF-8写回时可能乱码必须先排除。最后分享一点小习惯我在本地长期保留一个xcode-templates目录里面同时存着修改后的系统模板备份和用户级自定义模板每次换电脑、换版本、重装系统都能快速恢复。批处理脚本也是放在项目仓库里的下次再遇到历史文件清理不会等到需求来了才临时找代码。如果你也想动这个“小东西”建议先从用户级模板做起改完建一个文件实测再考虑要不要批量处理旧文件。