С 10:00 до 20:00

8 (800) 302-05-03

Скопировать

info@appfox.ru

Скопировать

Логотип Appfox
Кодим ваши мечты 8 (800) 302-05-03

Обсудить проект

#

Настройка property wrapper @FocusState: краткая инструкция

Время чтения: 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, повышая удобство взаимодействия пользователя с интерфейсом.