Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

create an attributed string out of plain (Android formatted) text in swift for iOS

I am reading strings out of a Localizable.strings which contains something like that which is basically what you have in an strings.xml of an Android app

"testShort" = "A <b>short</b>\ntest with another<b>bold text</b>";

The bold and and line feed are the only two formatting attributes I have in my texts. I am trying to develop a method like this for days now without success:

func ConvertText(inputText: String) -> NSAttributedString {
    // here comes the conversion to a representation with Helvetica 14pt and Helvetica-Bold 14pt including line feeds.
}

My final goal is to display the text in an UITextView's attributedText.

Being kinda new to Swift and iOS without knowing Objective-C I found its very difficult to do String manipulations as they are quite different and complex and all examples are in Objective-C. What makes it even harder is that most API methods are not available or different in Swift than in Objective-C...

Here is what I tried so far for the method body with the help of a lot of other posts here:

var test = inputText.dataUsingEncoding(NSUnicodeStringEncoding, allowLossyConversion: true)!
attrStr = NSAttributedString(
        data: test,
        options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
        documentAttributes: nil,
        error: nil)!
return attrStr

The main issues here are that \n isn't converted and the font is very small and different.

Next I tried to manually bold a part of a text. It seem to work like that:

var newText = NSMutableAttributedString(string: inputText)
newText.addAttribute(NSFontAttributeName, value: UIFont(name: "Helvetica-Bold", size: 14.0)!, range: NSRange(location:2,length:4))

Now I tried to search for the attributes in the text, deleting them and use the addAttribute kinda like that

// Start of bold text
var range = newText.rangeOfString("<b>")!
// End of bold text
var range2 = newText.rangeOfString("</b>")!

// replacing attributes
newText = newText.stringByReplacingCharactersInRange(range, withString: "")
newText = newText.stringByReplacingCharactersInRange(range2, withString: "")

// creating a range for bold => error "'String.Index' is not convertible to 'int'"
// how to do such a thing
var boldRange = NSMakeRange(range.startIndex, range2.endIndex -3)

// setting bold
newText.addAttribute(NSFontAttributeName, value: UIFont(name: "Helvetica-Bold", size: 14.0)!, range: boldRange)

This whole range thing is my main issue at the moment as its quite different to a simple position in the string.

This issue is a great example for the lack of (or well hidden) documentation:

The addAttribute wants an NSRange, the rangeOfString seems to deliver a generic Range according to an error message I get - but there is no info about it. The Search Documentation button in Xcode on rangeOfString() leads to NSString. Searching in there for rangeOfString()says it returns NSRange. Clicking on that leads to the info of a type alias for _NSRange which in turn has two NSUInteger properties named location and length. Where is the startIndex and endIndex property I see in XCode? Very confusing...

Would be great if you can give me some snippets or hints where I'm wrong here or even the method body as I'm still hoping its not too difficult if you know iOS and Swift well. I'm aiming for iOS 7.1 support but if its way easier with iOS 8 only its fine as well.

like image 891
Soko Avatar asked Nov 27 '14 07:11

Soko


People also ask

How do you make an attributed string bold in Swift?

let label = UILabel() label. attributedText = NSMutableAttributedString() . bold("Address: ") .

What is an attributed string?

Overview. Attributed strings are character strings that have attributes for individual characters or ranges of characters. Attributes provide traits like visual styles for display, accessibility for guided access, and hyperlink data for linking between data sources.


1 Answers

Regarding your first method with NSAttributedString:

  • The \n character in HTML is just ordinary white space. To get a line break you would have to replace it by <br /> first.
  • The font attributes can be controlled by a HTML <span>, see Parsing HTML into NSAttributedText - how to set font?.

This gives (now updated for Swift 2):

func convertText(inputText: String) -> NSAttributedString {

    var html = inputText

    // Replace newline character by HTML line break
    while let range = html.rangeOfString("\n") {
        html.replaceRange(range, with: "<br />")
    }

    // Embed in a <span> for font attributes:
    html = "<span style=\"font-family: Helvetica; font-size:14pt;\">" + html + "</span>"

    let data = html.dataUsingEncoding(NSUnicodeStringEncoding, allowLossyConversion: true)!
    let attrStr = try? NSAttributedString(
        data: data,
        options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
        documentAttributes: nil)
    return attrStr!
}

Regarding your second method:

There are two different rangeOfString() methods, one for (Swift) String and one for (Foundation) NSString. The String method returns a Range<String.Index> and the NSString method returns an NSRange.

Converting between these two is possible but complicated. The reason is that in a String each "extended grapheme cluster" counts as one character, whereas in NSString each UTF-16 unit is counted. An extended grapheme cluster can be one or more UTF-16 unit ("😄" is two UTF-16 units, "🇩🇪" is four).

The addAttribute() method accepts only an NSRange. The easiest method to solve this problem is to convert the Swift string to NSString and work with NSRange only. Then your method could look like this:

func convertText(inputText: String) -> NSAttributedString {

    let attrString = NSMutableAttributedString(string: inputText)
    let boldFont = UIFont(name: "Helvetica-Bold", size: 14.0)!

    var r1 = (attrString.string as NSString).rangeOfString("<b>")
    while r1.location != NSNotFound {
        let r2 = (attrString.string as NSString).rangeOfString("</b>")
        if r2.location != NSNotFound  && r2.location > r1.location {
            let r3 = NSMakeRange(r1.location + r1.length, r2.location - r1.location - r1.length)
            attrString.addAttribute(NSFontAttributeName, value: boldFont, range: r3)
            attrString.replaceCharactersInRange(r2, withString: "")
            attrString.replaceCharactersInRange(r1, withString: "")
        } else {
            break
        }
        r1 = (attrString.string as NSString).rangeOfString("<b>")
    }

    return attrString
}
like image 64
Martin R Avatar answered Nov 15 '22 08:11

Martin R