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