Swift: how to display bold words in textview without attributed string - ios

We're adding the finishing touches to an app we're working on, and that apparently means putting an entire "Terms and Conditions" and "FAQs" section, formatting, bullets, breaks and all.
So I tried copy-pasting it into a textView with "editable" set to off, which kept the bullets, but not the bolded text.
Now, I've done attributed string before, and I have to say, I'm not sure it will be easy to do that on some 12-pages worth of paragraphs, bulleted lists and breaks that are likely to change in a few years or so.
So my question is, is there a way to do this without using attributed string?
Barring that, perhaps there's a way to loop through the text, and look for a written tag that will apply the attributes?
EDIT:
Update. It's been suggested I use HTML tags, and web view. That's what was done for the FAQs (which uses a label), I neglected to mention I tried that too.
For some reason, it just shows a blank textview, albeit a large-sized one, as if there's text in it (there isn't any). Strange that copy-pasting works but this doesn't.
Here's my code for it:
override func awakeFromNib() {
super.awakeFromNib()
termsTitle.text = "Terms and Conditions"
htmlContent = "<p style=\"font-family:Helvetica Neue\"><br/><strong><br/> BLA BLA BLA BLA BLA BLA x 12 Pages"
do {
let str = try NSAttributedString(data: htmlContent.dataUsingEncoding(NSUnicodeStringEncoding, allowLossyConversion: true)!, options: [ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType], documentAttributes: nil)
termsTextView.attributedText = str
} catch {
print("Dim background error")
}
}

I'm pretty sure you can't do this in a Textview without using AttributedString. A possible solution would be using a WebView. Converting your "Terms and Conditions" and "FAQ" to HTML would probably be much easier than using an AttributedString.

If you still want to use your HTML in a UITextView you can try this function:
func getAttributedString(fileName: String) -> NSAttributedString? {
if let htmlLocation = NSBundle.mainBundle().URLForResource(fileName, withExtension: "html"), data = NSData(contentsOfURL: htmlLocation) {
do {
let attrString = try NSAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType], documentAttributes: nil)
return attrString
} catch let err as NSError {
print("Attributed String Creation Error")
print(err.localizedDescription)
return nil
}
} else {
return nil
}
}
This function assumes you have a .html file in your main bundle. You pass it the name (minus extension) of the file (that should be in your project) and then use it like so:
textView.attributedText = getAttributedString("TermsAndConditions")
Just to clarify, the textView is a #IBOutlet on a View Controller in this example.
This function returns nil if either the .html file does not exist or the NSAttributedString conversion failed.

Related

UILabel text is not updated even in MainThread

UILabel.text is not updated inside the main thread. labelOne is updated but labelTwo which is to show translated word is not updated. When I print translatedWord it prints right string to console but UILabel is not updated.
datatask = session.dataTask(with: request, completionHandler: {data, response, error in
if error == nil {
let receivedData = try? JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any]
DispatchQueue.main.async {
self.labelOne.text = wordTobeTranslated
let data = "\(String(describing: receivedData!["text"]))"
let all = data.components(separatedBy: "(")
let afterAll = all[2]
let last = afterAll.components(separatedBy:")" )
self.translatedWord = last[0]
self.importantWords.append(last[0])
self.labelTwo.text = self.translatedWord
print(self.translatedWord)
}
}
})
datatask?.resume()
Some tips to debug your issue:
Try to set some hard coded string to labelTwo and check whether it
is displaying. (or)
Update any value for labelTwo in Storyboard and check whether is is
displaying.
If string is displayed from any of the above steps, then labelTwo is configured correctly in storyboard.
You can also try self.labelTwo.layoutIfNeeded() method after updating the text to force update the UI.
If none of the steps helps you, check the Font color of UILabel. If it is same as background color, it would not be seen.
Looking at your code sample, it would appear that you’re trying to retrieve:
describing: receivedData!["text"]
from:
\(String(describing: receivedData!["text"]))
The problem is that the \(...) in a String literal results in string interpolation where the expression inside \( and ) will be evaluated and that’s what will be place in the string. And the String(describing: ...) will interpret the value and return a string representation. So, let’s say that receivedData!["text"] contained the word “Foo”. Then
let data = "\(String(describing: receivedData!["text"]))"
Would result in data containing the string, Optional("Foo").
If you want to remove that Optional(...) part, you should either unwrap the optional or use a nil-coalescing operator, ??. And frankly, rather than using string interpolation at all, I’d just do:
let data = String(describing: receivedData?["text"] ?? "")
I know this is an old question, but for new searchers: Try setting number of lines of UILabel to Zero. It might be a constraints issue.

