如何重用应用于文本视图的部分代码(识别器、工具栏)?

How can I reuse part of code (recognizer, toolbar) applied to a textview?

我有一个名为 ThemeVC 的 class,它有一个文本视图(与 IBoutlet 连接)和应用于它的功能(它有一个识别器来检测点击的单词)。

我的目标是提取该功能,并可能将其放在自己的 class 中或创建一个委托,以便我可以在其他文本视图上重用该功能。

有人知道怎么做吗?

我在下面粘贴了我的代码。 (这里的注释是应该从任何视图控制器调用的函数)

import UIKit

class ThemeVC: UIViewController, UITextViewDelegate, UINavigationControllerDelegate {

    @IBOutlet weak var themeTextView: UITextView!
    var tB = UIBarButtonItem()
    
    // Move away from ThemeVC ... ->
    var selectionDict = [String:Int]()
    var viewTagCount = Int()
    var tap = UIGestureRecognizer()
    var firstTimeGrouped = false
    // -> ... Move away from ThemeVC
    
    override func viewDidLoad() {
        super.viewDidLoad()
        themeTextView.delegate = self
        loadbuttons ()
        //HERE
        addTagSelectorToolBar ()
    }
    
    func loadbuttons () {
        tB = UIBarButtonItem(image: UIImage(systemName: "hand.point.up.left"), style: .plain, target: self, action: #selector(getTag(sender:)))
        navigationItem.rightBarButtonItems = [tB]
    }

    @objc func getTag(sender: AnyObject) {
        themeTextView.resignFirstResponder()
        //HERE
        startTagSelection()
    }
}

// Move away from ThemeVC ... ->
extension ThemeVC {
    func startTagSelection () {
        navigationController?.setToolbarHidden(false, animated: false)
        tap.isEnabled = true
        tB.isEnabled = false
        themeTextView.isEditable = false
        themeTextView.isSelectable = false
    }
}

extension ThemeVC {
    @objc func doneTagSelection(){
        navigationController?.setToolbarHidden(true, animated: false)
        tap.isEnabled = false
        tB.isEnabled = true
        themeTextView.isEditable = true
        themeTextView.isSelectable = true
        firstTimeGrouped = false
    }
}

extension ThemeVC {
    func addTagSelectorToolBar (){
        addTappedTagRecognizer()
        tap.isEnabled = false
        let done = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTagSelection))
        let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
        toolbarItems = [spacer, done]
    }
}

extension ThemeVC {
    func addTappedTagRecognizer () {
        tap = UITapGestureRecognizer(target: self, action: #selector(tapResponse(recognizer:)))
        tap.delegate = self as? UIGestureRecognizerDelegate
        themeTextView.addGestureRecognizer(tap)
    }
    
    @objc private func tapResponse(recognizer: UITapGestureRecognizer) {
        let location: CGPoint = recognizer.location(in: themeTextView)
        let position: CGPoint = CGPoint(x:location.x, y:location.y)
        let tapPosition: UITextPosition? = themeTextView.closestPosition(to:position)
        
        if tapPosition != nil {
            let textRange: UITextRange? = themeTextView.tokenizer.rangeEnclosingPosition(tapPosition!, with: UITextGranularity.word, inDirection: UITextDirection(rawValue: 1))
            if textRange != nil
            {
                let tappedWord: String? = themeTextView.text(in:textRange!)
                print(tappedWord ?? "Unable to get word")
            }
        }
    }
}
//  ... -> Move away from ThemeVC

如何测试我的代码:

看看您想从 ThemeVC 中移除的部分,我不得不说并不是所有的东西都应该从 ThemeVC 中移除。

例如,您将 startTagSelection 标记为要移走的内容,但您引用了属于视图控制器的 navigationController,因此理想情况下,您的 UITextView 不应该负责更新你的 UINavigationBar。

所以评论中讨论的两个想法是使用子类和协议。

协议是 Ptit Xav 的建议,所以我将展示一种可以使用的方法,如果有其他想法,Ptit Xav 可以添加答案。

我从创建协议开始

// Name the protocol as you see appropriate
// I add @objc so it can be accessible from Storyboard
// This will be used to `hand over` responsibility of
// a certain action / event
@objc
protocol CustomTextViewTagDelegate: class {
    func customTextViewDidStartSelection(_ textView: CustomTextView)
    func customTextViewDidFinishSelection(_ textView: CustomTextView)
}

接下来我将 UITextView 子类化以添加我自己的定制

@IBDesignable
class CustomTextView: UITextView {
    
