TextField: el campo de texto editable de SwiftUI

¿Qué es TextField?

El componente TextField es un campo de texto editable por el usuario. Será usado para que el usuario pueda introducir datos que la aplicación podrá leer. Es un componente equivalente a UITextField de UIKit.

Aquí podéis consultar la documentación oficial

El componente tiene varios ‘inicializadores’ con diferentes parámetros y propósitos. A nivel más básico, se crea de la siguiente forma:

TextField("Placeholder", text: .constant(""))

Nota: el parámetro text es una variable de estado que usará el componente TextField para asignar el valor que se escriba. Cuando no queremos controlar el valor del campo de texto (por ejemplo por motivos de pruebas como este caso) podemos usar la función .constant() que pertenece al struct Binding para cumplir con su implementación sin tener control sobre el dato.

Cómo leer datos de un TextField

En un caso real el valor del parámetro text deberá estar asociado a una variable de estado del propio struct de la vista para que así podamos tener control y consultar el dato del TextField.

struct ContentView: View {
    @State var inputText: String = ""
        
    var body: some View {
        Group {
            TextField("Write something", text: $inputText)
        }
    }
}

De esta forma cada vez que el usuario escriba algo en el campo de texto se modificará la variable de estado inputText, provocando que cualquier parte del código que dependa de esta variable se refresque para controlar el nuevo estado. Por ejemplo, vamos a mostrar un Text cuando el usuario haya escrito en el TextField y este Text tendrá el contenido del TextField.

struct ContentView: View {
    @State var inputText: String = ""
    
    var body: some View {
        Group {
            TextField("Write something", text: $inputText)
                .frame(height: 44)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            if !inputText.isEmpty {
                Text("User say: " + inputText)
            }
        }
    }
}

Modificadores comunes para TextField

A parte de los modificadores que se explicarán a continuación, el componente TextField  comparte los mismos métodos de personalización que el componente View y pueden ser consultados en el siguiente enlace.

textFieldStyle

Permite indicar el estilo del TextField. El parámetro pasado debe implementar el protocolo TextFieldStyle.

Por defecto existen las siguientes implementaciones (para iOS):

  • DefaultTextFieldStyle. Este estilo varía dependiendo de la plataforma. En el caso de iOS se aplica el estilo PlainTextFieldStyle.
  • PlainTextFieldStyle. Estilo de texto plano y sin bordes. A simple vista se vería como un Text.
  • RoundedBorderTextFieldStyle. Estilo del sistema. Tiene bordes redondeados.

background

Permite añadir una vista al fondo del componente.

TextField("Background", text: .constant(""))
        .padding()
        .background(Color.yellow)
        .clipShape(RoundedRectangle(cornerRadius: 8))

foreground

Modifica el color del texto. No afecta al placeholder.

TextField("Foreground placeholder", text: .constant("Text foreground"))
    .padding()
    .foregroundColor(.blue)

font

Permite modificar la fuente del texto. No afecta al placeholder.

TextField("Font placeholder", text: .constant("Text font"))
    .textFieldStyle(RoundedBorderTextFieldStyle())
    .padding()
    .font(.caption2)

textContentType

Permite indicar el tipo de contenido del TextField para que se activen sugerencias de texto relacionadas. Funciona a través del aprendizaje del propio teclado de iOS.

TextField("Email placeholder", text: .constant(""))
    .textFieldStyle(RoundedBorderTextFieldStyle())
    .padding()
    .textContentType(.emailAddress)

keyboardType

Permite modificar el tipo de teclado a mostrar cuando se edita el campo de texto.

TextField("keyboardType numberPad", text: .constant(""))
    .keyboardType(.numberPad)
TextField("keyboardType emailAddress", text: .constant(""))
    .keyboardType(.emailAddress)

autocapitalization

Permite indicar cómo se debe comportar el teclado con respecto a las mayúsculas automáticas.

TextField("Autocapitalization allCharacters", text: .constant(""))
    .autocapitalization(.allCharacters)
TextField("Autocapitalization words", text: .constant(""))
    .autocapitalization(.words)

Cómo añadir un borde personalizado

Para añadir un borde personalizado usaremos la combinación de los modificadores overlay y clipShape.

TextField("Background", text: .constant(""))
        .padding()
        .background(Color.yellow)
        .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.blue, lineWidth: 2))
        .clipShape(RoundedRectangle(cornerRadius: 10))

En este caso overlay pintará el borde que queramos y clipShape limitará el frame del componente para que no se pinte el background fuera de los bordes.

Cómo modificar el placeholder

Actualmente no hay forma de modificar el estilo del placeholder del componente, por lo que el componente no se adapta a todas las situaciones que podríamos requerir en un proyecto.

La forma de solucionar este problema es crear un ViewModifier que nos permita realizar modificaciones sobre el propio TextField para incluirle una vista personalizada que simulará un placeholder.

Para ello, crearemos un nuevo struct que implemente el protocolo ViewModifier.

struct CustomPlaceholder: ViewModifier {
    var placeholder: T
    var show: Bool
    
    func body(content: Content) -> some View {
        ZStack(alignment: .leading) {
            if show {
                placeholder
            }
            content
        }
    }
}

Para usarlo deberemos usar el método modifier sobre el propio TextView para ‘setear’ una instancia de CustomPlaceholder. Para hacerlo más fácil crearemos una extensión de View que aplique este cambio de forma más resumida:

extension View {
    func placeholder(_ view: T, show: Bool) -> some View {
        self
            .modifier(CustomPlaceholder(placeholder: view, show: show))
    }
}

Como resultado solo tendremos que llamar al método placeholder sobre nuestro componente TextView y asegurarnos de dejar el placeholder original del TextView vacío.

