---
title: "TextField e UIViewRepresentable"
url: https://www.sofapps.it/it/blog/textfield-uiviewrepresentable/
date: 2019-06-28
author: Costantino Pistagna
language: it
translated_from: en
tags: [SwiftUI, Swift, UIKit, Tutorial]
---

# TextField e UIViewRepresentable

> Il TextField di SwiftUI in iOS 13 beta 2 non permetteva di spostare il focus tra campi e non chiamava mai onCommit. Il post incapsula in una struct UIViewRepresentable una UITextField che fa da delegate di se stessa, usando closure per i cambi e per la fine dell'editing e i tag delle view per passare il focus al campo successivo.

A prima vista, quando ho provato SwiftUI per la prima volta dopo la presentazione del WWDC19, una delle preoccupazioni più grandi riguardava sicuramente l'oggetto TextField: non c'era modo di chiudere la tastiera, né di spostare il focus da un TextField all'altro.

>probabilmente alcuni dettagli non erano ancora pronti durante la presentazione iniziale.

**iOS13 beta2**, insieme a molti altri miglioramenti, sembra risolvere almeno in parte i problemi legati al **TextField**: quando si preme il tasto Invio, la tastiera ora viene chiusa correttamente. Non ci sono indizi, però, su un meccanismo per **cambiare il focus** tra **più** TextField all'interno dello stesso container View. Purtroppo dobbiamo anche segnalare un **bug importante** della beta2 che riguarda il meccanismo di chiamata della closure **onCommitHandler** quando la tastiera viene chiusa: quest'ultima non viene mai chiamata.

In questo articolo proveremo ad aggirare tutti questi problemi creando un **TextField** personalizzato che prende il meglio di quello che c'è oggi in **SwiftUI** e in **UIKit**. I lettori più attenti avranno probabilmente già capito dove voglio arrivare:

>Costruiremo un componente personalizzato in UIKit che usa la vecchia e perfettamente funzionante UITextField e lo esporremo in SwiftUI attraverso il protocol UIViewRepresentable.

## UITextField e i delegate

UIKit mette a disposizione un oggetto chiamato **UITextField** che sfrutta la **tecnica del delegate** per offrire un'esperienza di tastiera fluida ed efficace.

Quello che faremo è creare una class che eredita da **UITextField** e, allo stesso tempo, adotta il protocol **UITextFieldDelegate**. In questo modo avremo un unico oggetto da esporre per l'integrazione con **SwiftUI**.

La prima cosa da fare è dichiarare la nostra class, che chiameremo **WrappableTextField**:

```swift
class WrappableTextField: UITextField, UITextFieldDelegate {
    var textFieldChangedHandler: ((String)->Void)?
    var onCommitHandler: (()->Void)?
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
	...
    }
    
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
	...
    }
    
    func textFieldDidEndEditing(_ textField: UITextField) {
	...
    }
}
```

Il codice qui sopra mostra lo scheletro dei metodi delegate che implementeremo per completare la class Wrapper.

Come anticipato, la nostra class estende UITextField e adotta il protocol delegate UITextFieldDelegate. Abbiamo anche aggiunto due variabili locali che contengono due closure opzionali personalizzate. Rispettivamente:

- La closure **textFieldChangedHandler** verrà invocata ogni volta che la UITextField cambia.

- La closure **onCommitHandler** verrà invocata al termine dell'editing, alla pressione del tasto Invio.

Vediamo ora, passo per passo, le singole implementazioni dei metodi che aggiungeremo alla nostra class.

#### textField:shouldChangeCharactersInRange:replacementString:

Il metodo delegate shouldChangeCharactersInRange:replacementString: viene invocato automaticamente dal sistema ogni volta che scriviamo qualcosa con la tastiera. Ci permette di eseguire controlli sul contenuto, restituendo un valore booleano a seconda che il contenuto inserito sia permesso o meno dalle nostre regole di validazione.

```swift
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    if let currentValue = textField.text as NSString? {
        let proposedValue = currentValue.replacingCharacters(in: range, with: string)
        textFieldChangedHandler?(proposedValue as String)
    }
    return true
}
```

Nel nostro caso basterà comporre la nuova stringa risultante dall'inserimento dell'input dell'utente e di quella attualmente presente nella UITextField. Per semplicità convertiamo il tipo String in un NSString di Foundation, che offre tutti gli strumenti per comunicare direttamente con l'oggetto NSRange fornito come parametro di input dal metodo.

Una volta convertita la stringa, applichiamo la trasformazione proposta dal metodo:

```replacingCharacters(in: range, with: string)```

passando come parametri i due valori forniti in input dal metodo: range e la stringa inserita dall'utente. Il risultato sarà la composizione tra l'input dell'utente e il contenuto attuale della UITextField.

Questo valore sarà quello che passeremo in input alla closure definita in precedenza:

```textFieldChangedHandler?(proposedValue as String)```

#### textFieldDidEndEditing:

L'implementazione di questo metodo, richiamato dal sistema al termine della fase di editing, è molto semplice: dovrà solo invocare la closure personalizzata definita prima. In questo modo potremo eseguire un blocco di codice arbitrario ogni volta che l'editing termina.

```swift
func textFieldDidEndEditing(_ textField: UITextField) {
    onCommitHandler?()
}
```

#### textFieldShouldReturn:

