diff --git a/Package.swift b/Package.swift index 87950b8c..8a11bcdc 100644 --- a/Package.swift +++ b/Package.swift @@ -11,6 +11,7 @@ let package = Package( // MARK: - UIKit .library(name: "TIUIKitCore", targets: ["TIUIKitCore"]), .library(name: "TIUIElements", targets: ["TIUIElements"]), + .library(name: "TIWebView", targets: ["TIWebView"]), // MARK: - SwiftUI .library(name: "TISwiftUICore", targets: ["TISwiftUICore"]), @@ -55,6 +56,7 @@ let package = Package( // MARK: - UIKit .target(name: "TIUIKitCore", dependencies: ["TISwiftUtils"], path: "TIUIKitCore/Sources"), .target(name: "TIUIElements", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIUIElements/Sources"), + .target(name: "TIWebView", dependencies: ["TIUIKitCore", "TISwiftUtils", "TILogging"], path: "TIWebView/Sources"), // MARK: - SwiftUI .target(name: "TISwiftUICore", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TISwiftUICore/Sources"), diff --git a/TIWebView/Sources/ErrorHandler/BaseWebViewErrorHandler.swift b/TIWebView/Sources/ErrorHandler/BaseWebViewErrorHandler.swift new file mode 100644 index 00000000..1f6fa728 --- /dev/null +++ b/TIWebView/Sources/ErrorHandler/BaseWebViewErrorHandler.swift @@ -0,0 +1,36 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TILogging + +open class BaseWebViewErrorHandler: WebViewErrorHandlerProtocol { + + let logger: TILogger? + + public init(logger: TILogger? = nil) { + self.logger = logger + } + + open func didRecievedError(_ error: WebViewErrorModel) { + logger?.error("%@", "\(error)") + } +} diff --git a/TIWebView/Sources/ErrorHandler/WebViewErrorConstants.swift b/TIWebView/Sources/ErrorHandler/WebViewErrorConstants.swift new file mode 100644 index 00000000..86cdbc0e --- /dev/null +++ b/TIWebView/Sources/ErrorHandler/WebViewErrorConstants.swift @@ -0,0 +1,31 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public enum WebViewErrorConstants { + static var errorMessageName: String { + "error" + } + + static var errorPropertyName: String { + "message" + } +} diff --git a/TIWebView/Sources/ErrorHandler/WebViewErrorHandlerProtocol.swift b/TIWebView/Sources/ErrorHandler/WebViewErrorHandlerProtocol.swift new file mode 100644 index 00000000..3d8c0770 --- /dev/null +++ b/TIWebView/Sources/ErrorHandler/WebViewErrorHandlerProtocol.swift @@ -0,0 +1,25 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public protocol WebViewErrorHandlerProtocol { + func didRecievedError(_ error: WebViewErrorModel) +} diff --git a/TIWebView/Sources/ErrorHandler/WebViewErrorModel.swift b/TIWebView/Sources/ErrorHandler/WebViewErrorModel.swift new file mode 100644 index 00000000..9e1a82a9 --- /dev/null +++ b/TIWebView/Sources/ErrorHandler/WebViewErrorModel.swift @@ -0,0 +1,38 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public struct WebViewErrorModel { + public let url: URL? + public let error: Error? + public let jsErrorMessage: String? + + public init(url: URL? = nil, + error: Error? = nil, + jsErrorMessage: String? = nil) { + + self.url = url + self.error = error + self.jsErrorMessage = jsErrorMessage + } +} diff --git a/TIWebView/Sources/NavigationHandler/BaseWebViewNavigator.swift b/TIWebView/Sources/NavigationHandler/BaseWebViewNavigator.swift new file mode 100644 index 00000000..08b46016 --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/BaseWebViewNavigator.swift @@ -0,0 +1,60 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import enum WebKit.WKNavigationActionPolicy + +open class BaseWebViewNavigator: WebViewNavigatorProtocol { + + public typealias WebViewNavigationMap = [WebViewUrlComparator: NavigationResult] + + public let navigationMap: WebViewNavigationMap + + public init(navigationMap: WebViewNavigationMap) { + self.navigationMap = navigationMap + } + + public convenience init() { + self.init(navigationMap: [:]) + } + + open func shouldNavigate(toUrl url: URL) -> WKNavigationActionPolicy { + guard !navigationMap.isEmpty else { + return .cancel + } + + let decision = navigationMap.first { (comparator, _) in + url.compare(by: comparator) + }?.value + + switch decision { + case let .closure(closure): + return closure(url) + + case let .simpleResult(result): + return result + + case .none: + return .cancel + } + } +} diff --git a/TIWebView/Sources/NavigationHandler/NavigationResult.swift b/TIWebView/Sources/NavigationHandler/NavigationResult.swift new file mode 100644 index 00000000..7f8c07ab --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/NavigationResult.swift @@ -0,0 +1,30 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import TISwiftUtils +import enum WebKit.WKNavigationActionPolicy + +public enum NavigationResult { + case closure(Closure) + case simpleResult(WKNavigationActionPolicy) +} diff --git a/TIWebView/Sources/NavigationHandler/WebViewNavigationDelegate.swift b/TIWebView/Sources/NavigationHandler/WebViewNavigationDelegate.swift new file mode 100644 index 00000000..d69fb222 --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/WebViewNavigationDelegate.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import WebKit + +public protocol WebViewNavigationDelegate { + func navigationProgress(_ progress: Double, isLoading: Bool) + func didCommit(_ navigation: WKNavigation, forWebView webView: WKWebView) + func didFinish(_ navigation: WKNavigation!, forWebView webView: WKWebView) + func didFailProvisionalNavigation(_ navigation: WKNavigation!, withError error: Error, forWebView webView: WKWebView) + func didFail(_ navigation: WKNavigation!, withError error: Error, forWebView webView: WKWebView) +} + +public extension WebViewNavigationDelegate { + func navigationProgress(_ progress: Double, isLoading: Bool) { + // empty implementation + } + + func didCommit(_ navigation: WKNavigation, forWebView webView: WKWebView) { + // empty implementation + } + + func didFinish(_ navigation: WKNavigation!, forWebView webView: WKWebView) { + // empty implementation + } + + func didFail(_ navigation: WKNavigation!, withError error: Error, forWebView webView: WKWebView) { + // empty implementation + } +} diff --git a/TIWebView/Sources/NavigationHandler/WebViewNavigatorProtocol.swift b/TIWebView/Sources/NavigationHandler/WebViewNavigatorProtocol.swift new file mode 100644 index 00000000..e6a55a30 --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/WebViewNavigatorProtocol.swift @@ -0,0 +1,28 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import enum WebKit.WKNavigationActionPolicy + +public protocol WebViewNavigatorProtocol { + func shouldNavigate(toUrl url: URL) -> WKNavigationActionPolicy +} diff --git a/TIWebView/Sources/URLInjector/BaseWebViewUrlInjector.swift b/TIWebView/Sources/URLInjector/BaseWebViewUrlInjector.swift new file mode 100644 index 00000000..14b89331 --- /dev/null +++ b/TIWebView/Sources/URLInjector/BaseWebViewUrlInjector.swift @@ -0,0 +1,92 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import class WebKit.WKWebView + +open class BaseWebViewUrlInjector: WebViewUrlInjectorProtocol { + + public typealias URLInjection = [WebViewUrlComparator: [WebViewUrlInjection]] + + public var injection: URLInjection + + public init(injection: URLInjection) { + self.injection = injection + } + + public convenience init() { + self.init(injection: [:]) + } + + open func inject(onWebView webView: WKWebView) { + guard !injection.isEmpty, let url = webView.url else { + return + } + + injection.forEach { (comparator, injections) in + guard url.compare(by: comparator) else { + return + } + + injections.forEach { evaluteInjection(onWebView: webView, injection: $0) } + } + } + + private func evaluteInjection(onWebView webView: WKWebView, injection: WebViewUrlInjection) { + let jsScript = makeJsScript(fromInjection: injection) + + guard !jsScript.isEmpty else { + return + } + + webView.evaluateJavaScript(jsScript, completionHandler: nil) + } + + private func makeJsScript(fromInjection injection: WebViewUrlInjection) -> String { + switch injection { + case let .css(css): + return cssJsScript(css: css) + + case let .cssForFile(file): + guard let path = Bundle.main.path(forResource: file, ofType: "css") else { + return "" + } + + let css = try? String(contentsOfFile: path) + .components(separatedBy: .newlines) + .joined() + + return cssJsScript(css: css ?? "") + + case let .javaScript(script): + return script + } + } + + private func cssJsScript(css: String) -> String { + """ + var style = document.createElement('style'); + style.innerHTML = '\(css)'; + document.head.appendChild(style); + """ + } +} diff --git a/TIWebView/Sources/URLInjector/Helpers/URL+Comparator.swift b/TIWebView/Sources/URLInjector/Helpers/URL+Comparator.swift new file mode 100644 index 00000000..1a0e9182 --- /dev/null +++ b/TIWebView/Sources/URLInjector/Helpers/URL+Comparator.swift @@ -0,0 +1,50 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public extension URL { + func compare(by comparator: WebViewUrlComparator) -> Bool { + switch comparator { + case .any: + return true + + case let .absolutePath(path): + return absoluteString == path + + case let .host(host): + return self.host == host + + case let .query(query): + return (self.query ?? "").contains(query) + + case let .regex(stringRegex): + guard let regex = try? NSRegularExpression(pattern: stringRegex) else { + return false + } + + let range = NSRange(location: 0, length: absoluteString.utf16.count) + + return regex.firstMatch(in: absoluteString, range: range) != nil + } + } +} diff --git a/TIWebView/Sources/URLInjector/WebViewUrlComparator.swift b/TIWebView/Sources/URLInjector/WebViewUrlComparator.swift new file mode 100644 index 00000000..cdfa55d2 --- /dev/null +++ b/TIWebView/Sources/URLInjector/WebViewUrlComparator.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public enum WebViewUrlComparator: Hashable { + case any + case absolutePath(String) + case host(String) + case query(String) + case regex(String) +} diff --git a/TIWebView/Sources/URLInjector/WebViewUrlInjection.swift b/TIWebView/Sources/URLInjector/WebViewUrlInjection.swift new file mode 100644 index 00000000..9d2ae94a --- /dev/null +++ b/TIWebView/Sources/URLInjector/WebViewUrlInjection.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public enum WebViewUrlInjection { + case css(String) + case cssForFile(String) + case javaScript(String) +} diff --git a/TIWebView/Sources/URLInjector/WebViewUrlInjectorProtocol.swift b/TIWebView/Sources/URLInjector/WebViewUrlInjectorProtocol.swift new file mode 100644 index 00000000..e9b20ad3 --- /dev/null +++ b/TIWebView/Sources/URLInjector/WebViewUrlInjectorProtocol.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import class WebKit.WKWebView + +public protocol WebViewUrlInjectorProtocol { + func inject(onWebView webView: WKWebView) +} diff --git a/TIWebView/Sources/Views/BaseInitializableWebView.swift b/TIWebView/Sources/Views/BaseInitializableWebView.swift new file mode 100644 index 00000000..ec85ce2b --- /dev/null +++ b/TIWebView/Sources/Views/BaseInitializableWebView.swift @@ -0,0 +1,132 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIUIKitCore +import WebKit + +open class BaseInitializableWebView: WKWebView, + InitializableViewProtocol, + ConfigurableView, + WKNavigationDelegate { + + public var viewModel: WebViewModelProtocol? + public var delegate: WebViewNavigationDelegate? + + // MARK: - Init + + public init() { + super.init(frame: .zero, configuration: .init()) + + initializeView() + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - InitializableViewProtocol + + public func addViews() { + // override in subviews + } + + public func configureLayout() { + // override in subviews + } + + public func bindViews() { + navigationDelegate = self + configuration.preferences.javaScriptEnabled = true + + addObserver(self, + forKeyPath: #keyPath(WKWebView.estimatedProgress), + options: .new, + context: nil) + } + + public func configureAppearance() { + // override in subviews + } + + public func localize() { + // override in subviews + } + + // MARK: - Overrided methods + + open override func observeValue(forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey : Any]?, + context: UnsafeMutableRawPointer?) { + + if keyPath == #keyPath(WKWebView.estimatedProgress) { + delegate?.navigationProgress(estimatedProgress, isLoading: isLoading) + } + } + + // MARK: - ConfigurableView + + open func configure(with viewModel: WebViewModelProtocol) { + self.viewModel = viewModel + + configuration.userContentController.add(viewModel, name: WebViewErrorConstants.errorMessageName) + } + + // MARK: - WKNavigationDelegate + + open func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + + guard let url = navigationAction.request.url else { + return decisionHandler(.cancel) + } + + let decision = viewModel?.shouldNavigate(toUrl: url) ?? .cancel + decisionHandler(decision) + } + + open func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + delegate?.didCommit(navigation, forWebView: webView) + } + + open func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + delegate?.didFinish(navigation, forWebView: webView) + viewModel?.makeUrlInjection(forWebView: webView) + } + + open func webView(_ webView: WKWebView, + didFailProvisionalNavigation navigation: WKNavigation!, + withError error: Error) { + viewModel?.handleError(error, url: webView.url) + delegate?.didFailProvisionalNavigation(navigation, withError: error, forWebView: webView) + } + + open func webView(_ webView: WKWebView, + didFail navigation: WKNavigation!, + withError error: Error) { + + viewModel?.handleError(error, url: webView.url) + delegate?.didFail(navigation, withError: error, forWebView: webView) + } +} diff --git a/TIWebView/Sources/Views/ViewModels/BaseWebViewModel.swift b/TIWebView/Sources/Views/ViewModels/BaseWebViewModel.swift new file mode 100644 index 00000000..9686c61c --- /dev/null +++ b/TIWebView/Sources/Views/ViewModels/BaseWebViewModel.swift @@ -0,0 +1,77 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import WebKit + +open class BaseWebViewModel: NSObject, WebViewModelProtocol { + + public var injector: WebViewUrlInjectorProtocol + public var navigator: WebViewNavigatorProtocol + public var errorHandler: WebViewErrorHandlerProtocol + + // MARK: - Init + + public init(injector: WebViewUrlInjectorProtocol = BaseWebViewUrlInjector(), + navigator: WebViewNavigatorProtocol = BaseWebViewNavigator(), + errorHandler: WebViewErrorHandlerProtocol = BaseWebViewErrorHandler()) { + + self.injector = injector + self.navigator = navigator + self.errorHandler = errorHandler + + super.init() + } + + // MARK: - Open methods + + open func makeUrlInjection(forWebView webView: WKWebView) { + injector.inject(onWebView: webView) + } + + open func shouldNavigate(toUrl url: URL) -> WKNavigationActionPolicy { + navigator.shouldNavigate(toUrl: url) + } + + open func handleError(_ error: Error, url: URL?) { + errorHandler.didRecievedError(.init(url: url, error: error)) + } + + // MARK: - WKScriptMessageHandler + + open func userContentController(_ userContentController: WKUserContentController, + didReceive message: WKScriptMessage) { + + if message.name == WebViewErrorConstants.errorMessageName, + let error = parseError(message){ + let url = message.webView?.url + errorHandler.didRecievedError(.init(url: url, jsErrorMessage: error)) + } + } + + // MARK: - Private methods + + private func parseError(_ message: WKScriptMessage) -> String? { + let body = message.body as? [String: Any] + let error = body?[WebViewErrorConstants.errorPropertyName] as? String + return error + } +} diff --git a/TIWebView/Sources/Views/ViewModels/WebViewModelProtocol.swift b/TIWebView/Sources/Views/ViewModels/WebViewModelProtocol.swift new file mode 100644 index 00000000..6c2efc23 --- /dev/null +++ b/TIWebView/Sources/Views/ViewModels/WebViewModelProtocol.swift @@ -0,0 +1,32 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import WebKit + +public protocol WebViewModelProtocol: WKScriptMessageHandler { + var injector: WebViewUrlInjectorProtocol { get } + var navigator: WebViewNavigatorProtocol { get } + + func makeUrlInjection(forWebView webView: WKWebView) + func shouldNavigate(toUrl url: URL) -> WKNavigationActionPolicy + func handleError(_ error: Error, url: URL?) +} diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec new file mode 100644 index 00000000..dea77855 --- /dev/null +++ b/TIWebView/TIWebView.podspec @@ -0,0 +1,20 @@ +Pod::Spec.new do |s| + s.name = 'TIWebView' + s.version = '1.30.0' + s.summary = 'Universal web view API' + s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru', + 'castlele' => 'nikita.semenov@touchin.ru' } + s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + + s.ios.deployment_target = '13.0' + s.swift_versions = ['5.3'] + + s.source_files = s.name + '/Sources/**/*' + + s.dependency 'TIUIKitCore', s.version.to_s + s.dependency 'TISwiftUtils', s.version.to_s + s.dependency 'TILogging', s.version.to_s + +end