0

I have a UITextView that I need to sometimes show in all caps and sometimes in original case. This part is easy but when the user starts editing text I need to update the original string with the changes while keeping the original case of the string. For this reason I have not been able to use textDidChange and instead I am using shouldChangeTextIn and changing the original string. Mostly everything is working as expected except if multiple words are selected and you use a tap on a predictive word in the keyboard. There may be other things that break. What is the best way to fix this to keep the original string while showing a mutated string. Here is a minimal example.

import UIKit

class ViewController: UIViewController {

    lazy var textView : UITextView = {
        let txtv = UITextView(frame: CGRect(x: 0, y:40, width: self.view.frame.width, height: 200))
        txtv.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        txtv.delegate = self
        txtv.font = UIFont.systemFont(ofSize: 22)
        return txtv
    }()

    lazy var button : UIButton = {
        let btn = UIButton(frame: CGRect(x: 0, y:250, width: self.view.frame.width - 40, height: 50))
        btn.autoresizingMask = [.flexibleWidth,.flexibleHeight]

        btn.setTitleColor(UIColor.blue, for: .normal)
        btn.setTitle("SHOW TEXT", for: .normal)
        btn.addTarget(self, action: #selector(changeState), for: .touchUpInside)
        return btn
    }()

    var textString = "This is a test to see how we are doing"
    var shouldCapitalize : Bool = true
    private var hack_shouldIgnorePredictiveInput = false

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(textView)
        self.view.addSubview(button)
        //get things going
        updateTextView()
    }

    func updateTextView(){
        //we could change font size here or size of textview
        var textToShow = self.textString
        if shouldCapitalize == true{
            textToShow = textToShow.uppercased()
        }
        textView.text = textToShow
        print("the text string we really care about is::::: \(textString)")
    }

    @objc func changeState(){
        shouldCapitalize = !shouldCapitalize
        updateTextView()
    }
}

extension ViewController : UITextViewDelegate{

    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {

        if hack_shouldIgnorePredictiveInput {
            hack_shouldIgnorePredictiveInput = false
            return false
        }

        hack_shouldIgnorePredictiveInput = true

        //how do i replace the characters or text changging without changing the case of the original string
        print("the textview text is \(textView.text)")
        print("the textview text is \((textView.text as NSString).length)")
        print("the range is \(range)")
        print("the text is \(text)")

        let selectedRange = self.textView.selectedRange

        if let str = textView.text as? NSString{
            if range.length == 0 && text == ""{
                print("the ext is empty")

                textString = ""
                updateTextView()
            }
            if let tracker = textString as? NSString{
                if range.location == tracker.length{
                    print("adding")
                    print("the lenght of the tracker is \(tracker.length)")
                    let newRange = NSMakeRange(tracker.length, range.length)
                    print("the new range is \(newRange)")
                    let newString = tracker.replacingCharacters(in: newRange, with: text)
                    print("the new text is \(newString)")

                    textString = newString
                    updateTextView()
                }else if range.location < tracker.length{
                    let newString = tracker.replacingCharacters(in: range, with: text)
                    print("the new text is \(newString)")

                    textString = newString
                    updateTextView()
                    if (newString as NSString).length > tracker.length{
                        print("setting cursor \(NSMakeRange(range.location + range.length, 0))")
                        self.textView.selectedRange = NSMakeRange(range.location + range.length + 1, 0)
                    }else{
                        self.textView.selectedRange = NSMakeRange(range.location, 0)
                    }



                }else{
                   //the problem seems to be in here
                    print("maybe adding")
                    print("the lenght of the tracker is \(tracker.length)")
                    let newRange = NSMakeRange(tracker.length, range.length)
                    print("the new range is \(newRange)")
                    let newString = str.replacingCharacters(in: newRange, with: text)
                    print("the new text is \(newString)")
                    textString = newString
                    updateTextView()
                }
            }
        }

        hack_shouldIgnorePredictiveInput = false
        return false
    }

}
agibson007
  • 4,173
  • 2
  • 19
  • 24
  • Your final else statement has the comment `//the problem seems to be in here`. The only time you will get into that else condition is if `range.location` is greater than `tracker.length` because the other `if...else` have caught the other cases. It appears that the only way to have `range.location` greater than `tracker.length` is if you set `textString = ""` up above where you check `range.length == 0 && text == ""`. I don't quite follow all that. Perhaps you could provide the output of the print statements for more context. Give each print statement unique text to avoid ambiguity. – jimmyg May 18 '18 at 23:42
  • @jimmyg you helped me pointing out that my last else should not be needed which I believed was unneeded when I wrote it. The reason it was being called was the textview was trying to add a space with predictive text and I intercepted that and set the string to empty which made the last else get hit. I think I got it now but thanks for triggering my gut. – agibson007 May 19 '18 at 03:44

