garysimpson.dev
Mobile development with swift and flutter

ViewModifier to Dismiss Keyboard

September 18, 2024 at 7:00AM

I recently needed to update my CustomTextField to display a Done button whenever the current keyboard type in iOS did not. Turns out a ViewModifier helped with that. The modifier itself, an isInputActive param, and the implementation are all this is needed.

CustomTextField

Add the following to the CustomTextField

  • param @FocusState var isInputActive: Bool
  • conditional TextField [UIKeyboardType.numberPad, .phonePad].contains(keyboardType)
  • TextField.focus .focused($isInputActive)
  @FocusState var isInputActive: Bool

 if [UIKeyboardType.numberPad, .phonePad].contains(keyboardType)  {
                    TextField(placeholderText, text: $input,
                              onEditingChanged: { editing in
                        isEditing = editing
                    })
                    .foregroundColor(.grey50)
                    .multilineTextAlignment(textFieldAlignment)
                    .keyboardType(keyboardType)
                    .padding()
                    .disabled( status == .disabled )
                    .focused($isInputActive)
                } else {
                    TextField(placeholderText, text: $input,
                              onEditingChanged: { editing in
                        isEditing = editing
                    })
                    .foregroundColor(.grey50)
                    .multilineTextAlignment(textFieldAlignment)
                    .keyboardType(keyboardType)
                    .padding()
                    .disabled( status == .disabled )
                }...

Modifier

/// View modifier for use with views that display number input without return/done keys in keyboard.
///
/// NOTE: This needs to be added in the containing Form/View instead of the TextField directly or duplicates will appear.
/// see. https://stackoverflow.com/questions/71279700/duplicate-toolbar-in-swiftui
///
struct UnitTxtFieldDoneModifier : ViewModifier {
    @FocusState<Bool>.Binding var isInputActive: Bool
    
    func body(content: Content) -> some View {
        content
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    /// Only display "Done" when we have a state.
                    if $isInputActive.wrappedValue {
                        Spacer()
                        
                        Button("Done") {
                            isInputActive = false
                        }
                    }
                }
            }
    }
}

Modifier View Extension

extension View {
func textFieldDoneButton(isInputActive: FocusState<Bool>.Binding) -> some View {
        ModifiedContent(content: self, modifier: TextFieldDoneModifier(isInputActive: isInputActive))
    }
}

Lasly, be sure to update the Form/View that presents the CustomTextField. As noted in the code comment there is a bug in SwiftUI that causes duplicates if this modification is used directly on the TextField. It MUST be used on the containing form/view.

View

Add the FocusState param to view and cutsomTextField. then call the .textFieldDoneButton(...).

  @FocusState var isInputActive: Bool

...
View {
  VStack {
    CustomTextField(status: shouldAllowEditing,
                              overlineText: "Phone Number",
                              input: $phoneNumber,
                              keyboardType: .phonePad,
                              isInputActive: _isInputActive)
                    .frame(height: 72)
  } .textFieldDoneButton(isInputActive: $isInputActive)
}


Happy Coding ;-)