语言 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》中关于成瘾设计的批判