From cbe3dab050a9b0a3f645cec992083311b2ca8c28 Mon Sep 17 00:00:00 2001 From: Stanislav Shemiakov Date: Tue, 9 Jun 2020 18:52:43 +0300 Subject: [PATCH 1/7] Started migration to the WKWebView --- FolioReaderKit.podspec | 2 +- Source/FolioReaderCenter.swift | 7 ++++- Source/FolioReaderPage.swift | 37 ++++++++++++++++---------- Source/FolioReaderWebView.swift | 47 ++++++++++++++++++++++++--------- 4 files changed, 65 insertions(+), 28 deletions(-) diff --git a/FolioReaderKit.podspec b/FolioReaderKit.podspec index a056d5076..ecb2c5fc0 100755 --- a/FolioReaderKit.podspec +++ b/FolioReaderKit.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.social_media_url = 'https://twitter.com/hebertialmeida' s.swift_version = '4.2' - s.platform = :ios, '9.0' + s.platform = :ios, '10.0' s.requires_arc = true s.source_files = [ diff --git a/Source/FolioReaderCenter.swift b/Source/FolioReaderCenter.swift index 7042ea769..626648d8d 100644 --- a/Source/FolioReaderCenter.swift +++ b/Source/FolioReaderCenter.swift @@ -474,7 +474,12 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl let jsTag = "" + "" - let toInject = "\n\(cssTag)\n\(jsTag)\n" + // Inject dynamic style + let overflow = cell.webView?.cssOverflowProperty ?? "scroll" + let htmlList = "html{overflow:\(overflow)}" + let styleTag = "" + + let toInject = "\n\(cssTag)\n\(jsTag)\n\(styleTag)\n" html = html.replacingOccurrences(of: "", with: toInject) // Font class name diff --git a/Source/FolioReaderPage.swift b/Source/FolioReaderPage.swift index 5b013a0fd..389cf62fb 100755 --- a/Source/FolioReaderPage.swift +++ b/Source/FolioReaderPage.swift @@ -9,6 +9,7 @@ import UIKit import SafariServices import MenuItemKit +import WebKit /// Protocol which is used from `FolioReaderPage`s. @objc public protocol FolioReaderPageDelegate: class { @@ -35,7 +36,7 @@ import MenuItemKit @objc optional func pageTap(_ recognizer: UITapGestureRecognizer) } -open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRecognizerDelegate { +open class FolioReaderPage: UICollectionViewCell, WKNavigationDelegate, UIGestureRecognizerDelegate { weak var delegate: FolioReaderPageDelegate? weak var readerContainer: FolioReaderContainer? @@ -80,13 +81,12 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe if webView == nil { webView = FolioReaderWebView(frame: webViewFrame(), readerContainer: readerContainer) webView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] - webView?.dataDetectorTypes = .link webView?.scrollView.showsVerticalScrollIndicator = false webView?.scrollView.showsHorizontalScrollIndicator = false webView?.backgroundColor = .clear self.contentView.addSubview(webView!) } - webView?.delegate = self + webView?.navigationDelegate = self if colorView == nil { colorView = UIView() @@ -110,7 +110,7 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe deinit { webView?.scrollView.delegate = nil - webView?.delegate = nil + webView?.navigationDelegate = nil NotificationCenter.default.removeObserver(self) } @@ -145,7 +145,8 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe let tempHtmlContent = htmlContentWithInsertHighlights(htmlContent) // Load the html into the webview webView?.alpha = 0 - webView?.loadHTMLString(tempHtmlContent, baseURL: baseURL) + let viewportScale = "" + webView?.loadHTMLString(viewportScale + tempHtmlContent, baseURL: baseURL) } // MARK: - Highlights @@ -187,9 +188,9 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe return tempHtmlContent as String } - // MARK: - UIWebView Delegate + // MARK: - WKNavigation Delegate - open func webViewDidFinishLoad(_ webView: UIWebView) { + open func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { guard let webView = webView as? FolioReaderWebView else { return } @@ -225,7 +226,14 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe delegate?.pageDidLoad?(self) } - open func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebView.NavigationType) -> Bool { + open func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + let handledAction = handlePolicy(for: navigationAction) + let policy: WKNavigationActionPolicy = handledAction ? .allow : .cancel + decisionHandler(policy) + } + + private func handlePolicy(for navigationAction: WKNavigationAction) -> Bool { + let request = navigationAction.request guard let webView = webView as? FolioReaderWebView, let scheme = request.url?.scheme else { @@ -299,7 +307,7 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe } else if scheme == "mailto" { print("Email") return true - } else if url.absoluteString != "about:blank" && scheme.contains("http") && navigationType == .linkClicked { + } else if url.absoluteString != "about:blank" && scheme.contains("http") && navigationAction.navigationType == .linkActivated { let safariVC = SFSafariViewController(url: request.url!) safariVC.view.tintColor = self.readerConfig.tintColor self.folioReader.readerCenter?.present(safariVC, animated: true, completion: nil) @@ -511,11 +519,12 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe if (self.folioReader.nightMode == true) { // omit create webView and colorView - let script = "document.documentElement.offsetHeight" - let contentHeight = webView.stringByEvaluatingJavaScript(from: script) - let frameHeight = webView.frame.height - let lastPageHeight = frameHeight * CGFloat(webView.pageCount) - CGFloat(Double(contentHeight!)!) - colorView.frame = CGRect(x: webView.frame.width * CGFloat(webView.pageCount-1), y: webView.frame.height - lastPageHeight, width: webView.frame.width, height: lastPageHeight) + // let script = "document.documentElement.offsetHeight" + // let contentHeight = webView.stringByEvaluatingJavaScript(from: script) + // let frameHeight = webView.frame.height + // let lastPageHeight = frameHeight * CGFloat(webView.pageCount) - CGFloat(Double(contentHeight!)!) + // colorView.frame = CGRect(x: webView.frame.width * CGFloat(webView.pageCount-1), y: webView.frame.height - lastPageHeight, width: webView.frame.width, height: lastPageHeight) + colorView.frame = CGRect.zero } else { colorView.frame = CGRect.zero } diff --git a/Source/FolioReaderWebView.swift b/Source/FolioReaderWebView.swift index 9c3b7531a..f3bad52c4 100644 --- a/Source/FolioReaderWebView.swift +++ b/Source/FolioReaderWebView.swift @@ -6,13 +6,14 @@ // Copyright (c) 2016 Folio Reader. All rights reserved. // -import UIKit +import WebKit /// The custom WebView used in each page -open class FolioReaderWebView: UIWebView { +open class FolioReaderWebView: WKWebView { var isColors = false var isShare = false var isOneWord = false + fileprivate(set) var cssOverflowProperty = "scroll" fileprivate weak var readerContainer: FolioReaderContainer? @@ -31,14 +32,12 @@ open class FolioReaderWebView: UIWebView { return readerContainer.folioReader } - override init(frame: CGRect) { - fatalError("use init(frame:readerConfig:book:) instead.") - } - init(frame: CGRect, readerContainer: FolioReaderContainer) { self.readerContainer = readerContainer - super.init(frame: frame) + let configuration = WKWebViewConfiguration() + configuration.dataDetectorTypes = .link + super.init(frame: frame, configuration: configuration) } required public init?(coder aDecoder: NSCoder) { @@ -347,11 +346,36 @@ open class FolioReaderWebView: UIWebView { // MARK: - Java Script Bridge @discardableResult open func js(_ script: String) -> String? { - let callback = self.stringByEvaluatingJavaScript(from: script) - if callback!.isEmpty { return nil } + var callback: String? + let semaphore = DispatchSemaphore(value: 0) + jsAsync(script) { result in + callback = result + semaphore.signal() + } + semaphore.wait(timeout: .now()+0.0001) return callback } + open func jsAsync(_ script: String, completion: ((String?) -> ())? = nil) { + evaluateJavaScript(script) { result, error in + let output: String? + if let result = result { + let stringResult = "\(result)" + if stringResult.isEmpty { + output = nil + } else { + output = stringResult + } + } else { + output = nil + } + if let error = error { + debugPrint("evaluateJavaScript returned an error:", error) + } + completion?(output) + } + } + // MARK: WebView func clearTextSelection() { @@ -366,13 +390,12 @@ open class FolioReaderWebView: UIWebView { switch self.readerConfig.scrollDirection { case .vertical, .defaultVertical, .horizontalWithVerticalContent: scrollView.isPagingEnabled = false - paginationMode = .unpaginated + cssOverflowProperty = "scroll" scrollView.bounces = true break case .horizontal: scrollView.isPagingEnabled = true - paginationMode = .leftToRight - paginationBreakingMode = .page + cssOverflowProperty = "-webkit-paged-x" scrollView.bounces = false break } From dd793fcec97a671638b41c1b57ed133391fd57db Mon Sep 17 00:00:00 2001 From: Stanislav Shemiakov Date: Tue, 9 Jun 2020 20:12:08 +0300 Subject: [PATCH 2/7] fixed JavaScript evaluations by making them asynchronous --- Example/Podfile | 2 +- Example/Podfile.lock | 2 +- Source/FolioReaderAudioPlayer.swift | 25 ++-- Source/FolioReaderCenter.swift | 36 +++--- Source/FolioReaderPage.swift | 80 ++++++------- Source/FolioReaderPageIndicator.swift | 7 +- Source/FolioReaderWebView.swift | 163 ++++++++++++++------------ Source/Models/Highlight+Helper.swift | 13 +- 8 files changed, 160 insertions(+), 168 deletions(-) diff --git a/Example/Podfile b/Example/Podfile index 9e61cf01a..414a27ad3 100755 --- a/Example/Podfile +++ b/Example/Podfile @@ -1,4 +1,4 @@ -platform :ios, '9.0' +platform :ios, '10.0' use_frameworks! inhibit_all_warnings! diff --git a/Example/Podfile.lock b/Example/Podfile.lock index a04b62021..e05cb3751 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -25,7 +25,7 @@ DEPENDENCIES: - Quick (= 1.3.2) SPEC REPOS: - https://github.com/cocoapods/specs.git: + https://github.com/CocoaPods/Specs.git: - AEXML - FontBlaster - MenuItemKit diff --git a/Source/FolioReaderAudioPlayer.swift b/Source/FolioReaderAudioPlayer.swift index e2cd6158a..0fdf67cf5 100644 --- a/Source/FolioReaderAudioPlayer.swift +++ b/Source/FolioReaderAudioPlayer.swift @@ -382,22 +382,19 @@ open class FolioReaderAudioPlayer: NSObject { } let playbackActiveClass = book.playbackActiveClass - guard let sentence = currentPage.webView?.js("getSentenceWithIndex('\(playbackActiveClass)')") else { - if (readerCenter.isLastPage() == true) { - self.stop() - } else { - readerCenter.changePageToNext() + currentPage.webView?.js("getSentenceWithIndex('\(playbackActiveClass)')") { sentence in + guard let sentence = sentence else { + if (readerCenter.isLastPage() == true) { + self.stop() + } else { + readerCenter.changePageToNext() + } + return } - - return + guard let href = readerCenter.getCurrentChapter()?.href else { return } + // TODO QUESTION: The previous code made it possible to call `playText` with the parameter `href` being an empty string. Was that valid? should this logic be kept? + self.playText(href, text: sentence) } - - guard let href = readerCenter.getCurrentChapter()?.href else { - return - } - - // TODO QUESTION: The previous code made it possible to call `playText` with the parameter `href` being an empty string. Was that valid? should this logic be kept? - self.playText(href, text: sentence) } func readCurrentSentence() { diff --git a/Source/FolioReaderCenter.swift b/Source/FolioReaderCenter.swift index 626648d8d..262606b27 100644 --- a/Source/FolioReaderCenter.swift +++ b/Source/FolioReaderCenter.swift @@ -653,18 +653,13 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl } scrollScrubber?.setSliderVal() - - if let readingTime = currentPage.webView?.js("getReadingTime()") { - pageIndicatorView?.totalMinutes = Int(readingTime)! - } else { - pageIndicatorView?.totalMinutes = 0 + currentPage.webView?.js("getReadingTime()") { readingTime in + self.pageIndicatorView?.totalMinutes = Int(readingTime ?? "0")! + self.pagesForCurrentPage(currentPage) + self.delegate?.pageDidAppear?(currentPage) + self.delegate?.pageItemChanged?(self.getCurrentPageItemNumber()) + completion?() } - pagesForCurrentPage(currentPage) - - delegate?.pageDidAppear?(currentPage) - delegate?.pageItemChanged?(self.getCurrentPageItemNumber()) - - completion?() } func pagesForCurrentPage(_ page: FolioReaderPage?) { @@ -1080,9 +1075,10 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl @objc func shareChapter(_ sender: UIBarButtonItem) { guard let currentPage = currentPage else { return } - if let chapterText = currentPage.webView?.js("getBodyText()") { + currentPage.webView?.js("getBodyText()") { chapterText in + guard let chapterText = chapterText else { return } let htmlText = chapterText.replacingOccurrences(of: "[\\n\\r]+", with: "
", options: .regularExpression) - var subject = readerConfig.localizedShareChapterSubject + var subject = self.readerConfig.localizedShareChapterSubject var html = "" var text = "" var bookTitle = "" @@ -1097,7 +1093,7 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl } // Get chapter name - if let chapter = getCurrentChapterName() { + if let chapter = self.getCurrentChapterName() { chapterName = chapter } @@ -1109,17 +1105,17 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl // Sharing html and text html = "" html += "

\(htmlText)



" - html += "

"+readerConfig.localizedShareAllExcerptsFrom+"

" + html += "

"+self.readerConfig.localizedShareAllExcerptsFrom+"

" html += "\(bookTitle)
" - html += readerConfig.localizedShareBy+" \(authorName)
" - - if let bookShareLink = readerConfig.localizedShareWebLink { + html += self.readerConfig.localizedShareBy+" \(authorName)
" + + if let bookShareLink = self.readerConfig.localizedShareWebLink { html += "\(bookShareLink.absoluteString)" shareItems.append(bookShareLink as AnyObject) } html += "
" - text = "\(chapterName)\n\n“\(chapterText)” \n\n\(bookTitle) \n\(readerConfig.localizedShareBy) \(authorName)" + text = "\(chapterName)\n\n“\(chapterText)” \n\n\(bookTitle) \n\(self.readerConfig.localizedShareBy) \(authorName)" let act = FolioReaderSharingProvider(subject: subject, text: text, html: html) shareItems.insert(contentsOf: [act, "" as AnyObject], at: 0) @@ -1132,7 +1128,7 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl actv.barButtonItem = sender } - present(activityViewController, animated: true, completion: nil) + self.present(activityViewController, animated: true, completion: nil) } } diff --git a/Source/FolioReaderPage.swift b/Source/FolioReaderPage.swift index 389cf62fb..b12f57b15 100755 --- a/Source/FolioReaderPage.swift +++ b/Source/FolioReaderPage.swift @@ -381,20 +381,20 @@ open class FolioReaderPage: UICollectionViewCell, WKNavigationDelegate, UIGestur self.delegate?.pageTap?(recognizer) if let _navigationController = self.folioReader.readerCenter?.navigationController, (_navigationController.isNavigationBarHidden == true) { - let selected = webView?.js("getSelectedText()") + webView?.js("getSelectedText()") { selected in + guard (selected == nil || selected?.isEmpty == true) else { + return + } - guard (selected == nil || selected?.isEmpty == true) else { - return + let delay = 0.4 * Double(NSEC_PER_SEC) // 0.4 seconds * nanoseconds per seconds + let dispatchTime = (DispatchTime.now() + (Double(Int64(delay)) / Double(NSEC_PER_SEC))) + + DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: { + if (self.shouldShowBar == true && self.menuIsVisible == false) { + self.folioReader.readerCenter?.toggleBars() + } + }) } - - let delay = 0.4 * Double(NSEC_PER_SEC) // 0.4 seconds * nanoseconds per seconds - let dispatchTime = (DispatchTime.now() + (Double(Int64(delay)) / Double(NSEC_PER_SEC))) - - DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: { - if (self.shouldShowBar == true && self.menuIsVisible == false) { - self.folioReader.readerCenter?.toggleBars() - } - }) } else if (self.readerConfig.shouldHideNavigationOnTap == true) { self.folioReader.readerCenter?.hideBars() self.menuIsVisible = false @@ -441,19 +441,19 @@ open class FolioReaderPage: UICollectionViewCell, WKNavigationDelegate, UIGestur */ open func handleAnchor(_ anchor: String, avoidBeginningAnchors: Bool, animated: Bool) { if !anchor.isEmpty { - let offset = getAnchorOffset(anchor) - - switch self.readerConfig.scrollDirection { - case .vertical, .defaultVertical: - let isBeginning = (offset < frame.forDirection(withConfiguration: self.readerConfig) * 0.5) - - if !avoidBeginningAnchors { - scrollPageToOffset(offset, animated: animated) - } else if avoidBeginningAnchors && !isBeginning { - scrollPageToOffset(offset, animated: animated) + getAnchorOffset(anchor) { offset in + switch self.readerConfig.scrollDirection { + case .vertical, .defaultVertical: + let isBeginning = (offset < self.frame.forDirection(withConfiguration: self.readerConfig) * 0.5) + + if !avoidBeginningAnchors { + self.scrollPageToOffset(offset, animated: animated) + } else if avoidBeginningAnchors && !isBeginning { + self.scrollPageToOffset(offset, animated: animated) + } + case .horizontal, .horizontalWithVerticalContent: + self.scrollPageToOffset(offset, animated: animated) } - case .horizontal, .horizontalWithVerticalContent: - scrollPageToOffset(offset, animated: animated) } } } @@ -466,13 +466,15 @@ open class FolioReaderPage: UICollectionViewCell, WKNavigationDelegate, UIGestur - parameter anchor: The #anchor id - returns: The element offset ready to scroll */ - func getAnchorOffset(_ anchor: String) -> CGFloat { + func getAnchorOffset(_ anchor: String, completion: @escaping ((CGFloat) -> ())) { let horizontal = self.readerConfig.scrollDirection == .horizontal - if let strOffset = webView?.js("getAnchorOffset('\(anchor)', \(horizontal.description))") { - return CGFloat((strOffset as NSString).floatValue) + webView?.js("getAnchorOffset('\(anchor)', \(horizontal.description))") { strOffset in + guard let strOffset = strOffset else { + completion(CGFloat(0)) + return + } + completion(CGFloat((strOffset as NSString).floatValue)) } - - return CGFloat(0) } // MARK: Mark ID @@ -502,11 +504,13 @@ open class FolioReaderPage: UICollectionViewCell, WKNavigationDelegate, UIGestur } if !webView.isShare && !webView.isColors { - if let result = webView.js("getSelectedText()") , result.components(separatedBy: " ").count == 1 { + webView.js("getSelectedText()") { result in + guard let result = result, result.components(separatedBy: " ").count == 1 else { + webView.isOneWord = false + return + } webView.isOneWord = true webView.createMenu(options: false) - } else { - webView.isOneWord = false } } @@ -534,18 +538,8 @@ open class FolioReaderPage: UICollectionViewCell, WKNavigationDelegate, UIGestur fileprivate func setupClassBasedOnClickListeners() { for listener in self.readerConfig.classBasedOnClickListeners { - self.webView?.js("addClassBasedOnClickListener(\"\(listener.schemeName)\", \"\(listener.querySelector)\", \"\(listener.attributeName)\", \"\(listener.selectAll)\")"); + self.webView?.js("addClassBasedOnClickListener(\"\(listener.schemeName)\", \"\(listener.querySelector)\", \"\(listener.attributeName)\", \"\(listener.selectAll)\")") } } - // MARK: - Public Java Script injection - - /** - Runs a JavaScript script and returns it result. The result of running the JavaScript script passed in the script parameter, or nil if the script fails. - - - returns: The result of running the JavaScript script passed in the script parameter, or nil if the script fails. - */ - open func performJavaScript(_ javaScriptCode: String) -> String? { - return webView?.js(javaScriptCode) - } } diff --git a/Source/FolioReaderPageIndicator.swift b/Source/FolioReaderPageIndicator.swift index fc1960556..dfa56052c 100644 --- a/Source/FolioReaderPageIndicator.swift +++ b/Source/FolioReaderPageIndicator.swift @@ -95,7 +95,12 @@ class FolioReaderPageIndicator: UIView { pagesLabel.text = " \(pagesRemaining) " + self.readerConfig.localizedReaderManyPagesLeft } - let minutesRemaining = Int(ceil(CGFloat((pagesRemaining * totalMinutes)/totalPages))) + let minutesRemaining: Int + if totalPages == 0 { + minutesRemaining = 0 + } else { + minutesRemaining = Int(ceil(CGFloat((pagesRemaining * totalMinutes)/totalPages))) + } if minutesRemaining > 1 { minutesLabel.text = "\(minutesRemaining) " + self.readerConfig.localizedReaderManyMinutes+" ·" } else if minutesRemaining == 1 { diff --git a/Source/FolioReaderWebView.swift b/Source/FolioReaderWebView.swift index f3bad52c4..0ee140fcc 100644 --- a/Source/FolioReaderWebView.swift +++ b/Source/FolioReaderWebView.swift @@ -8,6 +8,8 @@ import WebKit +public typealias JSCallback = (String?) ->() + /// The custom WebView used in each page open class FolioReaderWebView: WKWebView { var isColors = false @@ -76,11 +78,13 @@ open class FolioReaderWebView: WKWebView { let shareImage = UIAlertAction(title: self.readerConfig.localizedShareImageQuote, style: .default, handler: { (action) -> Void in if self.isShare { - if let textToShare = self.js("getHighlightContent()") { + self.js("getHighlightContent()") { textToShare in + guard let textToShare = textToShare else { return } self.folioReader.readerCenter?.presentQuoteShare(textToShare) } } else { - if let textToShare = self.js("getSelectedText()") { + self.js("getSelectedText()") { textToShare in + guard let textToShare = textToShare else { return } self.folioReader.readerCenter?.presentQuoteShare(textToShare) self.clearTextSelection() @@ -91,11 +95,13 @@ open class FolioReaderWebView: WKWebView { let shareText = UIAlertAction(title: self.readerConfig.localizedShareTextQuote, style: .default) { (action) -> Void in if self.isShare { - if let textToShare = self.js("getHighlightContent()") { + self.js("getHighlightContent()") { textToShare in + guard let textToShare = textToShare else { return } self.folioReader.readerCenter?.shareHighlight(textToShare, rect: sender.menuFrame) } } else { - if let textToShare = self.js("getSelectedText()") { + self.js("getSelectedText()") { textToShare in + guard let textToShare = textToShare else { return } self.folioReader.readerCenter?.shareHighlight(textToShare, rect: sender.menuFrame) } } @@ -123,92 +129,103 @@ open class FolioReaderWebView: WKWebView { } func remove(_ sender: UIMenuController?) { - if let removedId = js("removeThisHighlight()") { + js("removeThisHighlight()") { removedId in + guard let removedId = removedId else { return } Highlight.removeById(withConfiguration: self.readerConfig, highlightId: removedId) } setMenuVisible(false) } @objc func highlight(_ sender: UIMenuController?) { - let highlightAndReturn = js("highlightString('\(HighlightStyle.classForStyle(self.folioReader.currentHighlightStyle))')") - let jsonData = highlightAndReturn?.data(using: String.Encoding.utf8) - - do { - let json = try JSONSerialization.jsonObject(with: jsonData!, options: []) as! NSArray - let dic = json.firstObject as! [String: String] - let rect = NSCoder.cgRect(for: dic["rect"]!) - guard let startOffset = dic["startOffset"] else { - return - } - guard let endOffset = dic["endOffset"] else { - return - } - - createMenu(options: true) - setMenuVisible(true, andRect: rect) - - // Persist - guard - let html = js("getHTML()"), - let identifier = dic["id"], - let bookId = (self.book.name as NSString?)?.deletingPathExtension else { + js("highlightString('\(HighlightStyle.classForStyle(self.folioReader.currentHighlightStyle))')") { highlightAndReturn in + let jsonData = highlightAndReturn?.data(using: String.Encoding.utf8) + + do { + let json = try JSONSerialization.jsonObject(with: jsonData!, options: []) as! NSArray + let dic = json.firstObject as! [String: String] + let rect = NSCoder.cgRect(for: dic["rect"]!) + guard let startOffset = dic["startOffset"] else { return - } - - let pageNumber = folioReader.readerCenter?.currentPageNumber ?? 0 - let match = Highlight.MatchingHighlight(text: html, id: identifier, startOffset: startOffset, endOffset: endOffset, bookId: bookId, currentPage: pageNumber) - let highlight = Highlight.matchHighlight(match) - highlight?.persist(withConfiguration: self.readerConfig) + } + guard let endOffset = dic["endOffset"] else { + return + } - } catch { - print("Could not receive JSON") + self.createMenu(options: true) + self.setMenuVisible(true, andRect: rect) + + // Persist + self.js("getHTML()") { html in + + guard let html = html, let identifier = dic["id"], let bookId = (self.book.name as NSString?)?.deletingPathExtension + else { + return + } + let pageNumber = self.folioReader.readerCenter?.currentPageNumber ?? 0 + let match = Highlight.MatchingHighlight(text: html, id: identifier, startOffset: startOffset, endOffset: endOffset, bookId: bookId, currentPage: pageNumber) + let highlight = Highlight.matchHighlight(match) + highlight?.persist(withConfiguration: self.readerConfig) + } + + } catch { + print("Could not receive JSON:", error) + } } } @objc func highlightWithNote(_ sender: UIMenuController?) { - let highlightAndReturn = js("highlightStringWithNote('\(HighlightStyle.classForStyle(self.folioReader.currentHighlightStyle))')") - let jsonData = highlightAndReturn?.data(using: String.Encoding.utf8) + js("highlightStringWithNote('\(HighlightStyle.classForStyle(self.folioReader.currentHighlightStyle))')") { highlightAndReturn in + let jsonData = highlightAndReturn?.data(using: String.Encoding.utf8) - do { - let json = try JSONSerialization.jsonObject(with: jsonData!, options: []) as! NSArray - let dic = json.firstObject as! [String: String] - guard let startOffset = dic["startOffset"] else { return } - guard let endOffset = dic["endOffset"] else { return } + do { + let json = try JSONSerialization.jsonObject(with: jsonData!, options: []) as! NSArray + let dic = json.firstObject as! [String: String] + guard let startOffset = dic["startOffset"] else { return } + guard let endOffset = dic["endOffset"] else { return } - self.clearTextSelection() + self.clearTextSelection() - guard let html = js("getHTML()") else { return } - guard let identifier = dic["id"] else { return } - guard let bookId = (self.book.name as NSString?)?.deletingPathExtension else { return } - - let pageNumber = folioReader.readerCenter?.currentPageNumber ?? 0 - let match = Highlight.MatchingHighlight(text: html, id: identifier, startOffset: startOffset, endOffset: endOffset, bookId: bookId, currentPage: pageNumber) - if let highlight = Highlight.matchHighlight(match) { - self.folioReader.readerCenter?.presentAddHighlightNote(highlight, edit: false) + self.js("getHTML()") { html in + guard + let html = html, let identifier = dic["id"], + let bookId = (self.book.name as NSString?)?.deletingPathExtension else + { + return + } + + let pageNumber = self.folioReader.readerCenter?.currentPageNumber ?? 0 + let match = Highlight.MatchingHighlight(text: html, id: identifier, startOffset: startOffset, endOffset: endOffset, bookId: bookId, currentPage: pageNumber) + if let highlight = Highlight.matchHighlight(match) { + self.folioReader.readerCenter?.presentAddHighlightNote(highlight, edit: false) + } + } + } catch { + print("Could not receive JSON:", error) } - } catch { - print("Could not receive JSON") } } @objc func updateHighlightNote (_ sender: UIMenuController?) { - guard let highlightId = js("getHighlightId()") else { return } - guard let highlightNote = Highlight.getById(withConfiguration: readerConfig, highlightId: highlightId) else { return } - self.folioReader.readerCenter?.presentAddHighlightNote(highlightNote, edit: true) + js("getHighlightId()") { highlightId in + guard + let highlightId = highlightId, + let highlightNote = Highlight.getById(withConfiguration: self.readerConfig, highlightId: highlightId) else { return } + self.folioReader.readerCenter?.presentAddHighlightNote(highlightNote, edit: true) + } } @objc func define(_ sender: UIMenuController?) { - guard let selectedText = js("getSelectedText()") else { - return - } + js("getSelectedText()") { selectedText in + guard let selectedText = selectedText else { return } - self.setMenuVisible(false) - self.clearTextSelection() + self.setMenuVisible(false) + self.clearTextSelection() - let vc = UIReferenceLibraryViewController(term: selectedText) - vc.view.tintColor = self.readerConfig.tintColor - guard let readerContainer = readerContainer else { return } - readerContainer.show(vc, sender: nil) + let vc = UIReferenceLibraryViewController(term: selectedText) + vc.view.tintColor = self.readerConfig.tintColor + guard let readerContainer = self.readerContainer else { return } + readerContainer.show(vc, sender: nil) + } } @objc func play(_ sender: UIMenuController?) { @@ -240,7 +257,8 @@ open class FolioReaderWebView: WKWebView { func changeHighlightStyle(_ sender: UIMenuController?, style: HighlightStyle) { self.folioReader.currentHighlightStyle = style.rawValue - if let updateId = js("setHighlightStyle('\(HighlightStyle.classForStyle(style.rawValue))')") { + js("setHighlightStyle('\(HighlightStyle.classForStyle(style.rawValue))')") { updateId in + guard let updateId = updateId else { return } Highlight.updateById(withConfiguration: self.readerConfig, highlightId: updateId, type: style) } @@ -345,18 +363,7 @@ open class FolioReaderWebView: WKWebView { // MARK: - Java Script Bridge - @discardableResult open func js(_ script: String) -> String? { - var callback: String? - let semaphore = DispatchSemaphore(value: 0) - jsAsync(script) { result in - callback = result - semaphore.signal() - } - semaphore.wait(timeout: .now()+0.0001) - return callback - } - - open func jsAsync(_ script: String, completion: ((String?) -> ())? = nil) { + open func js(_ script: String, completion: JSCallback? = nil) { evaluateJavaScript(script) { result, error in let output: String? if let result = result { diff --git a/Source/Models/Highlight+Helper.swift b/Source/Models/Highlight+Helper.swift index 3d23a4054..537bd0fcf 100644 --- a/Source/Models/Highlight+Helper.swift +++ b/Source/Models/Highlight+Helper.swift @@ -288,16 +288,9 @@ extension Highlight { /// - Parameters: /// - page: The page containing the HTML. /// - highlightId: The ID to be removed - /// - Returns: The removed id - @discardableResult public static func removeFromHTMLById(withinPage page: FolioReaderPage?, highlightId: String) -> String? { - guard let currentPage = page else { return nil } - - if let removedId = currentPage.webView?.js("removeHighlightById('\(highlightId)')") { - return removedId - } else { - print("Error removing Highlight from page") - return nil - } + /// - completion: JSCallback with removed id + public static func removeFromHTMLById(withinPage page: FolioReaderPage?, highlightId: String, completion: JSCallback? = nil) { + page?.webView?.js("removeHighlightById('\(highlightId)')", completion: completion) } /** From 325ad8885a6b4b1c3220b6a5232db15f02b9fe7e Mon Sep 17 00:00:00 2001 From: Stanislav Shemiakov Date: Fri, 12 Jun 2020 02:36:07 +0300 Subject: [PATCH 3/7] injecting javascript code the WebKit way --- Source/FolioReaderCenter.swift | 12 ++++----- Source/FolioReaderScript.swift | 46 +++++++++++++++++++++++++++++++++ Source/FolioReaderWebView.swift | 9 +++++-- 3 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 Source/FolioReaderScript.swift diff --git a/Source/FolioReaderCenter.swift b/Source/FolioReaderCenter.swift index 262606b27..ee59aa950 100644 --- a/Source/FolioReaderCenter.swift +++ b/Source/FolioReaderCenter.swift @@ -465,21 +465,21 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl return cell } - let mediaOverlayStyleColors = "\"\(self.readerConfig.mediaOverlayColor.hexString(false))\", \"\(self.readerConfig.mediaOverlayColor.highlightColor().hexString(false))\"" - // Inject CSS - let jsFilePath = Bundle.frameworkBundle().path(forResource: "Bridge", ofType: "js") let cssFilePath = Bundle.frameworkBundle().path(forResource: "Style", ofType: "css") let cssTag = "" - let jsTag = "" + - "" + + // Inject JavaScript + FolioReaderScript.bridgeJS.addIfNeeded(to: cell.webView) + let mediaOverlayScript = FolioReaderScript.mediaOverlayStyleColors(from: readerConfig.mediaOverlayColor) + mediaOverlayScript.addIfNeeded(to: cell.webView) // Inject dynamic style let overflow = cell.webView?.cssOverflowProperty ?? "scroll" let htmlList = "html{overflow:\(overflow)}" let styleTag = "" - let toInject = "\n\(cssTag)\n\(jsTag)\n\(styleTag)\n" + let toInject = "\n\(cssTag)\n\(styleTag)\n" html = html.replacingOccurrences(of: "", with: toInject) // Font class name diff --git a/Source/FolioReaderScript.swift b/Source/FolioReaderScript.swift new file mode 100644 index 000000000..42d97705e --- /dev/null +++ b/Source/FolioReaderScript.swift @@ -0,0 +1,46 @@ +// +// FolioReaderScript.swift +// FolioReaderKit +// +// Created by Stanislav on 12.06.2020. +// Copyright (c) 2015 Folio Reader. All rights reserved. +// + +import WebKit + +class FolioReaderScript: WKUserScript { + + init(source: String) { + super.init(source: source, + injectionTime: .atDocumentStart, + forMainFrameOnly: false) + } + + static let bridgeJS: FolioReaderScript = { + let jsURL = Bundle.frameworkBundle().url(forResource: "Bridge", withExtension: "js")! + let jsSource = try! String(contentsOf: jsURL) + return FolioReaderScript(source: jsSource) + }() + + static func mediaOverlayStyleColors(from color: UIColor) -> FolioReaderScript { + let colors = "\"\(color.hexString(false))\", \"\(color.highlightColor().hexString(false))\"" + let scriptSource = "setMediaOverlayStyleColors(\(colors))" + return FolioReaderScript(source: scriptSource) + } + +} + +extension WKUserScript { + + func addIfNeeded(to webView: WKWebView?) { + guard let controller = webView?.configuration.userContentController else { return } + let alreadyAdded = controller.userScripts.contains { [unowned self] in + return $0.source == self.source && + $0.injectionTime == self.injectionTime && + $0.isForMainFrameOnly == self.isForMainFrameOnly + } + if alreadyAdded { return } + controller.addUserScript(self) + } + +} diff --git a/Source/FolioReaderWebView.swift b/Source/FolioReaderWebView.swift index 0ee140fcc..13d45cfd7 100644 --- a/Source/FolioReaderWebView.swift +++ b/Source/FolioReaderWebView.swift @@ -376,8 +376,13 @@ open class FolioReaderWebView: WKWebView { } else { output = nil } - if let error = error { - debugPrint("evaluateJavaScript returned an error:", error) + if let nsError = error as? NSError, + let url = nsError.userInfo["WKJavaScriptExceptionSourceURL"] as? NSURL, + url.absoluteString == "undefined" + { + // skip debugPrint - html hasn't loaded yet + } else if let error = error { + debugPrint("evaluateJavaScript(\(script)) returned an error:", error) } completion?(output) } From 767c88df1d71a8a6b08303a96434ed51daa9dfb7 Mon Sep 17 00:00:00 2001 From: Stanislav Shemiakov Date: Fri, 12 Jun 2020 04:00:21 +0300 Subject: [PATCH 4/7] fixed pages counter calculations --- Source/FolioReaderPage.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Source/FolioReaderPage.swift b/Source/FolioReaderPage.swift index b12f57b15..1290a9ca9 100755 --- a/Source/FolioReaderPage.swift +++ b/Source/FolioReaderPage.swift @@ -196,6 +196,12 @@ open class FolioReaderPage: UICollectionViewCell, WKNavigationDelegate, UIGestur } delegate?.pageWillLoad?(self) + } + + open func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + guard let webView = webView as? FolioReaderWebView else { + return + } // Add the custom class based onClick listener self.setupClassBasedOnClickListeners() @@ -222,8 +228,9 @@ open class FolioReaderPage: UICollectionViewCell, WKNavigationDelegate, UIGestur webView.isColors = false self.webView?.createMenu(options: false) }) - - delegate?.pageDidLoad?(self) + webView.js("document.readyState") { _ in + self.delegate?.pageDidLoad?(self) + } } open func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { From 49076e400c35bcfd2f0c2adbf27bde21f9755e84 Mon Sep 17 00:00:00 2001 From: Stanislav Shemiakov Date: Mon, 15 Jun 2020 18:06:25 +0300 Subject: [PATCH 5/7] fixed FolioReaderPage reference --- Source/FolioReaderCenter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/FolioReaderCenter.swift b/Source/FolioReaderCenter.swift index ee59aa950..4fdbfcaa5 100644 --- a/Source/FolioReaderCenter.swift +++ b/Source/FolioReaderCenter.swift @@ -8,6 +8,7 @@ import UIKit import ZFDragableModalTransition +import WebKit /// Protocol which is used from `FolioReaderCenter`s. @objc public protocol FolioReaderCenterDelegate: class { @@ -1282,7 +1283,7 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl // Perform the page after a short delay as the collection view hasn't completed it's transition if this method is called (the index paths aren't right during fast scrolls). delay(0.2, closure: { [weak self] in if (self?.readerConfig.scrollDirection == .horizontalWithVerticalContent), - let cell = ((scrollView.superview as? UIWebView)?.delegate as? FolioReaderPage) { + let cell = ((scrollView.superview as? WKWebView)?.navigationDelegate as? FolioReaderPage) { let currentIndexPathRow = cell.pageNumber - 1 self?.currentWebViewScrollPositions[currentIndexPathRow] = scrollView.contentOffset } From 8226e55095279d05cb9f4af18f63bbe5e9b56762 Mon Sep 17 00:00:00 2001 From: Stanislav Shemiakov Date: Mon, 15 Jun 2020 18:22:58 +0300 Subject: [PATCH 6/7] updated viewport tag injection --- Source/FolioReaderCenter.swift | 5 ++++- Source/FolioReaderPage.swift | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/FolioReaderCenter.swift b/Source/FolioReaderCenter.swift index 4fdbfcaa5..a6e24af78 100644 --- a/Source/FolioReaderCenter.swift +++ b/Source/FolioReaderCenter.swift @@ -465,6 +465,9 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl guard var html = try? String(contentsOfFile: resource.fullHref, encoding: String.Encoding.utf8) else { return cell } + + // Inject viewport + let viewportTag = "" // Inject CSS let cssFilePath = Bundle.frameworkBundle().path(forResource: "Style", ofType: "css") @@ -480,7 +483,7 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl let htmlList = "html{overflow:\(overflow)}" let styleTag = "" - let toInject = "\n\(cssTag)\n\(styleTag)\n" + let toInject = "\n\(viewportTag)\n\(cssTag)\n\(styleTag)\n" html = html.replacingOccurrences(of: "", with: toInject) // Font class name diff --git a/Source/FolioReaderPage.swift b/Source/FolioReaderPage.swift index 1290a9ca9..af764a16b 100755 --- a/Source/FolioReaderPage.swift +++ b/Source/FolioReaderPage.swift @@ -145,8 +145,7 @@ open class FolioReaderPage: UICollectionViewCell, WKNavigationDelegate, UIGestur let tempHtmlContent = htmlContentWithInsertHighlights(htmlContent) // Load the html into the webview webView?.alpha = 0 - let viewportScale = "" - webView?.loadHTMLString(viewportScale + tempHtmlContent, baseURL: baseURL) + webView?.loadHTMLString(tempHtmlContent, baseURL: baseURL) } // MARK: - Highlights From 36bf1f059f0c16d9556a5e6682d611fa3098cbb7 Mon Sep 17 00:00:00 2001 From: Stanislav Shemiakov Date: Mon, 15 Jun 2020 23:58:10 +0300 Subject: [PATCH 7/7] updated css and javascript injection --- Source/FolioReaderCenter.swift | 16 +--------------- Source/FolioReaderPage.swift | 3 +++ Source/FolioReaderScript.swift | 28 ++++++++++++++++++++++------ Source/FolioReaderWebView.swift | 9 ++++++++- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/Source/FolioReaderCenter.swift b/Source/FolioReaderCenter.swift index a6e24af78..5ec84230e 100644 --- a/Source/FolioReaderCenter.swift +++ b/Source/FolioReaderCenter.swift @@ -469,21 +469,7 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl // Inject viewport let viewportTag = "" - // Inject CSS - let cssFilePath = Bundle.frameworkBundle().path(forResource: "Style", ofType: "css") - let cssTag = "" - - // Inject JavaScript - FolioReaderScript.bridgeJS.addIfNeeded(to: cell.webView) - let mediaOverlayScript = FolioReaderScript.mediaOverlayStyleColors(from: readerConfig.mediaOverlayColor) - mediaOverlayScript.addIfNeeded(to: cell.webView) - - // Inject dynamic style - let overflow = cell.webView?.cssOverflowProperty ?? "scroll" - let htmlList = "html{overflow:\(overflow)}" - let styleTag = "" - - let toInject = "\n\(viewportTag)\n\(cssTag)\n\(styleTag)\n" + let toInject = "\n\(viewportTag)\n" html = html.replacingOccurrences(of: "", with: toInject) // Font class name diff --git a/Source/FolioReaderPage.swift b/Source/FolioReaderPage.swift index af764a16b..8ee1e3521 100755 --- a/Source/FolioReaderPage.swift +++ b/Source/FolioReaderPage.swift @@ -230,6 +230,9 @@ open class FolioReaderPage: UICollectionViewCell, WKNavigationDelegate, UIGestur webView.js("document.readyState") { _ in self.delegate?.pageDidLoad?(self) } + let overlayColor = readerConfig.mediaOverlayColor! + let colors = "\"\(overlayColor.hexString(false))\", \"\(overlayColor.highlightColor().hexString(false))\"" + webView.js("setMediaOverlayStyleColors(\(colors))") } open func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { diff --git a/Source/FolioReaderScript.swift b/Source/FolioReaderScript.swift index 42d97705e..1f0140df4 100644 --- a/Source/FolioReaderScript.swift +++ b/Source/FolioReaderScript.swift @@ -12,8 +12,8 @@ class FolioReaderScript: WKUserScript { init(source: String) { super.init(source: source, - injectionTime: .atDocumentStart, - forMainFrameOnly: false) + injectionTime: .atDocumentEnd, + forMainFrameOnly: true) } static let bridgeJS: FolioReaderScript = { @@ -22,10 +22,26 @@ class FolioReaderScript: WKUserScript { return FolioReaderScript(source: jsSource) }() - static func mediaOverlayStyleColors(from color: UIColor) -> FolioReaderScript { - let colors = "\"\(color.hexString(false))\", \"\(color.highlightColor().hexString(false))\"" - let scriptSource = "setMediaOverlayStyleColors(\(colors))" - return FolioReaderScript(source: scriptSource) + static let cssInjection: FolioReaderScript = { + let cssURL = Bundle.frameworkBundle().url(forResource: "Style", withExtension: "css")! + let cssString = try! String(contentsOf: cssURL) + return FolioReaderScript(source: cssInjectionSource(for: cssString)) + }() + + static func cssInjection(overflow: String) -> FolioReaderScript { + let cssString = "html{overflow:\(overflow)}" + return FolioReaderScript(source: cssInjectionSource(for: cssString)) + } + + private static func cssInjectionSource(for content: String) -> String { + let oneLineContent = content.components(separatedBy: .newlines).joined() + let source = """ + var style = document.createElement('style'); + style.type = 'text/css' + style.innerHTML = '\(oneLineContent)'; + document.head.appendChild(style); + """ + return source } } diff --git a/Source/FolioReaderWebView.swift b/Source/FolioReaderWebView.swift index 13d45cfd7..578ddb340 100644 --- a/Source/FolioReaderWebView.swift +++ b/Source/FolioReaderWebView.swift @@ -15,7 +15,12 @@ open class FolioReaderWebView: WKWebView { var isColors = false var isShare = false var isOneWord = false - fileprivate(set) var cssOverflowProperty = "scroll" + + fileprivate(set) var cssOverflowProperty = "scroll" { + didSet { + FolioReaderScript.cssInjection(overflow: cssOverflowProperty).addIfNeeded(to: self) + } + } fileprivate weak var readerContainer: FolioReaderContainer? @@ -40,6 +45,8 @@ open class FolioReaderWebView: WKWebView { let configuration = WKWebViewConfiguration() configuration.dataDetectorTypes = .link super.init(frame: frame, configuration: configuration) + FolioReaderScript.cssInjection.addIfNeeded(to: self) + FolioReaderScript.bridgeJS.addIfNeeded(to: self) } required public init?(coder aDecoder: NSCoder) {