В SwiftUI TextField - это goto View для захвата ввода произвольной формы от ваших пользователей. Он отлично работает для захвата строк, но, как и в случае любого стандартного API, существуют ограничения и поведение, которые могут застать вас врасплох, особенно если вы попытаетесь работать с дополнительными параметрами и другими типами данных. В этой статье будут представлены некоторые наблюдения, советы и уловки, которые я узнал, чтобы помочь вам эффективно работать с TextField.

  • Образец приложения также доступен на GitHub, если вы хотите опробовать код самостоятельно.
  • Код, написанный с использованием XCode 12.4 и iOS 14

Привязка к необязательным строкам

Если вы когда-либо пытались передать привязку, содержащую необязательную строку, в TextField, вы получите грубую ошибку компилятора: «Невозможно преобразовать значение типа 'Binding‹ String? ›' В ожидаемый тип аргумента 'Binding ‹String›'». Это особенно раздражает из-за UIKit, поскольку свойство .text для UITextField является необязательной строкой. Вы можете изменить природу вашего базового свойства с необязательной строки на простую строку и изменить базовый код, чтобы проверять наличие пустой строки вместо nil, но мне не нравится идея изменять мою модель данных только для соответствия вызовам UI API. Кроме того, использование конкретной строки приводит к потере памяти, дискового хранилища и сетевого трафика, когда объекты модели сериализуются и перемещаются (возможно, номинально, но реально).

Более элегантное решение, которое я нашел на Stack Overflow, - это расширить Optional вычисляемым свойством, которое разворачивает необязательное и предоставляет значение по умолчанию, когда встречается nil (в данном случае это пустая строка). Расширение выглядит так:

Обратите внимание, что геттер и сеттер правильно переводят в обоих направлениях. В геттере, когда базовое значение равно nil, возвращается пустая строка, а в сеттере, когда newValue является пустой строкой, базовое значение устанавливается в nil.

Использовать это новое свойство в SwiftUI так же просто, как TextField(“my title”, text: $optionalString.boundString). Вам не нужно было изменять модель данных, вы получили желаемую функциональность, и ваш код по-прежнему остается красивым и чистым. Довольно круто, да?

О поведении и обновлениях состояния

TextField имеет два основных конструктора, которые играют роль в том, как и когда обновляется состояние переменной. Большинство из нас знакомо с TextField(title: StringProtocol, text: Binding<String>). Текстовые поля, использующие этот конструктор, непрерывно обновляют свои связанные строки по мере того, как пользователь вводит текст на клавиатуре.

Второй конструктор, TextField(title: StringProtocol, value: Binding<T>, formatter: Formatter), позволяет передавать общие значения, которые преобразованы в исходный тип и обратно с помощью средства форматирования, но ведут себя по-другому. Состояние только обновляется для связанного значения при вызове .onCommit. Другими словами, только при нажатии клавиши возврата (или ее эквивалента) модуль форматирования оценивает текущую строку в текстовом поле и обновляет базовое значение в случае успеха.

Это может легко привести к разочаровывающим сценариям. Если вы работаете с числами и хотите использовать соответствующую цифровую клавиатуру, вы быстро обнаружите, что нет клавиши возврата и, следовательно, нет возможности вызвать .onCommit. Состояние переменной также останется неизменным, если пользователь коснется вашего TextField вместо нажатия клавиши возврата. Это кажется довольно большим недостатком. Не тратьте зря время, как я пытался найти обходные пути. Используйте ванильный TextField со строкой и выполните собственное преобразование, используя закрытие .onEditingChanged и .onCommit. А еще лучше прочитайте следующий раздел, чтобы узнать, как обернуть UITextField с помощью UIViewRepresentable.

Обертка UITextField для работы с числовыми значениями

Как я уже упоминал в последнем разделе, TextFields, использующие числовые значения и связанный Formatter, практически непригодны. Пронумерованные клавиатуры не имеют клавиши возврата, и состояние обновляется только при вызове .onCommit… чего в этом сценарии никогда не происходит. Вместо того, чтобы бороться с SwiftUI, мы можем создать собственное представление, которое обрабатывает числовые значения, обернув UITextField и следуя протоколу UIViewRepresentable. Мы заставим наше новое представление обновлять свое состояние всякий раз, когда представление теряет фокус, добавим некоторое удобное форматирование для валюты и процентов и предоставим пользователю предупреждение при обнаружении недопустимого ввода.

Наша структура, которую мы назовем NumericTextField, выглядит так:

