A collection of user interface components and drawing routines for building tasteful apps using AppKit.
appkit swift aqua ui mac

Add AquaWebViewController

+206
+125
Sources/AquaKit/Web Views/AquaWebViewController.swift
··· 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
··· 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 + }