L. Mees
L. Mees

Reputation: 31

How to manage focus in subviews in SwiftUI?

The Problem

Hi everyone,

I have a parentview with multiple childviews that contain multiple TextFields.

I want to add chevrons to the keyboard toolbar to easily move the focus to the next TextField, but I don't know how I can access the FocusState of the childviews.

Is there perhaps a way to get the view that corresponds to the focus identifier (?, don't know what to call it)?

ParentView

struct ParentView: View {
    
    @FocusState var focusedChild: Int?
    
    var body: some View {
        ForEach(1..<5) { index in
            ChildView(index: index)
                .focused($focusedChild, equals: index)
        }
        .toolbar {
            
            ToolbarItemGroup(placement: .keyboard) {
                Button {
                    // focusPrevious()                  <--
                    // Is there a way to access the view associated with the index or the current focusedChild?
                    // ChildView[index].focusState = ChildView[index].focusState + 1 % 3
                } label: {
                    Image(systemName: "chevron.left")
                }

                Button {
                    //
                } label: {
                    Image(systemName: "chevron.right")
                }
                
                Spacer()
                
                Button {
                    focusedChild = nil
                } label: {
                    Image(systemName: "keyboard.chevron.compact.down")
                }
                
            }
        }
    }
}

ChildView

struct ChildView: View {
    
    var index: Int
    
    @State var field1: String = ""
    @State var field2: String = ""
    @State var field3: String = ""
    
    @FocusState var focusedField: Int?
    
    var body: some View {
        VStack {
            Text(String(index))
            TextField("Field 1", text: $field1)
                .focused($focusedField, equals: 1)
            TextField("Field 2", text: $field2)
                .focused($focusedField, equals: 2)
            TextField("Field 3", text: $field3)
                .focused($focusedField, equals: 3)
        }
    }
}

What I've tried

I tried to add the Toolbar to the ChildView but that results in a pair of chevrons for every childView (in this case 3 pairs of chevrons), instead of just one pair.

Any help would be greatly appreciated, thanks!

Upvotes: 2

Views: 1083

Answers (1)

Sweeper
Sweeper

Reputation: 273860

Since the child views already takes an index, just assign different focused states depending on that. If you do this in a way such that they are consecutive numbers, “going to the next/previous text field” would be trivial. You'd assign them index * 3, index * 3 + 1 and index * 3 + 2.

Rather than having a FocusState in each child view, the child views should take a FocusState<Int?>.Binding (like a regular Binding, but for focus states).

struct ChildView: View {
    
    let index: Int
    
    @State var field1: String = ""
    @State var field2: String = ""
    @State var field3: String = ""
    
    let focusedField: FocusState<Int?>.Binding
    
    var body: some View {
        VStack {
            Text(String(index))
            TextField("Field 1", text: $field1)
                .focused(focusedField, equals: index * 3)
            TextField("Field 2", text: $field2)
                .focused(focusedField, equals: index * 3 + 1)
            TextField("Field 3", text: $field3)
                .focused(focusedField, equals: index * 3 + 2)
        }
    }
}

For a more flexible approach, ChildView could just take the focus states of all three text fields, like

let textField1Focus: Int
let textField2Focus: Int
let textField3Focus: Int

In any case, now you should remove the .focused modifier on ChildViews:

ForEach(0..<4) { index in
    ChildView(index: index, focusedField: $focusedChild)
}

Then the forwards and backwards button would just be:

Button {
    if let focusedChild, focusedChild > 0 {
        // be careful here, self.focusedChild and focusedChild are not the same thing!
        // focusedChild is the unwrapped value!
        self.focusedChild? -= 1
    }
} label: {
    Image(systemName: "chevron.left")
}

Button {
    if let focusedChild, focusedChild < 11 {
        self.focusedChild? += 1
    }
} label: {
    Image(systemName: "chevron.right")
}

Upvotes: 1

Related Questions