Things 3:集中したシンプリシティの芸術
Things 3が制約によってシンプリシティを達成した方法:自然な階層、WhenとDeadlineの分離、キーボードファーストデザイン、意味のある色の抑制。SwiftUI実装パターン付き。
Things 3:集中したシンプルさの美学
「シンプルは難しいと信じている」— Cultured Code
Things 3は、これまで作られた中で最も美しいタスク管理ツールとしばしば称される。その美しさは、重要なものへの徹底的なフォーカスと、それ以外すべての排除から生まれている。
Things 3が重要な理由
Things 3は、制約が明確さを生むことを示している。競合他社が機能を積み重ねる中(47のオプションを持つ繰り返しタスク、依存関係やガントチャートを持つプロジェクト)、Thingsはこう問いかける:人が物事を成し遂げるために本当に必要なものは何か?
主な成果: - 深さを犠牲にすることなく視覚的なシンプルさを実現 - 日付を意味あるものにした(Today、Evening、Someday) - 人間が仕事について考える方法を反映した階層を構築 - キーボードファーストでも美しくできることを証明した - macOS/iOSネイティブアプリデザインの標準を確立
重要なポイント
- 「いつやるか?」と「いつ期限か?」を分ける - Thingsのデュアルデートモデル(When + Deadline)は、ほとんどのタスクアプリを悩ませる曖昧さを解消する;スケジュールの意図とハードな締め切りは異なる目的を果たす
- Somedayは機能であり、失敗状態ではない - アイデアにプレッシャーのない居場所を与えることで、受信トレイの不安を防ぐ;Somedayにあるタスクは、Todayを散らかしたり罪悪感を引き起こしたりしない
- Areaは完了しない、Projectは完了する - この区別は人間の認知を反映している:「仕事」は継続的な人生の領域、「v2.0をリリース」には終点がある;これらを混ぜると混乱が生まれる
- 色は意味のために取っておく - Thingsのインターフェースは95%がニュートラル(黒、白、グレー);色は意味的情報にのみ現れる(黄色=Today、赤=Deadline、インディゴ=Evening)
- 完了は報酬感があるべき - チェックボックスのアニメーション(塗りつぶし → チェックマーク → サウンド)は500msかかるがドーパミンを提供する;タスクの完了が内発的に動機づけられるようになる
コアデザイン原則
1. ナチュラルヒエラルキー
Thingsは、人間が考える方法で作業を整理する:広いもの(人生の領域)から具体的なもの(個々のタスク)へ。
THINGSヒエラルキー:
エリア(仕事、プライベート、健康) ← 人生の大きなカテゴリ
│
└── プロジェクト(アプリリリース) ← タスクを含む有限の目標
│
└── 見出し(デザイン) ← 任意のグループ分け
│
└── To-Do ← 単一の実行可能な項目
│
└── チェックリスト ← タスク内のサブステップ
例:
┌─────────────────────────────────────────────────────────────┐
│ [A] 仕事(エリア) │
│ ├── [P] バージョン2.0リリース(プロジェクト) │
│ │ ├── [H] デザイン │
│ │ │ ├── [ ] 新しいアイコンを作成 │
│ │ │ └── [ ] カラーパレットを更新 │
│ │ └── [H] 開発 │
│ │ ├── [ ] 認証フローを実装 │
│ │ └── [ ] アナリティクスを追加 │
│ └── [P] ウェブサイトリデザイン(プロジェクト) │
│ └── [ ] ランディングページのコピーを作成 │
└─────────────────────────────────────────────────────────────┘
核心的なポイント:エリアは完了しません(人生のカテゴリーだから)。プロジェクトは完了します(金曜日までにリリース)。この区別が計画の麻痺を解消します。
データモデル:
// Thingsの階層構造をコードで表現
struct Area: Identifiable {
let id: UUID
var title: String
var projects: [Project]
var tasks: [Task] // プロジェクトに属さない単独タスク
}
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は「いつこれをやるか?」(Today、Evening、Someday)と「いつまでに終わらせなければならないか?」(Deadline)を明確に分離しています。ほとんどのアプリはこれらを混同しています。
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の日付モデル:
┌──────────────────────────────────────────────────────────────┐
│ タスク:レポートを書く │
│ いつ:今日(今日取り組む) │
│ 締め切り:3月15日(それまでに完了必須) │
│ │
│ 明確さ:2つの別々の質問、2つの別々の答え │
└──────────────────────────────────────────────────────────────┘
スマートリスト(自動フィルタリング):
┌───────────────────┬─────────────────────────────────────────┐
│ [I] INBOX │ 未分類のキャプチャ │
├───────────────────┼─────────────────────────────────────────┤
│ [*] 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) {
// クイックオプション
HStack(spacing: 8) {
WhenButton(
icon: "star.fill",
label: "今日",
color: .yellow,
isSelected: schedule == .today
) {
schedule = .today
}
WhenButton(
icon: "moon.fill",
label: "夕方",
color: .indigo,
isSelected: schedule == .evening
) {
schedule = .evening
}
WhenButton(
icon: "calendar",
label: "日付を選択",
color: .red,
isSelected: showDatePicker
) {
showDatePicker = true
}
WhenButton(
icon: "archivebox.fill",
label: "いつか",
color: .brown,
isSelected: schedule == .someday
) {
schedule = .someday
}
}
// 締め切り(「いつ」とは別)
if let deadline = deadline {
HStack {
Image(systemName: "flag.fill")
.foregroundStyle(.red)
Text("締め切り: \(deadline, style: .date)")
Spacer()
Button("クリア") { 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. キーボードファースト、マウスフレンドリー
すべての操作をキーボードだけで完結でき、マッスルメモリーによる効率的な操作が可能でありながら、マウスユーザーにとっても直感的に使えます。
KEYBOARD SHORTCUTS (記憶しやすいパターン):
NAVIGATION:
Cmd+1-6 スマートリストへジャンプ(受信箱、今日など)
Cmd+Up/Down セクション間を移動
Space クイックルック(タスク詳細をプレビュー)
TASK MANIPULATION:
Cmd+K タスクを完了
Cmd+S スケジュール設定(日時選択)
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)
// タスク入力
TextField("新しい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 }
// インボックスにタスクを作成
TaskStore.shared.createTask(title: taskTitle)
taskTitle = ""
}
}
4. 視覚的抑制
Thingsは色を控えめに使用し、意味がある場合にのみ色を使います。インターフェースの大部分はニュートラルで、色付きの要素が際立つようになっています。
THINGSでの色の使用:
ニュートラル(インターフェースの95%):
├── 背景:ピュアホワイト / ディープグレー
├── テキスト:黒 / 白
├── ボーダー:非常に控えめなグレー
└── アイコン:モノクローム
Areaは完了しない広い人生のカテゴリです(仕事、個人、健康)。Projectは明確な終了状態を持つ有限の目標です(アプリをリリース、休暇を計画)。Taskは単一の実行可能なアイテムです。Headingはオプションでプロジェクト内のタスクをグループ化します。これは人間が自然に仕事について考える方法を反映しています:継続的な責任には時間制限のある目標が含まれ、それには個別のアクションが含まれます。
視覚的な例:
┌─────────────────────────────────────────────────────────────┐
│ TODAY [*] (黄色) │
├─────────────────────────────────────────────────────────────┤
│ ( ) ドキュメントを書く │
│ プロジェクト名 - [!] 明日が期限 (赤) │
│ │
│ ( ) プルリクエストをレビュー │
│ [#] 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以降: 行がスライドして消え、リストが隙間を埋める
Audio: フレーム7-8で柔らかな「ピンク」音(完了の瞬間)
Haptic: 完了時に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 {
// 背景の円
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();
// 今日のショートカット
if (['today', 'tod', 'now'].includes(lower)) {
return { type: 'today' };
}
// 夕方
if (['tonight', 'evening', 'eve'].includes(lower)) {
return { type: 'evening' };
}
// 明日
if (['tomorrow', 'tom', 'tmrw'].includes(lower)) {
const date = new Date();
date.setDate(date.getDate() + 1);
return { type: 'date', date };
}
// いつか
if (['someday', 'later', 'eventually'].includes(lower)) {
return { type: 'someday' };
}
// 相対的な日数
const inMatch = lower.match(/in (\d+) days?/);
if (inMatch) {
const date = new Date();
date.setDate(date.getDate() + parseInt(inMatch[1]));
return { type: 'date', date };
}
// 曜日名
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;
}
デザインの教訓
- 関心の分離:「いつ取り組むか?」と「いつまでに終わらせるか?」は異なる問いである
- 色の節制:色は意味のために取っておく。ニュートラルがデフォルト
- 完了は報酬:タスクを終えることに心地よさを感じさせる
- キーボードは加速し、マウスは歓迎する:どちらも妥協せずに両方に対応する設計
- 自然な階層構造:人間が仕事について考える方法を反映する(エリア → プロジェクト → タスク)
- 「いつか」は機能である:プレッシャーを生まずにアイデアの居場所を与える
よくある質問
Things 3の「When」と「Deadline」の違いは何ですか?
「When」は「いつ取り組むか?」に答えます(Today、This Evening、Someday、または特定の日付)。「Deadline」は「いつまでに完了しなければならないか?」に答えます。ほとんどのタスクアプリはこれらを混同していますが、それぞれ異なる目的があります。タスクの期限が金曜日(Deadline)でも、水曜日に取り組む予定(When)かもしれません。この分離により、厳密な期限を追跡しながらも、意図に基づいて1日を計画できます。
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方法論にどのようにマッピングされるか