"满嘴大局观,实则不干活,还能把锅甩得极其自然。"
这是我 App 里"优雅吸血鬼"这个人格的 slogan。用户做完测试看到这句话,要么笑出声,要么沉默三秒然后截图发群里 @ 同事。
这个项目叫 NMTI,是个人格测试工具,但跟市面上那些正经 MBTI 测试不一样——题目全是职场场景,结果文案走毒舌路线,海报天然适合社交分享。后来又加了一套学生版题包,同样的人格换了一套完全不同的描述。于是问题来了:16 个人格 × 2 个场景 = 32 套文案,模型层怎么组织才不会写着写着想删项目?
四维度计分:为什么没用 MBTI 原版四轴
我自定义了 C/S/E/A 四个维度:
- C(Competence):能力自信程度
- S(Strategy):策略规划倾向
- E(Energy):社交能量输出
- A(Accountability):责任承担意愿
每个维度按得分高低取 +/- ,四个维度组合出 16 种编码(PRED、VAMP、WORK、GOS 等)。
为什么不直接用 E/I、S/N、T/F、J/P?因为原版四轴是为了描述认知功能,而我的产品定位是职场/校园场景下的行为倾向。用"你在项目着火时会不会主动接锅"来区分人,比"你偏好直觉还是感知"更直观、更好出毒舌文案。
struct DimensionScores: Codable, Equatable {
let c: Int
let s: Int
let e: Int
let a: Int
}
struct ArchiveEntry: Identifiable, Equatable {
let id: UUID
var nickname: String
let personalityCode: String
let scores: DimensionScores
let date: Date
let pack: QuizPack
}
ArchiveEntry 是一次测试存档。pack 标记这次测试用的是哪套题包——这个字段是 v1.1 才加的,后面会说怎么做兼容。
一个 Personality,两张面孔
产品需求很明确:同一个"六边形悍匪"(PRED),职场版描述是"推演到第七步容错方案",学生版描述是"验证自己的错误率收敛曲线"——说的都是同一种人格特质(极度理性的计划控),但语境完全不同。
最初想过用 protocol 多态,给 WorkplacePersonality 和 StudentPersonality 各自实现一套。但 16 × 2 = 32 个具体类型,光 Swift 文件就要建一堆,改个错别字得翻半天。
最终方案是:Personality 只有一份实例(持有 id、title、配图、人格关系等跨场景共享的属性),文案部分通过一个方法按 pack 分发:
struct PersonalityCopy {
let slogan: String
let tagline: String
let fullDesc: String
}
extension Personality {
func copy(for pack: QuizPack) -> PersonalityCopy {
switch pack {
case .workplace:
return PersonalityCopy(slogan: slogan, tagline: tagline, fullDesc: fullDesc)
case .student:
return Self.studentCopy[id]
?? PersonalityCopy(slogan: slogan, tagline: tagline, fullDesc: fullDesc)
}
}
}
学生版文案存在一个 [String: PersonalityCopy] 字典里,key 是人格 id。如果某个人格的学生版还没写完,fallback 到职场版。这样我可以一个一个慢慢补,不用 32 套全到位才能发版。
这个方案的好处是 UI 层完全不需要关心当前是哪个 pack,只管调 personality.copy(for: entry.pack) 就行,结果页、海报页、历史列表页共用同一个接口。
存档兼容:一行代码避免一星差评
v1.0 只有职场版,ArchiveEntry 里没有 pack 字段。v1.1 加了学生版,老用户升级后如果 decode 存档失败就会丢数据。我之前另一个项目就栽在这上面过,用户升级后历史记录全没了,直接被打一星。
这次学乖了,自定义 init(from:) 里用 decodeIfPresent:
pack = try c.decodeIfPresent(QuizPack.self, forKey: .pack) ?? .workplace
一行代码的事,但它决定了老用户升不升级后会不会骂你。我现在的习惯是:任何 Codable 模型新增字段一律 optional + 默认值,哪怕当前只有一个可能的值。
互测功能:为什么用了 URL scheme 而不是 Universal Links
"测朋友"功能的流程是:A 生成测试链接发给 B,B 完成测试后结果通过链接参数回传,双方各看到一个"相处指南"。
技术上我用的是 URL scheme 传参,没有后端。这个方案有个很明显的问题:对方没装 App 时 URL scheme 直接打不开,用户看到的是一个白屏或者报错。
Universal Links 能解决这个问题(未装 App 时跳 App Store),但它需要你有一个配置了 apple-app-site-association 文件的域名,还要处理 HTTPS 证书、CDN 缓存等一堆事。对于一个 v1.0 的 side project,我觉得先用 URL scheme 跑起来验证需求更实际。如果后面真有用户抱怨这个断裂的体验,再花一天配 Universal Links 不迟。
独立开发的取舍就是这样:不是不知道最优解是什么,是当前阶段验证需求比架构完美更重要。
文案才是真正的工期杀手
技术实现说实话没什么特别难的——SwiftUI + Codable + UserDefaults 存存档,标准的单机 App 架构。
真正耗时间的是写 32 套人格描述。每套包含 slogan(一句话毒舌)、tagline(约 100 字海报用)、fullDesc(400-500 字结果页用)。一个人写了差不多两周,中间反复调措辞,要保证每个人格的尖锐程度差不多,又不能真的冒犯人。
比如"金牌老黄牛"(WORK,C+S+E-A+,能力强、有策略、不爱社交但责任心重)的 fullDesc 里有一句:
"有人问你累不累,你说还好。因为你也不知道如果不做这些,自己还能是什么样的人。"
测试期间有朋友说看完沉默了一会儿。好的测试结果不只是搞笑,得让人笑完之后微微被扎一下,这样他才会截图分享。
现状
App 刚上线不久,数据还在冷启动阶段。测试类产品天然依赖社交裂变,没有第一批人分享海报,后面的飞轮就转不起来。目前在优化海报生成的视觉效果,想让分享出去的图本身就有传播力。
最后抛个问题:你们做"同一实体、多套文案/多语言/多场景"这种需求时,是倾向于用字典查表(像我这样),还是用 protocol + 泛型让编译器帮你检查完整性?我用字典的代价是某个 key 拼错了只有运行时才能发现,有点不踏实,但胜在加新场景时不用动已有代码。想听听大家的选择。