VButko

How to manage SwiftUI Focus State in iOS14 and before?

In most cases, when we're talking about focus state, we actually mean activating or deactivating the keyboard for input fields. This article will be focused specifically on these use cases.

In iOS15 we have the @FocusState property wrapper which makes handling focus states rather simple. Here is an article on this topic: How to manage Focus State using @FocusState in SwiftUI

In order to manage a focus state in iOS13 and iOS14, we have to use our old friends from UIKit - UITextField and UITextView which is also not so complicated but requires some boilerplates code to be implemented.

Setting requirements

First, let's set the requirements for the focus state implementation:

  1. Set the initial focus state. Should any of the fields be focused when the view appears.
  2. Ability to deactivate focus state. For example, the user wants to open a calendar picker when the text field is in focus. We should hide the keyboard and present him a calendar picker.
  3. Ability to restore focus state. When the user has finished picking the date, we should restore the focus state.

Adding control properties

First, let's add some properties that will be used to control our focus state. For our requirements these could be:

  • A flag to activate / deactivate focus state
  • A property to store the field we want to be in focus

For a sample implementation let's use a screen for adding new tasks with two fields, for the title, and for notes:

final class AddNewTaskViewModel: ObservableObject {
    enum Field {
        case title, note
    }

    @Published var isInFocus: Bool = true // focus is active by default
    @Published var focusedField: Field? = .title // default focused field
    ...
}

Bringing UIKit first responder management to SwiftUI

In UIKit we have becomeFirstResponder() and resignFirstResponder() methods to handle the focus state.

Let's bring them into SwiftUI by creating a UITextField representation. To do so, we should create a UIViewRepresentable struct and implement two protocol methods for creating and updating the view:

This View will receive the actual focus state (isFirstResponder) and the currently focused field (focusedField):

struct NewTaskUIKitTextField: UIViewRepresentable {
    
    let fieldKind: AddNewTaskViewModel.Field
    @Binding var text: String
    @Binding var focusedField: AddNewTaskViewModel.Field?

    var isFirstResponder: Bool = false // false by default
    
    func makeUIView(context: UIViewRepresentableContext<NewTaskUIKitTextField>) -> UITextField {
        let textField = UITextField(frame: .zero)
        textField.delegate = context.coordinator
        return textField
    }
    
    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<NewTaskUIKitTextField>) {
        uiView.text = text

        // Manage focus state using coordinator
        // We'll implement this later
    }
}

We pass to NewTaskUIKitTextField its kind (fieldKind), our screen current focused field (focusedField), and whether the text field is in focus state (isFirstResponder).

Next, we should add a Coordinator to communicate the UITextField changes using UITextFieldDelegate methods:

extension NewTaskUIKitTextField {
    class Coordinator: NSObject, UITextFieldDelegate {
        
        let fieldKind: AddNewTaskViewModel.Field
        @Binding var text: String
        var didBecomeFirstResponder: Bool? = nil
        var didResignFirstResponder: Bool? = nil
        
        @Binding var focusedField: AddNewTaskViewModel.Field?
        
        init(fieldKind: AddNewTaskViewModel.Field, text: Binding<String>, focusedField: Binding<AddNewTaskViewModel.Field?>) {
            self.fieldKind = fieldKind
            _text = text
            _focusedField = focusedField
        }
        
        func textFieldDidChangeSelection(_ textField: UITextField) {
            text = textField.text ?? ""
        }
        
        func textFieldDidBeginEditing(_ textField: UITextField) {
            focusedField = fieldKind
        }
    }
}

The next step is to start using this coordinator by implementing the UIViewRepresentable protocol method makeCoordinator:

struct NewTaskUIKitTextField: UIViewRepresentable {
    ...
    
    func makeCoordinator() -> NewTaskUIKitTextField.Coordinator {
        return Coordinator(fieldKind: fieldKind, text: $text, focusedField: $focusedField)
    }
}

Finally, we can now use our coordinator in the updateUIView method to actually manage the focus state:

struct NewTaskUIKitTextField: UIViewRepresentable {
    ...
    
    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<NewTaskUIKitTextField>) {
        uiView.text = text
        
        // Manage focus state using coordinator
        if isFirstResponder && context.coordinator.didBecomeFirstResponder != true {
            uiView.becomeFirstResponder()
            context.coordinator.didBecomeFirstResponder = true
            context.coordinator.didResignFirstResponder = false
        } else if !isFirstResponder && context.coordinator.didResignFirstResponder != true {
            uiView.resignFirstResponder()
            context.coordinator.didResignFirstResponder = true
            context.coordinator.didBecomeFirstResponder = false
        }
    }
}

Here we compare the focus state we want (isFirstResponder) with the actual state that we hold in didBecomeFirstResponder and didResignFirstResponder properties of the coordinator. If the condition is met, we call either becomeFirstResponer or resignFirstResponder and save in properties flags for future reference.

That is all we need to be able to manage the focus state on the screen for multiple input fields.

Adding View to the screen

When we add the NewTaskUIKitTextField to the screen, we provide it the following parameters:

  • text - bind it with the view model's property for the text value
  • focusedField - bind with the view model's property that where we hold the last focused field. The default value we set in the view model will be used for the initial load and then it will be changed automatically if the user selects one of our input fields.
  • isFirstResponder - tell the field whether it should be focused or not. Remember that we also have a flag isInFocus in the view model? This flag is a convenient way to hide the keyboard on the screen with the ability to restore it for the last focused field later.

Here is an example:

struct AddNewTaskViewIOS14: View {
    @ObservedObject var viewModel: AddNewTaskViewModel
    
    var body: some View {
        VStack {
            NewTaskUIKitTextField(
                text: $viewModel.title,
                focusedField: $viewModel.focusedField,
                isFirstResponder: (viewModel.isInFocus && viewModel.focusedField == .title) ? true : false,
            )
            .frame(height: 30) // is needed to correctly size the UIKit TextField
            .padding()

            NewTaskUIKitTextField(
                text: $viewModel.note,
                focusedField: $viewModel.focusedField,
                isFirstResponder: (viewModel.isInFocus && viewModel.focusedField == .note) ? true : false,
            )
            .frame(height: 30) // is needed to correctly size the UIKit TextField
            .padding()
        }
    }
}

Activating focus state

In the end, let's see if we have met our requirements and how we can manage them.

  1. Set the initial focus state - Done. If we need it, we can set the default value using view model's focusedField and isInFocus properties:
@Published var focusedField: Field? = .title
@Published var isInFocus: Bool = true
  1. Ability to deactivate focus state - Done. Just set the view model's isInFocus to false. For instance, when the user taps the date button, we can call the method:
private func didTapDateButton() {
    viewModel.isInFocus = false
    ...
}
  1. Ability to restore focus state - Done. As we store the last focused field in the focusedField, it will be focused when we set isInFocus to true. In our example, when the user has finished picking the date, change isInFocus back to true.

Conclusion

Managing focus state is a common task in app development and it's worth adding some logic to be able to control it. The implementation above has some flexibility and is easy to implement.

I hope you enjoyed this article. If you have any questions, suggestions, or feedback, please let me know on Twitter.

Thanks for reading!