[Development Diary Day1] Me gusta tanto Captio que quería hacer "esa experiencia" de nuevo
Esta página es una traducción automática del original en japonés. Leer el original en japonés
¿Conoces la aplicación llamada Captio?
Escribe una nota y pulsa el botón de enviar. Eso es todo lo que necesitas para recibir una nota a tu dirección de correo electrónico. En el momento en que lo inicies, se mostrará el teclado, y si escribes, envíalo, y si lo envías, desaparecerá. No hay funciones innecesarias en absoluto. Era una gran cantidad de aplicación.
Pero "eso es todo" fue increíblemente cómodo.
La idea que se me ocurrió en el tren de cercanías, la tarea que de repente me vino a la mente durante la reunión y la palabra que pasó por mi mente antes de irme a la cama - Captio inmediatamente los entregó todos a mi buzón. No hay problema para abrir la aplicación de notas, seleccionar una carpeta y añadir un título. La experiencia de "fricción cero" era algo que no podías dejar ir una vez que lo sabías.
El servicio Captio ha sido cancelado.
Probé varias aplicaciones alternativas, pero todas estaban lejos de esa experiencia. Inicio lento, hay elementos innecesarios en la interfaz de usuario, y las notas permanecen después de la transmisión - puede parecer una diferencia trivial, pero la esencia de la experiencia creada por Captio fue la acumulación de tales "cosas triviales".
Así que decidí hacerlo yo mismo. Vuelve a experimentar el Captio.
Este es el registro de desarrollo de "SimpleMemo" Day1. En este artículo, desmontaremos la experiencia Captio y registraremos todos los juicios técnicos sobre cómo reproducirla en una pila de tecnología moderna.
1. "Qué" de Captio fue bueno - Intenté desmontar la experiencia
Para verbalizar la experiencia de Captio, traté de dividirla en dos elementos de 3.
Elemento 1: Lanzamiento instantáneo a entrada
Cuando inicias Captio, el teclado se muestra inmediatamente y es posible introducir texto. La pantalla de inicio se muestra después de la pantalla de bienvenida, y no hay ningún procedimiento para presionar el botón de creación de notas desde allí. Inicio = Entrada. Llamamos a esto "tiempo desde el inicio hasta la entrada de texto" Time-to-Text. El Captio fue menos de 0.5 segundos en experiencia.
Elemento 2: Borrado instantáneo después del envío (Bojado instantáneo después del envío)
Cuando presiones el botón de enviar, la pantalla se borrará inmediatamente. " Enviando..." No tengo tiempo para esperar mientras miro fijamente la barra de progreso. En el momento en que la nota que escribiste desaparece, puedes pasar a la siguiente acción con una sensación de seguridad de "Oh, pude enviarla". Llamamos a esto "el tiempo de presionar el botón de enviar a la interfaz de usuario se borra" Send-to-Reset. El objetivo es vaciar la pantalla antes de que sientas que has estado esperando.
Elemento 3: Nada innecesario (Nada innecesario)
Función de carpeta, función de etiqueta, texto enriquecido, soporte de rebajas - ninguno de ellos estaba disponible en Captio. Solo hay un campo de entrada de texto y un botón de enviar. Esta limpieza acercó la carga cognitiva a cero. Una aplicación que no tiene que pensar en qué hacer. Era Captio.
Este elemento 3 se reproduce en el desarrollo moderno iOS. Esa es la misión de "SimpleMemo".
2. Arquitectura: ¿Por qué es UIKit "no SwiftUI"?
En el desarrollo de iOS en 2026, puede parecer extraño elegir UIKit en lugar de SwiftUI. Sin embargo, nuestro objetivo de máxima prioridad era claro: Tiempo para enviar mensajes de texto 500ms o menos.
Reevaluación `body` de SwiftUI, inicialización `StateObject`, resolución `@Environment` - estos gastos generales se pueden ignorar en el desarrollo normal de aplicaciones, pero no se pueden ignorar para lograr el "inicio y la entrada". Quiero centrarme en el campo de texto rápidamente, incluso con 1ms.
Lo que se adoptó allí es la construcción directa de UIKit sin usar Storyboard.
Crea una ventana con SceneDelegate y establece el ComposeViewController directamente en el rootViewController. Omita incluso el proceso de análisis de Storyboard.
// SceneDelegate.swift
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
window.rootViewController = ComposeViewController()
window.makeKeyAndVisible()
self.window = window
}
y con `viewDidAppear` en ComposeViewController, enfóquese instantáneamente en la vista de texto.
// ComposeViewController.swift
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
textView.becomeFirstResponder()
}
Solo con este código 2, el camino desde el inicio hasta la pantalla del teclado es el más corto.
El objetivo es 500ms o menos. En el registro medido más tarde por grabación de pantalla (2026 8 11, iPhone 16e, inicio en caliente), fue 0.4 segundos desde el toque del icono hasta la pantalla del teclado. En SwiftUI, es difícil controlar el tiempo de `@FocusState` y la reevaluación del cuerpo en detalle, y es difícil empaquetar este camino (los dos no se midieron y compararon en las mismas condiciones).
UIKit puede ser "viejo". Sin embargo, "rápido". En este punto 1, UIKit sigue siendo la mejor opción.
3. Un diseño que se da cuenta de que "desaparece cuando se envía" con un solo toque
Existe el juicio de diseño más importante para reproducir la experiencia de "desaparece cuando se envía" del Captio. Es para separar completamente la animación de transmisión y la limpieza de la interfaz de usuario de la respuesta de red.
El flujo de transmisión de las aplicaciones comunes es el siguiente:
- Presione el botón de enviar
- Mostrar indicador de carga
- Esperando una respuesta del servidor
- Borrar la interfaz de usuario si tiene éxito, mostrar el error si falla
En este flujo, el retraso de red se convierte en el retraso de la interfaz de usuario tal como está. Nuestro flujo es así:
- Presione el botón de enviar
- inmediatamenteborrar interfaz de usuario (animación: 0.25 segundos)
- Transmisión de red en segundo plano
- Éxito → Eliminar silenciosamente de la Outbox / Fallado → Reintentar en segundo plano
// ComposeViewController.swift
private func performSendAnimation() {
UIView.animate(withDuration: 0.25,
delay: 0,
options: .curveEaseOut) {
self.textView.alpha = 0
self.subjectField.alpha = 0
} completion: { _ in
self.textView.text = ""
self.subjectField.text = ""
self.textView.alpha = 1
self.subjectField.alpha = 1
self.textView.becomeFirstResponder()
}
}
Para los usuarios, en el momento en que se presiona el botón de enviar, la nota "desaparece" y pueden escribir la siguiente nota. El éxito o el fracaso de la red se maneja en un lugar invisible para el usuario.
"¿Pero qué pasa si la transmisión falla?" - La respuesta a esa pregunta es la arquitectura de la Outbox en la siguiente sección.
4. Arquitectura de la Outbox para evitar derrames
Separar la interfaz de usuario de transmisión de la respuesta de red significa que puede ocurrir una situación en la que "la interfaz de usuario se borró, pero la transmisión de correo electrónico realmente falló". Esto es absolutamente inaceptable. La nota que el usuario pensó que fue "envió" en realidad no se envió, esto es una pérdida fatal de confianza para la aplicación de notas.
El Patrón de Outbox se adoptó allí.
El flujo de envío de mensajes es el siguiente:
- Toque el botón de enviar: Cifre el mensaje AES-GCM y guárdelo en la Outbox (almacenamiento local)
- UI clear: Borra la pantalla al instante con performSendAnimation
- Transmisión en segundo plano: Extrae un mensaje de la Outbox y envíelo a Relay API
- Éxito: Eliminar mensaje de la Outbox
- Fallo: Déjalo esperando la transmisión y vuelve a enviarlo más tarde
- Offline: Supervise la conexión con NWPathMonitor y vuelva a enviarla automáticamente cuando se restablezca
Lo importante es guardarlo en la Outbox antes de transmitirlo a la red. Como resultado, incluso si la aplicación se bloquea repentinamente o el dispositivo se apaga, el mensaje escrito en la Outbox permanecerá.
Los mensajes almacenados en la Outbox están protegidos por el cifrado AES-GCM utilizando CryptoKit. La clave de cifrado se almacena en el llavero y no se puede acceder a ella desde fuera del sandbox de la aplicación.
// OutboxManager.swift
func enqueue(message: OutboxMessage) throws {
let key = try KeychainHelper.getOrCreateSymmetricKey()
let sealedBox = try AES.GCM.seal(
message.plainData,
using: key
)
let encrypted = EncryptedOutboxEntry(
id: message.id,
sealedData: sealedBox.combined!,
createdAt: Date(),
retryCount: 0
)
try persistenceStore.save(encrypted)
}
Además, debido a que todo lo implementas por ti mismo, la dependencia de las bibliotecas externas es cero. CryptoKit, NWPathMonitor, URLSession - todos completados con el marco Apple original. Estar libre del control de versiones de la biblioteca de terceros y la auditoría de seguridad es una gran ventaja para los equipos pequeños.
5. Retransmisión API: Envío que no depende de la cooperación de OAuth en el buzón
El mecanismo de transmisión de Captio no se revela oficialmente. La razón del anuncio de terminación del desarrollador es que ya no puede continuar pasando tiempo para mantener la estabilidad y seguridad de la aplicación, y el método de transmisión no se cita como una razón. Sin embargo, en general, el envío confiando en el correo electrónico externo API se ve fácilmente afectado por los cambios en los requisitos del proveedor y las restricciones de acceso.
Elegimos la configuración Cloudflare Workers + Resend API.
Cloudflare Workers es una plataforma informática de borde que ejecuta código en el centro de datos más cercano a los usuarios de todo el mundo. Las solicitudes de Japón se procesan en el borde de Japón, y las solicitudes de los Estados Unidos se procesan en el borde de los Estados Unidos. Esto casi elimina el problema del arranque en frío en comparación con el sin servidor convencional (AWS Lambda, etc.).
Para enviar correo se usa la API de Resend. Resend es una API de envío de correo para desarrolladores.
Flujo de autenticación de correo electrónico
Cuando el usuario establece la dirección de correo electrónico de destino, la autenticación de correo electrónico se realiza con el código de autenticación 6 dígitos. Esto evita el envío no autorizado a direcciones de correo electrónico de terceros.
Límite de velocidad multicapa
Se implementan restricciones de velocidad multicapa para evitar el uso no autorizado.
// Cloudflare Worker - Rate Limit Configuration
const RATE_LIMITS = {
devicePerMinute: 30, // 1デバイスあたり1分30回
devicePerDay: 200, // 1デバイスあたり1日200回
ipPerHour: 120, // 1IPあたり1時間120回
globalPerDay: 300 // サービス全体で1日300回(調整可能)
};
Al limitar la velocidad en la capa 3 en un dispositivo, IP y global, puede bloquear fugas y ataques de botnet en un solo dispositivo.
2026-09 Postscript: El límite superior para cada terminal y el límite superior del día total 1 se eliminaron en 2026 6. El límite máximo de transmisión actual es el límite superior por 1 horas por dirección IP.
Garantía de igualdad de poder
Para evitar la transmisión duplicada debido a la inestabilidad de la red, asigne un UUID a cada mensaje y realice una comprobación de duplicado en el lado de Relay API. Si se reenvía el mismo mensaje UUID, después del tiempo 2, devolverá una respuesta exitosa sin enviar un correo electrónico. Como resultado, incluso si se combina con el mecanismo de reintento del patrón de Outbox, el usuario no recibirá la misma nota dos veces.
Actualización de 2026-10: el servidor de envío actual no comprueba por sí mismo los duplicados. Pasa el identificador del mensaje como Idempotency-Key en la solicitud a Resend. Resend recuerda la misma clave durante 24 horas y devuelve la respuesta anterior ante una solicitud con el mismo contenido. Así se evita enviar de nuevo la misma nota durante 24 horas. Consulta el diseño de la Relay API.
6. Lo que hice hoy: Borrar la pequeña sensación de incomodidad uno por uno
Gran parte del trabajo de implementación de Day1 se pasó revisando el diálogo de calificación por estrellas. La evaluación de App Store es la línea de vida de la aplicación, pero no busque la evaluación de una manera que perjudique la experiencia del usuario.
Rediseño de las condiciones de visualización
Las condiciones de visualización del cuadro de diálogo de clasificación por estrellas se establecen de la siguiente manera:
- Mostrado cada 100: Mostrar solo a los usuarios que han utilizado la aplicación lo suficiente
- Intervalo mínimo de 14 días: Más de un 14 ha pasado desde la última pantalla
- No volver a mostrar durante 30 días tras cerrarlo: si el usuario lo cierra, no se muestra de nuevo durante 30 días
Corrección del área de toque del icono de la estrella
Apple Las directrices de interfaz humana recomiendan que el tamaño mínimo del objetivo táctil sea 44x44pt. El área de toque del icono de la estrella no cumpía con este estándar, por lo que se corrigió.
// StarRatingView.swift
private func createStarButton() -> UIButton {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.widthAnchor.constraint(greaterThanOrEqualToConstant: 44),
button.heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
])
return button
}
Corrección de distorsión del icono de estrella
UIStackView. `contentMode` se estableció en `.scaleAspectFit`, y la distribución de StackView se cambió a `.equalSpacing`.
Flujo después de la evaluación
- 5 estrellas: Transición a la pantalla de revisión de App Store (`SKStoreReviewController`)
- 4 estrellas o menos: Muestra silenciosamente el brindis de "Gracias" y termina. No conduzca a reseñas negativas a App Store.
7. Se ha resuelto la inconsistencia del estado de visualización del historial
Noté un error en la pantalla del historial de transmisión (Historial). Había un mensaje de que el estado seguía siendo "Enviando" a pesar de que la transmisión estaba completa.
Como resultado de la investigación de la causa, se descubrió que había un problema con la lógica de actualización de estado. En implementaciones anteriores, el contenido de texto del mensaje se utilizó como clave para actualizar el estado. Sin embargo, si envía una nota con el mismo contenido más de una vez, una búsqueda basada en texto no puede identificar el mensaje correcto.
La corrección fue sencilla y clara. Se cambió la búsqueda basada en texto a Actualización de estado basada en ID. A cada mensaje se le asigna un UUID al guardar la Outbox, así que use este ID para actualizar el estado del historial.
// HistoryManager.swift
// Before: テキストベースの検索(バグの原因)
// func updateStatus(forText text: String, status: SendStatus)
// After: IDベースのステータス更新
func updateStatus(forMessageId id: UUID, status: SendStatus) {
guard let index = entries.firstIndex(where: { $0.messageId == id }) else {
return
}
entries[index].status = status
persistEntries()
}
Con esta corrección, no importa cuántas veces envíes una nota con el mismo contenido, el estado de cada una ahora se muestra correctamente.
8. Obsesión panorámica con la privacidad
La información privada está escrita en la aplicación de notas. Contraseñas, preocupaciones personales, ideas de negocios: es imposible predecir lo que escribirán los usuarios. Es por eso que la protección de la privacidad no es "suficiente", pero "excesiva" es la adecuada.
Superposición de privacidad en el conmutador de aplicaciones
En el conmutador de aplicaciones iOS, la pantalla de la aplicación se muestra como una miniatura. Muestra una superposición de privacidad cuando la aplicación migra al fondo para que el contenido de la nota no se pueda ver desde la pantalla del conmutador de aplicaciones.
2026-09 Postscript: Esta protección no está habilitada en la aplicación actual (el procesamiento de la pantalla permanece en la fuente, pero no se llama). La nota que se muestra puede permanecer en la imagen del conmutador de aplicaciones. Para obtener más información, consulte el artículo sobre "Diseño de privacidad".
Sesión URL efímera
La sesión de URL normal almacena caché, cookies e información de autenticación en el disco. Al usar `URLSessionConfiguration.ephemeral`, nos aseguramos de que esta información no permanezca en el disco en absoluto. Esto es para evitar que el contenido de la nota permanezca en el almacenamiento como una caché de comunicación.
Tracos inofensivos
Tenga cuidado con la depuración de registros bajo desarrollo. " El proceso aparentemente inofensivo de "proportar el contenido de la nota enviada al registro" puede ser un agujero de seguridad. Nuestra política es clara:
- El contenido de la nota no está registrado en absoluto, incluso si es una compilación de DEBUG
- El registro de errores no utiliza `localizedDescription`, solo se registra `type(of: error)`
- Razón: Porque `localizedDescription` puede contener la entrada del usuario
// NetworkManager.swift
// BAD: エラーの詳細をログに出力
// Logger.error("Send failed: \(error.localizedDescription)")
// GOOD: エラーの型のみをログに出力
Logger.error("Send failed: \(type(of: error))")
Puede parecer paranoico. Sin embargo, creo que este grado de paranoia es necesario para confiar y usar la aplicación de notas.
9. Soporte de idioma 10: Quiero entregar a usuarios de Captio en todo el mundo
Captio tenía usuarios en todo el mundo, principalmente en países de habla inglesa. Para entregar "SimpleMemo" a los antiguos usuarios, el lenguaje 10 fue compatible desde la primera versión.
Idiomas admitidos: japonés, inglés, español, francés, alemán, italiano, portugués, coreano, chino (simplificado), árabe.
2026-09 Epospítula: En la notación actual App Store, los idiomas admitidos son japonés, inglés, español, francés, alemán, portugués, ruso, chino (simplificado), chino (tradicional) y árabe 10. Los italianos y coreanos de la lista anterior no están incluidos en la notación actual.
Especialmente El soporte árabe de RTL (de derecha a izquierda) fue un desafío técnico. No solo la dirección del texto, sino también todo el diseño de la interfaz de usuario debe reflejarse. Al utilizar constantemente la restricción "liderar"/`trailing" de Auto Layout, se da cuenta de un diseño natural incluso en un entorno RTL.
Cambio de idioma en la aplicación
Además del cambio de idioma en la aplicación después de iOS 16, también implementa su propio mecanismo de cambio de idioma. Cuando se cambia el idioma, el ViewController se reemplaza con la transición `cross-dislove` para proporcionar una experiencia de cambio fluida.
// LanguageManager.swift
func applyLanguageChange() {
guard let window = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first?.windows.first else { return }
let newVC = ComposeViewController()
newVC.view.frame = window.bounds
UIView.transition(with: window,
duration: 0.3,
options: .transitionCrossDissolve,
animations: {
window.rootViewController = newVC
})
}
10. El aprendizaje de hoy: El producto es un diseño de "sentido de seguridad"
Después de terminar Day1, lo que creo es que la esencia de este producto es un diseño de "sentido de seguridad".
Resuma los números técnicos:
- Tiempo de texto: 500ms o menos (objetivo)
- Send-to-Reset: Brevedad que no se siente esperando (obo)
- Mensaje derramado: Prevenir con el patrón de la Outbox
- Cifrado: AES-GCM (CryptoKit)
- Dependencia de la biblioteca externa: Cero
Sin embargo, todas estas cifras se resumen en un objetivo llamado "seguridad".
Inicio rápido → "Puedes escribir inmediatamente en cualquier momento" tranquilidad.
Desaparece cuando lo envías → "Procesado" sensación de seguridad.
Contramedidas contra derrames → "No desaparecerá sin transmisión" tranquilidad.
Cifrado → "Nadie puede ver" tranquilidad.
El Captio fue amado no por su excelente función. Porque la experiencia de "usar con tranquilidad" fue completamente pulida. Quiero hacerme cargo de eso.
11. La próxima vez (Day2) vista previa
Day2 planea trabajar en los siguientes temas:
- Sofisticación de la animación de transmisión: Desde el desvanecimiento del segundo 0.25 actual a una animación más cómoda
- Optimización del tiempo de las actualizaciones de estado del historial: Comparación de actualizaciones en tiempo real con actualizaciones por lotes
- ¿De verdad necesitas un brindis exitoso? : Consideración de la opción "no mostrar" para una retroalimentación de transmisión exitosa
Apuntando a la elegancia de "nada" de Captio, continúe con Day2.
Referencias
- Apple Desarrollador — Documentación de UIKit — Referencia oficial al marco UIKit. Se utiliza para construir la interfaz de usuario de SimpleMemo
- Desarrollador Apple — Documentación de CryptoKit — Utilizado para implementar la protección de datos de Outbox con cifrado AES-GCM
- Documentación de los trabajadores de Cloudflare — Aducido como una plataforma sin servidor para la retransmisión de correo API
- Chris Richardson — Patrón: Outbox transaccional (microservices.io) — Un patrón de diseño en el que el mensaje que se enviará se escribe primero en la mesa de espera (Outbox) y se envía mediante otro proceso. SimpleMemo utiliza la misma forma de pensar en el terminal
- RFC 5321 — Protocolo simple de transferencia de correo (SMTP) — Especificación estándar del protocolo de transmisión de correo