Things 3: L'Art de la Simplicité Concentrée

Comment Things 3 a atteint la simplicité par les contraintes : hiérarchie naturelle, séparation When vs Deadline, design keyboard-first et retenue colorée significative. Avec patterns d'implémentation SwiftUI.

7 min de lecture 1263 mots
Things 3: L'Art de la Simplicité Concentrée screenshot

Things 3 : L'art de la simplicité ciblée

« Nous croyons que la simplicité est difficile. » — Cultured Code

Things 3 est souvent qualifié de plus beau gestionnaire de tâches jamais créé. Cette beauté naît d'une focalisation implacable sur l'essentiel et de l'élimination de tout le superflu.


Pourquoi Things 3 compte

Things 3 démontre que les contraintes créent la clarté. Là où les concurrents empilent les fonctionnalités (tâches récurrentes avec 47 options, projets avec dépendances et diagrammes de Gantt), Things pose la question : de quoi une personne a-t-elle réellement besoin pour accomplir ses tâches ?

Réalisations clés : - A atteint la simplicité visuelle sans sacrifier la profondeur - A rendu les dates significatives (Aujourd'hui, Ce soir, Un jour) - A créé une hiérarchie qui reflète la façon dont les humains pensent leur travail - A prouvé qu'une approche clavier-first peut être élégante - A établi la référence en matière de design natif macOS/iOS


Points clés à retenir

  1. Séparer « quand vais-je le faire ? » de « quelle est l'échéance ? » - Le modèle à double date de Things (Quand + Échéance) élimine l'ambiguïté qui affecte la plupart des applications de tâches ; planifier une intention et fixer une échéance ferme servent des objectifs différents
  2. Un jour est une fonctionnalité, pas un état d'échec - Offrir aux idées un espace sans pression évite l'anxiété de la boîte de réception ; les tâches dans Un jour n'encombrent pas Aujourd'hui et ne génèrent pas de culpabilité
  3. Les domaines ne se terminent jamais, les projets oui - Cette distinction reflète la cognition humaine : « Travail » est un domaine de vie permanent, « Livrer la v2.0 » a une ligne d'arrivée ; mélanger les deux crée de la confusion
  4. Réserver la couleur au sens - L'interface de Things est à 95 % neutre (noir, blanc, gris) ; la couleur n'apparaît que pour l'information sémantique (jaune=Aujourd'hui, rouge=Échéance, indigo=Ce soir)
  5. Compléter une tâche doit procurer une récompense - L'animation de la case à cocher (remplissage → coche → son) prend 500 ms mais libère de la dopamine ; accomplir des tâches devient intrinsèquement motivant

Principes de design fondamentaux

1. La hiérarchie naturelle

Things organise le travail comme les humains le conçoivent : du général (les domaines de vie) au spécifique (les tâches individuelles).

HIÉRARCHIE DE THINGS :

Area (Work, Personal, Health)     ← Grandes catégories de vie
  │
  └── Project (Launch App)        ← Objectif fini avec des tâches
        │
        └── Heading (Design)      ← Regroupement optionnel
              │
              └── To-Do           ← Élément actionnable unique
                    │
                    └── Checklist ← Sous-étapes au sein d'une tâche

