diff --git a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/GalleryStateMessageHandler.swift b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/GalleryStateMessageHandler.swift new file mode 100644 index 00000000..7de908f9 --- /dev/null +++ b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/GalleryStateMessageHandler.swift @@ -0,0 +1,18 @@ +import WebKit + +/// Receives gallery open/close notifications posted by +/// `MMWebViewUserScripts.galleryStateObserver`. +@MainActor +final class GalleryStateMessageHandler: NSObject, WKScriptMessageHandler { + static let handlerName = "mmGalleryState" + + var onGalleryStateChange: ((Bool) -> Void)? + + func userContentController( + _ userContentController: WKUserContentController, + didReceive message: WKScriptMessage + ) { + guard let isOpen = message.body as? Bool else { return } + onGalleryStateChange?(isOpen) + } +} diff --git a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/InteractivePopGesture.swift b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/InteractivePopGesture.swift new file mode 100644 index 00000000..687e2f8a --- /dev/null +++ b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/InteractivePopGesture.swift @@ -0,0 +1,63 @@ +import SwiftUI +import UIKit + +public extension View { + /// Restores the interactive pop gesture (swipe from the leading edge to go back) + /// even when the system back button is hidden, and disables it on demand — + /// e.g. while an in-page image gallery is open and owns horizontal swipes. + func interactivePopGesture(enabled: Bool) -> some View { + background(InteractivePopGestureBridge(enabled: enabled)) + } +} + +private struct InteractivePopGestureBridge: UIViewControllerRepresentable { + let enabled: Bool + + func makeUIViewController(context: Context) -> BridgeViewController { + BridgeViewController() + } + + func updateUIViewController(_ controller: BridgeViewController, context: Context) { + controller.gestureEnabled = enabled + } +} + +private final class BridgeViewController: UIViewController, UIGestureRecognizerDelegate { + var gestureEnabled = true + + private weak var popGesture: UIGestureRecognizer? + private weak var originalDelegate: UIGestureRecognizerDelegate? + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + guard popGesture == nil, + let gesture = navigationController?.interactivePopGestureRecognizer else { return } + popGesture = gesture + originalDelegate = gesture.delegate + gesture.delegate = self + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + restoreDelegate() + } + + isolated deinit { + restoreDelegate() + } + + private func restoreDelegate() { + if let popGesture, popGesture.delegate === self { + popGesture.delegate = originalDelegate + } + popGesture = nil + originalDelegate = nil + } + + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let navigationController else { return false } + return gestureEnabled + && navigationController.viewControllers.count > 1 + && navigationController.transitionCoordinator == nil + } +} diff --git a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/MMWebView.swift b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/MMWebView.swift index 0d2f84e7..7ddb7ee5 100644 --- a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/MMWebView.swift +++ b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/MMWebView.swift @@ -11,6 +11,8 @@ public struct MMWebView: View { @State private var internalLinkURL: URL? @State private var page: WebPage? @State private var navigationDecider = MMNavigationDecider() + @State private var galleryStateHandler = GalleryStateMessageHandler() + @State private var isGalleryOpen = false @State private var reloadID = UUID() private let url: String? @@ -41,6 +43,7 @@ public struct MMWebView: View { page: $page, reloadTrigger: reloadID ) + .interactivePopGesture(enabled: !isGalleryOpen) .navigationDestination(item: $internalLinkURL) { url in MMWebView(url: url.absoluteString, dismissAction: dismissAction) .toolbar { @@ -62,12 +65,14 @@ public struct MMWebView: View { } } .onChange(of: colorScheme) { + isGalleryOpen = false page?.reload() } .onChange(of: removeAds) { if let cacheKey { WebPageCache.shared.removePage(for: cacheKey) } + isGalleryOpen = false page = nil reloadID = UUID() } @@ -102,6 +107,7 @@ private extension MMWebView { navigationDecider.onOpenComments = { [self] slug in commentsURL = slug } navigationDecider.onOpenInternalLink = { [self] url in internalLinkURL = url } + galleryStateHandler.onGalleryStateChange = { [self] isOpen in isGalleryOpen = isOpen } let configuration = makeConfiguration() let page: WebPage @@ -110,8 +116,11 @@ private extension MMWebView { page = WebPageCache.shared.page( for: cacheKey, configurationProvider: { configuration }, - navigationDecider: navigationDecider + navigationDecider: navigationDecider, + galleryStateHandler: galleryStateHandler ) + WebPageCache.shared.galleryHandler(for: cacheKey)? + .onGalleryStateChange = { [self] isOpen in isGalleryOpen = isOpen } } else { page = WebPage( configuration: configuration, @@ -138,6 +147,8 @@ private extension MMWebView { contentController.addUserScript(MMWebViewUserScripts.disableGallery) contentController.addUserScript(MMWebViewUserScripts.disableNewGallery) contentController.addUserScript(MMWebViewUserScripts.removeBackToBlog) + contentController.addUserScript(MMWebViewUserScripts.galleryStateObserver) + contentController.add(galleryStateHandler, name: GalleryStateMessageHandler.handlerName) return configuration } diff --git a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/MMWebViewUserScripts.swift b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/MMWebViewUserScripts.swift index 7618394a..49016531 100644 --- a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/MMWebViewUserScripts.swift +++ b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/MMWebViewUserScripts.swift @@ -49,6 +49,37 @@ public enum MMWebViewUserScripts { ) } + @MainActor + static var galleryStateObserver: WKUserScript { + WKUserScript( + source: """ + (function() { + if (window.__mmGalleryObserver) { return; } + var selectors = '.pswp--open, .fancybox-container, #fancybox-overlay, .mfp-wrap'; + var lastState = null; + function check() { + var open = !!document.querySelector(selectors); + if (open !== lastState) { + lastState = open; + window.webkit.messageHandlers.mmGalleryState.postMessage(open); + } + } + var observer = new MutationObserver(check); + observer.observe(document.documentElement, { + attributes: true, + childList: true, + subtree: true, + attributeFilter: ['class', 'style'] + }); + window.__mmGalleryObserver = observer; + check(); + })(); + """, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true + ) + } + @MainActor public static var hideSiteHeader: WKUserScript { WKUserScript( diff --git a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/WebPageCache.swift b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/WebPageCache.swift index e904c9b6..247d3749 100644 --- a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/WebPageCache.swift +++ b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Webview/WebPageCache.swift @@ -5,6 +5,7 @@ final class WebPageCache { static let shared = WebPageCache() private var cache: [String: WebPage] = [:] + private var galleryHandlers: [String: GalleryStateMessageHandler] = [:] private init() {} @@ -20,7 +21,8 @@ final class WebPageCache { func page( for key: String, configurationProvider: () -> WebPage.Configuration, - navigationDecider: some WebPage.NavigationDeciding + navigationDecider: some WebPage.NavigationDeciding, + galleryStateHandler: GalleryStateMessageHandler ) -> WebPage { if let existing = cache[key] { return existing @@ -30,14 +32,20 @@ final class WebPageCache { navigationDecider: navigationDecider ) cache[key] = page + galleryHandlers[key] = galleryStateHandler return page } + func galleryHandler(for key: String) -> GalleryStateMessageHandler? { + galleryHandlers[key] + } + func hasPage(for key: String) -> Bool { cache[key] != nil } func removePage(for key: String) { cache.removeValue(forKey: key) + galleryHandlers.removeValue(forKey: key) } }