I have an array of strings. I need a way to find all keywords (once or twice used) and underline them. At the moment, I'm converting them to NSMutableAttributedString which is working, but that just displays the text. Is there a way to write a for loop or something that would find all the ranges of keywords and apply underline attribute to them? Am I doing it all wrong? I'm teaching myself Swift by writing an app instead of taking a course... probably not very smart. Thank you for your help!
I understand that having the client prosses the strings for each keyword each time it is run is very ineffieciet. The strings need to be easily updated, so (for now) it's faster to prosses each time.
Code (full repository ):
(Note: all strings have been removed; the first arrays have 20-40 strings in them)
//
// TextData.swift
// QuizBox
//
// Created by Me on 2/27/22.
//
import Foundation
let iCor1R = [] as [String]
let iCor2R = [] as [String]
let iCor3R = [] as [String]
let iCor4R = [] as [String]
let iCorR = [
iCor1R,
iCor2R,
iCor3R,
iCor4R
]
var iCor1T = iCor1R.map { NSMutableAttributedString(string: $0) }
var iCor2T = iCor2R.map { NSMutableAttributedString(string: $0) }
var iCor3T = iCor3R.map { NSMutableAttributedString(string: $0) }
var iCor4T = iCor4R.map { NSMutableAttributedString(string: $0) }
var text = [] as [NSMutableAttributedString]
var allWords = [""]
var onceUsed = [""]
var twiceUsed = [""]
public func generateKeywords() {
if (allWords == [""]) { // if this function has not been run yet
for i in Range(0...ICorTEXT.count-1) { // create a combined array with the contents of all sub-arrays
text.append(contentsOf: ICorTEXT[i])
}
for i in text {
for j in i.string.components(separatedBy: " ") { // create an array of each individual word in the combined array
allWords.append(j)
}
}
for i in allWords {
if(allWords.filter{$0 == i}.count==1) { // find all words that occur only once
onceUsed.append(i)
}
if(allWords.filter{$0 == i}.count==2 && twiceUsed.filter{$0 == i}.count==0) { // find all words that occur only twice
twiceUsed.append(i)
}
}
for c in Range(0...ICorTEXT.count-1) { //for each chapter
for v in Range(0...ICorTEXT[c].count-1) { //for each verse
for w in (ICorTEXT[c][v].string) { // for each word
//Maybe like this??
}
}
}
// for i in onceUsed {
// for c in Range(0...ICorTEXT.count-1) {
// for v in ICorTEXT[c] {
// if let range = v.range(of: i) {
// //v.addAttributes(.font, range: range)
// } else {
//
// }
// }
// }
// }
}
}
var ICorTEXT = [
iCor1T,
iCor2T,
iCor3T,
iCor4T
] as [[NSMutableAttributedString]]
New Code:
import Foundation
import SwiftUI
struct MatchProperty
{
// Stores the index of which array the word
// was encountered
var index: Int
// Stores the range of the word in the string
var range: NSRange
}
extension Array where Element: NSMutableAttributedString
{
/// Returns a new attributed string by concatenating the elements of the sequence, adding the given separator between each element.
/// - parameters:
/// - separator: A string to insert between each of the elements in this sequence. The default separator is an empty string.
func joined(separator: NSMutableAttributedString = NSMutableAttributedString(string: "")) -> NSMutableAttributedString
{
var isFirst = true
return self.reduce(NSMutableAttributedString())
{
(r, e) in
if isFirst
{
isFirst = false
}
else
{
r.append(separator)
}
r.append(e)
return r
}
}
/// Returns a new attributed string by concatenating the elements of the sequence, adding the given separator between each element.
/// - parameters:
/// - separator: A string to insert between each of the elements in this sequence. The default separator is an empty string.
func joined(separator: String = "") -> NSMutableAttributedString
{
return joined(separator: NSMutableAttributedString(string: separator))
}
}
// Original data
let iCor1R = ["Lest any should say that I had baptized in mine own name.",
"Grace be unto you, and peace, from God our Father, and from the.",
"Even as the testimony of Christ was confirmed in you:"]
let iCor2R = ["And I was with you in weakness, and in fear, and in much trembling.",
"But he that is spiritual judgeth all things, yet God himself is judged of no man.",]
let iCor3R = ["I have planted, Apollos watered; but God gave the increase.",
"Therefore let no man glory in men. For all things are yours;",
"And ye are Christ’s; and Christ [is] God’s."]
var iCorR: [[String]] = []
var ICorTEXT: [[NSMutableAttributedString]] = []
// Hash to store words and where they occurred
// This assumes you want to count the occurrences in all your string arrays
// If you want to count it for each string array, initialize this inside the
// loop
var wordOccurrences: [String: [MatchProperty]] = [:]
/// Stores the array index and position a word belongs to
private func storeWordPositions()
{
iCorR = [
iCor1R,
iCor2R,
iCor3R
]
for (iCorRIndex, strings) in iCorR.enumerated()
{
// Loop through the string in the strings array
for (sentenceIndex, string) in strings.enumerated()
{
// Loop through all the words in each string of the string array
string.enumerateSubstrings(in: string.startIndex...,
options: .byWords)
{ [weak self] word, substringRange, _, _ in
// Lower case the word
if let word = word?.lowercased()
{
// Check if we have come across the word before or not
// If we have, retrieve the match property for the word
// If not, we will initialize a new array to store the match
// properties of the first occurrence
var matchesInfo = self?.wordOccurrences[word, default: []]
// Create a match location object with the index of where
// the word occurs in the strings array along with its range
let matchProperty = MatchProperty(iCorRIndex: iCorRIndex,
sentenceIndex: sentenceIndex,
range: NSRange(substringRange,
in: string))
// Store the match
matchesInfo?.append(matchProperty)
self?.wordOccurrences[word] = matchesInfo
}
}
}
}
}
/// Underlines the words that occur a specified number of times
private func underlineWords(thatOccur occur: Int)
{
// This will be used to store the updated versions of the strings
// with the underlines
var updatedStrings
= iCorR.map{ $0.map { NSMutableAttributedString(string: $0) } }
// Optimize by filtering only the words that need processing
let occurrences = wordOccurrences.filter { return $1.count == occur }
// Loop through the occurrences of words
for occurrence in occurrences.values
{
// Check which words occurred twice
if occurrence.count == occur
{
// Iterate over the occurrences
for matchProperty in occurrence
{
// Retrieve the attributed version of the string
let attributedString
= updatedStrings[matchProperty.iCorRIndex][matchProperty.sentenceIndex]
// Underline words that have occurred twice
attributedString.addAttribute(.underlineStyle,
value: NSUnderlineStyle.double.rawValue,
range: matchProperty.range)
// Add yellow background as well (optional, just to see easily)
attributedString.addAttribute(.backgroundColor,
value: UIColor.yellow,
range: matchProperty.range)
// Update the attributed string data
updatedStrings[matchProperty.iCorRIndex][matchProperty.sentenceIndex]
= attributedString
}
}
}
ICorTEXT = updatedStrings
}
/// Update the UI
private func updateLabel()
{
// I am joining all the updated versions of the original string
// This is not needed, I am just using it to display the original
// strings but with their updates
// REF: https://stackoverflow.com/a/48583402/1619193
let attributedString = ICorTEXT.flatMap{ $0 }.joined(separator: "\n")
// Set the attributed text where you want
let label = UILabel()
label.attributedText = attributedString
}
storeWordPositions()
underlineWords(thatOccur: 3)
updateLabel()
Errors:
error: MyPlayground.playground:90:21: error: cannot find 'self' in scope
{ [weak self] word, substringRange, _, _ in
^~~~
error: MyPlayground.playground:139:52: error: value of type 'MatchProperty' has no member 'iCorRIndex'
= updatedStrings[matchProperty.iCorRIndex][matchProperty.sentenceIndex]
~~~~~~~~~~~~~ ^~~~~~~~~~
error: MyPlayground.playground:139:78: error: value of type 'MatchProperty' has no member 'sentenceIndex'
= updatedStrings[matchProperty.iCorRIndex][matchProperty.sentenceIndex]
~~~~~~~~~~~~~ ^~~~~~~~~~~~~
error: MyPlayground.playground:142:48: error: cannot infer contextual base in reference to member 'underlineStyle'
attributedString.addAttribute(.underlineStyle,
~^~~~~~~~~~~~~~
error: MyPlayground.playground:147:48: error: cannot infer contextual base in reference to member 'backgroundColor'
attributedString.addAttribute(.backgroundColor,
~^~~~~~~~~~~~~~~
error: MyPlayground.playground:152:46: error: value of type 'MatchProperty' has no member 'iCorRIndex'
updatedStrings[matchProperty.iCorRIndex][matchProperty.sentenceIndex]
~~~~~~~~~~~~~ ^~~~~~~~~~
error: MyPlayground.playground:152:72: error: value of type 'MatchProperty' has no member 'sentenceIndex'
updatedStrings[matchProperty.iCorRIndex][matchProperty.sentenceIndex]
~~~~~~~~~~~~~ ^~~~~~~~~~~~~


