Detect text direction based on content

Viewed 3606

I wonder if is it possible to set UITextView's text direction dynamically and based on its content?

Default behavior is like this: if you start a line with an LTR language that line will be LTR, but if you begin the next line with an RTL language that line's direction changes to RTL.

enter image description here

What I want is set entire paragraphs direction based on first one.

Is it possible?

4 Answers

Based on Arash R's excellent answer, here is a Swift 4.2 extension that selects the text direction individually for each paragraph in the UITextView.

It determines the language of the last character in each paragraph. This way if you have a numbered list in an RTL language, the paragraph will be RTL.

Call the function from the text change method, and if you populate the UITextView when you first load the ViewController - from ViewDidLoad.

extension UITextView {
    func detectRightToLeft() {
        if let text = self.text, !text.isEmpty { // Set text, make sure it is not nil
            let cleanFile = text.replacingOccurrences(of: "\r", with: "\n")
            var newLineIndices:Array<Int> = []
            for (index, char) in cleanFile.enumerated() {
                if char == "\n" {
                    newLineIndices.append(index) // Get location of all newline characters
                }
            }
            newLineIndices.insert(-1, at: 0) // Place position 0 at the beginning of the array
            newLineIndices.append(cleanFile.count) // Add the location after last character

            let tagschemes = NSArray(objects: NSLinguisticTagScheme.language)
            let tagger = NSLinguisticTagger(tagSchemes: tagschemes as! [NSLinguisticTagScheme], options: 0)
            tagger.string = cleanFile

            for i in 0..<newLineIndices.count-1 {
                // Determine direction by the last character of paragraph
                var taggerCounter = newLineIndices[i+1]-1
                var language = tagger.tag(at: taggerCounter, scheme: NSLinguisticTagScheme.language, tokenRange: nil, sentenceRange: nil)
                // Neutral characters should make the tagger look at the character before
                while language == nil && taggerCounter >= 1 {
                    taggerCounter -= 1
                    language = tagger.tag(at: taggerCounter, scheme: NSLinguisticTagScheme.language, tokenRange: nil, sentenceRange: nil)
                }

                if String(describing: language).range(of: "he") != nil || String(describing: language).range(of: "ar") != nil || String(describing: language).range(of: "fa") != nil {
                    self.setBaseWritingDirection(.rightToLeft, for: self.textRange(from: self.position(from: self.beginningOfDocument, offset: newLineIndices[i]+1)!, to: self.position(from: self.beginningOfDocument, offset: newLineIndices[i+1])!)!)
                    print ("Right to Left Paragraph at character \(newLineIndices[i]+1)")

                } else {
                    self.setBaseWritingDirection(.leftToRight, for: self.textRange(from: self.position(from: self.beginningOfDocument, offset: newLineIndices[i]+1)!, to: self.position(from: self.beginningOfDocument, offset: newLineIndices[i+1])!)!)
                    print ("Left to Right Paragraph at character \(newLineIndices[i]+1)")
                }
            }
        }
    }
}

EDIT: Previous version included an option to choose by the first character of the paragraph. That option caused crashes, so I left it out for now. Instead, the current code includes treatment for neutral characters. Another edit: changed the taggerCounter minimal value to 1, to prevent it from ever becoming negative.

Based on Ron answer, Here is a more general extension for Strings

extension String {

    var isRTL: Bool {
        let cleanFile = self.replacingOccurrences(of: "\r", with: "\n")
        var newLineIndices: Array<Int> = []

        for (index, char) in cleanFile.enumerated() {
            if char == "\n" {
                newLineIndices.append(index)
            }
        }

        newLineIndices.insert(-1, at: 0)
        newLineIndices.append(cleanFile.count)

        let tagschemes = NSArray(objects: NSLinguisticTagScheme.language)
        let tagger = NSLinguisticTagger(tagSchemes: tagschemes as! [NSLinguisticTagScheme], options: 0)
        tagger.string = cleanFile

        for i in 0..<newLineIndices.count - 1 {
            let language = tagger.tag(at: newLineIndices[i + 1] - 1, scheme: NSLinguisticTagScheme.language, tokenRange: nil, sentenceRange: nil)

            if String(describing: language).range(of: "he") != nil || String(describing: language).range(of: "ar") != nil || String(describing: language).range(of: "fa") != nil {
                return true
            } else {
                return false
            }
        }

        return false
    }

    var isLTR: Bool {
        let cleanFile = self.replacingOccurrences(of: "\r", with: "\n")
        var newLineIndices: Array<Int> = []

        for (index, char) in cleanFile.enumerated() {
            if char == "\n" {
                newLineIndices.append(index)
            }
        }

        newLineIndices.insert(-1, at: 0)
        newLineIndices.append(cleanFile.count)

        let tagschemes = NSArray(objects: NSLinguisticTagScheme.language)
        let tagger = NSLinguisticTagger(tagSchemes: tagschemes as! [NSLinguisticTagScheme], options: 0)
        tagger.string = cleanFile

        for i in 0..<newLineIndices.count - 1 {
            let language = tagger.tag(at: newLineIndices[i + 1] - 1, scheme: NSLinguisticTagScheme.language, tokenRange: nil, sentenceRange: nil)

            if String(describing: language).range(of: "he") != nil || String(describing: language).range(of: "ar") != nil || String(describing: language).range(of: "fa") != nil {
                return false
            } else {
                return true
            }
        }

        return false
    }

}

As of iOS 12, we can use NLLanguageRecognizer:

import NaturalLanguage

extension String {
    var isRightToLeft: Bool {
        guard let language = NLLanguageRecognizer.dominantLanguage(for: self) else { return false }
        switch language {
        case .arabic, .hebrew, .persian, .urdu:
            return true
        default:
            return false
        }
    }
}
Related