SwiftUI:使用 WkWebView 突出显示具有不同颜色的文本

SwiftUI: Highlighting Text with different colors using WkWebView

我正在尝试使用 swiftUI 来创建自定义 EPUB reader。我环顾四周,但 none 符合我的需要。我希望能够自定义它。我 运行 遇到的问题是能够在阅读 O运行ge、蓝色、绿色等时突出显示文本。当突出显示文本然后弹出菜单栏时,我单击我的自定义应用程序崩溃时的菜单栏颜色。我发现这篇关于突出显示文本的文章,但使用的是 UIkit 而不是 SwiftUI。我一直在尝试“t运行slate”(不确定正确的术语是什么)以将其与 SwiftUI 一起使用,但由于无法识别 selector.Im 认为我没有正确设置而崩溃。不确定是否值得再使用 SwiftUI,此时只是将我的应用程序切换到 UIKit,因为我无法使用 swiftUI 找到很多资源。这是突出显示文本的文章:https://dailong.medium.com/highlight-text-in-wkwebview-1659a19715e6 刚开始学习swiftUI,不确定WebView设置的方式是否正确。

这是所有代码的 gitHub link https://github.com/longvudai/demo/tree/master/highlight-webview/highlight-webview 使用 SwiftUI 我所做的只是复制并粘贴文件。唯一的区别是 SwiftUI 包装了 WebView,除此之外其他一切都是一样的。

SWIFTUI

   `struct WebView: UIViewRepresentable {
    class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
    var webView: CustomView?
    var serializedObject: SerializedObject?
    private var dataStack = Stack<Highlights>()
    
    func webView(_ webView: WKWebView?, didFinish navigation: WKNavigation!) {
        self.webView = webView as? CustomView
    }
    
    // receive message from wkwebview
    func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        if let markerHandler = MarkerScript.Handler(message) {
            guard
                let dataString = message.body as? String,
                let data = dataString.data(using: .utf8)
                else { return }
            let decoder = JSONDecoder()
            guard let serialized = try? decoder.decode(
                SerializedObject.self,
                from: data
                ) else { return }
            receiveMarkerMessage(markerHandler, data: serialized)
        }
    }
    
    func receiveMarkerMessage(_ handler: MarkerScript.Handler, data: SerializedObject) {
        switch handler {
        case .serialize:
            serializedObject = data
            
            // your callback here
            
            let script = MarkerScript.Evaluate.clearSelection()
            self.webView?.evaluateJavaScript(script)
        case .erase:
            serializedObject = data
            let highlights = data.highlights
            let listId = highlights.map { [=10=].id }
            guard let top = dataStack.top  else { return }
            let newData = top.filter { listId.contains([=10=].id) }
            if newData != top {
                dataStack.push(newData)
            }
        }
    }
    func highlight(_ color: MarkerColor) {
        let script =
            MarkerScript.Evaluate.highlightSelectedTextWithColor(color)
        webView?.evaluateJavaScript(script)
        print("highlightfunction")
    }
    
    func removeAll() {
        let script = MarkerScript.Evaluate.removeAllHighlights()
        self.webView?.evaluateJavaScript(script)
        dataStack.push([])
    }
    
    func erase() {
        let script = MarkerScript.Evaluate.erase()
        self.webView?.evaluateJavaScript(script)
    }
    
    
    @objc func highlightthiscolor() {
        highlight(MarkerColor.orange)
    }
    
}


func makeCoordinator() -> Coordinator {
    return Coordinator()
}

func makeUIView(context: Context) -> CustomView {
    let coordinator = makeCoordinator()
    let configuration = WKWebViewConfiguration()
    let uc = configuration.userContentController
    uc.addUserScript(WKUserScript.injectViewPort())
    
    // Jquery
    uc.addUserScript(JQueryScript.core())
    
    // Rangy
    uc.addUserScript(RangyScript.core())
    uc.addUserScript(RangyScript.classapplier())
    uc.addUserScript(RangyScript.highlighter())
    uc.addUserScript(RangyScript.selectionsaverestore())
    uc.addUserScript(RangyScript.textrange())
    
    // Marker
    uc.addUserScript(MarkerScript.css())
    uc.addUserScript(MarkerScript.jsScript())
    
    uc.add(coordinator, name: MarkerScript.Handler.serialize.rawValue)
    uc.add(coordinator, name: MarkerScript.Handler.erase.rawValue)
    
    let _wkwebview = CustomView(frame: .zero, configuration: configuration)
    _wkwebview.navigationDelegate = coordinator
    
    return _wkwebview
}

func updateUIView(_ webView: CustomView, context: Context) {
    guard let path: String = Bundle.main.path(forResource: "sample", ofType: "html") else { return }
    let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false)
    webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl)
    addCustomContextMenu()
}


func addCustomContextMenu(){
    //Has to be type of WKWebView
    let colorOrange:UIMenuItem = UIMenuItem(title: "Orange", action: #selector(Coordinator.highlightthiscolor))
    UIMenuController.shared.menuItems = [colorOrange]
}

}`

