Оглавление
Время чтения: 6 минут
В разработке приложений для iOS часто возникает задача организации удобного ввода данных — будь то имя, контакты, адреса, номера документов, банковские карты или реквизиты. В проектах с большим количеством таких полей важно обеспечить удобное переключение между ними. Для решения этой задачи в SwiftUI, начиная с iOS 15, используется property wrapper @FocusState, который значительно упрощает управление фокусом на элементах ввода и улучшает взаимодействие пользователя с интерфейсом.
Ниже представлен пример создания UI-компонента с использованием @FocusState и модификатора .toolbar для переключения фокуса между текстовыми полями. Этот подход позволяет переиспользовать код в различных частях приложения, что снижает дублирование, особенно на проектах с множеством форм и анкет.
Создание контейнера для полей ввода с кнопками на клавиатуре
Первым шагом создается контейнер, внутри которого размещаются поля ввода. В этом контейнере настраивается тулбар с кнопками "Назад" и "Далее" для управления фокусом.
struct ContainerView<Content: View>: View {
@ViewBuilder let content: () -> Content
var body: some View {
VStack {
content()
}
.toolbar {
ToolbarItem(placement: .keyboard) {
toolbarItem
}
}
}
var toolbarItem: some View {
HStack {
Spacer()
Button("Назад") {
moveToPreviousField()
}
Button("Далее") {
moveToNextField()
}
}
}
}
Взаимодействие родительского и дочерних представлений через PreferenceKey и EnvironmentKey
Для связи между контейнером и дочерними view создаются специальные ключи:
- FocusedFieldPreferences — хранит массив UUID всех дочерних view.
- FocusFieldEnvironment — передает UUID того дочернего view, который должен быть в фокусе.
struct FocusedFieldPreferences: PreferenceKey {
static var defaultValue: [UUID] = []
static func reduce(value: inout [UUID], nextValue: () -> [UUID]) {
value.append(contentsOf: nextValue())
}
}
struct FocusFieldEnvironment: EnvironmentKey {
static let defaultValue: Binding<UUID?> = .constant(nil)
}
extension EnvironmentValues {
var focusField: Binding<UUID?> {
get { self[FocusFieldEnvironment.self] }
set { self[FocusFieldEnvironment.self] = newValue }
}
}
С помощью FocusedFieldPreferences в контейнере будет получаться список всех дочерних UUID, а через FocusFieldEnvironment будет устанавливаться текущее активное поле.
Расширение ContainerView для управления фокусом
Добавляются состояния для текущего индекса и списка идентификаторов дочерних view, а также для текущего сфокусированного поля:
struct ContainerView<Content: View>: View {
@ViewBuilder let content: () -> Content
@State private var currentIndex: Int = 0
@State private var childViewsIDs: [UUID?] = []
@State private var focusedFieldID: UUID?
var body: some View {
VStack {
content()
}
.onPreferenceChange(FocusedFieldPreferences.self) { ids in
focusedFieldID = ids.first
childViewsIDs = ids
}
.environment(\.focusField, $focusedFieldID)
.toolbar {
ToolbarItem(placement: .keyboard) {
toolbarItem
}
}
}
}
Функции для кнопок тулбара
Определены функции переключения фокуса на предыдущее и следующее поле с циклическим обходом:
private func moveToPreviousField() {
guard
let currentID = focusedFieldID,
let currentIndex = childViewsIDs.firstIndex(of: currentID),
!childViewsIDs.isEmpty
else { return }
let previousIndex = (currentIndex - 1 + childViewsIDs.count) % childViewsIDs.count
focusedFieldID = childViewsIDs[previousIndex]
}
private func moveToNextField() {
guard
let currentID = focusedFieldID,
let currentIndex = childViewsIDs.firstIndex(of: currentID),
!childViewsIDs.isEmpty
else { return }
let nextIndex = (currentIndex + 1) % childViewsIDs.count
focusedFieldID = childViewsIDs[nextIndex]
}
Добавление модификатора для дочерних view с поддержкой фокуса
Для управления фокусом у отдельных полей создаётся ViewModifier, который использует @FocusState и связывается с текущим активным UUID через окружение.
struct FocusableModifier: ViewModifier {
@State private var id = UUID()
@FocusState private var isFocused: Bool
@Environment(\.focusField) private var focusFromEnvironment
func body(content: Content) -> some View {
content
.preference(key: FocusedFieldPreferences.self, value: [id])
.onChange(of: focusFromEnvironment.wrappedValue) { newValue in
if newValue == id {
isFocused = true
}
}
.onChange(of: isFocused) { value in
if value {
focusFromEnvironment.wrappedValue = id
}
}
.focused($isFocused)
}
}
extension View {
func focusable() -> some View {
self.modifier(FocusableModifier())
}
}
Использование модификатора на конкретном поле ввода
Применение модификатора focusable() позволяет подключить поддержку управления фокусом к любому View, например, к TextField:
struct CustomTextField: View {
var body: some View {
TextField("Введите текст", text: .constant(""))
.focusable()
}
}
Итог
Реализация такого подхода позволяет быстро и удобно переключаться между полями ввода в формах приложений под iOS, повышая удобство взаимодействия пользователя с интерфейсом.