Displaying HTML text in UITextView crash occurred in swift 4? - html

I am using this code snipped
var htmlToAttributedString: NSAttributedString? {
guard let data = data(using: .utf8) else { return NSAttributedString() }
do {
return try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding:String.Encoding.utf8.rawValue], documentAttributes: nil) // Get crash on this line
} catch let error {
print(error.localizedDescription)
return NSAttributedString()
}
}
var htmlToString: String {
return htmlToAttributedString?.string ?? ""
}
showing HTML text in UITableViewCell
cell.textViewMessage.attributedText = msg.htmlToAttributedString
Launching first time there is no crash but after that when I run the code got a crash and not working after that.
Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)
#Edit HTML String to display in cell
<p>Here\'s a short video tour. Press play to start.</p><br><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"https://www.youtube.com\"></iframe><br>
#Edit 1 - I am trying to run this code in Playground and it's just working fine except now it's showing an error. Please see the attached image

Looks like the problem is the tag, not every tag can show up in uitextview.
You can display better these tag in uiwebview
I thinks the problem is iframe tag.
To display iFrame use uiwebview instead, or wkwebview.
Thanks

The reason for this issue is the table view, this error will occur very randomly and hard to reproduce because its more specific to device memory and UI draw process which might result in executing the method in the background thread. While reallocating and deallocating the table cells, deep down somewhere the table cells might call this method on a background thread while the HTML importer uses a non-thread-safe WebKit importer and expects to be on the main thread.
How to reproduce this error: Run the code using UITest and it will crash more often since the unit test slows down the UI draw process significantly
Solution: decode HTML to String should be on the main thread but do this in the model layer on main thread instead of doing it during cell creation. This will make the UI more fluid as well.
Why the crash was not caught in catch block: Your app has crashed due to an unhandled language exception, as used by the exception handling infrastructure for Objective-C. SWIFT is like a nice wrapper around Cocoa’s NSError. Swift is not able to handle Objective-C exceptions, and thus an exception through by Objective-C code in the frameworks will not be caused by your Swift error handler.

Here is a solution inspired by this repo. Basically we remove the iframe tag and replace it with clickable img:
let msg = "<p>Here\'s a short video tour. Press play to start.</p><br><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"https://www.youtube.com/embed/wJcOvdkI7mU\"></iframe><br>"
//Let's get the video id
let range = NSRange(location: 0, length: msg.utf16.count)
let regex = try! NSRegularExpression(pattern: "((?<=(v|V)/)|(?<=be/)|(?<=(\\?|\\&)v=)|(?<=embed/))([\\w-]++)")
guard let match = regex.firstMatch(in: msg, options: [], range: range) else {
fatalError("Couldn't find the video ID")
}
let videoId: String = String(msg[Range(match.range, in: msg)!])
//Let's replace the the opening iframe tag
let regex2 = try! NSRegularExpression(pattern:
"<[\\s]*iframe[\\s]+.*src=")
let str2 = regex2.stringByReplacingMatches(in: msg, options: [], range: range, withTemplate: "<a href=")
//And then replace the closing tag
let regex3 = try! NSRegularExpression(pattern:
"><\\/iframe>")
let range2 = NSRange(location: 0, length: str2.utf16.count)
let str3 = regex3.stringByReplacingMatches(in: str2, options: [], range: range2, withTemplate: "><img src=\"https://img.youtube.com/vi/" + videoId + "/0.jpg\" alt=\"\" width=\"\(textView.frame.width)\" /></a>") // You could adjust the width and height to your liking
//Set the text of the textView
textView.attributedText = str3.htmlToAttributedString
textView.delegate = self
To open the Youtube app when the user taps and holds on the image, implement this delegate method:
extension NameOfYourViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
UIApplication.shared.open(URL, options: [:])
return true
}
}
If the youtube app is not installed, then the video will be played in Safari.
Here is the result:

Write in main async is helped me.
DispatchQueue.main.async {
self.myLabel.attributedText = self.myAtributedText
}

Try this for UITextView:
let string = "<h2>The bedding was hardly able to cover it.</h2>"
if !string.isEmpty{
if let htmlData = string.data(using:String.Encoding.unicode) {
do {
let attributedText = try NSAttributedString(data: htmlData, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType], documentAttributes: nil)
cell.textViewMessage.attributedText = attributedText
} catch let e as NSError {
print("Couldn't translate \(string): \(e.localizedDescription) ")
}
}
Try this for UILabel for setting html text to uilabel with html tags:
extension String {
var withoutHtmlTags: String {
let a = self.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil)
return a.replacingOccurrences(of: "&[^;]+;", with: "", options: String.CompareOptions.regularExpression, range: nil)
}
}
let string = "<h2>The bedding was hardly able to cover it.</h2>"
textUIlabel.text = string.withoutHtmlTags

Changes your options to:
let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html,
NSAttributedString.DocumentReadingOptionKey.characterEncoding: String.Encoding.utf8.rawValue
]

Related

Unable to show new paragraph for html data into Textview in iOS Swift

