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

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