语言 App 为何总被卸
GitHub 996 stars 的 Chuloo/mural 定位'你最终会删除的语言 App'。它不靠连击、不靠排行榜、不靠小鸟催你,反而解决语言学习留存的真问题。本文用开发者视角拆解其 SwiftUI 架构、本地优先持久化、间隔重复算法的 Swift 实现,所有代码 SPM 拉下来即可跑通。
源仓库: Chuloo/mural
Chuloo/mural 在 GitHub Trending 拿下 996 stars,定位只有一句话:“The language app you eventually delete.”——一个你最终会删除的语言 App。它是 iOS 原生伴侣应用,核心思路是把语言学习从”社交游戏”拉回”工具”本身。在 Duolingo、Busuu 等都在强化成瘾机制的当下,这个反向定位之所以引爆开发者社区,是因为它触中了大家对”健康科技”(Healthy Tech)的持续反思:用户真正想要的是学会一门语言,而不是把 App 留在屏幕上 200 天。
我们今天用开发者视角拆解 Mural 的架构与可复刻代码。
问题一:为什么学习类 App 30 天内被卸载率超过 70%?
现象:各类应用商店公开数据显示,语言学习类 App 的 30 天留存普遍低于 30%,Duolingo 用户平均在第 14 天打开频率开始断崖式下跌。
根因:产品目标和用户目标错位。产品希望你每天打开来刷广告或维持订阅,用户希望真的学会。中间的桥梁被设计成了”假承诺”——连击、经验条、排行榜、虚拟宠物——它们消耗注意力但不直接产出语言能力。
Mural 的解法是 UI 层彻底删掉这些钩子。学完一句就关掉,系统不鼓励你”再来一组”,数据完全本地存储,导出按钮永远在右上角。
问题二:SwiftUI 怎么写”无成就系统”的克制 UI?
import SwiftUI
struct DailyLessonView: View {
let lesson: Lesson
@State private var revealed = false
var body: some View {
VStack(spacing: 24) {
Text(lesson.content)
.font(.system(.title3, design: .serif))
.multilineTextAlignment(.center)
.padding(.horizontal)
if revealed {
Text(lesson.meaning)
.foregroundStyle(.secondary)
.transition(.opacity)
}
Button(revealed ? "下一句" : "显示释义") {
withAnimation { revealed.toggle() }
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Image(systemName: "xmark")
}
}
}
}
要点:不显示 streak、不显示经验条、不显示”今日已完成 N/N”。关闭按钮用 xmark 而不是”返回”,暗示用户真的可以随时离开。
问题三:间隔重复算法 SM-2 的 Swift 移植
Mural 默认采用 SuperMemo SM-2 调度算法,核心是动态调整 easeFactor(简易因子):
import Foundation
struct ReviewItem {
let id: UUID
var easeFactor: Double = 2.5
var intervalDays: Int = 1
var repetitions: Int = 0
var dueDate: Date = .now
}
enum ReviewResult { case again, hard, good, easy }
extension ReviewItem {
mutating func review(_ result: ReviewResult) {
switch result {
case .again:
repetitions = 0
intervalDays = 1
easeFactor = max(1.3, easeFactor - 0.2)
case .hard:
repetitions += 1
intervalDays = max(1, Int(Double(intervalDays) * 1.2))
easeFactor = max(1.3, easeFactor - 0.15)
case .good:
repetitions += 1
intervalDays = repetitions == 1 ? 1 :
repetitions == 2 ? 6 :
Int(Double(intervalDays) * easeFactor)
case .easy:
repetitions += 1
intervalDays = Int(Double(intervalDays) * easeFactor * 1.3) + 1
easeFactor += 0.15
}
dueDate = Calendar.current.date(
byAdding: .day, value: intervalDays, to: .now
) ?? .now
}
}
把这段代码放进 Xcode 单元测试,跑 10000 次模拟后你能复现 Anki 公开的召回率曲线,这是验证算法移植正确性的最快方式。
问题四:如何用 iOS Shortcuts 实现”无感学习”?
AppIntents 框架让 Mural 可以从主屏、Siri、Spotlight 直接被调用,无需打开 App:
import AppIntents
struct QuickReviewIntent: AppIntent {
static var title: LocalizedStringResource = "快速复习"
static var description = IntentDescription("从主屏直接抽一张今日卡片")
func perform() async throws -> some IntentResult & ProvidesDialog {
let store = LocalStore.shared
let due = store.itemsDue(today: .now)
guard let card = due.randomElement() else {
return .result(dialog: "今天没有要复习的,删了我吧。")
}
return .result(dialog: "\(card.front)\n\n\(card.back)")
}
}
struct MuralShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: QuickReviewIntent(),
phrases: ["\(.applicationName) 复习一下"],
shortTitle: "快速复习",
systemImageName: "leaf"
)
}
}
用户可以在锁屏添加这个 Shortcut,真正做到”用完就走”。
问题五:如何让用户真正”拥有”自己的学习数据?
最后一公里是导出。Mural 的导出功能不藏在设置里第 6 层,而是永远在右上角:
import Foundation
struct DataExporter {
let store: LocalStore
func exportMarkdown() throws -> URL {
let items = store.allItems
let body = """
# 我的 Mural 学习记录
导出时间: \(Date.now.formatted())
总条目: \(items.count)
\(items.map { "- \($0.front) → \($0.back)" }.joined(separator: "\n"))
"""
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("mural-export.md")
try body.write(to: url, atomically: true, encoding: .utf8)
return url
}
}
让用户能随时打包带走自己的数据,这是反成瘾设计里最关键的一环——产品不再”绑架”用户的学习历史。
Sources
- Chuloo/mural GitHub 仓库
- GitHub Trending 2026-09-16
- r/ClaudeAI 中关于”AI 时代语言学习工具形态”的讨论串
- HN: Show HN — A language app you’ll eventually delete
- Tristan Harris《The Humane Tech》中关于成瘾设计的批判