Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dark mode support #57

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions MarkdownView.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
6DCD1FDB24E9191B00957E92 /* index_dark.html in Resources */ = {isa = PBXBuildFile; fileRef = 6DCD1FD924E9191B00957E92 /* index_dark.html */; };
6DCD1FDC24E9191B00957E92 /* main_dark.css in Resources */ = {isa = PBXBuildFile; fileRef = 6DCD1FDA24E9191B00957E92 /* main_dark.css */; };
FC9CC4C51EC43FA90013238C /* index.html in Resources */ = {isa = PBXBuildFile; fileRef = FC9CC4BD1EC43FA90013238C /* index.html */; };
FC9CC4C61EC43FA90013238C /* main.css in Resources */ = {isa = PBXBuildFile; fileRef = FC9CC4BE1EC43FA90013238C /* main.css */; };
FC9CC4C71EC43FA90013238C /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = FC9CC4BF1EC43FA90013238C /* main.js */; };
Expand All @@ -15,6 +17,8 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
6DCD1FD924E9191B00957E92 /* index_dark.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = index_dark.html; path = webassets/dist/index_dark.html; sourceTree = SOURCE_ROOT; };
6DCD1FDA24E9191B00957E92 /* main_dark.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = main_dark.css; path = webassets/dist/main_dark.css; sourceTree = SOURCE_ROOT; };
FC9CC4BD1EC43FA90013238C /* index.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = index.html; path = webassets/dist/index.html; sourceTree = SOURCE_ROOT; };
FC9CC4BE1EC43FA90013238C /* main.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = main.css; path = webassets/dist/main.css; sourceTree = SOURCE_ROOT; };
FC9CC4BF1EC43FA90013238C /* main.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = main.js; path = webassets/dist/main.js; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -65,6 +69,8 @@
FCA050201EC4123C001DAD5F /* Assets */ = {
isa = PBXGroup;
children = (
6DCD1FD924E9191B00957E92 /* index_dark.html */,
6DCD1FDA24E9191B00957E92 /* main_dark.css */,
FC9CC4BD1EC43FA90013238C /* index.html */,
FC9CC4BE1EC43FA90013238C /* main.css */,
FC9CC4BF1EC43FA90013238C /* main.js */,
Expand Down Expand Up @@ -125,6 +131,7 @@
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
);
mainGroup = FCA0500B1EC41211001DAD5F;
Expand All @@ -145,6 +152,8 @@
FC9CC4C61EC43FA90013238C /* main.css in Resources */,
FC9CC4C51EC43FA90013238C /* index.html in Resources */,
FC9CC4C71EC43FA90013238C /* main.js in Resources */,
6DCD1FDC24E9191B00957E92 /* main_dark.css in Resources */,
6DCD1FDB24E9191B00957E92 /* index_dark.html in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

264 changes: 168 additions & 96 deletions MarkdownView/MarkdownView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,125 +7,197 @@ import WebKit
- Note: [How to get height of entire document with javascript](https://stackoverflow.com/questions/1145850/how-to-get-height-of-entire-document-with-javascript)
*/
open class MarkdownView: UIView {

private var webView: WKWebView?

fileprivate var intrinsicContentHeight: CGFloat? {
didSet {
self.invalidateIntrinsicContentSize()
// 可能需要对外,提供截图的功能
public var webView: WKWebView?

// 是否跟随系统自动切换 light/dark 风格,默认 true
public var isFollowSystemUIStyle = true

public var isDarkUIStyle = false {
didSet {
reloadMarkdownView()
}
}

fileprivate var intrinsicContentHeight: CGFloat? {
didSet {
self.invalidateIntrinsicContentSize()
}
}
}

@objc public var isScrollEnabled: Bool = true {
@objc public var isScrollEnabled: Bool = true {

didSet {
webView?.scrollView.isScrollEnabled = isScrollEnabled
webView?.scrollView.isScrollEnabled = isScrollEnabled
}

}

@objc public var onTouchLink: ((URLRequest) -> Bool)?

@objc public var onRendered: ((CGFloat) -> Void)?

public convenience init() {
self.init(frame: CGRect.zero)
}

override init (frame: CGRect) {
super.init(frame : frame)
}

public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

open override var intrinsicContentSize: CGSize {
if let height = self.intrinsicContentHeight {
return CGSize(width: UIView.noIntrinsicMetric, height: height)
} else {
return CGSize.zero
}
}

@objc public func load(markdown: String?, enableImage: Bool = true) {
guard let markdown = markdown else { return }
@objc public var onTouchLink: ((URLRequest) -> Bool)?

let bundle = Bundle(for: MarkdownView.self)
@objc public var onRendered: ((CGFloat) -> Void)?

@objc public var didChangeInterfaceStyle: ((Bool, Error?) -> Void)?

let htmlURL: URL? =
bundle.url(forResource: "index",
withExtension: "html") ??
bundle.url(forResource: "index",
withExtension: "html",
subdirectory: "MarkdownView.bundle")

if let url = htmlURL {
let templateRequest = URLRequest(url: url)
public convenience init() {
self.init(frame: CGRect.zero)
}

let escapedMarkdown = self.escape(markdown: markdown) ?? ""
let imageOption = enableImage ? "true" : "false"
let script = "window.showMarkdown('\(escapedMarkdown)', \(imageOption));"
let userScript = WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
override init (frame: CGRect) {
super.init(frame : frame)
}

let controller = WKUserContentController()
controller.addUserScript(userScript)
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

let configuration = WKWebViewConfiguration()
configuration.userContentController = controller
open override var intrinsicContentSize: CGSize {
if let height = self.intrinsicContentHeight {
return CGSize(width: UIView.noIntrinsicMetric, height: height)
} else {
return CGSize.zero
}
}

let wv = WKWebView(frame: self.bounds, configuration: configuration)
wv.scrollView.isScrollEnabled = self.isScrollEnabled
wv.translatesAutoresizingMaskIntoConstraints = false
wv.navigationDelegate = self
addSubview(wv)
wv.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
wv.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
wv.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
wv.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
wv.backgroundColor = self.backgroundColor
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *), isFollowSystemUIStyle {
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
isDarkUIStyle = (traitCollection.userInterfaceStyle == .dark ? true : false)
reloadMarkdownView()
}
}
}

self.webView = wv
@objc public func load(markdown: String?, enableImage: Bool = true) {
guard let markdown = markdown else { return }

let bundle = Bundle(for: MarkdownView.self)

var htmlName = "index"

if isDarkUIStyle {
htmlName = "index_dark"
}

let htmlURL: URL? =
bundle.url(forResource: htmlName,
withExtension: "html") ??
bundle.url(forResource: htmlName,
withExtension: "html",
subdirectory: "MarkdownView.bundle")

if let url = htmlURL {
let templateRequest = URLRequest(url: url)

let escapedMarkdown = self.escape(markdown: markdown) ?? ""
let imageOption = enableImage ? "true" : "false"
let script = "window.showMarkdown('\(escapedMarkdown)', \(imageOption));"
let userScript = WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true)

let controller = WKUserContentController()
controller.addUserScript(userScript)

let configuration = WKWebViewConfiguration()
configuration.userContentController = controller
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
configuration.preferences = preferences

let wv = WKWebView(frame: self.bounds, configuration: configuration)
wv.scrollView.isScrollEnabled = self.isScrollEnabled
wv.translatesAutoresizingMaskIntoConstraints = false
wv.navigationDelegate = self
wv.isOpaque = false
if #available(iOS 13.0, *) {
wv.backgroundColor = UIColor.systemBackground
wv.scrollView.backgroundColor = UIColor.systemBackground
}
addSubview(wv)
wv.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
wv.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
wv.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
wv.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
wv.backgroundColor = self.backgroundColor

self.webView = wv

wv.load(templateRequest)
} else {
// TODO: raise error
}
}

wv.load(templateRequest)
} else {
// TODO: raise error
private func escape(markdown: String) -> String? {
return markdown.addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics)
}

