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 NavigationLink 显示为突出显示