UIKit

protocol MarkerLogic {
func erase()
func highlight(_ color: MarkerColor)
func removeAll()
 }

 class Marker: NSObject {
 weak var webView: WKWebView?
 var serializedObject: SerializedObject?
 private var dataStack = Stack<Highlights>()
 }

 extension Marker: MarkerLogic {
  func highlight(_ color: MarkerColor) {
    let script =
        MarkerScript.Evaluate.highlightSelectedTextWithColor(color)
    webView?.evaluateJavaScript(script)
}

func removeAll() {
    let script = MarkerScript.Evaluate.removeAllHighlights()
    webView?.evaluateJavaScript(script)
    dataStack.push([])
}

func erase() {
    let script = MarkerScript.Evaluate.erase()
    webView?.evaluateJavaScript(script)
}
 }

 // MARK: - WKScriptMessageHandler
extension Marker: WKScriptMessageHandler {
func userContentController(
    _ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage
) {
    if let markerHandler = MarkerScript.Handler(message) {
        guard
            let dataString = message.body as? String,
            let data = dataString.data(using: .utf8)
            else { return }
        let decoder = JSONDecoder()
        guard let serialized = try? decoder.decode(
            SerializedObject.self,
            from: data
            ) else { return }
        receiveMarkerMessage(markerHandler, data: serialized)
    }
}
func receiveMarkerMessage(_ handler: MarkerScript.Handler, data: SerializedObject) {
    switch handler {
    case .serialize:
        serializedObject = data
        
        // your callback here
        
        let script = MarkerScript.Evaluate.clearSelection()
        webView?.evaluateJavaScript(script)
    case .erase:
        serializedObject = data
        let highlights = data.highlights
        let listId = highlights.map { [=11=].id }
        guard let top = dataStack.top  else { return }
        let newData = top.filter { listId.contains([=11=].id) }
        if newData != top {
            dataStack.push(newData)
        }
    }
}
}

--- ViewDidLoad