1 Answers1

0

So it seems my bottom else was being called because the text was set to empty all driven by the fact that the delegate is called sometimes twice to input a space. The comment from jimmyg helped me see that he was right and the last else should never be hit which was my thought when I wrote the function. Luckily this answer helped explain why empty text was happening. If you are here I recommend upvoting that answer. I also recommend upvoting this answer that helps stop some of the unnecessary calls with predictive text and can be seen in my code. Finally all the logic seems to be in place and much more simple and switching between intended text and transformed text inside the UITextView is working.

import UIKit

class ViewController: UIViewController {

    lazy var textView : UITextView = {
        let txtv = UITextView(frame: CGRect(x: 0, y:40, width: self.view.frame.width, height: 200))
        txtv.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        txtv.delegate = self
        txtv.font = UIFont.systemFont(ofSize: 22)
        return txtv
    }()

    lazy var button : UIButton = {
        let btn = UIButton(frame: CGRect(x: 0, y:250, width: self.view.frame.width - 40, height: 50))
        btn.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        btn.setTitleColor(UIColor.blue, for: .normal)
        btn.setTitle("SHOW TEXT", for: .normal)
        btn.addTarget(self, action: #selector(changeState), for: .touchUpInside)
        return btn
    }()

    var textString = "This is a test to see how we are doing"
    var shouldCapitalize : Bool = true
    private var hack_shouldIgnorePredictiveInput = false

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(textView)
        self.view.addSubview(button)
        //get things going
        updateTextView()
    }

    func updateTextView(){
        //we could change font size here or size of textview
        var textToShow = self.textString
        if shouldCapitalize == true{
            textToShow = textToShow.uppercased()
        }
        textView.text = textToShow
        print("realString:: \(textString)")
        print("textView showing:: \(textView.text)")
    }


    @objc func changeState(){
        shouldCapitalize = !shouldCapitalize
        updateTextView()
    }
}

extension ViewController : UITextViewDelegate{

    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {

        if hack_shouldIgnorePredictiveInput {
            hack_shouldIgnorePredictiveInput = false
            return false
        }

        hack_shouldIgnorePredictiveInput = true

            if range.length == 0 && text == "" && self.textString.count > 0 && range.length == self.textString.count{
                textString = ""
                updateTextView()
            }
            let tracker = (textString as NSString)
            if range.location == tracker.length{
                let newRange = NSMakeRange(tracker.length, range.length)
                let newString = tracker.replacingCharacters(in: newRange, with: text)
                textString = newString
                updateTextView()
            }else if range.location < tracker.length{
                if text.isEmpty && range.length == 0 && range.location > 0{
                    let newString = tracker.replacingCharacters(in:range, with: text)
                    textString = newString
                    updateTextView()

                    self.textView.selectedRange = NSMakeRange(range.upperBound, 0)
                }else{
                    let newString = tracker.replacingCharacters(in: range, with: text)
                    textString = newString
                    updateTextView()
                   self.textView.selectedRange = NSMakeRange(range.upperBound - tracker.length + (newString as NSString).length , 0)
                }
            }

        hack_shouldIgnorePredictiveInput = false
        return false
    }
}
agibson007
  • 4,173
  • 2
  • 19
  • 24