0

I am implementing a screen that shows a complete question

For Example:

"First day of the week is .........."

when the user touch the dots an alert appears and allows user to enter an answer to submit and replace the dots with his answers

My steps are:

1. I generated ranges for the dots by :

func getRangesOfMissingWords(by missingWord: String = completePlace) -> [(answer: String, range: NSRange)]? {
    let body: NSString = theQuestionBody.text as NSString
    var ranges : [(answer: String, range: NSRange)] = []
    var searchRange = NSRange(location: 0, length: body.length)
    while searchRange.location < body.length {
        searchRange.length = body.length - searchRange.location
        let foundRange = self.getRangeOf(word: missingWord, in: searchRange)
        if foundRange != nil {
            ranges.append((answer: missingWord, range: foundRange!))
            searchRange.location = foundRange!.location + foundRange!.length
        }
        else {
            break
        }
    }
    if ranges.isEmpty == false {
        return ranges
    }
    else {
        return nil
    }
}

and then

2.Generated a frames around the missing word ranges by:

func createAnswersFrames(from ranges: [(answer: String, range: NSRange)]) {
    var frames : [WordFrame] = []

    for range in ranges {
        let pos2 = self.theQuestionBody.position(from: self.theQuestionBody.beginningOfDocument, offset: (range.range.location + (range.range.length - 1)))
        let pos1 = self.theQuestionBody.position(from: self.theQuestionBody.beginningOfDocument, offset: range.range.location)
        print("pos1 : \(pos1.debugDescription)")
        print("pos2 : \(pos2.debugDescription)")
        let textRange : UITextRange = self.theQuestionBody.textRange(from: pos1!, to: pos2!)!
        let frame: CGRect = self.theQuestionBody.firstRect(for: textRange)
        frames.append(WordFrame(rect: frame))



    }

    self.missingWordsFrames = frames

}

and

3.added a UITapGestureRecognizer for the UITextView

let sigleTap : UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(tapRecognizer:)))
theQuestionBody.addGestureRecognizer(sigleTap)

4.and in the observer for the tap

func handleTap(tapRecognizer: UITapGestureRecognizer) {
    guard let ranges = self.missingWordsRanges else {
        return
    }

    self.createAnswersFrames(from: ranges)

    let touchPoint = tapRecognizer.location(in: theQuestionBody)

    if self.missingWordsFrames.count > 0 {
        for (index,frame) in self.missingWordsFrames.enumerated() {
            frame.isSelected = false
            let validFrame = frame.rect
            if (validFrame.contains(touchPoint)) {
                frame.isSelected = true
                let alertTitle = Helper.getSerroundingText(of: self.missingWordsRanges![index].range, from: self.question.text)
                let alert = UIAlertController(title: alertTitle , message:"", preferredStyle: .alert)
                alert.addTextField { (textField) in
                    textField.placeholder = "Complete..."
                }
                alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] (_) in
                    let textField = alert!.textFields![0] 
                    textField.becomeFirstResponder()
                    let answer = textField.text
                    if answer != "" {
                        self.submitAnswer(answer!) 
                    }

                }))
                self.present(alert, animated: true, completion: nil)
                break
            }

        }
    }
}

The problem is that the frames work fine for the first time the view is appeared, but when the user insert an answer that has long number of characters so the rest of the text will be placed in a new line the frames doesn't work fine then .

Example

"The x is a .......... . And the y is .........."

answer -> "SomeLongWordAnswer"

"The x is a SomeLongWordAnswer . And the y is

.........."

Here the frame for the second dots stays on the first line as it was !

1 Answers1

0

I have changed the way of implementing it by removing the frames and depend on the delegate method of UITextViewDelegate textViewDidChangeSelection

func textViewDidChangeSelection(_ textView: UITextView) {

        UIMenuController.shared.isMenuVisible = false
        if let missingWordRanges = self.missingWordsRanges {
            for (index,range) in missingWordRanges.enumerated() {
                if range.range == textView.selectedRange {

                    let alertTitle = Helper.getSerroundingText(of: self.missingWordsRanges![index].range, from: self.question.text)
                    let alert = UIAlertController(title: alertTitle , message:"", preferredStyle: .alert)
                    alert.addTextField { (textField) in
                        textField.placeholder = "Complete..."
                    }
                    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] (_) in
                        let textField = alert!.textFields![0] // Force unwrapping because we know it exists.
                        textField.becomeFirstResponder()
                        let answer = textField.text
                        if answer != "" {
                            self.submit(answer!, in: index) // cann't press ok if not submitted with an answer so force rapping is okay
                        }

                    }))
                    self.present(alert, animated: true, completion: nil)
                    return
                }
            }
        }



}

then here comes the problem that this method is called when selection is changed and in the iPhone a selection is changed by double tap or long tap

so i have updated my gesture

let sigleTap : UITapGestureRecognizer = UITapGestureRecognizer.init(target: self, action: #selector(didRecieveGestureOnText(recognizer:)))
theQuestionBody.addGestureRecognizer(sigleTap)

and in the gesture handler based in hris.to answer

func didRecieveGestureOnText(recognizer: UIGestureRecognizer)  {
    let textView : UITextView = recognizer.view as! UITextView
    let location = recognizer.location(in: self.theQuestionBody)
    let tapPosition = textView.closestPosition(to: location)
    let textRange = textView.tokenizer.rangeEnclosingPosition(tapPosition!, with: UITextGranularity.word, inDirection: UITextLayoutDirection.right.rawValue)

    self.theQuestionBody.selectedTextRange = textRange
    }

so by this line self.theQuestionBody.selectedTextRange = textRange will trigger the textViewDidChangeSelection and everything works as expected