Покажу вам фокус: настраиваем property wrapper @FocusState — короткая инструкция

от автора

Всем привет! Меня зовут Дмитрий Демми, компания 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()     } }

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/932844/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *