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

Posted

技术标签:

【中文标题】SwiftUI:使用 WkWebView 突出显示具有不同颜色的文本【英文标题】:SwiftUI: Highlighting Text with different colors using WkWebView 【发布时间】:2021-12-31 01:29:16 【问题描述】:

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

这里是所有代码https://github.com/longvudai/demo/tree/master/highlight-webview/highlight-webview 的 gitHub 链接使用 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  $0.id 
            guard let top = dataStack.top  else  return 
            let newData = top.filter  listId.contains($0.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  $0.id 
        guard let top = dataStack.top  else  return 
        let newData = top.filter  listId.contains($0.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($0)
        $0.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) 

【问题讨论】:

您包含的内容不足以重现您的问题 - 您没有包含许多缺失的类型。 @jnpdx 我觉得添加所有代码太多了。如果有帮助,我有 git hub 链接抱歉。 理想情况下,您可以将其缩减为 minimal reproducible example @jnpdx 据我了解,错误来自我的 webView Wrapper。这是我上面发布的代码。我所做的只是从 UIkit 中包装 webview,以便使用 UIViewRepresentable 在 swiftUI 中使用它。但是当我启动应用程序时,html 加载正常。问题是当我突出显示一个单词然后从我的 customMenu 工具栏中单击我想要的颜色时,应用程序崩溃了。这就是为什么我倾向于它是 WebView:UIViewRepresentable 【参考方案1】:

通过查看帖子,我终于能够使代码正常工作:Call evaluateJavascript from a SwiftUI button。我遇到的问题是我无法运行 javascript 函数来突出显示。使用Combine 我能够在视图中创建一个按钮,并且当单击该按钮时能够运行 javascript 代码。将下面的代码发布给任何有兴趣的人。

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  $0.id 
                guard let top = dataStack.top  else  return 
                let newData = top.filter  listId.contains($0.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)
    
    

【讨论】:

以上是关于SwiftUI:使用 WkWebView 突出显示具有不同颜色的文本的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI如何监听WKWebView加载网页的进度

SwiftUI 中的 WKWebView - 用户与网站交互时如何切换视图?

在 SwiftUI 中突出显示语音话语

从目标视图返回时,SwiftUI NavigationLink 显示为突出显示

表单/表格中的 SwiftUI 多个 NavigationLink - 条目保持突出显示

在 SwiftUI 中关闭按钮突出显示