Files
omnivore/apple/OmnivoreKit/Sources/Views/Article/WebView.swift
Satindar Dhillon 6f86495cf4 lint fixes
2022-06-17 13:54:40 -07:00

313 lines
9.8 KiB
Swift

import Utils
import WebKit
/// Describes actions that can be sent from the WebView back to native views.
/// The names on the javascript side must match for an action to be handled.
public enum WebViewAction: String, CaseIterable {
case highlightAction
case readingProgressUpdate
}
public final class WebView: WKWebView {
#if os(iOS)
private var panGestureRecognizer: UIPanGestureRecognizer?
private var tapGestureRecognizer: UITapGestureRecognizer?
#endif
override init(frame: CGRect, configuration: WKWebViewConfiguration) {
super.init(frame: frame, configuration: configuration)
#if os(iOS)
initNativeIOSMenus()
#endif
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func updateFontFamily() {
if let fontFamily = UserDefaults.standard.value(forKey: UserDefaultKey.preferredWebFont.rawValue) as? String {
dispatchEvent(.updateFontFamily(family: fontFamily))
}
}
public func updateFontSize() {
if let fontSize = UserDefaults.standard.value(forKey: UserDefaultKey.preferredWebFontSize.rawValue) as? Int {
dispatchEvent(.updateFontSize(size: fontSize))
}
}
public func updateMaxWidthPercentage() {
if let maxWidthPercentage = UserDefaults.standard.value(
forKey: UserDefaultKey.preferredWebMaxWidthPercentage.rawValue
) as? Int {
dispatchEvent(.updateMaxWidthPercentage(maxWidthPercentage: maxWidthPercentage))
}
}
public func updateLineHeight() {
if let height = UserDefaults.standard.value(forKey: UserDefaultKey.preferredWebLineSpacing.rawValue) as? Int {
dispatchEvent(.updateLineHeight(height: height))
}
}
public func updateTextContrast() {
let isHighContrast = UserDefaults.standard.value(
forKey: UserDefaultKey.prefersHighContrastWebFont.rawValue
) as? Bool
if let isHighContrast = isHighContrast {
dispatchEvent(.handleFontContrastChange(isHighContrast: isHighContrast))
}
}
public func shareOriginalItem() {
dispatchEvent(.share)
}
public func dispatchEvent(_ event: WebViewDispatchEvent) {
evaluateJavaScript(event.script) { obj, err in
if let err = err { print(err) }
if let obj = obj { print(obj) }
}
}
#if os(iOS)
override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle else { return }
dispatchEvent(.updateColorMode(isDark: traitCollection.userInterfaceStyle == .dark))
}
#elseif os(macOS)
override public func viewDidChangeEffectiveAppearance() {
super.viewDidChangeEffectiveAppearance()
switch effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) {
case .some(.darkAqua):
dispatchEvent("switchToDarkMode")
default:
dispatchEvent("switchToLightMode")
}
}
#endif
}
#if os(iOS)
extension WebView: UIGestureRecognizerDelegate, WKScriptMessageHandler {
func initNativeIOSMenus() {
isUserInteractionEnabled = true
NotificationCenter.default
.addObserver(self,
selector: #selector(menuDidHide),
name: UIMenuController.didHideMenuNotification,
object: nil)
setDefaultMenu()
}
public func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
guard let messageBody = message.body as? [String: Any] else { return }
guard let actionID = messageBody["actionID"] as? String else { return }
switch actionID {
case "showMenu":
if let rectX = messageBody["rectX"] as? Double,
let rectY = messageBody["rectY"] as? Double,
let rectWidth = messageBody["rectWidth"] as? Double,
let rectHeight = messageBody["rectHeight"] as? Double
{ // swiftlint:disable:this opening_brace
showHighlightMenu(CGRect(x: rectX, y: rectY, width: rectWidth, height: rectHeight))
}
default:
break
}
}
private func setDefaultMenu() {
let annotate = UIMenuItem(title: "Annotate", action: #selector(annotateSelection))
let highlight = UIMenuItem(title: "Highlight", action: #selector(highlightSelection))
// let share = UIMenuItem(title: "Share", action: #selector(shareSelection))
UIMenuController.shared.menuItems = [highlight, /* share, */ annotate]
}
private func setHighlightMenu() {
let annotate = UIMenuItem(title: "Annotate", action: #selector(annotateSelection))
let remove = UIMenuItem(title: "Remove", action: #selector(removeSelection))
// let share = UIMenuItem(title: "Share", action: #selector(shareSelection))
UIMenuController.shared.menuItems = [remove, /* share, */ annotate]
}
override public var canBecomeFirstResponder: Bool {
true
}
@objc func menuDidHide() {
setDefaultMenu()
}
// swiftlint:disable:next line_length
public func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer) -> Bool {
true
}
override public func canPerformAction(_ action: Selector, withSender _: Any?) -> Bool {
switch action {
case #selector(annotateSelection): return true
case #selector(highlightSelection): return true
case #selector(shareSelection): return true
case #selector(removeSelection): return true
case #selector(copy(_:)): return true
case Selector(("_lookup:")): return true
default: return false
}
}
@objc private func gestureHandled() {
hideMenuAndDismissHighlight()
}
@objc private func annotateSelection() {
dispatchEvent(.annotate)
hideMenu()
}
@objc private func highlightSelection() {
dispatchEvent(.highlight)
hideMenu()
}
@objc private func shareSelection() {
dispatchEvent(.share)
hideMenu()
}
@objc private func removeSelection() {
dispatchEvent(.remove)
hideMenu()
}
@objc override public func copy(_ sender: Any?) {
super.copy(sender)
dispatchEvent(.copyHighlight)
hideMenu()
}
private func hideMenu() {
UIMenuController.shared.hideMenu()
if let tapGestureRecognizer = tapGestureRecognizer {
removeGestureRecognizer(tapGestureRecognizer)
self.tapGestureRecognizer = nil
}
if let panGestureRecognizer = panGestureRecognizer {
removeGestureRecognizer(panGestureRecognizer)
self.panGestureRecognizer = nil
}
setDefaultMenu()
}
private func hideMenuAndDismissHighlight() {
hideMenu()
dispatchEvent(.dismissHighlight)
}
private func showHighlightMenu(_ rect: CGRect) {
setHighlightMenu()
// When the highlight menu is displayed we set up gesture recognizers so it
// can be dismissed if the user interacts with another part of the view.
// This isn't needed for the default menu as the system will handle that.
if tapGestureRecognizer == nil {
let tap = UITapGestureRecognizer(target: self, action: #selector(gestureHandled))
tap.delegate = self
addGestureRecognizer(tap)
tapGestureRecognizer = tap
}
if panGestureRecognizer == nil {
let pan = UIPanGestureRecognizer(target: self, action: #selector(gestureHandled))
pan.delegate = self
addGestureRecognizer(pan)
panGestureRecognizer = pan
}
UIMenuController.shared.showMenu(from: self, rect: rect)
}
}
#endif
public enum WebViewDispatchEvent {
case handleFontContrastChange(isHighContrast: Bool)
case updateLineHeight(height: Int)
case updateMaxWidthPercentage(maxWidthPercentage: Int)
case updateFontSize(size: Int)
case updateColorMode(isDark: Bool)
case updateFontFamily(family: String)
case saveAnnotation(annotation: String)
case annotate
case highlight
case share
case remove
case copyHighlight
case dismissHighlight
var script: String {
"var event = new Event('\(eventName)');\(scriptPropertyLine)document.dispatchEvent(event);"
}
private var eventName: String {
switch self {
case .handleFontContrastChange:
return "handleFontContrastChange"
case .updateLineHeight:
return "updateLineHeight"
case .updateMaxWidthPercentage:
return "updateMaxWidthPercentage"
case .updateFontSize:
return "updateFontSize"
case .updateColorMode:
return "updateColorMode"
case .updateFontFamily:
return "updateFontFamily"
case .saveAnnotation:
return "saveAnnotation"
case .annotate:
return "annotate"
case .highlight:
return "highlight"
case .share:
return "share"
case .remove:
return "remove"
case .copyHighlight:
return "copyHighlight"
case .dismissHighlight:
return "dismissHighlight"
}
}
private var scriptPropertyLine: String {
switch self {
case let .handleFontContrastChange(isHighContrast: isHighContrast):
return "event.fontContrast = '\(isHighContrast ? "high" : "normal")';"
case let .updateLineHeight(height: height):
return "event.lineHeight = '\(height)';"
case let .updateMaxWidthPercentage(maxWidthPercentage: maxWidthPercentage):
return "event.maxWidthPercentage = '\(maxWidthPercentage)';"
case let .updateFontSize(size: size):
return "event.fontSize = '\(size)';"
case let .updateColorMode(isDark: isDark):
return "event.isDarkMode = '\(isDark)';"
case let .updateFontFamily(family: family):
return "event.fontFamily = '\(family)';"
case let .saveAnnotation(annotation: annotation):
return "event.annotation = '\(annotation)';"
case .annotate, .highlight, .share, .remove, .copyHighlight, .dismissHighlight:
return ""
}
}
}