11

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?

Maysam
  • 7,246
  • 13
  • 68
  • 106
  • take a look here: http://stackoverflow.com/questions/4905500/change-the-uitextview-text-direction I guess it can help. – PawelPawel Mar 23 '16 at 16:26

4 Answers4

11

I've had this problem myself. With a little bit of search, made this extension for UITextView which detects the first letter's language and makes it RTL if it's needed. You need to call the function after setting text, so you may want to call it in UITextViewDelegate text change" method.

extension UITextView {
func detectRightToLeft() {
    if let text = self.text where !text.isEmpty {
        let tagschemes = NSArray(objects: NSLinguisticTagSchemeLanguage)
        let tagger = NSLinguisticTagger(tagSchemes: tagschemes as! [String], options: 0)
        tagger.string = text

        let language = tagger.tagAtIndex(0, scheme: NSLinguisticTagSchemeLanguage, tokenRange: nil, sentenceRange: nil)
        if language?.rangeOfString("he") != nil || language?.rangeOfString("ar") != nil || language?.rangeOfString("fa") != nil {
            self.text = text.stringByReplacingOccurrencesOfString("\n", withString: "\n")
            self.textAlignment = .Right
            self.makeTextWritingDirectionRightToLeft(nil)
        }else{
            self.textAlignment = .Left
            self.makeTextWritingDirectionLeftToRight(nil)
        }
    }
}
}

Surely this is messy and not perfect. But it worked for me. You can get the idea.

Swift 3:

extension UITextView {
    func detectRightToLeft() {
        if let text = self.text, !text.isEmpty {
            let tagschemes = NSArray(objects: NSLinguisticTagSchemeLanguage)
            let tagger = NSLinguisticTagger(tagSchemes: tagschemes as! [String], options: 0)
            tagger.string = text

            let language = tagger.tag(at: 0, scheme: NSLinguisticTagSchemeLanguage, tokenRange: nil, sentenceRange: nil)
            if language?.range(of: "he") != nil || language?.range(of: "ar") != nil || language?.range(of: "fa") != nil {
                self.text = text.replacingOccurrences(of: "\n", with: "\n")
                self.textAlignment = .right
                self.makeTextWritingDirectionRightToLeft(nil)
            }else{
                self.textAlignment = .left
                self.makeTextWritingDirectionLeftToRight(nil)
            }
        }
    }
}
Benjamin Kindle
  • 1,736
  • 12
  • 23
Arash R
  • 402
  • 6
  • 14
1

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.

Ron Regev
  • 459
  • 2
  • 18
1

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
        }
    }
}
Yonat
  • 4,382
  • 2
  • 28
  • 37
0

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
    }

}
Mickael Belhassen
  • 2,970
  • 1
  • 25
  • 46