I am working on iOS (Swift) application. I am getting some server response like below.
"description":"This is sample text to show in UI. When doing everyday activities.\u003cbr /\u003eclass is a strong predictor of life, and again sample text here.\u003cbr /\u003eSample text can show here also."
So, Above text has 3 paragraphs, I am trying to displaying them in Textview, But it is showing in plain with new line instead of New Paragraph.
override func viewDidLoad() {
super.viewDidLoad()
let description = jsonResponse["description"] as! String
self.textView.attributedText = description.htmlAttributedString()
}
extension String {
func htmlAttributedString() -> NSAttributedString? {
guard let data = self.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }
guard let html = try? NSMutableAttributedString(
data: data,
options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil) else { return nil }
return html
}
}
The issue is it is showing text, But like new line showing instead of new paragraph. How to fix this?
I have fixed this issue by following, And after conversion server response into json serialization, the special characters code showing as
So, I have fixed like below
let description = jsonResponse["description"] as! String
let formattedString = description.replacingOccurrences(of: "<br />", with: " \n\n")
self.textView.text = formattedString

Is there an easy solution for parsing html in swift to get individual elements into their own variable?

I have code which I found online that pulls the HTML off a website and then prints it out. I need to save these into a variable to I can display/ use these in my app.
I am fairly new to this kind of thing and really just need pointers, I don't mind researching! I just need to know what steps I need to be looking into!
import UIKit
// run asynchronously in a playground
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
// create a url
let url = URL(string: "https://www.stackoverflow.com")
// create a data task
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("there's a problem")
}
print(String(data: data!, encoding: String.Encoding.utf8) ?? "")
}
//running the task w/ resume
task.resume()
This (in Xcode playground) takes the HTML and prints it out using:
print(String(data: data!, encoding: String.Encoding.utf8) ?? "")
Can anyone please help me out getting maybe the <title>...</title> element into its own variable?
Parsing HTML without a third party is not achievable without a WebView, BUT YOU CAN easily use a webView and run a getElementsByTagName with JS on it to get anything from the HTML code like this:
1- Define the js code:
let js = "document.getElementsByTagName("title")[0].innerHTML"
2- Import WebKit and load the html into a webView
class MyViewController : UIViewController {
let html = """
<#the HTML code, can be loaded from anywhere#>
"""
override func loadView() {
let webView = WKWebView()
webView.navigationDelegate = self // Here is the Delegate
webView.loadHTMLString(html, baseURL: nil)
self.view = webView
}
}
3- Take the delegation and implement this method:
extension MyViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript(js) {(result, error) in
guard error == nil else {
print(error!)
return
}
print(String(describing: result))
}
}
}
Note 1: remember getElementsByTagName returns an array and you must pass the index you want the get like [0]
Note 2: since it use JavaScriptCore, it can't be done without webView, and it must be run on mainThread. Only safari can do this off main thread, because it has V8 engine.
Note 3: You must wait for delegate to be completed even if you pass the HTML statically
Note 4: you can use a third party framework like SwiftSoap to do this.

Error "Garbage at end" when serializing valid JSON data with Alamofire

