You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

带Previous/Next按钮的UIToolbar定制:需外部类调用按钮动作方法

How to Add Custom Callbacks for Input Toolbar Buttons

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:

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 UITextField without subclassing it, keeping your code flexible

内容的提问来源于stack exchange,提问作者Steve Gear

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:35:57