diff --git a/r2-navigator-swift.xcodeproj/project.pbxproj b/r2-navigator-swift.xcodeproj/project.pbxproj index 911fabc1..e2a4d764 100644 --- a/r2-navigator-swift.xcodeproj/project.pbxproj +++ b/r2-navigator-swift.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ CAAABA9B24D695E5004A4466 /* TargetAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAABA9A24D695E5004A4466 /* TargetAction.swift */; }; CAB9086B22492D4C00711C3F /* Navigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB9086A22492D4C00711C3F /* Navigator.swift */; }; CAC2A6D72292E4BA000AA2A7 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC2A6D62292E4BA000AA2A7 /* WebView.swift */; }; + CAC6C98A261DB0B900EAF2BD /* WebViewResourceHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC6C989261DB0B900EAF2BD /* WebViewResourceHandler.swift */; }; CACE84F82254BE5F00E19E8B /* PDFDocumentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CACE84F72254BE5F00E19E8B /* PDFDocumentView.swift */; }; CACE84FB2254BFEE00E19E8B /* EditingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = CACE84FA2254BFEE00E19E8B /* EditingAction.swift */; }; CACE851F225CDE3400E19E8B /* EPUBFixedSpreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CACE851E225CDE3300E19E8B /* EPUBFixedSpreadView.swift */; }; @@ -51,6 +52,7 @@ CAAABA9A24D695E5004A4466 /* TargetAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TargetAction.swift; sourceTree = ""; }; CAB9086A22492D4C00711C3F /* Navigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigator.swift; sourceTree = ""; }; CAC2A6D62292E4BA000AA2A7 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; + CAC6C989261DB0B900EAF2BD /* WebViewResourceHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewResourceHandler.swift; sourceTree = ""; }; CACE84F72254BE5F00E19E8B /* PDFDocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFDocumentView.swift; sourceTree = ""; }; CACE84FA2254BFEE00E19E8B /* EditingAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingAction.swift; sourceTree = ""; }; CACE851E225CDE3300E19E8B /* EPUBFixedSpreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EPUBFixedSpreadView.swift; sourceTree = ""; }; @@ -118,6 +120,7 @@ CA479DC2226493570053445E /* UIView.swift */, CA479DC42264AEA20053445E /* UIColor.swift */, CAC2A6D62292E4BA000AA2A7 /* WebView.swift */, + CAC6C989261DB0B900EAF2BD /* WebViewResourceHandler.swift */, ); path = Toolkit; sourceTree = ""; @@ -313,6 +316,7 @@ CACE84F82254BE5F00E19E8B /* PDFDocumentView.swift in Sources */, CAC2A6D72292E4BA000AA2A7 /* WebView.swift in Sources */, CA1E4F4B240037E6009C4DE3 /* CompletionList.swift in Sources */, + CAC6C98A261DB0B900EAF2BD /* WebViewResourceHandler.swift in Sources */, CACE8521225CDFB000E19E8B /* EPUBReflowableSpreadView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/r2-navigator-swift/EPUB/EPUBReflowableSpreadView.swift b/r2-navigator-swift/EPUB/EPUBReflowableSpreadView.swift index 82e52468..56dd207e 100644 --- a/r2-navigator-swift/EPUB/EPUBReflowableSpreadView.swift +++ b/r2-navigator-swift/EPUB/EPUBReflowableSpreadView.swift @@ -52,7 +52,8 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { return } let link = spread.leading - guard let url = link.url(relativeTo: publication.baseURL) else { + guard let url = URL(string: "readium-pub://" + link.href) else { +// guard let url = link.url(relativeTo: publication.baseURL) else { log(.error, "Can't get URL for link \(link.href)") return } diff --git a/r2-navigator-swift/EPUB/EPUBSpreadView.swift b/r2-navigator-swift/EPUB/EPUBSpreadView.swift index bbe1e885..667c8e3a 100644 --- a/r2-navigator-swift/EPUB/EPUBSpreadView.swift +++ b/r2-navigator-swift/EPUB/EPUBSpreadView.swift @@ -64,7 +64,16 @@ class EPUBSpreadView: UIView, Loggable, PageView { private(set) var spreadLoaded = false - required init(publication: Publication, spread: EPUBSpread, resourcesURL: URL?, readingProgression: ReadingProgression, userSettings: UserSettings, animatedLoad: Bool = false, editingActions: EditingActionsController, contentInset: [UIUserInterfaceSizeClass: EPUBContentInsets]) { + required init( + publication: Publication, + spread: EPUBSpread, + resourcesURL: URL?, + readingProgression: ReadingProgression, + userSettings: UserSettings, + animatedLoad: Bool = false, + editingActions: EditingActionsController, + contentInset: [UIUserInterfaceSizeClass: EPUBContentInsets] + ) { self.publication = publication self.spread = spread self.resourcesURL = resourcesURL @@ -72,8 +81,16 @@ class EPUBSpreadView: UIView, Loggable, PageView { self.userSettings = userSettings self.editingActions = editingActions self.animatedLoad = animatedLoad - self.webView = WebView(editingActions: editingActions) self.contentInset = contentInset + if #available(iOS 11.0, *) { + let scheme = "readium-pub" + self.webView = WebView( + editingActions: editingActions, + schemeHandler: (scheme: scheme, handler: WebViewResourceHandler(scheme: scheme, publication: publication)) + ) + } else { + self.webView = WebView(editingActions: editingActions) + } super.init(frame: .zero) diff --git a/r2-navigator-swift/Toolkit/WebView.swift b/r2-navigator-swift/Toolkit/WebView.swift index 63c85c50..078d25ce 100644 --- a/r2-navigator-swift/Toolkit/WebView.swift +++ b/r2-navigator-swift/Toolkit/WebView.swift @@ -15,14 +15,21 @@ import WebKit /// A custom web view which: /// - Forwards copy: menu action to an EditingActionsController. final class WebView: WKWebView { - + private let editingActions: EditingActionsController - init(editingActions: EditingActionsController) { + init(editingActions: EditingActionsController, configuration: WKWebViewConfiguration = .init()) { self.editingActions = editingActions - super.init(frame: .zero, configuration: .init()) + super.init(frame: .zero, configuration: configuration) } - + + @available(iOS 11.0, *) + convenience init(editingActions: EditingActionsController, schemeHandler: (scheme: String, handler: WKURLSchemeHandler)) { + let configuration = WKWebViewConfiguration() + configuration.setURLSchemeHandler(schemeHandler.handler, forURLScheme: schemeHandler.scheme) + self.init(editingActions: editingActions, configuration: configuration) + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/r2-navigator-swift/Toolkit/WebViewResourceHandler.swift b/r2-navigator-swift/Toolkit/WebViewResourceHandler.swift new file mode 100644 index 00000000..b5967513 --- /dev/null +++ b/r2-navigator-swift/Toolkit/WebViewResourceHandler.swift @@ -0,0 +1,128 @@ +// +// Copyright 2021 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. +// + +import Foundation +import R2Shared +import WebKit + +@available(iOS 11.0, *) +final class WebViewResourceHandler: NSObject, WKURLSchemeHandler, Loggable { + + enum HandlerError: Error { + case noURLProvided + case unsupportedScheme(String?) + case taskNotStarted(WKURLSchemeTask) + } + + private let scheme: String + private let publication: Publication + private var tasks: [ObjectIdentifier: Task] = [:] + + init(scheme: String, publication: Publication) { + self.scheme = scheme + self.publication = publication + } + + func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + guard let url = urlSchemeTask.request.url else { + urlSchemeTask.didFailWithError(HandlerError.noURLProvided) + return + } + guard url.scheme == scheme else { + urlSchemeTask.didFailWithError(HandlerError.unsupportedScheme(url.scheme)) + return + } + + let href = url.absoluteString.removingPrefix(scheme + "://") + let resource = publication.get(href) + let task = Task(url: url, task: urlSchemeTask, resource: resource) + tasks[ObjectIdentifier(urlSchemeTask)] = task + task.start() + } + + func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { + guard let task = tasks[ObjectIdentifier(urlSchemeTask)] else { + urlSchemeTask.didFailWithError(HandlerError.taskNotStarted(urlSchemeTask)) + return + } + + task.cancel() + } + + private final class Task { + private let url: URL + private let resource: Resource + private var isCancelled = false + + /// Underlying WKURLSchemeTask. + /// Use `withTask()` to access it safely. + private let _task: WKURLSchemeTask + + init(url: URL, task: WKURLSchemeTask, resource: Resource) { + self.url = url + self.resource = resource + self._task = task + } + + func start() { + let request = _task.request + + DispatchQueue.global(qos: .userInitiated).async { + let href = self.resource.link.href + do { + log(.info, "Will serve \(href), headers: \(request.allHTTPHeaderFields ?? [:])") + let length = try self.resource.length.get() + let response = URLResponse( + url: self.url, + mimeType: self.resource.link.type, + expectedContentLength: Int(length), + textEncodingName: nil + ) + self.withTask { $0.didReceive(response) } + + var available = length + var offset: UInt64 = 0 + var data: Data = Data() + repeat { + let upperBound = offset + min(available, 32 * 1024) + data = try self.resource.read(range: offset.. 0 && !self.isCancelled + + self.withTask { $0.didFinish() } + + } catch { + log(.error, "Failed to serve \(href): \(error.localizedDescription)") + self.withTask { $0.didFailWithError(error) } + } + + self.resource.close() + } + } + + func cancel() { + assert(Thread.isMainThread) + self.isCancelled = true + } + + /// Any API call to WKURLSchemeTask will crash the app if the task is cancelled by a call to: + /// `webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask)`. + /// + /// To prevent this, we need to synchronize on the main thread every API calls to the task. + private func withTask(callback: (WKURLSchemeTask) -> Void) { + assert(!Thread.isMainThread) + DispatchQueue.main.sync { + if !self.isCancelled { + callback(self._task) + } + } + } + } +}