My code return a Code=3840 "Garbage at end." when I try to keep my data of a request to my api ... The JSON return is a Valid JSON accorded to jsonlint (tested with Postman):
{
"error": 0,
"message": "transaction_completed"
}
this is my code :
func request(urle : url, parameters : Parameters, completion: #escaping (JSON) -> Void)
{
Alamofire.request(getUrl(urlw: urle), method: .post, parameters: parameters).responseJSON
{
response in
if response.data != nil {
do{
let json = try JSON(data: response.data!)
completion(json)
}catch{
print(error)
}
}
}
}
and this is when I called the request function:
let parameters: Parameters=[
"key" : user.key,
"uid": user.id
]
api.request(urle: .buyStack, parameters: parameters) { json in
print(json)
}
Where did I go wrong?
So apparently your JSON is not valid, it has at the end some invalid values.
First thing to do. For the sake of the keeping the logic, you can use force unwrap (using !) because it's debugging. I'm not sure that this code compile, it's just a logic presentation.
let responseString = String(data: response.data, encoding: .utf8)
print("responseString: \(responseString)")
This gives:
{"error":1,"message":"Undefined APIKey"}[]
There is extra [] at the end, and it's then not a valid JSON. You can ask the developper to fix it. If you really can't, or want to continue developing while it's in progress in their side, you can remove the extra [].
You can check this answer to remove the last two characters, and then:
let cleanResponseJSONString = //check the linked answer
let cleanResponseData = cleanResponseJSONString.data(encoding: .utf8)
let json = try JSON(data: cleanResponseData)
Side note and debugging idea if it was more complicate:
I ask for print("data: \(response.data as! NSData)") because this print the hex data. Your issue could have been due to an invisible character at the end. If you don't know them, the least you can do is according to previous answer:
let jsonString = "{\"error\":1,\"message\":\"Undefined APIKey\"}" (that's almost reponseString)
let jsonData = jsonString.data(encoding: .utf8)
print("jsonData: \(jsonData as! NSData)")
And compare what the end looks like.
A debugger tip, you can use a answer like this one to convert hexDataString into Data and debug from it. I'd recommend to add a space, "<" and ">" removal before so you can easily copy/paste it from the debugger output.
Why? If it's long (many manipulation) to go where your issue lies (login in the app, certain actions to do etc.), this could save you time to debug it on another app (Playground, at the start of your AppDelegate, etc.).
Don't forget to remove all the debug code afterwards ;)
Not related to the issue:
if response.data != nil {
do {
let json = try JSON(data: response.data!)
...
} catch {
...
}
}
Should be:
if let data = response.data {
do {
let json = try JSON(data: data)
...
} catch {
...
}
}
Use if let, guard let to unwrap, avoid using force unwrap.

Does NSHTMLTextDocumentType support HTML table?

In my app, i want to display a text in a UILabel. I use HTML to store the text in my data base to dynamically from my text in my app. I actually use this (Swift 3.2, iOS 8+) :
if let data = text.data(using: .utf8) {
let htmlString = try? NSMutableAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, NSCharacterEncodingDocumentAttribute: String.Encoding.utf8.rawValue], documentAttributes: nil)
self.textLabel.attributedText = htmlString
}
It's work great for the HTML stuff i used like
<b>Text</b>
<i>Test</i>
And more...
Now, i want to display a table in my label. This is the HTML code for the table :
<table border="2px solid black">
<tr><th>Symbole</th><th>Å</th><th>↓</th><th>■</th><th>╩</th><th>¬</th><th>▓</th><th>Ø</th><th>±</th><th> º </th><th>¶</th><th>░</th></tr>
<tr><td>Utilisation</td><td>1</td><td>11</td><td>11</td><td>5</td><td>1</td><td>4</td><td>12</td><td>4</td><td>1</td><td>5</td><td>1</td></tr>
</table>
This code displays a table form but there is no border in the table. I want to display the table border like the reel HTML render. It's possible or not ?
Weird issue, I didn't understand why this simple thing didn't work, however I managed to make the border appear by adding a random attribute to the NSAttributedString, which makes me believe it's a NSAttributedString rendering bug.
Here's the function that I used (this is Swift 4 but can be converted to earlier versions):
extension String {
func attributedString() -> NSAttributedString? {
guard let data = self.data(using: String.Encoding.utf8,
allowLossyConversion: false) else { return nil }
let options: [NSAttributedString.DocumentReadingOptionKey : Any] = [
NSAttributedString.DocumentReadingOptionKey.characterEncoding : String.Encoding.utf8.rawValue,
NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.html
]
let htmlString = try? NSMutableAttributedString(data: data, options: options, documentAttributes: nil)
// Removing this line makes the bug reappear
htmlString?.addAttribute(NSAttributedStringKey.backgroundColor, value: UIColor.clear, range: NSMakeRange(0, 1))
return htmlString
}
}

".SFUIText-Regular" doesn't exist on watchOS

I am creating an app for Apple Watch and iOS. I have HTML data which I transform into NSAttributedString to display in a UITextView (on iOS). I also want to send it to the watch to display it in a label.
Everything looks ok in the text view (e.g., correct background color). On the watch, it only displays the text (without any colors) and returns this error:
app-Watch Extension[2994:212335] CoreText: PostScript name ".SFUIText-Regular" does not exist.
Here is my code:
let mutAttText = NSMutableAttributedString(attributedString: self.textView.attributedText)
let attributedOptions : [String: AnyObject] = [
NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
NSCharacterEncodingDocumentAttribute: NSUTF8StringEncoding
]
var data: NSData = NSData()
do {
data = try mutAttText.dataFromRange(NSMakeRange(0, mutAttText.length), documentAttributes: attributedOptions)
} catch {
}
let htmlString = NSString(data: data, encoding: NSUTF8StringEncoding)
print(htmlString)
var attrStr = NSMutableAttributedString()
do {
attrStr = try NSMutableAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType], documentAttributes: nil)
attrStr.enumerateAttribute(NSFontAttributeName, inRange: NSMakeRange(0, attrStr.length), options: NSAttributedStringEnumerationOptions.LongestEffectiveRangeNotRequired, usingBlock: { (attribute: AnyObject?, range: NSRange, stop: UnsafeMutablePointer<ObjCBool>) -> Void in
if let attributeFont = attribute as? UIFont {
let newPointSize = CGFloat(15)
let scaledFont = UIFont(descriptor: attributeFont.fontDescriptor(), size: newPointSize)
attrStr.addAttribute(NSFontAttributeName, value: scaledFont, range: range)
}
})
self.textView.attributedText = attrStr
self.sendText(attrStr)
}
catch {
print("error creating attributed string")
}
Although iOS and watchOS both use the San Francisco font, the fonts do differ between platforms:
iOS, tvOS, and OS X uses San Francisco (SF-UI).
watchOS uses San Francisco Compact (SF-Compact).
It looks like you're trying to scale the pointSize of an iOS system font, but .SFUIText-Regular doesn't exist on watchOS.
You also may want to use systemFontOfSize: instead of trying to scale the point size of a named font, since there are different (text and display) versions depending on point size. This will allow the system to automatically select the appropriate (text or display) variant for that point size.