tangled
alpha
login
or
join now
jonmsterling.com
/
AquaKit
4
fork
atom
A collection of user interface components and drawing routines for building tasteful apps using AppKit.
appkit
swift
aqua
ui
mac
4
fork
atom
overview
issues
1
pulls
pipelines
Add AquaWebViewController
jonmsterling.com
1 month ago
ab3ea543
031cba51
+206
2 changed files
expand all
collapse all
unified
split
Sources
AquaKit
Web Views
AquaWebViewController.swift
AquaWebViewScrollBridge.swift
+125
Sources/AquaKit/Web Views/AquaWebViewController.swift
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import AppKit
2
+
import WebKit
3
+
4
+
open class AquaWebViewController: NSViewController {
5
+
public let webView = WKWebView()
6
+
public let horizontalScroller = AquaScroller(orientation: .horizontal)
7
+
public let verticalScroller = AquaScroller(orientation: .vertical)
8
+
private let scrollBridge: AquaWebViewScrollBridge = AquaWebViewScrollBridge(messageName: "scrollObserver")
9
+
10
+
private var verticalScrollerWidth: CGFloat {
11
+
AquaScroller.scrollerWidth(
12
+
for: verticalScroller.controlSize,
13
+
scrollerStyle: verticalScroller.scrollerStyle
14
+
)
15
+
}
16
+
17
+
private var horizontalScrollerWidth: CGFloat {
18
+
AquaScroller.scrollerWidth(
19
+
for: horizontalScroller.controlSize,
20
+
scrollerStyle: horizontalScroller.scrollerStyle
21
+
)
22
+
}
23
+
24
+
open override func viewDidLoad() {
25
+
super.viewDidLoad()
26
+
27
+
view.wantsLayer = true
28
+
29
+
for scroller in [verticalScroller, horizontalScroller] {
30
+
scroller.translatesAutoresizingMaskIntoConstraints = false
31
+
scroller.scrollerStyle = .legacy
32
+
scroller.isEnabled = false
33
+
scroller.target = self
34
+
scroller.action = #selector(scroll(_:))
35
+
scroller.isHidden = true
36
+
view.addSubview(scroller)
37
+
}
38
+
39
+
NSLayoutConstraint.activate([
40
+
verticalScroller.rightAnchor.constraint(equalTo: view.rightAnchor),
41
+
verticalScroller.topAnchor.constraint(equalTo: view.topAnchor),
42
+
verticalScroller.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
43
+
verticalScroller.widthAnchor.constraint(equalToConstant: verticalScrollerWidth),
44
+
])
45
+
46
+
NSLayoutConstraint.activate([
47
+
horizontalScroller.leftAnchor.constraint(equalTo: view.leftAnchor),
48
+
horizontalScroller.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor),
49
+
horizontalScroller.bottomAnchor.constraint(equalTo: view.bottomAnchor),
50
+
horizontalScroller.heightAnchor.constraint(equalToConstant: horizontalScrollerWidth),
51
+
])
52
+
53
+
view.addSubview(webView)
54
+
webView.translatesAutoresizingMaskIntoConstraints = false
55
+
NSLayoutConstraint.activate(webView.constraintsAnchoring(toLayoutGuide: view.safeAreaLayoutGuide))
56
+
57
+
let hideScrollbarsScript = WKUserScript(
58
+
source: """
59
+
(() => {
60
+
const style = document.createElement("style");
61
+
style.innerHTML = `
62
+
html, body {
63
+
scrollbar-width: none !important;
64
+
}
65
+
::-webkit-scrollbar {
66
+
width: 0 !important;
67
+
height: 0 !important;
68
+
}
69
+
`;
70
+
document.head.appendChild(style);
71
+
})();
72
+
""",
73
+
injectionTime: .atDocumentEnd,
74
+
forMainFrameOnly: true
75
+
)
76
+
77
+
let contentController = webView.configuration.userContentController
78
+
contentController.add(scrollBridge, name: scrollBridge.messageName)
79
+
contentController.addUserScript(scrollBridge.observerScript)
80
+
contentController.addUserScript(hideScrollbarsScript)
81
+
82
+
scrollBridge.delegate = self
83
+
}
84
+
85
+
@objc func scroll(_ sender: AquaScroller) {
86
+
webView.scroll(toFraction: sender.doubleValue, orientation: sender.orientation)
87
+
}
88
+
}
89
+
90
+
extension WKWebView {
91
+
fileprivate func scroll(
92
+
toFraction fraction: Double,
93
+
orientation: Orientation,
94
+
completionHandler: (@MainActor (Any?, (any Error)?) -> Void)? = nil
95
+
) {
96
+
let script =
97
+
switch orientation {
98
+
case .vertical:
99
+
"""
100
+
(() => {
101
+
const maxOffset = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - window.innerHeight;
102
+
window.scrollTo(window.scrollX, maxOffset * \(fraction));
103
+
})();
104
+
"""
105
+
case .horizontal:
106
+
"""
107
+
(() => {
108
+
const maxOffset = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth) - window.innerWidth;
109
+
window.scrollTo(maxOffset * \(fraction), window.scrollY);
110
+
})();
111
+
"""
112
+
}
113
+
114
+
evaluateJavaScript(script, completionHandler: completionHandler)
115
+
}
116
+
}
117
+
118
+
extension AquaWebViewController: @MainActor AquaWebViewScrollBridge.Delegate {
119
+
public func webScrollBridge(_ bridge: AquaWebViewScrollBridge, didScrollTo state: AquaScrollState) {
120
+
verticalScroller.adopt(state: state)
121
+
horizontalScroller.adopt(state: state)
122
+
view.additionalSafeAreaInsets.right = verticalScroller.isHidden ? 0 : verticalScrollerWidth
123
+
view.additionalSafeAreaInsets.bottom = horizontalScroller.isHidden ? 0 : horizontalScrollerWidth
124
+
}
125
+
}
+81
Sources/AquaKit/Web Views/AquaWebViewScrollBridge.swift
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import WebKit
2
+
3
+
public final class AquaWebViewScrollBridge: NSObject, WKScriptMessageHandler {
4
+
public protocol Delegate: AnyObject {
5
+
func webScrollBridge(_ bridge: AquaWebViewScrollBridge, didScrollTo state: AquaScrollState)
6
+
}
7
+
8
+
public let messageName: String
9
+
public weak var delegate: Delegate?
10
+
11
+
public init(messageName: String, delegate: Delegate? = nil) {
12
+
self.messageName = messageName
13
+
self.delegate = delegate
14
+
}
15
+
16
+
static let scrollYKey = "scrollY"
17
+
static let scrollXKey = "scrollX"
18
+
static let contentHeightKey = "contentHeight"
19
+
static let visibleHeightKey = "visibleHeight"
20
+
static let contentWidthKey = "contentWidth"
21
+
static let visibleWidthKey = "visibleWidth"
22
+
23
+
public var observerScript: WKUserScript {
24
+
WKUserScript(
25
+
source: """
26
+
(() => {
27
+
function reportScroll() {
28
+
window.webkit.messageHandlers.\(messageName).postMessage({
29
+
\(Self.scrollYKey): window.scrollY,
30
+
\(Self.scrollXKey): window.scrollX,
31
+
\(Self.contentHeightKey): Math.max(
32
+
document.body.scrollHeight,
33
+
document.documentElement.scrollHeight
34
+
),
35
+
\(Self.visibleHeightKey): window.innerHeight,
36
+
\(Self.contentWidthKey): Math.max(
37
+
document.body.scrollWidth,
38
+
document.documentElement.scrollWidth
39
+
),
40
+
\(Self.visibleWidthKey): window.innerWidth
41
+
});
42
+
}
43
+
44
+
window.addEventListener("scroll", reportScroll, { passive: true });
45
+
window.addEventListener("resize", reportScroll);
46
+
47
+
reportScroll();
48
+
})();
49
+
""",
50
+
injectionTime: .atDocumentEnd,
51
+
forMainFrameOnly: true
52
+
)
53
+
}
54
+
55
+
func decodeScrollState(from message: WKScriptMessage) -> AquaScrollState? {
56
+
guard
57
+
message.name == messageName,
58
+
let dict = message.body as? [String: Any],
59
+
let scrollY = dict[Self.scrollYKey] as? CGFloat,
60
+
let scrollX = dict[Self.scrollXKey] as? CGFloat,
61
+
let contentHeight = dict[Self.contentHeightKey] as? CGFloat,
62
+
let visibleHeight = dict[Self.visibleHeightKey] as? CGFloat,
63
+
let contentWidth = dict[Self.contentWidthKey] as? CGFloat,
64
+
let visibleWidth = dict[Self.visibleWidthKey] as? CGFloat
65
+
else { return nil }
66
+
67
+
return AquaScrollState(
68
+
scrollPosition: NSPoint(x: scrollX, y: scrollY),
69
+
contentSize: NSSize(width: contentWidth, height: contentHeight),
70
+
visibleSize: NSSize(width: visibleWidth, height: visibleHeight)
71
+
)
72
+
}
73
+
74
+
public func userContentController(
75
+
_ userContentController: WKUserContentController,
76
+
didReceive message: WKScriptMessage
77
+
) {
78
+
guard let state = decodeScrollState(from: message) else { return }
79
+
delegate?.webScrollBridge(self, didScrollTo: state)
80
+
}
81
+
}