Saltar al contenido

Cómo crear un formulario de contacto en iOS con Swift y SwiftUI

Un formulario de contacto en SwiftUI que te envía cada mensaje a través de tu propio buzón – sin backend y sin contraseña SMTP en la app. Usa el paquete de Swift de Frontmail.

Actualizado:

El vídeo está en inglés; los pasos de abajo son el tutorial completo.

Vas a añadir un formulario de contacto a una app iOS con SwiftUI y el paquete de Swift de Frontmail. La app solo guarda una clave pública; Frontmail envía el correo a través de tu buzón, así que no hay contraseña SMTP en la app ni ningún servidor que mantener.

Necesitas: una cuenta gratuita de Frontmail, un proyecto de Xcode con SwiftUI y unos quince minutos.

1. Conecta tu buzón y crea la plantilla

  1. Servicios de correo → Añadir servicio: conecta Gmail, Outlook, SMTP o un proveedor transaccional, activa Marcar como predeterminado y pulsa Verificar y guardar.
  2. Plantillas → Desde la galería → Formulario de contacto → Usar plantilla. Parámetros obligatorios: name, email, message; opcionales: phone, topic.
  3. Copia el ID de la plantilla (tpl_…) de la pestaña Fragmentos.

2. Permite las apps móviles

  • Seguridad → Apps móviles → Permitir apps móviles → Guardar. Las apps no envían cabecera Origin, así que hasta entonces la API responde origin_not_allowed.
  • Plantilla → Ajustes → Protección → Exigir CAPTCHA (Turnstile). Las apps usan el widget compartido de Turnstile de Frontmail; no necesitas claves de Cloudflare.

3. Añade el paquete

En Xcode elige File → Add Package Dependencies…, introduce https://github.com/frontmail-dev/frontmail-swift y añade la librería Frontmail al target de tu app. Después copia TurnstileView de la sección Turnstile de la documentación del SDK de iOS a tu proyecto: un pequeño UIViewRepresentable alrededor de WKWebView que es tuyo.

4. Crea el formulario

import SwiftUI
import Frontmail

let frontmail = try! Frontmail(publicKey: "pk_…") // create once, share

struct ContactMessage: Encodable {
    let name: String
    let email: String
    let message: String
}

struct ContactView: View {
    @State private var name = ""
    @State private var email = ""
    @State private var message = ""
    @State private var widget: Turnstile.Widget?
    @State private var token: String?
    @State private var resetID = 0
    @State private var info = ""

    var body: some View {
        Form {
            TextField("Name", text: $name)
            TextField("Email", text: $email)
                .keyboardType(.emailAddress)
                .textInputAutocapitalization(.never)
            TextField("Message", text: $message, axis: .vertical)
                .lineLimit(4...8)
            if let widget {
                TurnstileView(widget: widget, size: .flexible, resetID: resetID) { event in
                    if case let .token(t) = event { token = t } else { token = nil }
                }
                .frame(height: Turnstile.Size.flexible.recommendedHeight)
            }
            Button("Send") { Task { await send() } }
                .disabled(token == nil)
            Text(info)
        }
        .task { widget = try? await Turnstile.widget(client: frontmail) }
    }

    private func send() async {
        guard let widget, let token else { return }
        defer { self.token = nil; resetID += 1 } // a token works only once
        do {
            _ = try await frontmail.send(
                templateID: "tpl_…",
                params: ContactMessage(name: name, email: email, message: message),
                options: SendOptions(turnstileToken: token, turnstileKey: widget.turnstileKey)
            )
            info = "Thank you! We’ll get back to you soon."
        } catch let error as FrontmailError {
            info = error.message
        } catch {
            info = error.localizedDescription
        }
    }
}

Cualquier struct Encodable sirve como parámetros, así que el compilador comprueba los nombres de los campos por ti. Los errores de red y las respuestas 5xx se reintentan con una misma clave de idempotencia: nada de correos duplicados con una conexión inestable.

5. Pruébalo

Ejecuta la app en el simulador o en un dispositivo, rellena el formulario y pulsa Send. El correo llega a tu bandeja y aparece en Historial en el panel.

  • origin_not_allowed – Permitir apps móviles está desactivado o no se guardó.
  • captcha_required – todavía no hay token de Turnstile; Send sigue desactivado hasta que termine la verificación.
  • invalid_template_params – un campo obligatorio está vacío o el correo no es válido.

Cuando se te acaban los créditos, el mensaje se acepta con el estado .held y se envía automáticamente después de recargar: no se pierde nada.

Próximos pasos

  • Confirma el mensaje al usuario con la plantilla Respuesta automática: gracias por tu mensaje de la galería.
  • Envía SendOptions(locale: Locale.preferredLanguages.first) para usar una variante de idioma de la plantilla en el idioma del usuario.
Todos los tutoriales