简体   繁体   中英

Change text of an attributed string and retain attributes in Swift

For output in a database program, I have certain text that I've inserted marks to indicate bold or italics, as well as some text that is substituted for images. For instance:

"%Important% ^All employees to the breakroom^" should have final output as:

Important All employees to the breakroom

I have code written to find the text with "%" signs around it and "^" signs, but the trouble I have now is the text outputs like:

%Important% ^ All employees to the breakroom ^

I'd like to remove these % and ^'s while retaining the string's formatting.

This is the code I'm using up until it breaks:

func processText(inString string: String) -> NSAttributedString {

let pattern = ["(?<=\\^).*?(?=\\^)","(?<=\\%).*?(?=\\%)","\\^", "\\%"]
let italicsRegex = NSRegularExpression(pattern: pattern[0], options: .allZeros, error: nil)
let range = NSMakeRange(0, count(string))
let italicsMatches = italicsRegex?.matchesInString(string, options: .allZeros, range: range) as? [NSTextCheckingResult]

var attributedText = NSMutableAttributedString(string: string)

for match in italicsMatches! {
    attributedText.addAttribute(NSFontAttributeName, value: UIFont(name: "Helvetica-Oblique", size: 14.0)!, range: match.range)
}

let boldRegex = NSRegularExpression(pattern: pattern[1], options: .allZeros, error: nil)
let boldMatches = boldRegex?.matchesInString(string, options: .allZeros, range: range) as? [NSTextCheckingResult]

for match in boldMatches!   {
    attributedText.addAttribute(NSFontAttributeName, value: UIFont(name: "Helvetica-Bold", size: 14.0)!, range: match.range)
}

let removeItalicsMarksRegex = NSRegularExpression(pattern: pattern[2], options: .allZeros, error: nil)
let removeItalicsMarksMatches = removeItalicsMarksRegex?.matchesInString(string, options: .allZeros, range: range) as? [NSTextCheckingResult]

var numberOfLoops = 0
for match in removeItalicsMarksMatches! {

    attributedText.replaceCharactersInRange(match.range, withString: "")

}

return attributedText.copy() as! NSAttributedString
}

This works for the % match (but only the first character) and causes a crash on the ^ character immediately.

Any help or advice with resolving this would be appreciated. Thanks.

Martin,

I ended up using something very similar, but I decided to change the regular expression to include the ^ marks. In doing so, I was able to then clip the first and last characters of the included attributed substring with the "replaceCharactersInRange" method. This works a little better for my purposes so far because it's working from the attributed string so it doesn't screw up or remove any of its attributes.

I've attached the regex and the portion of the code that deals with italics for anyone's future reference (and thanks, again!):

func processText(inString string: String) -> NSAttributedString {

let pattern = ["\\^.*?\\^"] //Presented as an array here because in the full code there are a lot of patterns that are run.




let italicsRegex = NSRegularExpression(pattern: pattern[0], options: .allZeros, error: nil) 

//In addition to building the match for this first regular expression, I also gather build the regular expressions and gather matches for all other matching patterns on the initial string ("string") before I start doing any processing.

    let range = NSMakeRange(0, count(string.utf16))

let italicsMatches = italicsRegex?.matchesInString(string, options: .allZeros, range: range) as? [NSTextCheckingResult]

var attributedText = NSMutableAttributedString(string: string)

var charactersRemovedFromString = 0

for match in italicsMatches! {

    let newRange = NSMakeRange(match.range.location - charactersRemovedFromString, match.range.length) // Take the updated range for when this loop iterates, otherwise this crashes.
    attributedText.addAttribute(NSFontAttributeName, value: UIFont(name: "Helvetica-Oblique", size: 12.0)!, range: newRange)

    let rangeOfFirstCharacter = NSMakeRange(match.range.location - charactersRemovedFromString, 1)

    attributedText.replaceCharactersInRange(rangeOfFirstCharacter, withString: "")

    charactersRemovedFromString += 2

    let rangeOfLastCharacter = NSMakeRange(match.range.location + match.range.length - charactersRemovedFromString, 1)

    attributedText.replaceCharactersInRange(rangeOfLastCharacter, withString: "")
    }

return attributedText
}

Here is a possible solution, essentially a translation of how to catch multiple instances special indicated **characters** in an NSString and bold them in between? from Objective-C to Swift.

The idea is to add the attributes and remove the delimiters in one loop. The shift variable is needed to adjust the matching ranges after the first delimiters have been removed.

For the sake of simplicity, only the "^...^" processing is shown.

func processText(inString string: String) -> NSAttributedString {

    let pattern = "(\\^)(.*?)(\\^)"

    let regex = NSRegularExpression(pattern: pattern, options: nil, error: nil)!
    var shift = 0 // number of characters removed so far
    let attributedText = NSMutableAttributedString(string: string)
    regex.enumerateMatchesInString(string, options: nil, range: NSMakeRange(0, count(string.utf16))) {
        (result, _, _) -> Void in
        var r1 = result.rangeAtIndex(1) // Location of the leading delimiter
        var r2 = result.rangeAtIndex(2) // Location of the string between the delimiters
        var r3 = result.rangeAtIndex(3) // Location of the trailing delimiter
        // Adjust locations according to the string modifications:
        r1.location -= shift
        r2.location -= shift
        r3.location -= shift
        // Set attribute for string between delimiters:
        attributedText.addAttribute(NSFontAttributeName, value: UIFont(name: "Helvetica-Oblique", size: 14.0)!, range: r2)
        // Remove leading and trailing delimiters:
        attributedText.mutableString.deleteCharactersInRange(r3)
        attributedText.mutableString.deleteCharactersInRange(r1)
        // Update offset:
        shift += r1.length + r3.length
    }

    return attributedText.copy() as! NSAttributedString
}

Note that enumerateMatchesInString() takes an NSRange , therefore you have to compute the number of UTF-16 characters and not the number of Swift characters.

Example:

let text = "🇩🇪😀aaa ^🇭🇰😁bbb^ 🇳🇱😆eee"
let attrText = processText(inString: text)
println(attrText)

Output:

🇩🇪😀aaa {
}🇭🇰😁bbb{
    NSFont = " font-family: \"Helvetica-Oblique\"; font-weight: normal; font-style: italic; font-size: 14.00pt";
} 🇳🇱😆eee{
}

That worked for me!

extension UILabel {
    func updateAttributedText(_ text: String) {
        if let attributedText = attributedText {
            let mutableAttributedText = NSMutableAttributedString(attributedString: attributedText)
            mutableAttributedText.mutableString.setString(text)
            self.attributedText = mutableAttributedText
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM