Showing posts with label WKWebView. Show all posts
Showing posts with label WKWebView. Show all posts

Monday, June 4, 2018

How to convert HTML to NSAttributedString?

Well, simply don't. Use WKWebView instead.
This will allow you to insert pictures, reuse CSS-styles, execute scripts etc.

Wait, why not to use NSAttributedString?

While Apple provides several methods to initialize an instance of NSAttributedString from an HTML data, like: init(data:options:documentAttributes:), they are all extremely slow, about 1000 times slower than WKWebView which is as fast as Safari itself. Additionally, NSAttributedString supports a limited set of HTML attributes.

You may find many third-party solutions on GitHub (DTCoreText and many others). They can be faster, then the built-in converter, but still slower comparing to WebKit. I tried some, and they all were buggy and most are poorly supported after iOS7 release brought those "initWithHTML" methods.

The only advantage of NSAttributedString is the ability to put it in UI elements like UILabel or UIButton.

WKWebView example

Here follows super-advanced example demonstrating widest possible abilities of displaying rich and interactive text with HTML, CSS, and JavaScript. To make things convenient and reusable the HTML document is split into parts:

Tutorial_main.html

The main HTML that contains necessary elements that supposed to be involved in every page of the tutorial. Including:
  • A <head> block containing:
    • A raw CSS <style> section, containing easy-configurable constants.
    • An external CSS "stylesheet".
    • The extremely important "viewport" <meta> tag, required to scale content of a WKWebView correctly.
    • A <script> tag that provides Tutorial.js script. Not important enough to be listed here.
  • A customisable <body> section.
<html> <head> <style type="text/css"> :root { --softTextColor: %@; --darkTextColor: %@; --actionColor: %@; --backgroundColor: %@; --bodyFontSize: %@; --titleFontSize: %@; } </style> <link rel="stylesheet" type="text/css" href="Tutorial.css" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="Tutorial.js"></script> </head> <body> %@ </body> </html> The %@ format specifiers will be replaced later with strings. Tutorial.css and Tutorial.js are contained in the Main Bundle root folder, so you don't need to specify baseURL to the WKWebView. Yes, you can refer local files, even use anchor links.

Tutorial.css

body { color: var(--softTextColor); font-family: "-apple-system"; font-size: var(--bodyFontSize); margin-top: 0px; text-align: left; -webkit-user-select: none; -webkit-touch-callout: none; background-color: var(--backgroundColor); } h1 { text-align: center; font-size: var(--titleFontSize); font-weight: 300; color: var(--darkTextColor); margin-top: 0.51em; margin-bottom: 0; margin-left: 0; margin-right: 0; } a { color: var(--actionColor); text-decoration: none; } It's handy to keep CSS in a separate file. You can see some constants which already were listed before.

step2_body.html

<h1>Сonnection</h1> <p>Connect your device to your computer using the USB cable.</p> <div class="spoiler"> <p>You can sync the device with iTunes using Wi-Fi. In this case you will need the USB cable only once: to activate wireless syncing. <a href="https://support.apple.com/en-us/HT203075">Read more.</a></p> </div> <div class="centered"> <a class="showMoreButton" href="showmore://"> <img src="show_more.svg" width=20 height=15 />show more </a> <a class="showLessButton" href="showless://"> <img src="show_less.svg" width=20 height=15 />show less </a> </div> The body of the second step. Look at two buttons, there is something interesting about them. First, they use .svg images, because WKWebView supports vector graphics. (Images are also placed in the root directory of the Main bundle). Also, buttons use custom-scheme anchors, e.g.: href="showmore://". These anchors allows to intercept button clicks on the Swift-side using the webView(,decidePolicyFor:,decisionHandler:) method of WKNavigationDelegate.

Assemble

Time to put all the things together. func assembleHTMLString(bodyFileName: String) -> String? { // All resources are contained in the Main Bundle let bundle = Bundle.main guard let mainHTMLURL = bundle.url(forResource: "Tutorial_main", withExtension: "html") else { print("No main HTML!") return nil } // The contents of Tutorial_main is not just a String, it's a format (see below) guard let mainHTMLFormat = try? String(contentsOf: mainHTMLURL, encoding: .utf8) else { print("Failed to parse \(mainHTMLURL.lastPathComponent)!") return nil } guard let bodyURL = bundle.url(forResource: bodyFileName, withExtension: "html") else { print("No body with name \(bodyFileName)!") return nil } guard let htmlBody = try? String(contentsOf: bodyURL, encoding: .utf8) else { debugLog("Failed to parse \(bodyFileName)!") return nil } // Prepare some necessary arguments... let arguments = [ // Following values will be used as CSS constants UIColor.lightGray.hexString, // softTextColor UIColor.gray.hexString, // darkTextColor UIColor.green.hexString, // actionColor UIColor.white.hexString, // backgroundColor "15px", // bodyFontSize "22px", // titleFontSize htmlBody // the body, yes ] // And here we ASSEMBLE! Finally. let assembledHTMLString = withVaList(arguments) { // The `arguments` substitute %@ in the `format`, one by one. (NSString(format: mainHTMLFormat, arguments: $0)) as String } return assembledHTMLString } Wondering where is hexString property of UIColor coming from? – Read about the Power of Extensions.

Configure WKWebView

You can configure it on your own. But if you don't know how to disable zooming, here is some help: webView.scrollView.delegate = self extension TutorialViewController: UIScrollViewDelegate { func viewForZooming(in scrollView: UIScrollView) -> UIView? { return nil // Just return nil here } } When you have the assembled HTML string, there is not much left to do: webView.loadHTMLString(assembledHTMLString, baseURL: Bundle.main.bundleURL)
This all may seem overcomplicated to you, but once you implement the logic, you will have a very useful tool.

How to Record Calls on iPhone