Подобно TextView, наше текстовое поле имеет title (значение-заполнитель), привязку к лежащему в основе value (основная истина) и formatter (отвечает за перевод между нашим типом сохраненного значения и строковым представлением). Мы пока отложим рассмотрение нашего Coordinator, но он будет отвечать за проверку ввода и обновление нашей привязки value. Чтобы соответствовать UIViewRepresentable, наша точка зрения должна реализовывать makeUIView и updateUIView. В makeUIView мы создаем UITextField, устанавливаем делегата на наш coordinator и, если указан заголовок, устанавливаем текст-заполнитель текстового поля. Мы также устанавливаем остальную эстетику и тип клавиатуры, прежде чем возвращать наше новое представление. В updateUIView, который обрабатывает поток данных SwiftUI - ›UIKit, мы устанавливаем текст нашего UITextField в текстовое представление value, используя метод .textFor нашего координатора. Последняя функция, makeCoordinator, возвращает экземпляр нашего Координатора, передавая привязку к value, formatter и привязку к строке, в которой хранится сообщение об ошибке.

Теперь давайте посмотрим на наш класс координатора:

Первые две функции в Координаторе - это вспомогательные методы. Метод textFor<T>(value: T) -> String? переводит нашу привязку value в строку. Мы видели его использование в .updateView. Другой метод, scrubbedText(currentText: String) -> String, обеспечивает дополнительное удобство форматирования валюты и процентов. Класс NumberFormatter довольно разборчив, когда вы выбираете .currency или .percentage в качестве numberStyle. Валютные строки должны начинаться с соответствующего символа валюты (например, «1 доллар США» для США), а проценты должны иметь завершающий символ процента (например, «5%»). Средство форматирования всегда возвращает nil, если эти символы опущены, поэтому мы немного упростим жизнь и добавим их во входную строку, если они отсутствуют.

Теперь перейдем к UITextFieldDelegate методам. Здесь мы собираемся реализовать только два из них. В textFieldShouldReturn, который вызывается при нажатии клавиши возврата, мы откажемся от первого респондента и вернем истину. Второй метод textFieldDidEndEditing, который вызывается всякий раз, когда текстовое поле теряет фокус, - это то место, где наш координатор выполняет свою наиболее важную работу. Сначала мы разворачиваем необязательное свойство .text нашего UITextField и очищаем текст с помощью функции scrubbedText, чтобы убедиться, что он содержит соответствующий префикс / суффикс, если это необходимо. Затем мы используем наш форматтер для захвата текущего значения нашего объекта с помощью метода .getObjectValue. Я должен отметить, что наш formatter на самом деле Formatter из Objective C, который использует указатели, поэтому, если все выглядит менее быстрым, чем обычно, вот почему. Затем мы проверяем, вернул ли наш форматтер ошибку, посмотрев на значение errorContainer. Если обнаружена ошибка, мы пытаемся установить для свойства .text нашего UITextField предыдущее значение value или nil, если это не удается. Затем мы отображаем предупреждение для пользователя, сообщая ему, что он предоставил неправильный ввод (полный код см. В примере проекта). Если ошибки нет, мы пытаемся преобразовать valueContainer в качестве нашего универсального типа T и установить value на newValue.

Это в значительной степени заботится о нашем новом представлении. Вы можете попробовать новый вид, используя что-то вроде NumericTextField<Double>(title: "Amount in dollars", value: $amountPaid, numberFormatter: currencyFormatter, keyboardType: .numbersAndPunctuation).

Выглядит неплохо! NumericTextField всегда обновляет свое связанное значение, когда теряет фокус. Более того, нам больше не нужно беспокоиться о том, чтобы наши пользователи нажимали клавишу возврата для захвата ввода, и мы можем использовать любую клавиатуру с номерами, которые имеют смысл.

Резюме

В первый раз, когда я использовал TextField с общим значением и Formatter, я запутался, когда не увидел обновлений состояния в реальном времени. Хотя это имеет смысл, учитывая, что многим Formatters требуется полная строка для правильной интерпретации своих значений (например, дат), это затрудняет работу с API. И я не уверен, что согласен с дизайнерским решением предоставлять только обновления состояния в закрытии .onCommit, поскольку, как мы видели, это делает работу с числовыми значениями неуклюжей, если не невозможной. Нашим решением в этом случае было вернуться к UIKit, чтобы получить желаемое поведение. На данный момент это приемлемо, но разве я единственный, у кого есть этот странный драйв, чтобы попытаться написать все на чистом SwiftUI? В любом случае, я надеюсь, что в следующей итерации SwiftUI мы увидим больше TextField с батареями, чтобы я снова смог стать пуристом. Спасибо за чтение!