    var selectionDict = [String:Int]()
    var viewTagCount = Int()
    var tap = UIGestureRecognizer()
    var firstTimeGrouped = false
    
    // Name it as you wish
    // @IBInspectable added for storyboard accessibility
    // You could also make it an IBOutlet if your prefer
    // that interaction
    @IBInspectable
    weak var tagDelegate: CustomTextViewTagDelegate?
    
    func startTagSelection () {
        // Remove the commented lines as this should the responsibility of
        // the view controller, manage in the view controller using the delegate
        // navigationController?.setToolbarHidden(false, animated: false)
        // tB.isEnabled = false
        
        tap.isEnabled = true
        isEditable = false
        isSelectable = false
        
        // Hand over responsibility of this action back whatever
        // has subscribed as the delegate to implement anything else
        // for this action
        tagDelegate?.customTextViewDidStartSelection(self)
    }
    
    func addTappedTagRecognizer () {
        tap = UITapGestureRecognizer(target: self,
                                     action: #selector(tapResponse(recognizer:)))
        tap.delegate = self as? UIGestureRecognizerDelegate
        addGestureRecognizer(tap)
    }
    
    @objc private func tapResponse(recognizer: UITapGestureRecognizer) {
        let location: CGPoint = recognizer.location(in: self)
        let position: CGPoint = CGPoint(x:location.x,
                                        y: location.y)
        
        let tapPosition: UITextPosition? = closestPosition(to:position)
        
        if tapPosition != nil {
            let textRange: UITextRange? = tokenizer.rangeEnclosingPosition(tapPosition!,
                                                                           with: UITextGranularity.word,
                                                                           inDirection: UITextDirection(rawValue: 1))
            if textRange != nil
            {
                let tappedWord: String? = text(in:textRange!)
                print(tappedWord ?? "Unable to get word")
            }
        }
    }
    
    @objc func doneTagSelection() {
        
        // This is not the text view's responsibility, manage in the
        // view controller using the delegate
        // navigationController?.setToolbarHidden(true, animated: false)
        // tB.isEnabled = true
        
        tap.isEnabled = false
        isEditable = true
        isSelectable = true
        firstTimeGrouped = false
        
        // Hand over responsibility of this action back whatever
        // has subscribed as the delegate to implement anything else
        // for this action
        tagDelegate?.customTextViewDidFinishSelection(self)
    }
}

最后像这样使用它

class ThemeVC: UIViewController {
    
    // Change UITextView to CustomTextView
    @IBOutlet weak var themeTextView: CustomTextView!
    
    var tB = UIBarButtonItem()
    
    // If you do not set up the delegate in your
    // storyboard, you need to it in your code
    // call this function from didLoad or something
    // if needed
    private func configureTextView() {
        themeTextView.tagDelegate = self
    }
    
    // All your other implementation
}

extension ThemeVC: CustomTextViewTagDelegate {
    func customTextViewDidStartSelection(_ textView: CustomTextView) {
        navigationController?.setToolbarHidden(false,
                                               animated: false)
        tB.isEnabled = false
    }
    
    func customTextViewDidFinishSelection(_ textView: CustomTextView) {
        navigationController?.setToolbarHidden(true,
                                               animated: false)
        tB.isEnabled = true
    }
}

我没有添加 addTagSelectorToolBar 作为 CustomTextView 实现的一部分,因为这不是该模块的一部分,因为它的所有代码都与视图控制器相关,所以我不推荐成为 CustomTextView 实现的一部分。