Bear: 排版优先的写作

Bear的排版优先设计如何赢得Apple Design Award:嵌套标签、主题系统、专注模式和内联Markdown。包含Swift实现模式。

4 分钟阅读 204 字
Bear: 排版优先的写作 screenshot

Bear:以排版为先的写作体验

"使用 Bear 时,你会感觉自己又在使用真正的 Apple 电脑了。没有加载动画,没有骨架屏,没有弹窗提示。只有流畅的动画和随时待命的内容。"

Bear 是无干扰设计的典范之作。从排版到标签系统,每一个决策都服务于那些想要专注思考、而非管理工具的写作者。


核心要点

  1. 零加载状态 - 内容始终就绪;同步在后台悄然进行
  2. 标签取代文件夹 - 写作时随手添加 #标签 胜过事后整理文件夹,笔记还可以同时存在于多个位置
  3. 排版控制尊重读者 - 字体、字号、行高和宽度控制让用户为自己的眼睛优化阅读体验
  4. 一键切换整体主题 - 28 种以上精心策划的主题胜过零散的颜色选择器
  5. 专注模式作为终极方案 - 当无干扰还不够彻底时,一个手势即可移除一切

Bear 为何重要

Bear 荣获 2017 年 Apple 设计大奖及多项编辑精选奖,证明了笔记应用可以同时兼具强大功能与精美设计。

主要成就: - 让非开发者也能轻松使用 Markdown - 发明了嵌套标签,作为文件夹的灵活替代方案 - 为不同写作场景创建了 28 种以上主题 - 设计了OLED专属主题(Dieci),实现纯正黑色显示 - 零加载状态:内容始终就绪


核心设计理念

零阻力原则

Bear 消除了思想与文字之间的所有障碍:

阻力模式(其他应用)                   BEAR 的方式
───────────────────────────────────────────────────────────────────
创建前弹出文件夹对话框                 直接开始输入
格式工具栏遮挡视野                     Markdown 内联,无形融入
同步加载动画打断心流                   后台同步,无任何指示器
设置分散在各个菜单中                   排版选项触手可及
需要颜色选择器来高亮                   主题一键切换所有样式

核心洞察:每个 UI 元素都是潜在的干扰。尽可能减少它们。


模式库

1. 无限嵌套标签

Bear 的标签系统用灵活的内联组织方式取代了僵化的文件夹层级结构。

传统文件夹 vs Bear 标签:

文件夹方式                            BEAR 的标签方式
───────────────────────────────────────────────────────────────────
📁 Work                               笔记包含: #work/meetings
├── 📁 Meetings                       笔记包含: #work/meetings/q1
│   ├── 📁 Q1
│   │   └── standup-2025-01.md       一条笔记可以有多个标签:
│   └── 📁 Q2                         #work/meetings #action-items #q1
└── 📁 Projects

单一位置                              多个位置(虚拟)
移动 = 文件操作                       标签 = 直接输入即可
在文件浏览器中可见                    在侧边栏 + 笔记正文中可见

标签语法:

单个标签:       #ideas
嵌套标签:       #work/meetings/2025
深层嵌套:       #journal/2025/01/17

侧边栏结果:
├─ 📁 工作
│   └─ 📁 会议
│       └─ 📄 2025
├─ 📁 日记
│   └─ 📁 2025
│       └─ 📁 01
│           └─ 📄 17

核心洞察:标签是通过内联输入的,而非从菜单中选择。书写的过程即创建组织结构的过程。


2. 排版控制系统

Bear 提供了其他笔记应用所隐藏的精细排版控制:

┌─ 排版设置 ──────────────────────────────────────────────────────────┐
│                                                                    │
│  字体                                                              │
│  [Avenir Next ▼]        ← 系统字体 + 自定义字体                    │
│                                                                    │
│  字号                                                              │
│  [─────●────────]  16pt                                           │
│                                                                    │
│  行高                                                              │
│  [────────●─────]  1.6                                            │
│                                                                    │
│  行宽                                                              │
│  [──●───────────]  窄幅   ← 最佳阅读宽度                           │
│                                                                    │
│  段落间距                                                          │
│  [─────●────────]  中等                                           │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

Swift 实现方法:

struct TypographySettings: Codable {
    var fontName: String = "Avenir Next"
    var fontSize: CGFloat = 16
    var lineHeightMultiple: CGFloat = 1.6
    var lineWidth: LineWidth = .comfortable
    var paragraphSpacing: CGFloat = 12

    enum LineWidth: String, Codable {
        case narrow = "narrow"      // 约60个字符
        case comfortable = "medium" // 约75个字符
        case wide = "wide"          // 全宽
    }
}