L'ultimo metodo da implementare è textFieldShouldReturn. La sua implementazione ci permetterà di costruire un meccanismo molto semplice per spostare il focus dal nostro TextField ad altre class simili contenute nella stessa container view. Vediamo come funziona.

La prima cosa che facciamo è chiedere all'elemento textField corrente un riferimento alla propria superview?.superview?, seguendo uno schema simile a quello rappresentato nel diagramma seguente:

![Diagramma di una VStack che contiene due view SATextField, ciascuna con una WrapperView all'interno, con frecce .superView? che risalgono al container](https://www.sofapps.it/blog/textfield-uiviewrepresentable/fig-1.jpg)

Se viene restituito un oggetto valido, invochiamo il suo metodo viewWithTag, passando il tag corrente incrementato di uno. In questo modo siamo riusciti a creare un meccanismo di navigazione molto elementare, che permette di risalire fino al container e chiedergli altre view con un tag specifico.

```swift
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    if let nextField = textField.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField {
        nextField.becomeFirstResponder()
    } else {
        textField.resignFirstResponder()
    }
    return false
}
```

Se il valore restituito esiste ed è di tipo UITextField, abbiamo trovato un'altra UITextField e possiamo passarle il controllo. Altrimenti chiudiamo semplicemente la tastiera.

La nostra class Wrappable è pronta. Ora non resta che collegarla a SwiftUI.

### UIViewRepresentable

SwiftUI mette a disposizione un protocol molto utile, chiamato UIViewRepresentable, che permette di esporre tutti quegli oggetti UIKit che finora non sono stati integrati nativamente in SwiftUI.

Adottando questo protocol è possibile costruire un oggetto che espone nativamente a SwiftUI il contenuto di una UIView. Richiede di implementare solo due metodi:

```swift
struct SATextField: UIViewRepresentable {

    func makeUIView(context: UIViewRepresentableContext<SATextField>) -> WrappableTextField {
	...
    }
    
    func updateUIView(_ uiView: WrappableTextField, context: UIViewRepresentableContext<SATextField>) {
	...
    }
}
```

Il primo metodo si occupa di creare la nostra UIView e di fornirla a SwiftUI in un formato some View compatibile, ammesso dal protocol di SwiftUI come abbiamo visto nei tutorial precedenti.

Il secondo metodo verrà invocato ogni volta che la nostra UIView deve essere aggiornata per riflettere un cambiamento nell'interfaccia utente (un po' come la property body che abbiamo visto negli oggetti nativi di SwiftUI).

Infine aggiungiamo al nostro oggetto alcune variabili locali che useremo per guidare il comportamento della class WrappableTextField dichiarata in precedenza:

```swift
struct SATextField: UIViewRepresentable {
    private let tmpView = WrappableTextField()
    var tag:Int = 0
    var placeholder:String?
    var changeHandler:((String)->Void)?
    var onCommitHandler:(()->Void)?

    ...
}
```

Queste variabili saranno i parametri richiesti per creare l'oggetto SwiftUI. Verranno passate alla class Wrapper istanziata prima ed esprimono, rispettivamente: il valore del tag da associare al TextField, le closure necessarie per gestire i cambiamenti dell'input dell'utente e la fine dell'editing, e il valore da associare al placeholder della nostra UITextField.

Il contenuto del metodo makeUIView associa semplicemente i parametri inseriti dall'utente durante la creazione dell'oggetto SwiftUI ai parametri corrispondenti esposti dalla class WrappableTextField.

```swift
func makeUIView(context: UIViewRepresentableContext<SATextField>) -> WrappableTextField {
    tmpView.tag = tag
    tmpView.delegate = tmpView
    tmpView.placeholder = placeholder
    tmpView.onCommitHandler = onCommitHandler
    tmpView.textFieldChangedHandler = changeHandler
    return tmpView
}
```

Il metodo updateUIView imposta la hugging priority della nostra UITextField in modo che si adatti a tutto lo spazio disponibile nel Container View, in modo coerente con tutti gli altri oggetti:

```swift
func updateUIView(_ uiView: WrappableTextField, context: UIViewRepresentableContext<SATextField>) {
    uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
    uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
}
```

### Mettiamo tutto insieme: un esempio reale

Proviamo a usare il nostro nuovissimo SATextField in un esempio pratico:

```swift
struct ContentView : View {
    @State var username:String = ""
    @State var email:String = ""
    
    var body: some View {
        VStack {
            HStack {
                Text(username)
                Spacer()
                Text(email)
            }
            SATextField(tag: 0, placeholder: "@username", changeHandler: { (newString) in
                self.username = newString
            }, onCommitHandler: {
                print("commitHandler")
            }).padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
            SATextField(tag: 1, placeholder: "@email", changeHandler: { (newString) in
                self.email = newString
            }, onCommitHandler: {
                print("commitHandler")
            }).padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
        }
    }
}
```

Fatto, ragazzi! Con poche righe di codice abbiamo creato un oggetto TextField che risponde ai cambi di focus e al completamento dell'editing, in attesa che la prossima beta di iOS13 risolva i problemi attuali dell'oggetto TextField nativo.

![Registrazione dello schermo su iPhone dei due campi di testo personalizzati, in rosso, con i placeholder @username e @email](https://www.sofapps.it/blog/textfield-uiviewrepresentable/fig-2.gif)
