mink23
mink23

Reputation: 154

Split Paragraphs and Save Ranges in UITextView

I am trying to split paragraphs into an array of components using the code I have typed below. I am having trouble with the Ranges element though. My problem is that if the user types a string multiple times it returns the first times range and not the one I am wanting. 

Example:
Paragraph 1: Banana
Paragraph 2: Apple
Paragraph 3: Banana

Both Paragraph 1 and 3 will have Paragraph 1's range. What is a smarter way of going about this to fix this problem?

lazy var models: [ParagraphModel] = []

struct ParagraphModel {
    let text: String
    let range: Range<String.Index>
}

func getParagraphs(){
  let components = textView.text.components(separatedBy: "\n")

  let models = components.compactMap { component -> ParagraphModel? in
    if let range = textView.text.range(of: component) {
        return ParagraphModel(text: component, range: range)
    }
    return nil
  }
 self.models = models
}

Updated Code from @Rob:

func getParagraphModel() {
    guard let text = noteContents.text else { return }

    var currentModels: [ParagraphModel] = []
    text.enumerateSubstrings(in: text.startIndex..., options: .byParagraphs) { substring, range, _, stop in
        if  let substring = substring, !substring.isEmpty,
            let textRange = self.noteContents.text.range(of: substring)
        {
            currentModels.append(ParagraphModel(text: substring, range: textRange))
        }
    }
    self.models = currentModels
}

When typing the example Text the printout to the console is:

[Project.ViewController.ParagraphModel(text: "Banana", range: Range(Swift.String.Index(_rawBits: 0)..<Swift.String.Index(_rawBits: 393216))), Project.ViewController.ParagraphModel(text: "Apple", range: Range(Swift.String.Index(_rawBits: 458752)..<Swift.String.Index(_rawBits: 786432))), Project.ViewController.ParagraphModel(text: "Banana", range: Range(Swift.String.Index(_rawBits: 0)..<Swift.String.Index(_rawBits: 393216)))]

Upvotes: 1

Views: 641

Answers (1)

Rob
Rob

Reputation: 437432

The problem in your code snippet is that you’re always going to find the first occurrence of the target string. So, when it’s searching for the third paragraph, “Banana”, it’s going to find the first occurrence of that string.

I would suggest text.enumerateSubstrings(in:options:_:) to get the ranges of the paragraphs within text. This will enumerate the substrings and their respective ranges, which avoids you searching for the text yourself, avoiding problems that can arise if the same string appears multiple times.

Thus:

func getParagraphs() {
    guard let textView = textView, let text = textView.text else { return }

    models = []
    text.enumerateSubstrings(in: text.startIndex..., options: .byParagraphs) { substring, range, _, _ in
        guard let substring = substring, !substring.isEmpty else {
            return
        }

        self.models.append(ParagraphModel(text: substring, range: range))
    }
}

That produces:

[
    MyApp.ParagraphModel(text: "Paragraph 1: Banana", range: Range(Swift.String.Index(_rawBits: 0)..<Swift.String.Index(_rawBits: 1245184))),
    MyApp.ParagraphModel(text: "Paragraph 2: Apple", range: Range(Swift.String.Index(_rawBits: 1376256)..<Swift.String.Index(_rawBits: 2555904))),
    MyApp.ParagraphModel(text: "Paragraph 3: Banana", range: Range(Swift.String.Index(_rawBits: 2686976)..<Swift.String.Index(_rawBits: 3932160)))
]

Or, if range in ParagraphModel was a UITextRange, you’d do:

func getParagraphs() {
    guard let textView = textView, let text = textView.text else { return }

    models = []
    text.enumerateSubstrings(in: text.startIndex..., options: .byParagraphs) { substring, range, _, stop in
        let nsRange = NSRange(range, in: text)

        if  let substring = substring,
            !substring.isEmpty,
            let start = textView.position(from: textView.beginningOfDocument, offset: nsRange.location),
            let end = textView.position(from: start, offset: nsRange.length),
            let textRange = self.textView.textRange(from: start, to: end)
        {
            self.models.append(ParagraphModel(text: substring, range: textRange))
        }
    }
}

Producing:

[
    MyApp.ParagraphModel(text: "Paragraph 1: Banana", range: <_UITextKitTextRange: 0x6000037983e0> (0, 19)F),
    MyApp.ParagraphModel(text: "Paragraph 2: Apple", range: <_UITextKitTextRange: 0x600003798260> (21, 18)F),
    MyApp.ParagraphModel(text: "Paragraph 3: Banana", range: <_UITextKitTextRange: 0x600003799f20> (41, 19)F)
]

Upvotes: 3

Related Questions