Skip to content
Open
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
136 changes: 128 additions & 8 deletions Sources/MarkdownWebView/MarkdownWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ import WebKit
public func makeCoordinator() -> Coordinator { .init(parent: self) }

#if os(macOS)
public func makeNSView(context: Context) -> CustomWebView { context.coordinator.platformView }
public func makeNSView(context: Context) -> UIView { context.coordinator.containerView }
#elseif os(iOS)
public func makeUIView(context: Context) -> CustomWebView { context.coordinator.platformView }
public func makeUIView(context: Context) -> UIView { context.coordinator.containerView }
#endif

func updatePlatformView(_ platformView: CustomWebView, context _: Context) {
Expand All @@ -43,9 +43,9 @@ import WebKit
}

#if os(macOS)
public func updateNSView(_ nsView: CustomWebView, context: Context) { updatePlatformView(nsView, context: context) }
public func updateNSView(_ nsView: UIView, context: Context) { updatePlatformView(context.coordinator.platformView, context: context) }
#elseif os(iOS)
public func updateUIView(_ uiView: CustomWebView, context: Context) { updatePlatformView(uiView, context: context) }
public func updateUIView(_ uiView: UIView, context: Context) { updatePlatformView(context.coordinator.platformView, context: context) }
#endif

public func onLinkActivation(_ linkActivationHandler: @escaping (URL) -> Void) -> Self {
Expand All @@ -56,15 +56,86 @@ import WebKit
.init(markdownContent, customStylesheet: customStylesheet, linkActivationHandler: linkActivationHandler, renderedContentHandler: renderedContentHandler)
}

public class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
let parent: MarkdownWebView
let platformView: CustomWebView
final class ContainerView: UIView {
weak var webView: CustomWebView?

override var intrinsicContentSize: CGSize {
// delegate intrinsic size to the web view
return webView?.intrinsicContentSize ?? .zero
}
}

public class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
let containerView = ContainerView()

private let parent: MarkdownWebView
var platformView: CustomWebView!

deinit {
removeScriptMessageHandlers()
deinitWillEnterForegroundNotification()
}

init(parent: MarkdownWebView) {
self.parent = parent
platformView = .init()

super.init()

self.initPlatformView()

self.initWillEnterForegroundNotification()
}

private func deinitWillEnterForegroundNotification() {
NotificationCenter.default.removeObserver(self, name: UIScene.willEnterForegroundNotification, object: nil)
}

private func initWillEnterForegroundNotification() {
NotificationCenter.default.addObserver(
self,
selector: #selector(willEnterForeground),
name: UIScene.willEnterForegroundNotification,
object: nil
)
}

@objc func willEnterForeground(_ notification: Notification) {
performBlankCheck()
}

private func performBlankCheck() {
// Avoid checking while page is still loading to reduce false positives
guard let webView = platformView, !webView.isLoading else { return }

// Quick JS returning body text length (fast)
webView.evaluateJavaScript("document && document.body ? document.body.innerText.length : 0") { [weak self] result, error in
guard let self else { return }
if let error = error {
// JS errored — print debug (page might be blank / bad state)
//print("⚠️ MarkdownWebView blank-check JS error: \(error.localizedDescription)")
recreatePlatformViewAsync()
return
}

if let len = result as? Int {
if len == 0 {
//print("⚠️ MarkdownWebView appears blank (body text length == 0).")
recreatePlatformViewAsync()
} else {
// optional: for debug you can print length
//print("✅ MarkdownWebView content length: \(len)")
}
} else {
// unexpected type, print for debugging
//print("⚠️ MarkdownWebView blank-check unexpected result: \(String(describing: result))")
recreatePlatformViewAsync()
}
}
}

private func initPlatformView() {
platformView = .init()

platformView.navigationDelegate = self

#if DEBUG && os(iOS)
Expand Down Expand Up @@ -114,6 +185,52 @@ import WebKit
.replacingOccurrences(of: "PLACEHOLDER_SCRIPT", with: script)
.replacingOccurrences(of: "PLACEHOLDER_STYLESHEET", with: self.parent.customStylesheet ?? defaultStylesheet)
platformView.loadHTMLString(htmlString, baseURL: nil)

attach(platformView, to: containerView)
}

private func recreatePlatformViewAsync() {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
guard let platformView else { return }

removeScriptMessageHandlers()

detach(platformView)

initPlatformView()
}
}

private func removeScriptMessageHandlers() {
platformView.configuration.userContentController.removeScriptMessageHandler(
forName: "sizeChangeHandler")
platformView.configuration.userContentController.removeScriptMessageHandler(
forName: "renderedContentHandler")
platformView.configuration.userContentController.removeScriptMessageHandler(
forName: "copyToPasteboard")
}

private func attach(_ webView: CustomWebView, to container: ContainerView) {
webView.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(webView)
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
webView.topAnchor.constraint(equalTo: container.topAnchor),
webView.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])

container.webView = webView
}

private func detach(_ webView: CustomWebView) {
webView.removeFromSuperview()
}

public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
//print("⚠️ Process terminated!")
recreatePlatformViewAsync()
}

/// Update the content on first finishing loading.
Expand Down Expand Up @@ -151,6 +268,9 @@ import WebKit
else { return }
platformView.contentHeight = contentHeight
platformView.invalidateIntrinsicContentSize()

containerView.invalidateIntrinsicContentSize()

case "renderedContentHandler":
guard let renderedContentHandler = parent.renderedContentHandler,
let renderedContentBase64Encoded = message.body as? String,
Expand Down