带Previous/Next按钮的UIToolbar定制:需外部类调用按钮动作方法
Hey, great job getting the basic input toolbar working! To let other classes (like your proxy/delegate classes) run custom code when the Previous, Next, or Done buttons are tapped, we can modify your existing implementation to support either closure callbacks (super flexible for one-off use cases) or a delegate protocol (better for shared logic across multiple classes). Let's break down both approaches:
Option 1: Closure Callbacks (Recommended for Most Cases)
We'll update your addInputAccessoryForTextFields method to accept optional closure parameters, so you can inject custom logic directly when setting up the toolbar. We'll also keep the original focus-switching behavior intact.
Updated Extension Code
import UIKit extension UIViewController { func addInputAccessoryForTextFields( textFields: [UITextField], dismissable: Bool = true, previousNextable: Bool = false, onPreviousTap: ((UITextField) -> Void)? = nil, onNextTap: ((UITextField) -> Void)? = nil, onDoneTap: (() -> Void)? = nil ) { for (index, textField) in textFields.enumerated() { let toolbar = UIToolbar() toolbar.sizeToFit() var items = [UIBarButtonItem]() if previousNextable { // Configure Previous Button let previousButton = UIBarButtonItem( image: UIImage(named: "Backward Arrow"), style: .plain, target: self, action: #selector(handlePreviousTap(_:)) ) previousButton.width = 30 previousButton.isEnabled = textField != textFields.first previousButton.tag = index // Store index to find the previous field // Configure Next Button let nextButton = UIBarButtonItem( image: UIImage(named: "Forward Arrow"), style: .plain, target: self, action: #selector(handleNextTap(_:)) ) nextButton.width = 30 nextButton.isEnabled = textField != textFields.last nextButton.tag = index items.append(contentsOf: [previousButton, nextButton]) } // Add Spacer and Done Button let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDoneTap)) items.append(contentsOf: [spacer, doneButton]) toolbar.setItems(items, animated: false) textField.inputAccessoryView = toolbar // Store closures as associated objects on the text field if let onPrev = onPreviousTap { objc_setAssociatedObject(textField, &AssociatedKeys.onPreviousTap, onPrev, .OBJC_ASSOCIATION_COPY_NONATOMIC) } if let onNext = onNextTap { objc_setAssociatedObject(textField, &AssociatedKeys.onNextTap, onNext, .OBJC_ASSOCIATION_COPY_NONATOMIC) } if let onDone = onDoneTap { objc_setAssociatedObject(textField, &AssociatedKeys.onDoneTap, onDone, .OBJC_ASSOCIATION_COPY_NONATOMIC) } // Store the text field array for reference objc_setAssociatedObject(textField, &AssociatedKeys.associatedTextFields, textFields, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } // MARK: - Button Action Handlers @objc private func handlePreviousTap(_ sender: UIBarButtonItem) { guard let currentTextField = view.findFirstResponder() as? UITextField, let textFields = objc_getAssociatedObject(currentTextField, &AssociatedKeys.associatedTextFields) as? [UITextField], sender.tag > 0 else { return } let previousField = textFields[sender.tag - 1] previousField.becomeFirstResponder() // Trigger custom closure if provided let onPrev = objc_getAssociatedObject(currentTextField, &AssociatedKeys.onPreviousTap) as? ((UITextField) -> Void) onPrev?(previousField) } @objc private func handleNextTap(_ sender: UIBarButtonItem) { guard let currentTextField = view.findFirstResponder() as? UITextField, let textFields = objc_getAssociatedObject(currentTextField, &AssociatedKeys.associatedTextFields) as? [UITextField], sender.tag < textFields.count - 1 else { return } let nextField = textFields[sender.tag + 1] nextField.becomeFirstResponder() // Trigger custom closure if provided let onNext = objc_getAssociatedObject(currentTextField, &AssociatedKeys.onNextTap) as? ((UITextField) -> Void) onNext?(nextField) } @objc private func handleDoneTap() { view.endEditing(true) // Trigger custom closure if provided if let currentTextField = view.findFirstResponder() as? UITextField { let onDone = objc_getAssociatedObject(currentTextField, &AssociatedKeys.onDoneTap) as? (() -> Void) onDone?() } } } // MARK: - Helper Extensions & Associated Keys private enum AssociatedKeys { static var onPreviousTap = "onPreviousTap" static var onNextTap = "onNextTap" static var onDoneTap = "onDoneTap" static var associatedTextFields = "associatedTextFields" } extension UIView { func findFirstResponder() -> UIView? { if isFirstResponder { return self } for subview in subviews { if let responder = subview.findFirstResponder() { return responder } } return nil } }
How to Use It in Another Class
Now you can pass custom logic directly when setting up the toolbar:
class YourFormViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let field1 = UITextField() field1.placeholder = "First Name" let field2 = UITextField() field2.placeholder = "Last Name" addInputAccessoryForTextFields( [field1, field2], dismissable: true, previousNextable: true, onPreviousTap: { previousField in // Custom logic when Previous is tapped print("Switched to previous field: \(previousField.placeholder ?? "Unknown")") // Example: Validate current field before switching if let currentField = self.view.findFirstResponder() as? UITextField, currentField.text?.isEmpty == true { currentField.layer.borderColor = UIColor.red.cgColor } }, onNextTap: { nextField in // Custom logic when Next is tapped print("Switched to next field: \(nextField.placeholder ?? "Unknown")") }, onDoneTap: { // Custom logic when Done is tapped print("Done tapped! Submitting form...") self.submitForm() } ) // Add fields to view (don't forget this!) field1.frame = CGRect(x: 20, y: 100, width: view.frame.width - 40, height: 40) field2.frame = CGRect(x: 20, y: 160, width: view.frame.width - 40, height: 40) field1.borderStyle = .roundedRect field2.borderStyle = .roundedRect view.addSubview(field1) view.addSubview(field2) } private func submitForm() { // Your form submission logic here } }
Option 2: Delegate Protocol (For Shared Logic)
If you need multiple classes to handle toolbar button taps the same way, a delegate protocol is a cleaner approach.
Step 1: Define the Protocol
protocol InputToolbarDelegate: AnyObject { func inputToolbarDidTapPrevious(to textField: UITextField) func inputToolbarDidTapNext(to textField: UITextField) func inputToolbarDidTapDone() }
Step 2: Update the Extension
extension UIViewController { func addInputAccessoryForTextFields( textFields: [UITextField], dismissable: Bool = true, previousNextable: Bool = false, delegate: InputToolbarDelegate? ) { for (index, textField) in textFields.enumerated() { let toolbar = UIToolbar() toolbar.sizeToFit() var items = [UIBarButtonItem]() if previousNextable { let previousButton = UIBarButtonItem( image: UIImage(named: "Backward Arrow"), style: .plain, target: self, action: #selector(handlePreviousWithDelegate(_:)) ) previousButton.width = 30 previousButton.isEnabled = textField != textFields.first previousButton.tag = index let nextButton = UIBarButtonItem( image: UIImage(named: "Forward Arrow"), style: .plain, target: self, action: #selector(handleNextWithDelegate(_:)) ) nextButton.width = 30 nextButton.isEnabled = textField != textFields.last nextButton.tag = index items.append(contentsOf: [previousButton, nextButton]) } let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDoneWithDelegate)) items.append(contentsOf: [spacer, doneButton]) toolbar.setItems(items, animated: false) textField.inputAccessoryView = toolbar // Store delegate and text fields as associated objects objc_setAssociatedObject(textField, &AssociatedKeys.toolbarDelegate, delegate, .OBJC_ASSOCIATION_ASSIGN) objc_setAssociatedObject(textField, &AssociatedKeys.associatedTextFields, textFields, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } @objc private func handlePreviousWithDelegate(_ sender: UIBarButtonItem) { guard let currentTextField = view.findFirstResponder() as? UITextField, let textFields = objc_getAssociatedObject(currentTextField, &AssociatedKeys.associatedTextFields) as? [UITextField], let delegate = objc_getAssociatedObject(currentTextField, &AssociatedKeys.toolbarDelegate) as? InputToolbarDelegate, sender.tag > 0 else { return } let previousField = textFields[sender.tag - 1] previousField.becomeFirstResponder() delegate.inputToolbarDidTapPrevious(to: previousField) } @objc private func handleNextWithDelegate(_ sender: UIBarButtonItem) { guard let currentTextField = view.findFirstResponder() as? UITextField, let textFields = objc_getAssociatedObject(currentTextField, &AssociatedKeys.associatedTextFields) as? [UITextField], let delegate = objc_getAssociatedObject(currentTextField, &AssociatedKeys.toolbarDelegate) as? InputToolbarDelegate, sender.tag < textFields.count - 1 else { return } let nextField = textFields[sender.tag + 1] nextField.becomeFirstResponder() delegate.inputToolbarDidTapNext(to: nextField) } @objc private func handleDoneWithDelegate() { view.endEditing(true) guard let currentTextField = view.findFirstResponder() as? UITextField, let delegate = objc_getAssociatedObject(currentTextField, &AssociatedKeys.toolbarDelegate) as? InputToolbarDelegate else { return } delegate.inputToolbarDidTapDone() } } // Update AssociatedKeys to include the delegate private enum AssociatedKeys { static var toolbarDelegate = "toolbarDelegate" static var associatedTextFields = "associatedTextFields" } // Reuse the findFirstResponder extension from Option 1 extension UIView { func findFirstResponder() -> UIView? { if isFirstResponder { return self } for subview in subviews { if let responder = subview.findFirstResponder() { return responder } } return nil } }
How to Use It in Another Class
Make your class conform to the InputToolbarDelegate protocol and implement the required methods:
class YourFormViewController: UIViewController, InputToolbarDelegate { override func viewDidLoad() { super.viewDidLoad() let field1 = UITextField() field1.placeholder = "Email" let field2 = UITextField() field2.placeholder = "Password" addInputAccessoryForTextFields( [field1, field2], dismissable: true, previousNextable: true, delegate: self ) // Add fields to view field1.frame = CGRect(x: 20, y: 100, width: view.frame.width - 40, height: 40) field2.frame = CGRect(x: 20, y: 160, width: view.frame.width - 40, height: 40) field1.borderStyle = .roundedRect field2.borderStyle = .roundedRect view.addSubview(field1) view.addSubview(field2) } // MARK: - InputToolbarDelegate func inputToolbarDidTapPrevious(to textField: UITextField) { print("Delegate: Switched to \(textField.placeholder ?? "unknown") field") // Custom logic here, like clearing error states textField.layer.borderColor = UIColor.systemGray.cgColor } func inputToolbarDidTapNext(to textField: UITextField) { print("Delegate: Switched to \(textField.placeholder ?? "unknown") field") // Custom logic here, like validating the previous field if let currentField = view.findFirstResponder() as? UITextField, currentField.text?.isEmpty == true { currentField.layer.borderColor = UIColor.red.cgColor } } func inputToolbarDidTapDone() { print("Delegate: Done tapped, attempting login...") // Custom logic here, like performing login attemptLogin() } private func attemptLogin() { // Your login logic here } }
Key Notes
- Both approaches preserve your original focus-switching behavior while adding hooks for custom code
- Closures are great for quick, one-off customizations; delegates work better for shared logic across multiple screens
- We use associated objects to store extra data (closures, delegates, text field arrays) on
UITextFieldwithout subclassing it, keeping your code flexible
内容的提问来源于stack exchange,提问作者Steve Gear