Exemple :
┌─────────────────────────────────────────────────────────────┐
│ [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                 │
└─────────────────────────────────────────────────────────────┘

L'insight clé : Les domaines ne se terminent jamais (ce sont des catégories de vie). Les projets se terminent (livrer avant vendredi). Cette distinction élimine la paralysie de planification.

Modèle de données :

// 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. Quand, pas seulement l'échéance

Things sépare « quand vais-je le faire ? » (Aujourd'hui, Ce soir, Un jour) de « quand cela doit-il être terminé ? » (Échéance). La plupart des applications confondent ces deux notions.

MODÈLE DE DATE TRADITIONNEL :
┌──────────────────────────────────────────────────────────────┐
│ Task: Write report                                           │
│ Due: March 15th                                              │
│                                                              │
│ Problème : Le 15 mars, est-ce quand je vais le faire,       │
│            ou quand c'est dû ? Si c'est l'échéance,         │
│            quand devrais-je commencer ?                      │
└──────────────────────────────────────────────────────────────┘

MODÈLE DE DATE DE THINGS :
┌──────────────────────────────────────────────────────────────┐
│ Task: Write report                                           │
│ When: Today (je vais y travailler aujourd'hui)              │
│ Deadline: March 15th (doit être terminé avant cette date)   │
│                                                              │
│ Clarté : Deux questions distinctes, deux réponses distinctes│
└──────────────────────────────────────────────────────────────┘

LISTES INTELLIGENTES (filtrage automatique) :

┌───────────────────┬─────────────────────────────────────────┐
│ [I] INBOX         │ Captures non catégorisées               │
├───────────────────┼─────────────────────────────────────────┤
│ [*] 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                      │
└───────────────────┴─────────────────────────────────────────┘

L'insight clé : « Un jour » n'est pas un état d'échec — c'est une soupape de décompression. Les idées ont un espace dédié sans encombrer Aujourd'hui.

Implémentation 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. Clavier d'abord, compatible souris

L'ensemble des fonctionnalités est utilisable entièrement au clavier avec une efficacité qui repose sur la mémoire musculaire, tout en restant intuitif pour les utilisateurs de souris.

RACCOURCIS CLAVIER (patterns mémorisables) :

NAVIGATION :
  Cmd+1-6      Accéder aux listes intelligentes (Inbox, Today, etc.)
  Cmd+Up/Down  Se déplacer entre les sections
  Space        Quick Look (aperçu des détails de la tâche)

MANIPULATION DES TÂCHES :
  Cmd+K        Terminer la tâche
  Cmd+S        Planifier (sélecteur When)
  Cmd+Shift+D  Définir une deadline
  Cmd+Shift+M  Déplacer vers un projet
  Cmd+Shift+T  Ajouter des tags

CRÉATION :
  Space/Return Créer une nouvelle tâche (contextuel)
  Cmd+N        Nouvelle tâche dans Inbox
  Cmd+Opt+N    Nouveau projet

Quick Entry (raccourci global) :
  Ctrl+Space   Ouvre la saisie rapide flottante depuis n'importe où
  ┌─────────────────────────────────────────────────┐
  │ [ ] |                                           │
  │   La nouvelle tâche apparaît où que vous soyez  │
  └─────────────────────────────────────────────────┘

L'idée clé : Les raccourcis doivent être découvrables mais jamais obligatoires. L'interface suggère les touches sans les imposer.

Pattern CSS (indicateurs de raccourcis) :

.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;
}

/* Afficher au survol */
.action-button:hover .shortcut-hint {
  opacity: 1;
}

/* Toujours visible lors de la navigation au clavier */
.action-button:focus-visible .shortcut-hint {
  opacity: 1;
}

Quick Entry en 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) {
                // Checkbox (visual only)
                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. Retenue visuelle

Things utilise la couleur avec parcimonie, la réservant uniquement pour véhiculer du sens. La majeure partie de l'interface est neutre, permettant aux éléments colorés de ressortir.

UTILISATION DE LA COULEUR DANS THINGS :

Neutre (95% de l'interface) :
├── Arrière-plan : Blanc pur / Gris foncé
├── Texte : Noir / Blanc
├── Bordures : Gris très subtil
└── Icônes : Monochromes

Couleur porteuse de sens (5%) :
├── Jaune [*] = Aujourd'hui
├── Rouge [!] = Échéance / En retard
├── Bleu [#] = Tags (assignés par l'utilisateur)
├── Vert [x] = Animation de complétion
└── Indigo [E] = Ce soir

EXEMPLE VISUEL :

┌─────────────────────────────────────────────────────────────┐
│ AUJOURD'HUI                                  [*] (jaune)    │
├─────────────────────────────────────────────────────────────┤
│ ( ) Rédiger la documentation                                │
│     Nom du projet - [!] Échéance demain (rouge)             │
│                                                             │
│ ( ) Relire les pull requests                                │
│     [#] travail (tag bleu)                                  │
│                                                             │
│ ( ) Appeler le dentiste                                     │
│     [E] Ce soir (badge indigo)                              │
└─────────────────────────────────────────────────────────────┘

Remarque : La majeure partie du texte est en noir. La couleur n'apparaît que là où elle porte du sens.

L'enseignement clé : Quand tout est coloré, rien ne ressort. Réservez la couleur pour l'information.

Pattern CSS :

:root {
  /* Palette neutre (majeure partie de l'UI) */
  --surface-primary: #ffffff;
  --surface-secondary: #f5f5f7;
  --text-primary: #1d1d1f;
  --text-secondary: #86868b;
  --border: rgba(0, 0, 0, 0.08);

  /* Couleurs sémantiques (utilisées avec parcimonie) */
  --color-today: #f5c518;
  --color-deadline: #ff3b30;
  --color-evening: #5856d6;
  --color-complete: #34c759;
  --color-tag: #007aff;
}

/* Ligne de tâche - majoritairement neutre */
.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);
}

/* La couleur uniquement pour le sens */
.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. Une complétion gratifiante

L'animation de complétion procure une récompense, pas un simple retour visuel. Un son « plink » satisfaisant et une animation de coche fluide rendent l'achèvement des tâches agréable.

SÉQUENCE DE COMPLÉTION :

Image 1 :      ○ Titre de la tâche
Images 2-4 :   ◔ (le cercle se remplit dans le sens horaire)
Images 5-7 :   ● (entièrement rempli, courte pause)
Images 8-10 :  ✓ (la coche apparaît, légèrement agrandie)
Images 11+ :   La ligne glisse, la liste comble l'espace

Audio : « Plink » discret aux images 7-8 (moment de complétion)
Haptique : Légère vibration sur iOS à la complétion

Implémentation 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)

                // Fill animation
                Circle()
                    .trim(from: 0, to: fillAmount)
                    .stroke(.green, lineWidth: 1.5)
                    .frame(width: 22, height: 22)
                    .rotationEffect(.degrees(-90))

                // Checkmark
                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() {
        // Phase 1: Fill the circle
        withAnimation(.easeInOut(duration: 0.2)) {
            animationPhase = .filling
        }

        // Phase 2: Show checkmark with bounce
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
            withAnimation(.spring(response: 0.3, dampingFraction: 0.5)) {
                animationPhase = .checked
            }
        }

        // Phase 3: Settle and mark complete
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            withAnimation(.easeOut(duration: 0.15)) {
                animationPhase = .done
            }
            isComplete = true
        }
    }
}