// 应用于编辑器
func applyTypography(_ settings: TypographySettings, to textView: UITextView) {
    let style = NSMutableParagraphStyle()
    style.lineHeightMultiple = settings.lineHeightMultiple
    style.paragraphSpacing = settings.paragraphSpacing

    let attributes: [NSAttributedString.Key: Any] = [
        .font: UIFont(name: settings.fontName, size: settings.fontSize)!,
        .paragraphStyle: style
    ]

    textView.typingAttributes = attributes
}

3. 主题系统

Bear 的主题会同时影响所有元素——无需逐个挑选颜色。

主题结构:

struct BearTheme {
    // 背景层级
    let sidebarBackground: Color
    let noteListBackground: Color
    let editorBackground: Color

    // 文本层次
    let textPrimary: Color
    let textSecondary: Color
    let textMuted: Color

    // 语义高亮
    let tagColor: Color
    let linkColor: Color
    let codeBackground: Color
    let headingColor: Color

    // 选择和焦点
    let selectionColor: Color
    let cursorColor: Color
}

// 示例:Red Graphite(默认浅色主题)
let redGraphite = BearTheme(
    sidebarBackground: Color(hex: "#F7F7F7"),
    noteListBackground: Color(hex: "#FFFFFF"),
    editorBackground: Color(hex: "#FFFFFF"),
    textPrimary: Color(hex: "#333333"),
    textSecondary: Color(hex: "#888888"),
    textMuted: Color(hex: "#BBBBBB"),
    tagColor: Color(hex: "#D14C3E"),      // 标志性红色
    linkColor: Color(hex: "#B44B41"),
    codeBackground: Color(hex: "#F5F5F5"),
    headingColor: Color(hex: "#333333"),
    selectionColor: Color(hex: "#FFE4E1"),
    cursorColor: Color(hex: "#D14C3E")
)

// 示例:Dieci(OLED 优化)
let dieci = BearTheme(
    sidebarBackground: Color(hex: "#000000"),  // 纯黑色
    noteListBackground: Color(hex: "#000000"), // 纯黑色
    editorBackground: Color(hex: "#000000"),   // 纯黑色
    textPrimary: Color(hex: "#FFFFFF"),
    textSecondary: Color(hex: "#888888"),
    textMuted: Color(hex: "#555555"),
    tagColor: Color(hex: "#FF9500"),
    linkColor: Color(hex: "#FF9500"),
    codeBackground: Color(hex: "#1C1C1C"),
    headingColor: Color(hex: "#FFFFFF"),
    selectionColor: Color(hex: "#3A3A3C"),
    cursorColor: Color(hex: "#FF9500")
)

主题分类: - 浅色主题:Red Graphite、High Contrast、Solarized Light - 深色主题:Dark Graphite、Dracula、Nord - OLED 主题:Dieci、Charcoal(纯黑色,节省电量) - 特色主题:Shibuya Jazz、Everforest(氛围定制)


4. 专注模式

Bear 的专注模式移除一切干扰,只留下文字——连光标都变得不显眼。

普通模式
┌────────┬────────────┬───────────────────────────────────────────┐
│        │            │                                           │
│侧边栏  │ 笔记列表   │  编辑器                                   │
│        │            │                                           │
│ #work  │  会议      │  # 会议笔记                               │
│ #ideas │  想法      │                                           │
│ #books │  草稿      │  今天我们讨论了...                        │
│        │            │                                           │
└────────┴────────────┴───────────────────────────────────────────┘

专注模式(键盘快捷键或滑动)
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                                                                 │
│                   # 会议笔记                                    │
│                                                                 │
│                   今天我们讨论了...                             │
│                                                                 │
│                                                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

一切消失。只剩文字。

实现原则: - 单一手势或快捷键触发 - 动画快速响应(无冗长过渡效果) - 光标闪烁柔和,不会分散注意力 - 边距为文本周围提供呼吸空间


5. TagCons(可视化标签图标)

Bear 会自动为常用标签分配图标,使侧边栏一目了然。

带有 TagCons 的侧边栏:
├─ 💡 ideas
├─ 📚 books
├─ ✏️ writing
├─ 📝 journal
├─ 🏃 fitness
├─ 💼 work
│   ├─ 📅 meetings
│   └─ 📋 projects
└─ 🎯 goals

图标分配逻辑:

enum TagConCategory {
    static let mappings: [String: String] = [
        "ideas": "💡",
        "books": "📚",
        "reading": "📖",
        "writing": "✏️",
        "journal": "📝",
        "diary": "📓",
        "work": "💼",
        "meetings": "📅",
        "projects": "📋",
        "goals": "🎯",
        "fitness": "🏃",
        "health": "❤️",
        "recipes": "🍳",
        "travel": "✈️",
        "music": "🎵",
        "code": "💻",
    ]

    static func icon(for tag: String) -> String? {
        let normalized = tag.lowercased()
        return mappings[normalized]
    }
}

核心要点:图标自动分配但可自定义。智能默认值减少设置摩擦。


