Всем привет! Меня зовут Дмитрий Демми, компания AGIMA. Мы часто разрабатываем приложения для банков или еком-продуктов. И в большинстве из них нужно заполнять поля: вписывать имя, контакты, адрес, номера документов, банковских карт или реквизиты. Иногда таких граф бывает много, и чтобы пользователям было удобно переключаться между ними, в iOS-разработке используется property wrapper @FocusState. Если вы пока не сталкивались с таким, то ниже всё объясняю и показываю.

Property wrapper @FocusState появляется в SwiftUI начиная с iOS 15. Он сильно упростил управление фокусом для view и улучшил взаимодействие пользователя с приложением. Ниже расскажу, как создать UI-элемент, который включает в себя @FocusState и модификатор .toolbar для переключения фокуса между полями ввода. 

Его можно переиспользовать в разных частях приложения. Это снижает дублирование кода, что особенно важно на проектах с большим количеством форм, анкет и других подобных решений, перечисленных выше.

Первым делом создадим view-контейнер для полей ввода, в котором настроим кнопки для 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.

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 в ContainerView будем получать массив uuid всех дочерних view, а в FocusFieldEnvironment будем передавать uuid только того дочернего view, которое должно быть в состоянии фокуса.

Теперь дорабатываем наш ContainerView:

struct ContainerView<Content: View>: View {
    
    @ViewBuilder let content: () -> Content
    
    @State var currentIndex: Int = 0
    
    @State var childViewsIDs: [UUID?] = []
    @State 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]
    }

Последним шагом нужно добавить ViewModifier для дочерних view, которым мы хотим добавить поддержку управления фокусом.

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())
    }

Теперь достаточно применить этот модифаер к нужной view.

struct CustomTextField: View {
    
    var body: some View {
        
        TextField("Введите текст", text: .constant(""))
            .focusable()
    }
}

В итоге получаем что-то вроде этого:

Если у вас остались вопросы, буду рад ответить. Также делитесь опытом в комментариях и подписывайтесь на телеграм-канал моего коллеги Саши Ворожищева — там всё про мобильную разработку. 

Что еще почитать

Комментарии (0)