Things 3: El arte de la simplicidad enfocada
Cómo Things 3 logró simplicidad a través de restricciones: jerarquía natural, separación When vs Deadline, diseño keyboard-first y contención de color significativa. Con patrones de implementación SwiftUI.
Things 3: El Arte de la Simplicidad Enfocada
“Creemos que lo simple es difícil.” — Cultured Code
Things 3 es frecuentemente llamado el gestor de tareas más hermoso jamás creado. Esa belleza surge de un enfoque implacable en lo que importa y la eliminación de todo lo demás.
Por Qué Things 3 Importa
Things 3 demuestra que las restricciones crean claridad. Mientras los competidores acumulan funciones (tareas recurrentes con 47 opciones, proyectos con dependencias y diagramas de Gantt), Things pregunta: ¿qué necesita realmente una persona para hacer las cosas?
Logros clave: - Alcanzó simplicidad visual sin sacrificar profundidad - Hizo que las fechas sean significativas (Today, Evening, Someday) - Creó una jerarquía que refleja cómo los humanos piensan sobre el trabajo - Demostró que la prioridad del teclado puede ser hermosa - Estableció el estándar para el diseño de apps nativas en macOS/iOS
Conclusiones Clave
- Separar “¿cuándo lo haré?” de “¿cuándo vence?” - El modelo de doble fecha de Things (When + Deadline) elimina la ambigüedad que afecta a la mayoría de las apps de tareas; la intención de programación y las fechas límite estrictas sirven propósitos diferentes
- Someday es una función, no un estado de fracaso - Darle a las ideas un hogar libre de presión previene la ansiedad de la bandeja de entrada; las tareas en Someday no saturan Today ni generan culpa
- Las Áreas nunca se completan, los Proyectos sí - Esta distinción refleja la cognición humana: “Trabajo” es un dominio continuo de la vida, “Lanzar v2.0” tiene una línea de meta; mezclar estos conceptos crea confusión
- Reservar el color para el significado - La interfaz de Things es 95% neutral (negro, blanco, gris); el color aparece solo para información semántica (amarillo=Today, rojo=Deadline, índigo=Evening)
- Completar tareas debe sentirse gratificante - La animación de la casilla de verificación (rellenar → marca de verificación → sonido) toma 500ms pero libera dopamina; completar tareas se vuelve intrínsecamente motivador
Principios Fundamentales de Diseño
1. La Jerarquía Natural
Things organiza el trabajo de la forma en que los humanos piensan sobre él: desde lo amplio (áreas de la vida) hasta lo específico (tareas individuales).
THINGS HIERARCHY:
Area (Work, Personal, Health) ← Categorías amplias de vida
│
└── Project (Launch App) ← Meta finita con tareas
│
└── Heading (Design) ← Agrupación opcional
│
└── To-Do ← Elemento accionable individual
│
└── Checklist ← Sub-pasos dentro de la tarea
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 │
└─────────────────────────────────────────────────────────────┘
La Idea Clave: Las Áreas nunca se completan (son categorías de vida). Los Proyectos se completan (entregar para el viernes). La distinción elimina la parálisis de planificación.
Modelo de Datos:
// 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. Cuándo, No Solo Vencimiento
Things separa “¿cuándo lo haré?” (Today, Evening, Someday) de “¿cuándo debe estar listo?” (Deadline). La mayoría de las apps mezclan estos conceptos.
TRADITIONAL DATE MODEL:
┌──────────────────────────────────────────────────────────────┐
│ Task: Write report │
│ Due: March 15th │
│ │
│ Problema: ¿El 15 de marzo es cuando lo haré, o cuando vence?│
│ Si es la fecha límite, ¿cuándo debería empezar? │
└──────────────────────────────────────────────────────────────┘
THINGS DATE MODEL:
┌──────────────────────────────────────────────────────────────┐
│ Task: Write report │
│ When: Today (Trabajaré en esto hoy) │
│ Deadline: March 15th (debe estar completo para entonces) │
│ │
│ Claridad: Dos preguntas separadas, dos respuestas separadas │
└──────────────────────────────────────────────────────────────┘
SMART LISTS (filtrado automático):
┌───────────────────┬─────────────────────────────────────────┐
│ [I] INBOX │ Capturas sin categorizar │
├───────────────────┼─────────────────────────────────────────┤
│ [*] 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 │
└───────────────────┴─────────────────────────────────────────┘
La Idea Clave: “Someday” no es un estado de fracaso—es una válvula de escape. Las ideas tienen un lugar sin saturar Today.
Implementación en 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. Teclado primero, compatible con ratón
Todo se puede operar completamente mediante el teclado con la eficiencia de la memoria muscular, y al mismo tiempo resulta intuitivo para los usuarios de ratón.
KEYBOARD SHORTCUTS (memorizable patterns):
NAVIGATION:
Cmd+1-6 Jump to smart lists (Inbox, Today, etc.)
Cmd+Up/Down Move between sections
Space Quick Look (preview task details)
TASK MANIPULATION:
Cmd+K Complete task
Cmd+S Schedule (When picker)
Cmd+Shift+D Set deadline
Cmd+Shift+M Move to project
Cmd+Shift+T Add tags
CREATION:
Space/Return Create new task (context-aware)
Cmd+N New task in Inbox
Cmd+Opt+N New project
Quick Entry (global hotkey):
Ctrl+Space Opens floating quick entry from anywhere
┌─────────────────────────────────────────────────┐
│ [ ] | │
│ New task appears wherever you are │
└─────────────────────────────────────────────────┘
La idea clave: Los atajos deben ser descubribles pero no obligatorios. La interfaz sugiere las teclas sin exigirlas.
Patrón CSS (indicadores de atajos):
.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;
}
/* Show on hover */
.action-button:hover .shortcut-hint {
opacity: 1;
}
/* Always visible when focused via keyboard */
.action-button:focus-visible .shortcut-hint {
opacity: 1;
}
Entrada rápida 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. Moderación visual
Things usa el color con mesura, reservándolo únicamente para transmitir significado. La mayor parte de la interfaz es neutral, lo que permite que los elementos con color destaquen.
USO DEL COLOR EN THINGS:
Neutral (95% de la interfaz):
├── Fondo: Blanco puro / Gris profundo
├── Texto: Negro / Blanco
├── Bordes: Gris muy sutil
└── Iconos: Monocromáticos
Color con significado (5%):
├── Amarillo [*] = Hoy
├── Rojo [!] = Fecha límite / Vencido
├── Azul [#] = Etiquetas (asignadas por el usuario)
├── Verde [x] = Animación de completado
└── Índigo [E] = Noche
EJEMPLO VISUAL:
┌─────────────────────────────────────────────────────────────┐
│ HOY [*] (amarillo) │
├─────────────────────────────────────────────────────────────┤
│ ( ) Escribir documentación │
│ Nombre del proyecto - [!] Vence mañana (rojo) │
│ │
│ ( ) Revisar pull requests │
│ [#] trabajo (etiqueta azul) │
│ │
│ ( ) Llamar al dentista │
│ [E] Esta noche (insignia índigo) │
└─────────────────────────────────────────────────────────────┘
Observa: La mayor parte del texto es negro. El color solo aparece donde transmite significado.
La idea clave: Cuando todo es colorido, nada destaca. Reserva el color para la información.
Patrón CSS:
:root {
/* Paleta neutral (la mayor parte de la UI) */
--surface-primary: #ffffff;
--surface-secondary: #f5f5f7;
--text-primary: #1d1d1f;
--text-secondary: #86868b;
--border: rgba(0, 0, 0, 0.08);
/* Colores semánticos (uso moderado) */
--color-today: #f5c518;
--color-deadline: #ff3b30;
--color-evening: #5856d6;
--color-complete: #34c759;
--color-tag: #007aff;
}
/* Fila de tarea - mayormente neutral */
.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);
}
/* Color solo para significado */
.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. Completado gratificante
La animación de completado ofrece una recompensa, no una simple confirmación. Un satisfactorio sonido "plink" y una suave animación de verificación hacen que completar tareas se sienta bien.
SECUENCIA DE COMPLETADO:
Fotograma 1: ○ Título de la tarea
Fotograma 2-4: ◔ (el círculo se llena en sentido horario)
Fotograma 5-7: ● (completamente lleno, pausa breve)
Fotograma 8-10: ✓ (aparece la marca, escala ligeramente hacia arriba)
Fotograma 11+: La fila se desliza, la lista cierra el espacio
Audio: Suave "plink" en el fotograma 7-8 (momento de completado)
Háptico: Toque ligero en iOS al completar
Implementación en 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
}
}
}
Patrones Transferibles
Patrón 1: Detalles Progresivos de Tareas
Los detalles de las tareas se expanden en línea en lugar de navegar a una vista separada.
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)
}
}
Patrón 2: Entrada en Lenguaje Natural
Things interpreta lenguaje natural para establecer fechas: “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;
}
Patrón 3: Drag and Drop Basado en Listas
La reordenación es manipulación directa con retroalimentación visual clara.
.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;
}
Lecciones de Diseño
- Separar responsabilidades: "¿Cuándo voy a hacer esto?" vs "¿Cuándo vence?" son preguntas diferentes
- Moderación con el color: Reservar el color para transmitir significado; lo neutro es el estado predeterminado
- Completar es la recompensa: Hacer que terminar tareas se sienta gratificante
- El teclado acelera, el ratón da la bienvenida: Diseñar para ambos sin comprometer ninguno
- Jerarquías naturales: Reflejar cómo los humanos ya piensan sobre el trabajo (áreas → proyectos → tareas)
- Algún día es una funcionalidad: Dar a las ideas un lugar que no genere presión
Preguntas Frecuentes
¿Cuál es la diferencia entre "When" y "Deadline" en Things 3?
"When" responde a "¿cuándo voy a trabajar en esto?" (Today, This Evening, Someday, o una fecha específica). "Deadline" responde a "¿cuándo debe estar completado?" La mayoría de las aplicaciones de tareas combinan estos conceptos, pero cumplen propósitos diferentes. Una tarea puede vencer el viernes (Deadline) pero planeas trabajar en ella el miércoles (When). Esta separación te permite planificar tu día en torno a intenciones mientras sigues controlando las fechas límite estrictas.
¿Cómo funciona la jerarquía de Things 3 (Areas, Projects, Tasks)?
Las Areas son categorías amplias de la vida que nunca se completan (Trabajo, Personal, Salud). Los Projects son objetivos finitos con un estado final claro (Lanzar App, Planificar Vacaciones). Las Tasks son elementos individuales accionables. Los Headings agrupan opcionalmente tareas dentro de los proyectos. Esto refleja cómo los humanos piensan naturalmente sobre el trabajo: las responsabilidades continuas contienen metas con límite de tiempo, que a su vez contienen acciones concretas.
¿Por qué Things 3 usa tan poco color?
Things reserva el color exclusivamente para el significado semántico. La interfaz es 95% neutra (negro, blanco, gris) para que cuando aparece el color, comunique algo: amarillo significa Today, rojo significa Deadline/Overdue, índigo significa Evening, azul marca los tags. Si todo fuera colorido, nada destacaría. La moderación hace que los colores significativos sean imposibles de pasar por alto.
¿Qué es "Someday" en Things 3 y por qué es valioso?
Someday es un espacio dedicado para tareas que quieres recordar pero que no quieres que abarroten tus listas activas. No es un estado de fracaso, es una válvula de escape para la presión. Las ideas para "algún día" (aprender español, leer ese libro) tienen un lugar sin generar culpa diaria. Puedes revisar Someday semanalmente y promover tareas a Today cuando estés listo.
¿Cómo funciona la animación de completado de Things 3?
Cuando tocas el checkbox, el círculo se llena en sentido horario (200ms), luego aparece una marca de verificación con un ligero rebote (spring animation), acompañado de un suave sonido "plink" y retroalimentación háptica en iOS. La fila luego se desliza hacia fuera (150ms). La secuencia completa dura aproximadamente 500ms, pero este timing deliberado crea un pequeño golpe de dopamina que hace que completar tareas se sienta gratificante.
Recursos
- Sitio web: culturedcode.com/things
- Filosofía de diseño: Publicaciones del blog de Cultured Code sobre simplicidad
- Atajos de teclado: Menú de Ayuda integrado (Cmd+/)
- Integración con GTD: Cómo Things se mapea a la metodología Getting Things Done