Formatear valores en SwiftUI: menos String, más intención
Arturo Rivas Arias
🧩 En SwiftUI es muy tentador convertirlo todo a String antes de pintarlo en pantalla. Un número se interpola, una fecha se pasa por un DateFormatter, una cantidad se concatena con un símbolo de moneda y un porcentaje se multiplica a mano. Funciona, sí, pero también es una forma rápida de llenar la interfaz de pequeños futuros problemas: separadores decimales incorrectos, indicador de divisa mal colocados, fechas que resultan poco naturales o campos de texto que aceptan valores imposibles.
✨ La alternativa moderna es expresar el formato como parte del componente de la vista. SwiftUI permite usar FormatStyle directamente en Text, TextField y otros componentes, de forma que el valor conserva su tipo real —Int, Double, Date, Measurement, etc.— y la capa de la vista decide cómo presentarlo. No es solo una mejora estética: es una forma de hacer que la interfaz sea aún más declarativa, más localizada (en términos de idioma) y menos propensa a errores.
🌍 El punto importante está precisamente en la localización. Mostrar 1234.56 no significa lo mismo en todos los países. En España esperamos algo como 1.234,56, mientras que en otros entornos se usará 1,234.56. Si el formato se construye a mano, la app acaba imponiendo una convención que quizá no coincide con la configuración del usuario. Con FormatStyle, el frameowrk Foundation aplica las reglas adecuadas según el Locale activo.
struct RevenueSummaryView: View {
let activeSubscriptions = 12450
let monthlyRevenue = 39284.75
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Salida con es-ES: 12 mil
Text(activeSubscriptions, format: .number.notation(.compactName))
.font(.largeTitle.bold())
// Salida con es-ES: 39.284,75 €
Text(monthlyRevenue, format: .currency(code: "EUR"))
.font(.title3)
}
}
}
🔢 Los números son el caso más inmediato. .number permite controlar agrupación, signo, notación compacta, notación científica y precisión decimal. Esto evita tener que tirar de String(format:), que además no encaja especialmente bien con SwiftUI ni con valores localizados.
🧠 La diferencia frente a construir el texto manualmente sí es importante. Si escribes algo como Text("\(monthlyRevenue) €"), el valor deja de tratarse como una cantidad monetaria y pasa a ser simplemente una cadena cualquiera. SwiftUI ya no sabe que representa dinero, solo ve texto. En cambio, con .currency(code: "EUR") el valor sigue siendo un Double hasta el último momento. Eso permite que Foundation aplique automáticamente las reglas correctas para el idioma y la región del usuario: posición del símbolo, separadores decimales, agrupación de miles o incluso el espaciado típico de cada Locale.
🌍 Por ejemplo, la misma cantidad podría mostrarse como 39.284,75 € en España o como €39,284.75 en Estados Unidos sin cambiar una sola línea de código. El formato describe el significado del dato —esto es dinero en euros— y el sistema decide cómo representarlo visualmente según el contexto cultural del usuario. La clave no es solo “formatear bonito”, sino conservar la semántica del dato el máximo tiempo posible. Mientras SwiftUI siga viendo una cantidad monetaria y no una String, puede adaptar automáticamente la representación, el parseo y futuras interacciones de la interfaz.
struct SensorReadingView: View {
let noiseLevel = 67.438
let samples = 15320
var body: some View {
VStack(alignment: .leading) {
Text(noiseLevel, format: .number.precision(.fractionLength(1)))
Text(samples, format: .number.grouping(.automatic))
Text(samples, format: .number.notation(.scientific))
}
}
}
📐 La precisión no debe confundirse con cortar decimales. Cuando usas .precision(.fractionLength(1)), el valor se redondea para su presentación. El dato original sigue siendo el mismo; lo que cambia es cómo se muestra. Esa separación entre el modelo y su representación es justo lo que conviene mantener en interfaces reactivas.
🧾 En formularios, TextField puede trabajar directamente con valores que no son String. En lugar de guardar texto temporal, convertirlo, validarlo y sincronizarlo con otro estado, puedes enlazar el campo a un Double, un Int o una cantidad porcentual usando el inicializador con value y format.
struct InvoiceEditorView: View {
@State private var hours: Double = 6.5
@State private var hourlyRate: Double = 55
@State private var taxRate: Double = 0.21
private var subtotal: Double { hours * hourlyRate }
private var total: Double { subtotal * (1 + taxRate) }
var body: some View {
Form {
TextField("Horas", value: $hours, format: .number.precision(.fractionLength(0...2)))
.keyboardType(.decimalPad)
TextField("Precio por hora", value: $hourlyRate, format: .currency(code: "EUR"))
.keyboardType(.decimalPad)
TextField("IVA", value: $taxRate, format: .percent.precision(.fractionLength(0...2)))
.keyboardType(.decimalPad)
LabeledContent("Total") {
Text(total, format: .currency(code: "EUR"))
}
}
}
}
⚠️ Aquí hay un matiz importante: no todos los formatos sirven igual para mostrar que para introducir datos. Para que un TextField pueda convertir texto de vuelta al tipo original, el formato debe ser parseable. Por eso SwiftUI usa estilos capaces de transformar en ambos sentidos: del valor al texto y del texto al valor. En la práctica, esto funciona muy bien para números, porcentajes y monedas, pero no conviene asumir que cualquier formato complejo será igual de cómodo como entrada.
🧮 También hay que recordar que el tipo del estado manda. Si el TextField está enlazado a un Int, la entrada acabará comportándose como un entero. Si está enlazado a un Double, podrá representar decimales. Esto parece obvio, pero evita una fuente clásica de bugs: tratar la validación como un problema del teclado cuando en realidad es un problema del modelo.
struct StockAdjustmentView: View {
@State private var units: Int = 12
@State private var weight: Double = 2.75
var body: some View {
Form {
TextField("Unidades", value: $units, format: .number)
.keyboardType(.numberPad)
TextField("Peso", value: $weight, format: .number.precision(.fractionLength(0...3)))
.keyboardType(.decimalPad)
}
}
}
📅 Las fechas son otro terreno donde FormatStyle brilla. Muchas apps siguen creando DateFormatter compartidos o extensiones sobre Date para cada caso. Eso sigue siendo válido en ciertos contextos, pero en SwiftUI suele ser más expresivo declarar el formato directamente junto al Text.
struct DeliveryStatusView: View {
let estimatedDelivery: Date
let windowStart: Date
let windowEnd: Date
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(estimatedDelivery, format: .dateTime.weekday(.wide).day().month(.wide))
Text(windowStart..<windowEnd, format: .interval.hour().minute())
Text("Entrega estimada \(estimatedDelivery, format: .relative(presentation: .named))")
}
}
}
🗓️ La ventaja no está solo en escribir menos código. Un formato como .dateTime.weekday(.wide).day().month(.wide) comunica la intención exacta de la UI. No dice usa este patrón raro con letras mágicas; dice quiero día de la semana, día del mes y mes. Es más legible, más mantenible y menos dependiente de recordar símbolos como dd, MMM o yyyy.
📏 Las medidas permiten representar unidades con conversión localizada. Temperaturas, distancias, pesos o tamaños de archivo pueden mostrarse respetando la configuración regional del usuario. Si una app guarda una distancia en kilómetros, puede presentarla como millas en un entorno donde esa sea la convención habitual.
struct HikingRouteView: View {
let distance = Measurement(value: 12.4, unit: UnitLength.kilometers)
let elevation = Measurement(value: 830, unit: UnitLength.meters)
var body: some View {
VStack(alignment: .leading) {
Text(distance, format: .measurement(width: .abbreviated))
Text(elevation, format: .measurement(width: .wide))
}
}
}
🧵 Otro detalle interesante es que estos formatos pueden usarse dentro de interpolaciones de Text. No hace falta romper una frase en varios componentes ni convertir manualmente cada valor. SwiftUI mantiene el valor tipado y aplica el formato dentro de la cadena localizada.
struct BackupInfoView: View {
let backupSize = Measurement(value: 734_003_200, unit: UnitInformationStorage.bytes)
let nextBackup = Date.now.addingTimeInterval(60 * 60 * 5)
var body: some View {
Text("La próxima copia ocupará \(backupSize, format: .byteCount(style: .file)) y empezará \(nextBackup, format: .relative(presentation: .numeric)).")
}
}
🔤 Para listas de valores también hay estilos útiles. En vez de unir elementos con joined(separator:), puedes dejar que el sistema use la forma correcta para el idioma y la región actuales. Esto importa más de lo que parece: una enumeración natural no se construye igual en todos los casos.
struct SharedFolderView: View {
let collaborators = ["Lucía", "Nora", "Daniel"]
var body: some View {
Text(collaborators, format: .list(type: .and))
}
}
🧱 Como regla práctica, conviene evitar tres patrones: interpolar números sin formato cuando se muestran al usuario, concatenar símbolos de moneda o porcentaje a mano, y usar DateFormatter para cada Text sencillo. No es que esas técnicas estén prohibidas, pero en SwiftUI moderno suelen indicar que la vista está trabajando demasiado con texto y demasiado poco con valores.
✅ Una buena arquitectura de presentación separa tres capas: el modelo conserva el dato real, la vista decide el formato y Foundation aplica las reglas regionales del usuario. FormatStyle encaja justo en ese punto intermedio. No sustituye a toda validación de negocio, pero reduce muchísimo la complejidad entre los estado, los formularios y la representación visual.
🎯 Formatear bien no consiste en aplicar hechizos sobre las cadenas. Consiste en no perder el significado del dato antes de tiempo. Mientras una cantidad siga siendo número, una fecha siga siendo fecha y una distancia siga siendo una medición, SwiftUI puede ayudarte a presentarlas mejor. Convertirlo todo a String demasiado pronto es cómodo al principio, pero acaba robándole información al dato justo cuando más la necesita.