视觉设计系统

调色板(红色石墨主题)

extension Color {
    // 标志性强调色
    static let bearRed = Color(hex: "#D14C3E")

    // 背景
    static let sidebarBg = Color(hex: "#F7F7F7")
    static let editorBg = Color(hex: "#FFFFFF")

    // 文本
    static let textPrimary = Color(hex: "#333333")
    static let textSecondary = Color(hex: "#888888")

    // 代码块
    static let codeBg = Color(hex: "#F5F5F5")
    static let codeText = Color(hex: "#333333")
}

排版

struct BearTypography {
    // 编辑器字体
    static let bodyFont = Font.custom("Avenir Next", size: 16)
    static let headingFont = Font.custom("Avenir Next", size: 24).weight(.semibold)
    static let monoFont = Font.custom("SF Mono", size: 14)

    // 行高
    static let bodyLineHeight: CGFloat = 1.6
    static let headingLineHeight: CGFloat = 1.3

    // 最佳阅读宽度
    static let maxLineWidth: CGFloat = 680  // 约75个字符
}

动画设计理念

无加载状态

Bear 的核心动画原则:内容始终处于就绪状态。

// 反模式:加载指示器
struct LoadingNote: View {
    var body: some View {
        ProgressView()  // Bear 从不这样做
    }
}

// Bear 的方式:乐观更新,即时响应
struct NoteEditor: View {
    @State private var note: Note

    var body: some View {
        TextEditor(text: $note.content)
            .onAppear {
                // 内容已从本地缓存中获取
                // 同步在后台无感进行
            }
    }
}

流畅的面板过渡

// 侧边栏折叠/展开
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
    sidebarVisible.toggle()
}

// 专注模式过渡
withAnimation(.easeInOut(duration: 0.2)) {
    focusMode = true
}

Markdown 体验

实时预览(内联样式)

Bear 在您输入时即时渲染 Markdown,无需分屏视图。

您输入的内容:              您看到的效果:
───────────────────────────────────────────────────────

# Heading                   标题(大号,粗体)

**粗体文本**               粗体文本(带样式,** 隐藏)

- list item                 • list item(显示项目符号)

`code`                      code(等宽字体,高亮显示)

[link](url)                 链接(带样式,URL 隐藏)

实现思路:

class MarkdownTextStorage: NSTextStorage {
    private var backingStore = NSMutableAttributedString()

    override func replaceCharacters(in range: NSRange, with str: String) {
        beginEditing()
        backingStore.replaceCharacters(in: range, with: str)
        edited(.editedCharacters, range: range, changeInLength: str.count - range.length)
        endEditing()
    }

    override func processEditing() {
        super.processEditing()
        applyMarkdownStyling()
    }

    private func applyMarkdownStyling() {
        // 根据 Markdown 模式应用样式
        // 隐藏语法字符(**、`、# 等)
        // 在保留纯文本源的同时进行内联渲染
    }
}

对我们工作的启示

1. 零加载状态

如果内容存在于本地,就立即显示。同步在后台进行。

2. 标签 > 文件夹

写作时内联添加标签比事后整理文件夹更快。

3. 排版即用户体验

让用户控制字体、大小、行高和宽度,体现了对阅读体验的尊重。

4. 一次性主题化所有元素

不要让用户挑选 12 种颜色,而是提供精心策划的完整主题。

5. 专注模式作为终极方案

当无干扰模式还不够无干扰时,一个手势就能移除所有界面元素。


常见问题

Bear 的嵌套标签是如何工作的?

在笔记中的任意位置输入 #parent/child/grandchild,Bear 会自动在侧边栏中创建层级结构。与文件夹不同,一条笔记可以有多个标签,同时出现在多个"位置"。标签通过输入来创建,而非通过菜单导航。

为什么 Bear 使用主题而不是单独的颜色设置?

主题确保视觉的一致性。当用户单独选择颜色时,往往会创建出对比度差或色调冲突的组合。Bear 提供的 28+ 款精心策划的主题,保证了所有 UI 元素具有可读性和美学上的一致性。

Bear 的 Markdown 与其他编辑器有何不同?

Bear 在您输入时实时渲染 Markdown。 语法字符(**#、反引号)在输入后会自动隐藏,只显示样式化的结果。你在编辑时就能看到最终效果,无需单独的预览面板。

Bear 如何实现零加载状态?

Bear 将所有内容存储在本地,并从缓存即时加载。iCloud 同步在后台进行,没有转圈或进度指示器。即使离线打开 Bear,一切都能正常使用。连接恢复后,同步会静默完成。

我可以将 Bear 笔记导出为其他格式吗?

Bear 支持导出为 Markdown、PDF、HTML、DOCX 和纯文本格式。笔记保留其 Markdown 源文件,因此你拥有数据的所有权。标签系统会根据格式导出为 frontmatter 或文件结构。