class ViewController: UIViewController, WKScriptMessageHandler {
let marker: Marker = Marker()

let orangeButton: UIButton = {
    let v = UIButton()
    v.tag = 0
    v.backgroundColor = MarkerColor.orange.value
    v.layer.cornerRadius = 10
    
    v.addTarget(self, action: #selector(highlight(_:)), for: .touchUpInside)
    
    return v
}()

let cyanButton: UIButton = {
    let v = UIButton()
    v.tag = 1
    v.backgroundColor = MarkerColor.cyan.value
    v.layer.cornerRadius = 10
    
    v.addTarget(self, action: #selector(highlight(_:)), for: .touchUpInside)
    
    return v
}()

let pinkButton: UIButton = {
    let v = UIButton()
    v.tag = 2
    v.backgroundColor = MarkerColor.pink.value
    v.layer.cornerRadius = 10
    
    v.addTarget(self, action: #selector(highlight(_:)), for: .touchUpInside)
    
    return v
}()

let eraseButton: UIButton = {
    let v = UIButton()
    v.setTitle("Erase", for: .normal)
    v.setTitleColor(.systemBlue, for: .normal)
    
    v.addTarget(self, action: #selector(erase), for: .touchUpInside)
    
    return v
}()

let eraseAllButton: UIButton = {
    let v = UIButton(type: .close)
    
    v.addTarget(self, action: #selector(eraseAll), for: .touchUpInside)
    
    return v
}()

lazy var toolBars: UIStackView = {
    let v = UIStackView(arrangedSubviews: [orangeButton, cyanButton, pinkButton, eraseButton, eraseAllButton])
    v.axis = .horizontal
    v.distribution = .fillEqually
    v.spacing = 20
    return v
}()



// This is to make the makeUIView
lazy var webView: WKWebView = {
    let config = WKWebViewConfiguration()
    let uc = config.userContentController
    
    uc.addUserScript(WKUserScript.injectViewPort())
    
    // Jquery
    uc.addUserScript(JQueryScript.core())
    
    // Rangy
    uc.addUserScript(RangyScript.core())
    uc.addUserScript(RangyScript.classapplier())
    uc.addUserScript(RangyScript.highlighter())
    uc.addUserScript(RangyScript.selectionsaverestore())
    uc.addUserScript(RangyScript.textrange())
    
    // Marker
    uc.addUserScript(MarkerScript.css())
    uc.addUserScript(MarkerScript.jsScript())
    
    uc.add(self.marker, name: MarkerScript.Handler.serialize.rawValue)
    uc.add(self.marker, name: MarkerScript.Handler.erase.rawValue)
    
    let v = WKWebView(frame: .zero, configuration: config)
    
    return v
}()

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    
    marker.webView = webView
    
    let path = Bundle.main.path(forResource: "sample", ofType: "html")!
    let url = URL(fileURLWithPath: path)
    webView.loadFileURL(url, allowingReadAccessTo: url)
    
    let views = [webView, toolBars]
    views.forEach {
        view.addSubview([=12=])
        [=12=].translatesAutoresizingMaskIntoConstraints = false
    }
    
    NSLayoutConstraint.activate([
        webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
        webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        
        toolBars.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
        toolBars.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
        toolBars.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
        toolBars.heightAnchor.constraint(equalToConstant: 40)
    ])
}

// MARK: - Selector
@objc func highlight(_ sender: UIButton) {
    switch sender.tag {
    case 0:
        marker.highlight(MarkerColor.orange)
    case 1:
        marker.highlight(MarkerColor.cyan)
    case 2:
        marker.highlight(MarkerColor.pink)
    default:
        break
    }
}

@objc func erase() {
    marker.erase()
}

@objc func eraseAll() {
    marker.removeAll()
}

// MARK: - WKScriptMessageHandler
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
}

}

通过查看 post:,我终于能够使代码正常工作。我 运行 遇到的问题是我无法 运行 javascript 功能以突出显示。使用 Combine 我能够在视图中创建一个按钮,当单击该按钮时能够 运行 javascript 代码。将post下面的代码提供给任何有兴趣的人。

import WebKit
import SwiftUI
import Combine

class WebViewData: ObservableObject {
    @Published var parsedText: NSAttributedString? = nil
    
    var functionCaller = PassthroughSubject<Void,Never>()
    
    var isInit = false
    var shouldUpdateView = true
}

struct ContentView: View {
    @StateObject var webViewData = WebViewData()
    
    var body: some View {
        VStack {
            Button(action: {
                webViewData.functionCaller.send()
                
            }) {
                Text("Orange")
            }
            WebView(data: webViewData)
        }
    }
}

struct WebView: UIViewRepresentable {
    @StateObject var data: WebViewData
    class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
        //var webView: WKWebView?
        var serializedObject: SerializedObject?
        private var dataStack = Stack<Highlights>()
        var parent: WebView
        var webView: WKWebView? = nil
        private var cancellable : AnyCancellable?
        
        init(view: WebView) {
            self.parent = view
            super.init()
        }
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            self.webView = webView
        }
        
        // receive message from wkwebview
        func userContentController(
            _ userContentController: WKUserContentController,
            didReceive message: WKScriptMessage
        ) {
            if let markerHandler = MarkerScript.Handler(message) {
                guard
                    let dataString = message.body as? String,
                    let data = dataString.data(using: .utf8)
                else { return }
                let decoder = JSONDecoder()
                guard let serialized = try? decoder.decode(
                    SerializedObject.self,
                    from: data
                ) else { return }
                receiveMarkerMessage(markerHandler, data: serialized)
            }
        }
        
        func receiveMarkerMessage(_ handler: MarkerScript.Handler, data: SerializedObject) {
            switch handler {
            case .serialize:
                serializedObject = data
                // your callback here
                let script = MarkerScript.Evaluate.clearSelection()
                self.webView?.evaluateJavaScript(script)
            case .erase:
                serializedObject = data
                let highlights = data.highlights
                let listId = highlights.map { [=10=].id }
                guard let top = dataStack.top  else { return }
                let newData = top.filter { listId.contains([=10=].id) }
                if newData != top {
                    dataStack.push(newData)
                }
            }
        }
        func tieFunctionCaller(data: WebViewData) {
            cancellable = data.functionCaller.sink(receiveValue: { _ in
                self.webView?.evaluateJavaScript("highlightSelectedTextWithColor('orange')")
            })
        }
    
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(view: self)
    }
    
    func makeUIView(context: Context) -> WKWebView {
        
        let coordinator = makeCoordinator()
        let configuration = WKWebViewConfiguration()
        let uc = configuration.userContentController
        uc.addUserScript(WKUserScript.injectViewPort())
        
        // Jquery
        uc.addUserScript(JQueryScript.core())
        
        // Rangy
        uc.addUserScript(RangyScript.core())
        uc.addUserScript(RangyScript.classapplier())
        uc.addUserScript(RangyScript.highlighter())
        uc.addUserScript(RangyScript.selectionsaverestore())
        uc.addUserScript(RangyScript.textrange())
        
        // Marker
        uc.addUserScript(MarkerScript.css())
        uc.addUserScript(MarkerScript.jsScript())
        uc.add(coordinator, name: MarkerScript.Handler.serialize.rawValue)
        uc.add(coordinator, name: MarkerScript.Handler.erase.rawValue)
        
        let _wkwebview = WKWebView(frame: .zero, configuration: configuration)
        _wkwebview.navigationDelegate = coordinator
        
        return _wkwebview
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
        
        guard data.shouldUpdateView else {
            data.shouldUpdateView = false
            return
        }
        context.coordinator.tieFunctionCaller(data: data)
        context.coordinator.webView = webView
        
        guard let path: String = Bundle.main.path(forResource: "sample", ofType: "html") else { return }
        let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false)
        webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl)
    }
    
}