TextField("", text: $customPlaceholderText1)
        .padding()
        .foregroundColor(.blue)
        .placeholder(Text("Custom placeholder 1").padding().foregroundColor(.purple), show: customPlaceholderText1.isEmpty)
TextField("", text: $customPlaceholderText2)
        .padding()
        .foregroundColor(.white)
        .placeholder(Text("Custom placeholder 2").padding().foregroundColor(.orange), show: customPlaceholderText2.isEmpty)
        .background(Color.black)
        .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.red, lineWidth: 3))
        .clipShape(RoundedRectangle(cornerRadius: 10))

Truco: Este placeholder no es exclusivo de un TextField. Se puede usar para cualquier componente, como por ejemplo para poner una imagen mientras se carga la imagen real desde una url.

Cómo controlar la entrada de datos de un TextField 

El componente TextField recibe una variable de estado donde podemos recoger los datos que escribe el usuario. Hay ocasiones en las que necesitaremos controlar lo que escribe el usuario en el mismo momento en el que escribe, como por ejemplo cuando el TextField solo acepta valores numéricos o tiene una longitud máxima.

Para controlar la entrada de datos debemos hacer uso de Combine que nos permitirá conocer los cambios que se realizan sobre la variable de estado asociada al TextField.

TextField con valores numéricos

Para este caso podemos pensar que solo es necesario cambiar el tipo de teclado del TextField con el modificador .numberPad.

Para este caso podemos pensar que solo es necesario cambiar el tipo de teclado del TextField con el modificador .numberPad.

@State var onlyNumbersValue: String = ""

...

TextField("Only numbers", text: $onlyNumbersValue)
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .keyboardType(.numberPad)

Si bien este es un primer paso, cambiar el tipo de teclado no limita los caracteres que el usuario puede introducir. Si nosotros ejecutamos este ejemplo en un simulador, podemos escribir con el teclado todos los caracteres que queramos, y lo mismo se podría hacer en un dispositivo sin se conecta un teclado externo.

Para realizar el control correcto debemos escuchar los cambios de la variable onlyNumbersValue para modificarla si no se cumple la condición que definamos, que en este caso es que solo contenga valores numéricos.

import SwiftUI
import Combine

@State var onlyNumbersValue: String = ""

...

TextField("Only numbers", text: $onlyNumbersValue)
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .keyboardType(.numberPad)
        .onReceive(Just(onlyNumbersValue)) { value in
            let filtered = "(value)".filter { "0123456789".contains($0) }
            if filtered != value {
                self.onlyNumbersValue = "(filtered)"
            }
        }

*Hemos desactivado el teclado de tipo numérico para poder ver el comportamiento en el ejemplo.

TextField con una longitud máxima

Para limitar el TextField a una longitud máxima vamos a escuchar los cambios de la variable de estado de tipo String donde se almacena el texto (maxLenghtValue), y vamos a cortar el texto a la longitud deseada:

import SwiftUI
import Combine

@State var maxLenghtValue: String = ""

...

TextField("Max Lenght (10 chars)", text: $maxLenghtValue)
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .onReceive(Just(maxLenghtValue)) { value in
            let shortString = String(value.prefix(10))
            if shortString != value {
                self.maxLenghtValue = shortString
            }
        }

Cómo mostrar un campo de texto seguro

En UIKit el componente UITextField tenía el parámetro secureTextEntry para indicar que el campo de texto debía ocultar los caracteres. Esto se usa comúnmente para los campos de tipo contraseña.

En SwiftUI para conseguir el mismo comportamiento no existe un modificador sobre TextField. En su lugar se usa otro componente: SecureField.

SecureField

El componente SecureField se comporta de la misma forma que un TextField, pero el contenido que se escriba quedará oculto. Cualquier comportamiento que podamos aplicar a un TextField también podrá ser aplicado a un SecureField.

Cómo ocultar el teclado

Hay ocasiones en las que es necesario ocultar el teclado si lo estamos mostrando, ya que hemos pulsado un botón que implica la finalización de la toma de datos.

Para estos casos SwiftUI no incluye una forma de ocultar el teclado, pero podemos crearnos nuestro propio código con esta funcionalidad.

En este caso vamos a realizar una extensión de View con la implementación necesaria para ocultar el teclado:

import SwiftUI
import UIKit

extension View {
    func hideKeyboard() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

Para usarlo solo tenemos que invocar al método hideKeyboard cuando queramos ocultar el teclado.

Vamos a ver dos casos que suelen ser útiles:

Al pulsar un botón

En este caso tenemos que invocar al método cuando entre por el evento de pulsación del botón:

struct ContentView: View {
    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: 15) {
                    TextField("Hide keyboard", text: .constant(""))
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                    Button("Done!") {
                        self.hideKeyboard()
                    }
                }
            }.navigationBarTitle("TextField Style", displayMode: .inline)
            .padding()
        }
    }
}

Al pulsar en la pantalla

Para este caso debemos implementar el método onTapGesture de la vista que pueda ocultar el teclado cuando pulsemos sobre ella. Normalmente será la vista que contenga a todos los elementos de la pantalla. Si esa vista es transparente se necesitará también el modificador contentShape para que se pueda recibir correctamente el evento de tap sobre la vista (consultar la documentación de View para más información).

struct ContentView: View {
    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: 15) {
                    TextField("Hide keyboard", text: .constant(""))
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                }
            }.navigationBarTitle("TextField Style", displayMode: .inline)
            .padding()
            .contentShape(Rectangle())
            .onTapGesture {
                self.hideKeyboard()
            }
        }
    }
}

Ejemplo

Puedes encontrar este ejemplo en github.com bajo el apartado TextField.

Rafael Fernández,
iOS Tech Lider