Things 3: 專注簡潔的藝術
Things 3如何透過約束實現簡潔:自然層次結構、When與Deadline分離、鍵盤優先設計、有意義的色彩節制。包含SwiftUI實作模式。
Things 3:專注簡約的藝術
「我們相信簡單是困難的。」— Cultured Code
Things 3 經常被譽為史上最美的任務管理工具。這份美感源自於對重要事物的極致專注,以及對一切多餘元素的果斷捨棄。
為什麼 Things 3 如此重要
Things 3 證明了限制能夠創造清晰。當競爭對手不斷堆疊功能(提供 47 種重複任務選項、附帶相依性的專案和甘特圖),Things 卻在思考:一個人究竟需要什麼才能把事情完成?
關鍵成就: - 在不犧牲深度的情況下達成視覺簡潔 - 讓日期變得有意義(今天、傍晚、將來某天) - 創建了符合人類思考方式的層級結構 - 證明了鍵盤優先也能兼具美感 - 為 macOS/iOS 原生應用程式設計樹立了標竿
核心要點
- 區分「我什麼時候做這件事?」與「截止日期是什麼時候?」 - Things 的雙日期模型(When + Deadline)消除了困擾大多數任務應用程式的模糊性;計劃意圖和硬性截止日期服務於不同目的
- 「將來某天」是功能,不是失敗狀態 - 給予想法一個無壓力的歸屬,能防止收件匣焦慮;「將來某天」中的任務不會堆積在「今天」,也不會引發內疚感
- 領域永不完成,專案會完成 - 這個區分反映了人類認知:「工作」是持續的人生領域,「發布 v2.0」則有終點線;混淆這兩者會造成困惑
- 將顏色保留給有意義的地方 - Things 的介面 95% 是中性色(黑、白、灰);顏色只出現在語義資訊上(黃色=今天、紅色=截止日期、靛藍色=傍晚)
- 完成任務應該帶來滿足感 - 勾選動畫(填滿 → 打勾 → 音效)雖然耗時 500 毫秒,卻能釋放多巴胺;完成任務本身就成為了內在動力
核心設計原則
1. 自然的層級結構
Things 按照人類思考的方式組織工作:從宏觀(生活領域)到具體(個別任務)。
THINGS HIERARCHY:
Area (Work, Personal, Health) ← Broad life categories
│
└── Project (Launch App) ← Finite goal with tasks
│
└── Heading (Design) ← Optional grouping
│
└── To-Do ← Single actionable item
│
└── Checklist ← Sub-steps within task
Example:
┌─────────────────────────────────────────────────────────────┐
│ [A] WORK (Area) │
│ ├── [P] Ship Version 2.0 (Project) │
│ │ ├── [H] Design │
│ │ │ ├── [ ] Create new icons │
│ │ │ └── [ ] Update color palette │
│ │ └── [H] Development │
│ │ ├── [ ] Implement auth flow │
│ │ └── [ ] Add analytics │
│ └── [P] Website Redesign (Project) │
│ └── [ ] Write copy for landing page │
└─────────────────────────────────────────────────────────────┘
關鍵洞察:領域永遠不會完成(它們是生活的類別)。專案會完成(週五前發布)。這個區分消除了規劃時的猶豫不決。
資料模型:
// Things' hierarchy expressed in code
struct Area: Identifiable {
let id: UUID
var title: String
var projects: [Project]
var tasks: [Task] // Loose tasks not in projects
}
struct Project: Identifiable {
let id: UUID
var title: String
var notes: String?
var deadline: Date?
var headings: [Heading]
var tasks: [Task]
var isComplete: Bool
}
struct Heading: Identifiable {
let id: UUID
var title: String
var tasks: [Task]
}
struct Task: Identifiable {
let id: UUID
var title: String
var notes: String?
var when: TaskSchedule?
var deadline: Date?
var tags: [Tag]
var checklist: [ChecklistItem]
var isComplete: Bool
}
enum TaskSchedule {
case today
case evening
case specificDate(Date)
case someday
}
2. 「何時做」,而非僅有「截止日」
Things 將「我什麼時候做這件事?」(今天、傍晚、將來某天)與「這件事什麼時候必須完成?」(截止日期)分開處理。大多數應用程式將這兩者混為一談。
TRADITIONAL DATE MODEL:
┌──────────────────────────────────────────────────────────────┐
│ Task: Write report │
│ Due: March 15th │
│ │
│ Problem: Is March 15th when I'll do it, or when it's due? │
│ If it's due, when should I start? │
└──────────────────────────────────────────────────────────────┘
THINGS DATE MODEL:
┌──────────────────────────────────────────────────────────────┐
│ Task: Write report │
│ When: Today (I'll work on it today) │
│ Deadline: March 15th (must be complete by then) │
│ │
│ Clarity: Two separate questions, two separate answers │
└──────────────────────────────────────────────────────────────┘
SMART LISTS (automatic filtering):
┌───────────────────┬─────────────────────────────────────────┐
│ [I] INBOX │ Uncategorized captures │
├───────────────────┼─────────────────────────────────────────┤
│ [*] TODAY │ when == .today OR deadline == today │
├───────────────────┼─────────────────────────────────────────┤
│ [E] THIS EVENING │ when == .evening │
├───────────────────┼─────────────────────────────────────────┤
│ [D] UPCOMING │ when == .specificDate (future) │
├───────────────────┼─────────────────────────────────────────┤
│ [A] ANYTIME │ when == nil AND deadline == nil │
├───────────────────┼─────────────────────────────────────────┤
│ [S] SOMEDAY │ when == .someday │
├───────────────────┼─────────────────────────────────────────┤
│ [L] LOGBOOK │ isComplete == true │
└───────────────────┴─────────────────────────────────────────┘
關鍵洞察:「將來某天」不是失敗狀態——它是壓力釋放閥。想法有了歸屬,就不會堆積在「今天」。
SwiftUI 實作:
struct WhenPicker: View {
@Binding var schedule: TaskSchedule?
@Binding var deadline: Date?
@State private var showDatePicker = false
var body: some View {
VStack(spacing: 12) {
// Quick options
HStack(spacing: 8) {
WhenButton(
icon: "star.fill",
label: "Today",
color: .yellow,
isSelected: schedule == .today
) {
schedule = .today
}
WhenButton(
icon: "moon.fill",
label: "Evening",
color: .indigo,
isSelected: schedule == .evening
) {
schedule = .evening
}
WhenButton(
icon: "calendar",
label: "Pick Date",
color: .red,
isSelected: showDatePicker
) {
showDatePicker = true
}
WhenButton(
icon: "archivebox.fill",
label: "Someday",
color: .brown,
isSelected: schedule == .someday
) {
schedule = .someday
}
}
// Deadline (separate from "when")
if let deadline = deadline {
HStack {
Image(systemName: "flag.fill")
.foregroundStyle(.red)
Text("Deadline: \(deadline, style: .date)")
Spacer()
Button("Clear") { self.deadline = nil }
}
.font(.caption)
}
}
}
}
struct WhenButton: View {
let icon: String
let label: String
let color: Color
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 4) {
Image(systemName: icon)
.font(.title2)
Text(label)
.font(.caption2)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(isSelected ? color.opacity(0.2) : .clear)
.cornerRadius(8)
}
.buttonStyle(.plain)
.foregroundStyle(isSelected ? color : .secondary)
}
}
3. 鍵盤優先,滑鼠友善
所有功能都能完全透過鍵盤操作,達到肌肉記憶般的效率,同時對滑鼠用戶依然保持直覺易用。
鍵盤快捷鍵(易於記憶的模式):
導航:
Cmd+1-6 跳轉到智慧列表(收件匣、今天等)
Cmd+Up/Down 在區段之間移動
Space 快速查看(預覽任務詳情)
任務操作:
Cmd+K 完成任務
Cmd+S 排程(When 選擇器)
Cmd+Shift+D 設定截止日期
Cmd+Shift+M 移動到專案
Cmd+Shift+T 新增標籤
建立:
Space/Return 建立新任務(依據上下文)
Cmd+N 在收件匣新增任務
Cmd+Opt+N 新增專案
快速輸入(全域快捷鍵):
Ctrl+Space 從任何地方開啟浮動快速輸入
┌─────────────────────────────────────────────────┐
│ [ ] | │
│ 新任務會出現在你所在的位置 │
└─────────────────────────────────────────────────┘
核心洞見:快捷鍵應該是可發現的,但不是必須的。介面會暗示按鍵操作,但不會強制要求。
CSS 模式(快捷鍵提示):
.action-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
}
.shortcut-hint {
font-size: 11px;
font-family: var(--font-mono);
padding: 2px 6px;
background: var(--surface-subtle);
border-radius: 4px;
color: var(--text-tertiary);
opacity: 0;
transition: opacity 0.15s ease;
}
/* 滑鼠懸停時顯示 */
.action-button:hover .shortcut-hint {
opacity: 1;
}
/* 透過鍵盤聚焦時始終可見 */
.action-button:focus-visible .shortcut-hint {
opacity: 1;
}
SwiftUI 快速輸入:
struct QuickEntryPanel: View {
@Binding var isPresented: Bool
@State private var taskTitle = ""
@FocusState private var isFocused: Bool
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 12) {
// 勾選框(僅供視覺呈現)
Circle()
.strokeBorder(.tertiary, lineWidth: 1.5)
.frame(width: 20, height: 20)
// Task input
TextField("New To-Do", text: $taskTitle)
.textFieldStyle(.plain)
.font(.body)
.focused($isFocused)
.onSubmit {
createTask()
}
}
.padding()
.background(.ultraThinMaterial)
.cornerRadius(12)
.shadow(color: .black.opacity(0.2), radius: 20, y: 10)
}
.frame(width: 500)
.onAppear {
isFocused = true
}
.onExitCommand {
isPresented = false
}
}
private func createTask() {
guard !taskTitle.isEmpty else { return }
// Create task in Inbox
TaskStore.shared.createTask(title: taskTitle)
taskTitle = ""
}
}
4. 視覺節制
Things 對色彩的使用極為克制,僅在需要傳達意義時才運用。介面的絕大部分保持中性,讓有色彩的元素能夠脫穎而出。
THINGS 中的色彩運用:
中性色(佔介面 95%):
├── 背景:純白 / 深灰
├── 文字:黑色 / 白色
├── 邊框:極淡的灰色
└── 圖示:單色
具有語意的色彩(佔 5%):
├── 黃色 [*] = 今天
├── 紅色 [!] = 截止日期 / 逾期
├── 藍色 [#] = 標籤(使用者自訂)
├── 綠色 [x] = 完成動畫
└── 靛藍色 [E] = 傍晚
視覺範例:
┌─────────────────────────────────────────────────────────────┐
│ 今天 [*](黃色) │
├─────────────────────────────────────────────────────────────┤
│ ( ) 撰寫文件 │
│ 專案名稱 - [!] 明天截止(紅色) │
│ │
│ ( ) 審查 pull requests │
│ [#] work(藍色標籤) │
│ │
│ ( ) 打電話給牙醫 │
│ [E] 今晚(靛藍色標記) │
└─────────────────────────────────────────────────────────────┘
注意:大部分文字都是黑色。色彩只在具有意義的地方出現。
核心洞見:當所有東西都色彩繽紛時,就沒有任何東西能夠突出。將色彩保留給資訊傳達。
CSS 模式:
:root {
/* 中性色調(UI 的主要部分) */
--surface-primary: #ffffff;
--surface-secondary: #f5f5f7;
--text-primary: #1d1d1f;
--text-secondary: #86868b;
--border: rgba(0, 0, 0, 0.08);
/* 語意色彩(謹慎使用) */
--color-today: #f5c518;
--color-deadline: #ff3b30;
--color-evening: #5856d6;
--color-complete: #34c759;
--color-tag: #007aff;
}
/* 任務列 - 大部分為中性色 */
.task-row {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background: var(--surface-primary);
color: var(--text-primary);
}
/* 色彩僅用於傳達意義 */
.deadline-badge {
font-size: 12px;
color: var(--color-deadline);
}
.deadline-badge.overdue {
font-weight: 600;
color: var(--color-deadline);
}
.tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
background: var(--color-tag);
color: white;
}
.evening-badge {
color: var(--color-evening);
}
5. 令人愉悅的完成體驗
完成動畫傳達的是獎勵感,而非單純的回饋。令人滿足的「叮」聲和流暢的打勾動畫,讓完成任務變成一種享受。
完成動畫序列:
第 1 幀: ○ 任務標題
第 2-4 幀: ◔(圓圈順時針填滿)
第 5-7 幀: ●(完全填滿,短暫停頓)
第 8-10 幀:✓(打勾出現,略微放大)
第 11+ 幀: 列向上滑出,清單收合間隙
音效:第 7-8 幀(完成時刻)播放輕柔的「叮」聲
觸覺:iOS 上完成時輕觸回饋
SwiftUI 實作:
struct CompletableCheckbox: View {
@Binding var isComplete: Bool
@State private var animationPhase: AnimationPhase = .unchecked
enum AnimationPhase {
case unchecked, filling, checked, done
}
var body: some View {
Button {
completeWithAnimation()
} label: {
ZStack {
// Background circle
Circle()
.strokeBorder(.tertiary, lineWidth: 1.5)
.frame(width: 22, height: 22)
// 填充動畫
Circle()
.trim(from: 0, to: fillAmount)
.stroke(.green, lineWidth: 1.5)
.frame(width: 22, height: 22)
.rotationEffect(.degrees(-90))
// 勾選標記
Image(systemName: "checkmark")
.font(.system(size: 12, weight: .bold))
.foregroundStyle(.green)
.scaleEffect(checkScale)
.opacity(checkOpacity)
}
}
.buttonStyle(.plain)
.sensoryFeedback(.success, trigger: animationPhase == .checked)
}
private var fillAmount: CGFloat {
switch animationPhase {
case .unchecked: return 0
case .filling: return 1
case .checked, .done: return 1
}
}
private var checkScale: CGFloat {
animationPhase == .checked ? 1.2 : (animationPhase == .done ? 1.0 : 0)
}
private var checkOpacity: CGFloat {
animationPhase == .unchecked || animationPhase == .filling ? 0 : 1
}
private func completeWithAnimation() {
// 階段 1:填滿圓圈
withAnimation(.easeInOut(duration: 0.2)) {
animationPhase = .filling
}
// 階段 2:顯示勾選標記並帶彈跳效果
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation(.spring(response: 0.3, dampingFraction: 0.5)) {
animationPhase = .checked
}
}
// 階段 3:穩定並標記完成
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(.easeOut(duration: 0.15)) {
animationPhase = .done
}
isComplete = true
}
}
}
可移植模式
模式 1:漸進式任務詳情
任務詳情在原位展開,而非導航至獨立視圖。
struct ExpandableTask: View {
let task: Task
@State private var isExpanded = false
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// 始終可見的列
TaskRow(task: task, isExpanded: $isExpanded)
// 可展開的詳情
if isExpanded {
TaskDetails(task: task)
.padding(.leading, 44) // 與標題對齊
.transition(.asymmetric(
insertion: .push(from: .top).combined(with: .opacity),
removal: .push(from: .bottom).combined(with: .opacity)
))
}
}
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: isExpanded)
}
}
模式 2:自然語言輸入
Things 會解析自然語言來設定日期:「tomorrow」、「next week」、「June 15」。
function parseNaturalDate(input: string): TaskSchedule | null {
const lower = input.toLowerCase();
// Today shortcuts
if (['today', 'tod', 'now'].includes(lower)) {
return { type: 'today' };
}
// Evening
if (['tonight', 'evening', 'eve'].includes(lower)) {
return { type: 'evening' };
}
// Tomorrow
if (['tomorrow', 'tom', 'tmrw'].includes(lower)) {
const date = new Date();
date.setDate(date.getDate() + 1);
return { type: 'date', date };
}
// Someday
if (['someday', 'later', 'eventually'].includes(lower)) {
return { type: 'someday' };
}
// Relative days
const inMatch = lower.match(/in (\d+) days?/);
if (inMatch) {
const date = new Date();
date.setDate(date.getDate() + parseInt(inMatch[1]));
return { type: 'date', date };
}
// Day names
const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
const dayIndex = days.findIndex(d => lower.startsWith(d.slice(0, 3)));
if (dayIndex !== -1) {
const date = getNextDayOfWeek(dayIndex);
return { type: 'date', date };
}
return null;
}
模式 3:列表式拖放
重新排序是直接操作,具有清晰的視覺回饋。
.task-row {
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.task-row.dragging {
transform: scale(1.02);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.15),
0 0 0 2px var(--color-focus);
z-index: 100;
}
.task-row.drop-above::before {
content: '';
position: absolute;
top: -2px;
left: 44px;
right: 16px;
height: 4px;
background: var(--color-focus);
border-radius: 2px;
}
.task-row.drop-below::after {
content: '';
position: absolute;
bottom: -2px;
left: 44px;
right: 16px;
height: 4px;
background: var(--color-focus);
border-radius: 2px;
}
設計啟示
- 關注點分離:「我何時要做這件事?」vs「它的截止日期是什麼時候?」是兩個不同的問題
- 色彩的克制:將顏色保留給有意義的元素;中性色是預設
- 完成即是獎勵:讓完成任務帶來愉悅感
- 鍵盤加速,滑鼠歡迎:同時為兩者設計,不犧牲任何一方
- 自然的層級結構:反映人類對工作的自然思考方式(領域 → 專案 → 任務)
- 「將來/也許」是一種功能:為想法提供一個不會造成壓力的歸屬
常見問題
Things 3 中「When」和「Deadline」有什麼區別?
「When」回答的是「我何時會處理這件事?」(今天、今晚、將來/也許,或特定日期)。「Deadline」回答的是「這件事必須何時完成?」大多數任務應用程式將這兩者混為一談,但它們服務於不同目的。一個任務可能週五到期(Deadline),但你計劃週三處理它(When)。這種分離讓你能根據意圖規劃一天,同時仍能追蹤硬性的截止日期。
Things 3 的層級結構如何運作(領域、專案、任務)?
領域是永不完結的廣泛生活類別(工作、個人、健康)。專案是有明確終點狀態的有限目標(發布應用程式、規劃假期)。任務是單一可執行的項目。標題可選擇性地在專案內將任務分組。這反映了人類對工作的自然思考方式:持續性的職責包含有時限的目標,而目標又包含具體的行動。
為什麼 Things 3 使用如此少的顏色?
Things 將顏色專門保留給語意化的含義。介面有 95% 是中性色(黑、白、灰),所以當顏色出現時,它在傳達訊息:黃色表示今天,紅色表示截止日期/逾期,靛藍色表示今晚,藍色標記標籤。如果一切都是彩色的,就沒有什麼能夠突出。這種克制讓有意義的顏色變得不可能被忽視。
Things 3 中的「將來/也許」是什麼,為什麼它很有價值?
「將來/也許」是一個專門的空間,用於存放你想記住但不想讓它們佔據活躍列表的任務。它不是一種失敗狀態——它是一個壓力釋放閥。「將來/也許」的想法(學西班牙語、讀那本書)有了歸屬,而不會造成每日的罪惡感。你可以每週檢視「將來/也許」,在準備好時將任務提升到「今天」。
Things 3 的完成動畫如何運作?
當你點擊核取方塊時,圓圈順時針填滿(200ms),然後一個帶有輕微彈跳的勾號出現(彈簧動畫),伴隨著柔和的「叮」聲和 iOS 上的觸覺回饋。接著該行滑出消失(150ms)。整個序列大約需要 500ms,但這種刻意的時間設計創造了一個小小的多巴胺刺激,讓完成任務感覺像是一種獎勵。
資源
- 官方網站: culturedcode.com/things
- 設計理念: Cultured Code 關於簡約的部落格文章
- 鍵盤快捷鍵: 內建說明選單(Cmd+/)
- GTD 整合: Things 如何對應 Getting Things Done 方法論