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

Time:03-28

I have a class called ThemeVC which has a textview (connected with an IBoutlet) and functionalities applied to it (it has a recognizer that detects the tapped words).

My goal here is that I would like to extract that piece of functionality, and put it maybe in its own class or create a delegate so I could reuse that functionality on other textviews.

Anyone knows how?

I pasted my code below. (HERE comments, are functions that should be called from any view controller)

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

How to test my code:

  • Create a new project with a storyboard
  • On the left hand side rename viewcontroller with themeVC, and replace its code with the code I gave.
  • On the storyboard, embed the controller in a navigation controller, on right side, change in identity inspector class from view controller to themeVC
  • add a textview and link it to the IBoutlet

CodePudding user response:

Looking at the parts you want to move away from ThemeVC, I would have to say not everything should be moved away from ThemeVC.

For example, you marked startTagSelection as something you want to move away, but you reference the navigationController which belongs to the view controller so it should ideally not be the responsibility of your UITextView to update your UINavigationBar.

So the two ideas discussed in the comments was using SubClasses and Protocols.

Protocols was the suggestion of Ptit Xav so I will show one way that could be used, Ptit Xav could add an answer if something else was in mind.

I start with creating a protocol

// 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)
}

Next I subclass a UITextView to add my own customization

@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)
    }
}

And finally use it like so

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
    }
}

I did not add addTagSelectorToolBar as part of the CustomTextView implementation as this is not a good candidate to be part of that module as all of its code is related to the view controller so i don't recommend making a part of the CustomTextView implementation.

  • Related