// MARK: - Reload MarkdownView
private func reloadMarkdownView() {
guard let webView = self.webView else {
return
}
let cssFile = readFileBy(name: (isDarkUIStyle ? "main_dark" : "main"), type: "css")
let cssStyle = """
javascript:(function() {
var parent = document.getElementsByTagName('head').item(0);
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = window.atob('\(encodeStringTo64(fromString: cssFile)!)');
parent.appendChild(style)})()
"""
webView.evaluateJavaScript(cssStyle) { [weak self] result, error in
self?.didChangeInterfaceStyle?(self?.isDarkUIStyle ?? false, error)
}
}

// NOTE: Injecting css and javascript into WKWebView
// https://medium.com/@mahdi.mahjoobi/injection-css-and-javascript-in-wkwebview-eabf58e5c54e

// MARK: - Encode string to base 64
private func encodeStringTo64(fromString: String) -> String? {
let plainData = fromString.data(using: .utf8)
return plainData?.base64EncodedString(options: [])
}
}

private func escape(markdown: String) -> String? {
return markdown.addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics)
}
// MARK: - Reading contents of files
private func readFileBy(name: String, type: String) -> String {
guard let path = Bundle.main.path(forResource: name, ofType: type) ?? Bundle.main.path(forResource: name, ofType: type, inDirectory: "MarkdownView.bundle") else {
return "Failed to find path"
}

do {
return try String(contentsOfFile: path, encoding: .utf8)
} catch {
return "Unkown Error"
}
}

}

extension MarkdownView: WKNavigationDelegate {

public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let script = "document.body.scrollHeight;"
webView.evaluateJavaScript(script) { [weak self] result, error in
if let _ = error { return }

if let height = result as? CGFloat {
self?.onRendered?(height)
self?.intrinsicContentHeight = height
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let script = "document.body.scrollHeight;"
webView.evaluateJavaScript(script) { [weak self] result, error in
if let _ = error { return }

if let height = result as? CGFloat {
self?.onRendered?(height)
self?.intrinsicContentHeight = height
}
}
}
}

public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

switch navigationAction.navigationType {
case .linkActivated:
if let onTouchLink = onTouchLink, onTouchLink(navigationAction.request) {
decisionHandler(.allow)
} else {
decisionHandler(.cancel)
}
default:
decisionHandler(.allow)
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
switch navigationAction.navigationType {
case .linkActivated:
if let onTouchLink = onTouchLink, onTouchLink(navigationAction.request) {
decisionHandler(.allow)
} else {
decisionHandler(.cancel)
}
default:
decisionHandler(.allow)
}
}

}

}
12 changes: 12 additions & 0 deletions webassets/dist/index_dark.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="./main_dark.css" />
<script src="./main.js"></script>
</head>
<body>
<div class="container" id="contents"></div>
</body>
</html>
Loading