Swift reading RTF file doesn't apply Tags

I'm reading a rtf file but output result shows me simple string containing tags, It doesn't appear as NSAttributedString as it should
here is my code
if let rtfPath = Bundle.main.url(forResource: "SampleHTML", withExtension: "rtf") {
do {
let attributedStringWithRtf: NSAttributedString =
try NSAttributedString(url: rtfPath, options: [NSAttributedString.DocumentReadingOptionKey.documentType:
NSAttributedString.DocumentType.rtf], documentAttributes: nil)
}catch let error {
print("Got an error \(error)")
}
}
After reading the file its shows attribute string as :
< p >< em>Review the following information. Then select the best answer and click < strong>Submit< /strong>. After answering, click < strong>Next Question< /strong> to move forward.< /em>< /p> .
I've inserted spaces in tag to clarify the issue, without spaces StackOverflow applies tag to text, what am I missing here ?
After RTF to NSAttributedString you got (you can use code tag of SO to avoid the HTML tags to be interpreted):
<p><em>Review the following information. Then select the best answer and click <strong>Submit</strong>. After answering, click <strong>Next Question</strong> to move forward.</em></p>
So now, you have a HTML AttributedString.
And like RTF, there is a way to get NSAttributedString from HTML String.
So let's get first the HTML String: let htmlString = attributedStringWithRtf.string.
For the rest, the method is almost the same as the one with used, with changes: NSAttributedString.DocumentType.html instead of NSAttributedString.DocumentType.rtf (seems obvious), and there is an init with data instead of path, so you just have to use let htmlData = htmlString.data(using:.utf8)
So the code:
let rtfPath = Bundle.main.url(forResource: "SampleHTML", withExtension: "rtf")
let attributedStringWithRtf: NSAttributedString = NSAttributedString(url: rtfPath, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)
let htmlString = attributedStringWithRtf.string
let htmlData = htmlString.data(using:.utf8)
let attributedString: NSAttributedString = NSAttributedString(data: htmlData, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
Note: I am not using if let, try/catch on purpose because you already use them correctly, and I'm more interested in the logic behind your issue and how to get away with it.
Now, after discussion in comment:
You save HTML Text (ie with HTML tags) into a RTF file.
Clearly, that's overkill, because as you can see in the solution I gave you do twice the conversion to get the final NSAttributedString.
So, what I'd do is either use entirely RTF File, and set the RTF tags equivalent for <p>, <em>, <strong>, etc instead of the HTML ones.
Save you HTML String (with HTML tags) in a simple .txt.
For instance, I saved a RTF File using TextEdit.app, and the real content is:
{\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf830
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\paperw11900\paperh16840\margl1440\margr1440\vieww10800\viewh8400\viewkind0
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0
\f0\fs24 \cf0 myText}
And I just wrote myText, RTF adding a lot of if own tags.

Swift: Replace Strings to NSTextAttachment in UITextView after loading styled RTF

In my iOS/swift project I am loading a RTF document to a UITextView with the code below. The RTF itself contains styled text like "... blah blah [ABC.png] blah blah [DEF.png] blah..." wich is loaded to the UITextView just fine.
Now, I want to replace all occurrences of [someImage.png] with an actual image as NSTextAttachment. How can I do that?
I am aware of the possibility to embed images in RTF documents, but I can not do that in this project.
if let rtfPath = Bundle.main.url(forResource: "testABC", withExtension: "rtf")
{
do
{
//load RTF to UITextView
let attributedStringWithRtf = try NSAttributedString(url: rtfPath, options: [.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)
txtView.attributedText = attributedStringWithRtf
//find all "[ABC.png]" and replace with image
let regPattern = "\\[.*?\\]"
//now...?
}
}
Here is something that you could do.
Note: I'm not a Swift Developper, more an Objective-C one, so there may be some ugly Swift code (the try!, etc.). But it's more for the logic on using NSRegularExpression (which I used in Objective-C since it's shared in CocoaTouch)
So the main line directives:
Find where are the images placeholders.
Create a NSAttributeString/NSTextAttachment from it.
Replace the placeholder with the previous attributed string.
let regPattern = "\\[((.*?).png)\\]"
let regex = try! NSRegularExpression.init(pattern: regPattern, options: [])
let matches = regex.matches(in: attributedStringWithRtf.string, options: [], range: NSMakeRange(0, attributedStringWithRtf.length))
for aMatch in matches.reversed()
{
let allRangeToReplace = attributedStringWithRtf.attributedSubstring(from: aMatch.range(at: 0)).string
let imageNameWithExtension = attributedStringWithRtf.attributedSubstring(from: aMatch.range(at: 1)).string
let imageNameWithoutExtension = attributedStringWithRtf.attributedSubstring(from: aMatch.range(at: 2)).string
print("allRangeToReplace: \(allRangeToReplace)")
print("imageNameWithExtension: \(imageNameWithExtension)")
print("imageNameWithoutExtension: \(imageNameWithoutExtension)")
//Create your NSAttributedString with NSTextAttachment here
let myImageAttribute = ...
attributedStringWithRtf.replaceCharacters(in: imageNameRange, with: myImageAttributeString)
}
So what's the idea?
I used a modify pattern. I hard-wrote "png", but you could change it. I added some () to get easily the interesting parts. I thought that you may wanted to retrieve the name of the image, with or without the .png, that's why I got all theses print(). Maybe because you saved it in your app, etc. If you need to add the extension as a group, you may want to add it into parenthesis in your regPattern and check what aMatch.range(at: ??) to call. for using Bundle.main.url(forResource: imageName, withExtension: imageExtension)
I used the matches.reversed() because if you modify the length of the "match" with a replacement of different length, the previous ranges will be off. So starting from the end could do the trick.
Some code to transform a UIImage into NSAttributedString through NSTextAttachment: How to add images as text attachment in Swift using nsattributedstring

Calculating word count from a url in swift

I'm creating a reading list app, and I'd like to pass the read time of a user added link to a table cell in their reading list - and the only way to get that number is from that page's word count. I've found a few solutions, namely Parsehub, Parse and Mercury but they seem to be geared more towards use cases that need more advanced things to be scraped from a url. Is there a simpler way in Swift to calculate word count of a url?
First of all, you need to parse the HTML. HTML can only be parsed reliably with dedicated HTML parser. Please don't use Regular Expressions or any other search method to parse HTML. You may read it why from this link. If you are using swift, you may try Fuzi or Kanna. After you get the body text with any one of the library, you have to remove extra white spaces and count the words. I have written some basic code with Fuzi library for you to get started.
import Fuzi
// Trim
func trim(src:String) -> String {
return src.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
// Remove Extra double spaces and new lines
func clean(src:String) ->String {
return src.replacingOccurrences(
of: "\\s+",
with: " ",
options: .regularExpression)
}
let htmlUrl = URL(fileURLWithPath: ((#file as NSString).deletingLastPathComponent as NSString).appendingPathComponent("test.html"))
do {
let data = try Data(contentsOf: htmlUrl)
let document = try HTMLDocument(data: data)
// get body of text
if let body = document.xpath("//body").first?.stringValue {
let cleanBody = clean(src: body)
let trimmedBody = trim(src:cleanBody)
print(trimmedBody.components(separatedBy: " ").count)
}
} catch {
print(error)
}
If you are fancy, you may change my global functions to String extension or you can combine them in a single function. I wrote it for clarity.

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.
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
}

Resources