Patterns transférables

Pattern 1 : Détails progressifs des tâches

Les détails de la tâche se développent directement dans la liste plutôt que de naviguer vers une vue séparée.

struct ExpandableTask: View {
    let task: Task
    @State private var isExpanded = false

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            // Always visible row
            TaskRow(task: task, isExpanded: $isExpanded)

            // Expandable details
            if isExpanded {
                TaskDetails(task: task)
                    .padding(.leading, 44) // Align with title
                    .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)
    }
}

Pattern 2 : Saisie en langage naturel

Things analyse le langage naturel pour définir les dates : « 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;
}

Pattern 3 : Glisser-déposer dans les listes

La réorganisation repose sur la manipulation directe avec un retour visuel explicite.

.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;
}

Leçons de design

  1. Séparer les préoccupations : « Quand vais-je m'en occuper ? » et « Quelle est l'échéance ? » sont deux questions distinctes
  2. Retenue dans l'usage de la couleur : Réserver la couleur au sens ; le neutre est la norme
  3. L'accomplissement est une récompense : Rendre la finalisation des tâches gratifiante
  4. Le clavier accélère, la souris accueille : Concevoir pour les deux sans compromettre l'un ou l'autre
  5. Hiérarchies naturelles : Refléter la façon dont les humains pensent déjà le travail (domaines → projets → tâches)
  6. Un jour est une fonctionnalité : Offrir aux idées un espace qui ne crée pas de pression

Questions fréquentes

Quelle est la différence entre « When » et « Deadline » dans Things 3 ?

« When » répond à « quand vais-je travailler dessus ? » (Today, This Evening, Someday, ou une date précise). « Deadline » répond à « quand cela doit-il être terminé ? » La plupart des applications de gestion de tâches confondent ces deux notions, alors qu'elles servent des objectifs différents. Une tâche peut être due vendredi (Deadline) mais vous prévoyez d'y travailler mercredi (When). Cette séparation vous permet d'organiser votre journée autour de vos intentions tout en suivant les échéances fermes.

Comment fonctionne la hiérarchie de Things 3 (Areas, Projects, Tasks) ?

Les Areas sont de grandes catégories de vie qui ne se terminent jamais (Travail, Personnel, Santé). Les Projects sont des objectifs finis avec un état d'achèvement clair (Lancer une application, Planifier des vacances). Les Tasks sont des éléments d'action individuels. Les Headings regroupent optionnellement les tâches au sein des projets. Cela reflète la façon dont les humains pensent naturellement le travail : des responsabilités continues contiennent des objectifs limités dans le temps, qui contiennent des actions discrètes.

Pourquoi Things 3 utilise-t-il si peu de couleur ?

Things réserve la couleur exclusivement au sens sémantique. L'interface est à 95 % neutre (noir, blanc, gris) de sorte que lorsqu'une couleur apparaît, elle communique quelque chose : le jaune signifie Today, le rouge signifie Deadline/En retard, l'indigo signifie Evening, le bleu marque les tags. Si tout était coloré, rien ne ressortirait. Cette retenue rend les couleurs significatives impossibles à manquer.

Qu'est-ce que « Someday » dans Things 3 et pourquoi est-ce précieux ?

Someday est un espace dédié aux tâches que vous souhaitez garder en mémoire sans qu'elles encombrent vos listes actives. Ce n'est pas un état d'échec — c'est une soupape de décompression. Les idées pour « un jour » (apprendre l'espagnol, lire ce livre) ont un espace sans générer de culpabilité quotidienne. Vous pouvez passer en revue Someday chaque semaine et promouvoir des tâches vers Today quand vous êtes prêt.

Comment fonctionne l'animation d'achèvement de Things 3 ?

Lorsque vous appuyez sur la case à cocher, le cercle se remplit dans le sens horaire (200 ms), puis une coche apparaît avec un léger rebond (spring animation), accompagnée d'un doux son « plink » et d'un retour haptique sur iOS. La ligne glisse ensuite vers l'extérieur (150 ms). La séquence complète dure environ 500 ms, mais ce timing délibéré crée une petite libération de dopamine qui rend l'achèvement des tâches gratifiant.


Ressources

  • Site web : culturedcode.com/things
  • Philosophie de design : Articles du blog de Cultured Code sur la simplicité
  • Raccourcis clavier : Menu Aide intégré (Cmd+/)
  • Intégration GTD : Comment Things s'articule avec la méthodologie Getting Things Done