Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unit-testing the responder chain of textfields

Unit-testing the responder chain of textfields

Florian Fittschen

May 12, 2020
Tweet

Other Decks in Programming

Transcript

  1. FORMS • Entering data should be effortless • Known data

    should not be required to be entered again ➡ Enable auto-fill and automatically jump to next text field Swift Meetup Munich 12.05.2020 4
  2. AUTO-FILL TEXT CONTENT TYPES ARE YOUR FRIEND* * At least

    they were before iOS 13 Swift Meetup Munich 12.05.2020 6
  3. iOS Auto-fill < 13.0 ✅ 13.0 ..< 13.4 ❌ >=

    13.4 ✅ • Auto-fill was broken for 6 months and 19 days • Or rather almost a year if you were using the iOS 13 betas Swift Meetup Munich 12.05.2020 8
  4. AUTOMATICALLY JUMP • Old solution: IQKeyboardManager doing a lot of

    magic • New solution: Own KeyboardManager Swift Meetup Munich 12.05.2020 10
  5. KeyboardManager • Keeps track of all UITextFields in a view

    controller • Decides who should be the next text field by sorting the text fields • Fully configurable and no swizzling involved • Acts as delegate for all text fields • Fires all becomeFirstResponder() / resignFirstResponder() Swift Meetup Munich 12.05.2020 11
  6. SETUP • KeyboardManager • Lives in it's own module •

    Unit tests without host application Swift Meetup Munich 12.05.2020 12
  7. PITFALLS - I • Auto-correction looks / works like auto-fill

    • No suggestions for phone numbers Swift Meetup Munich 12.05.2020 13
  8. Auto-correction looks / works like auto-fill private let nameTextField: UITextField

    = { let textField = UITextField() textField.placeholder = "Full name" textField.textContentType = .name textField.autocorrectionType = .no textField.spellCheckingType = .no textField.autocapitalizationType = .words return textField }() Swift Meetup Munich 12.05.2020 14
  9. No suggestions for phone numbers private let phoneNumberTextField: UITextField =

    { let textField = UITextField() textField.placeholder = "Phone number" textField.textContentType = .telephoneNumber textField.keyboardType = .namePhonePad textField.autocorrectionType = .no textField.spellCheckingType = .no textField.autocapitalizationType = .none return textField }() Swift Meetup Munich 12.05.2020 15
  10. PITFALLS - I • Auto-correction looks / works like auto-fill

    • No suggestions for phone numbers Swift Meetup Munich 12.05.2020 16
  11. PITFALLS - II • UITextField needs to be in active

    view hierarchy • Add UITextField to UIView in UIViewController in UIWindow • Ensure that relevant methods of view lifecycle are called • Ensure that view is layed out Swift Meetup Munich 12.05.2020 17
  12. Add UITextField to UIView in UIViewController in UIWindow func test_jumpsToNextTextField()

    { let textField = UITextField() let viewController = KeyboardManagerTestViewController(textFields: [textField]) prepareView(of: viewController) // ... } private func prepareView(of viewController: UIViewController) { let size = viewController.view.frame.size let window = UIWindow(frame: CGRect(origin: .zero, size: viewController.view.bounds.size)) window.rootViewController = viewController window.isHidden = false } Swift Meetup Munich 12.05.2020 18
  13. Ensure that relevant methods of view lifecycle are called private

    func prepareView(of viewController: UIViewController) { let size = viewController.view.frame.size let window = UIWindow(frame: CGRect(origin: .zero, size: viewController.view.bounds.size)) window.rootViewController = viewController window.isHidden = false viewController.beginAppearanceTransition(true, animated: false) viewController.endAppearanceTransition() } Swift Meetup Munich 12.05.2020 19
  14. Ensure that view is layed out private func prepareView(of viewController:

    UIViewController) { let size = viewController.view.frame.size let window = UIWindow(frame: CGRect(origin: .zero, size: viewController.view.bounds.size)) window.rootViewController = viewController window.isHidden = false viewController.beginAppearanceTransition(true, animated: false) viewController.endAppearanceTransition() viewController.view.setNeedsLayout() viewController.view.layoutIfNeeded() if viewController.view.frame.size.width == 0 || viewController.view.frame.size.height == 0 { // Try to call sizeToFit() if the view still has invalid size viewController.view.sizeToFit() viewController.view.setNeedsLayout() viewController.view.layoutIfNeeded() } } ! pointfreeco/swift-snapshot-testing 20
  15. PITFALLS - II • UITextField needs to be in active

    view hierarchy • Add UITextField to UIView in UIViewController in UIWindow • Ensure that relevant methods of view lifecycle are called • Ensure that view is layed out ✅ Swift Meetup Munich 12.05.2020 21
  16. TEST CASE func test_jumpsToNextTextField() { // Given let textFields =

    [UITextField(), UITextField(), UITextField()] zip(textFields, ["foo", "bar", "baz"]).forEach { $0.text = $1 $0.autocorrectionType = .no } let viewController = KeyboardManagerTestViewController(textFields: textFields) prepareView(of: viewController) let keyboardManager = KeyboardManager(viewController: viewController) // When let result = keyboardManager.textField(textFields[0], shouldChangeCharactersIn: NSRange(location: 0, length: 0), replacementString: "Foo bar ") // Then XCTAssertFalse(result) XCTAssertEqual(textFields[0].text, "Foo Bar") XCTAssertTrue(textFields[1].isFirstResponder) } Swift Meetup Munich 12.05.2020 22
  17. RECAP • Configure UITextField to support auto-fill • set textContentType

    • set autocorrectionType to .no • set keyboardType • Ensure active view hierarchy Swift Meetup Munich 12.05.2020 23