diff --git a/TIWebView/Sources/ErrorHandler/BaseWebViewErrorHandler.swift b/TIWebView/Sources/ErrorHandler/BaseWebViewErrorHandler.swift index 4e059eaa..696b860d 100644 --- a/TIWebView/Sources/ErrorHandler/BaseWebViewErrorHandler.swift +++ b/TIWebView/Sources/ErrorHandler/BaseWebViewErrorHandler.swift @@ -24,13 +24,11 @@ import TILogging open class BaseWebViewErrorHandler { - public var logger: TILogger? + public init() { - public init(logger: TILogger? = nil) { - self.logger = logger } - open func didRecievedError(_ error: WebViewErrorModel) { - logger?.error("%@", "\(error)") + open func didRecievedError(_ error: WebViewError) { + // override in subviews } } diff --git a/TIWebView/Sources/ErrorHandler/WebViewErrorModel.swift b/TIWebView/Sources/ErrorHandler/WebViewErrorModel.swift index 885d5af9..9de11b09 100644 --- a/TIWebView/Sources/ErrorHandler/WebViewErrorModel.swift +++ b/TIWebView/Sources/ErrorHandler/WebViewErrorModel.swift @@ -22,17 +22,7 @@ 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 - } +public enum WebViewError: Error { + case standardError(URL?, Error) + case jsError(URL?, String) } diff --git a/TIWebView/Sources/NavigationHandler/BaseWebViewNavigator.swift b/TIWebView/Sources/NavigationHandler/BaseWebViewNavigator.swift index 7482e0b3..154763b0 100644 --- a/TIWebView/Sources/NavigationHandler/BaseWebViewNavigator.swift +++ b/TIWebView/Sources/NavigationHandler/BaseWebViewNavigator.swift @@ -25,16 +25,14 @@ import enum WebKit.WKNavigationActionPolicy open class BaseWebViewNavigator { - public typealias WebViewNavigationMap = [WebViewUrlComparator: NavigationResult] + public let navigationMap: [NavigationPolicy] - public let navigationMap: WebViewNavigationMap - - public init(navigationMap: WebViewNavigationMap) { + public init(navigationMap: [NavigationPolicy]) { self.navigationMap = navigationMap } public convenience init() { - self.init(navigationMap: [:]) + self.init(navigationMap: []) } open func shouldNavigate(toUrl url: URL) -> WKNavigationActionPolicy { @@ -42,19 +40,7 @@ open class BaseWebViewNavigator { 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 - } + let allowPolicy = navigationMap.filter { $0.policy(for: url) == .allow } + return allowPolicy.isEmpty ? .cancel : .allow } } diff --git a/TIWebView/Sources/NavigationHandler/Helpers/URL+Validation.swift b/TIWebView/Sources/NavigationHandler/Helpers/URL+Validation.swift new file mode 100644 index 00000000..844160ce --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/Helpers/URL+Validation.swift @@ -0,0 +1,44 @@ +// +// Copyright (c) 2022 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 + +extension URL { + func validate(with regex: NSRegularExpression) -> Bool { + let range = NSRange(location: 0, length: absoluteString.utf16.count) + + return regex.firstMatch(in: absoluteString, range: range) != nil + } + + func validate(by component: URLComponent) -> Bool { + switch component { + case let .host(host): + return self.host == host + + case let .absolutePath(path): + return absoluteString == path + + case let .query(query): + return (self.query ?? "").contains(query) + } + } +} diff --git a/TIWebView/Sources/NavigationHandler/Helpers/URLComponent.swift b/TIWebView/Sources/NavigationHandler/Helpers/URLComponent.swift new file mode 100644 index 00000000..df504403 --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/Helpers/URLComponent.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2022 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 URLComponent { + case host(String) + case absolutePath(String) + case query(String) +} diff --git a/TIWebView/Sources/NavigationHandler/NavigationPolicy/AnyNavigationPolicy.swift b/TIWebView/Sources/NavigationHandler/NavigationPolicy/AnyNavigationPolicy.swift new file mode 100644 index 00000000..4d57c7f8 --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/NavigationPolicy/AnyNavigationPolicy.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) 2022 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 AnyNavigationPolicy: NavigationPolicy { + + public init() { + + } + + open func policy(for url: URL) -> WKNavigationActionPolicy { + .allow + } +} diff --git a/TIWebView/Sources/NavigationHandler/NavigationResult.swift b/TIWebView/Sources/NavigationHandler/NavigationPolicy/NavigationPolicy.swift similarity index 88% rename from TIWebView/Sources/NavigationHandler/NavigationResult.swift rename to TIWebView/Sources/NavigationHandler/NavigationPolicy/NavigationPolicy.swift index a973ecc9..57b013d4 100644 --- a/TIWebView/Sources/NavigationHandler/NavigationResult.swift +++ b/TIWebView/Sources/NavigationHandler/NavigationPolicy/NavigationPolicy.swift @@ -21,10 +21,8 @@ // import Foundation -import TISwiftUtils import enum WebKit.WKNavigationActionPolicy -public enum NavigationResult { - case closure(Closure) - case simpleResult(WKNavigationActionPolicy) +public protocol NavigationPolicy { + func policy(for url: URL) -> WKNavigationActionPolicy } diff --git a/TIWebView/Sources/NavigationHandler/NavigationPolicy/RegexNavigationPolicy.swift b/TIWebView/Sources/NavigationHandler/NavigationPolicy/RegexNavigationPolicy.swift new file mode 100644 index 00000000..4c51c362 --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/NavigationPolicy/RegexNavigationPolicy.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) 2022 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 RegexNavigationPolicy: AnyNavigationPolicy { + + public var regex: NSRegularExpression + + // MARK: - Init + + public init(regex: NSRegularExpression) { + self.regex = regex + + super.init() + } + + public convenience init?(stringRegex: String) { + guard let regex = try? NSRegularExpression(pattern: stringRegex) else { + return nil + } + + self.init(regex: regex) + } + + // MARK: - NavigationPolicy + + open override func policy(for url: URL) -> WKNavigationActionPolicy { + url.validate(with: regex) ? .allow : .cancel + } +} diff --git a/TIWebView/Sources/NavigationHandler/NavigationPolicy/URLComponentsNavigationPolicy.swift b/TIWebView/Sources/NavigationHandler/NavigationPolicy/URLComponentsNavigationPolicy.swift new file mode 100644 index 00000000..4a207ff4 --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/NavigationPolicy/URLComponentsNavigationPolicy.swift @@ -0,0 +1,44 @@ +// +// Copyright (c) 2022 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 + +/// Compares URL with combination of URL components. +open class URLComponentsNavigationPolicy: AnyNavigationPolicy { + + public var components: [URLComponent] + + // MARK: - Init + + public init(components: [URLComponent]) { + self.components = components + + super.init() + } + + // MARK: - NavigationPolicy + + open override func policy(for url: URL) -> WKNavigationActionPolicy { + components.allSatisfy { url.validate(by: $0) } ? .allow : .cancel + } +} diff --git a/TIWebView/Sources/StateDelegate/BaseWebViewStateHandler.swift b/TIWebView/Sources/StateDelegate/BaseWebViewStateHandler.swift index 8ea7cd03..2c661817 100644 --- a/TIWebView/Sources/StateDelegate/BaseWebViewStateHandler.swift +++ b/TIWebView/Sources/StateDelegate/BaseWebViewStateHandler.swift @@ -24,16 +24,10 @@ import WebKit open class BaseWebViewStateHandler: NSObject, WebViewStateHandler { - public weak var viewModel: WebViewModelProtocol? + public weak var viewModel: WebViewModel? // MARK: - WebViewStateHandler - open func navigationProgress(_ progress: Double, isLoading: Bool) { - // Override in subclass - } - - // MARK: - WKNavigationDelegate - open func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { @@ -47,6 +41,7 @@ open class BaseWebViewStateHandler: NSObject, WebViewStateHandler { } open func webView(_ webView: WKWebView, didCommit navigation: WKNavigation?) { + // override in subviews } open func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { @@ -56,6 +51,7 @@ open class BaseWebViewStateHandler: NSObject, WebViewStateHandler { open func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation?, withError error: Error) { + viewModel?.handleError(error, url: webView.url) } diff --git a/TIWebView/Sources/StateDelegate/WebViewStateHandler.swift b/TIWebView/Sources/StateDelegate/WebViewStateHandler.swift index 7dfc0423..6d084f15 100644 --- a/TIWebView/Sources/StateDelegate/WebViewStateHandler.swift +++ b/TIWebView/Sources/StateDelegate/WebViewStateHandler.swift @@ -23,7 +23,5 @@ import protocol WebKit.WKNavigationDelegate public protocol WebViewStateHandler: WKNavigationDelegate { - var viewModel: WebViewModelProtocol? { get set } - - func navigationProgress(_ progress: Double, isLoading: Bool) + var viewModel: WebViewModel? { get set } } diff --git a/TIWebView/Sources/URLInjector/Helpers/URL+Comparator.swift b/TIWebView/Sources/URLInjector/Helpers/URL+Comparator.swift index ed562896..e0968ca9 100644 --- a/TIWebView/Sources/URLInjector/Helpers/URL+Comparator.swift +++ b/TIWebView/Sources/URLInjector/Helpers/URL+Comparator.swift @@ -29,22 +29,19 @@ public extension URL { return true case let .absolutePath(path): - return absoluteString == path + return compare(by: .absolutePath(path)) case let .host(host): - return self.host == host + return compare(by: .host(host)) case let .query(query): - return (self.query ?? "").contains(query) + return compare(by: .query(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 + return validate(with: regex) } } } diff --git a/TIWebView/Sources/Views/BaseInitializableWebView.swift b/TIWebView/Sources/Views/BaseInitializableWebView.swift index 3907a332..e73ebdd8 100644 --- a/TIWebView/Sources/Views/BaseInitializableWebView.swift +++ b/TIWebView/Sources/Views/BaseInitializableWebView.swift @@ -20,6 +20,7 @@ // THE SOFTWARE. // +import TISwiftUtils import TIUIKitCore import WebKit @@ -27,23 +28,20 @@ open class BaseInitializableWebView: WKWebView, InitializableViewProtocol, ConfigurableView { - public var viewModel: WebViewModelProtocol? { + public var stateHandler: WebViewStateHandler + public var viewModel: WebViewModel? { didSet { - stateHandler?.viewModel = viewModel - } - } - public var stateHandler: WebViewStateHandler? { - didSet { - navigationDelegate = stateHandler + stateHandler.viewModel = viewModel } } // MARK: - Init - public init(stateHandler: WebViewStateHandler? = BaseWebViewStateHandler()) { + public init(stateHandler: WebViewStateHandler = BaseWebViewStateHandler()) { + self.stateHandler = stateHandler + super.init(frame: .zero, configuration: .init()) - self.stateHandler = stateHandler initializeView() } @@ -63,10 +61,7 @@ open class BaseInitializableWebView: WKWebView, } public func bindViews() { - addObserver(self, - forKeyPath: #keyPath(WKWebView.estimatedProgress), - options: .new, - context: nil) + navigationDelegate = stateHandler } public func configureAppearance() { @@ -77,23 +72,21 @@ open class BaseInitializableWebView: WKWebView, // 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) { - stateHandler?.navigationProgress(estimatedProgress, isLoading: isLoading) - } - } - // MARK: - ConfigurableView - open func configure(with viewModel: WebViewModelProtocol) { + open func configure(with viewModel: WebViewModel) { self.viewModel = viewModel configuration.userContentController.add(viewModel, name: WebViewErrorConstants.errorMessageName) } + + // MARK: - Public methods + + public func subscribe(onProgress: ParameterClosure? = nil) -> NSKeyValueObservation { + observe(\.estimatedProgress, options: [.new]) { webView, change in + if webView.isLoading, let newValue = change.newValue { + onProgress?(newValue) + } + } + } } diff --git a/TIWebView/Sources/Views/ViewModels/BaseWebViewModel.swift b/TIWebView/Sources/Views/ViewModels/DefaultWebViewModel.swift similarity index 81% rename from TIWebView/Sources/Views/ViewModels/BaseWebViewModel.swift rename to TIWebView/Sources/Views/ViewModels/DefaultWebViewModel.swift index 3ad6cbed..4d62c64a 100644 --- a/TIWebView/Sources/Views/ViewModels/BaseWebViewModel.swift +++ b/TIWebView/Sources/Views/ViewModels/DefaultWebViewModel.swift @@ -22,7 +22,7 @@ import WebKit -open class BaseWebViewModel: NSObject, WebViewModelProtocol { +open class DefaultWebViewModel: NSObject, WebViewModel { public var injector: BaseWebViewUrlInjector public var navigator: BaseWebViewNavigator @@ -41,20 +41,6 @@ open class BaseWebViewModel: NSObject, WebViewModelProtocol { super.init() } - // MARK: - Open methods - - open func makeUrlInjection(forWebView webView: WKWebView) { - injector.inject(on: 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, @@ -68,9 +54,9 @@ open class BaseWebViewModel: NSObject, WebViewModelProtocol { // MARK: - Private methods - private func parseError(_ message: WKScriptMessage) -> WebViewErrorModel { + private func parseError(_ message: WKScriptMessage) -> WebViewError { let body = message.body as? [String: Any] let error = body?[WebViewErrorConstants.errorPropertyName] as? String - return .init(jsErrorMessage: error) + return .jsError(message.webView?.url, error ?? "") } } diff --git a/TIWebView/Sources/Views/ViewModels/WebViewModelProtocol.swift b/TIWebView/Sources/Views/ViewModels/WebViewModel.swift similarity index 77% rename from TIWebView/Sources/Views/ViewModels/WebViewModelProtocol.swift rename to TIWebView/Sources/Views/ViewModels/WebViewModel.swift index 1542a233..5250bc69 100644 --- a/TIWebView/Sources/Views/ViewModels/WebViewModelProtocol.swift +++ b/TIWebView/Sources/Views/ViewModels/WebViewModel.swift @@ -22,7 +22,7 @@ import WebKit -public protocol WebViewModelProtocol: WKScriptMessageHandler { +public protocol WebViewModel: WKScriptMessageHandler { var injector: BaseWebViewUrlInjector { get } var navigator: BaseWebViewNavigator { get } var errorHandler: BaseWebViewErrorHandler { get } @@ -31,3 +31,18 @@ public protocol WebViewModelProtocol: WKScriptMessageHandler { func shouldNavigate(toUrl url: URL) -> WKNavigationActionPolicy func handleError(_ error: Error, url: URL?) } + +public extension WebViewModel { + + func makeUrlInjection(forWebView webView: WKWebView) { + injector.inject(on: webView) + } + + func shouldNavigate(toUrl url: URL) -> WKNavigationActionPolicy { + navigator.shouldNavigate(toUrl: url) + } + + func handleError(_ error: Error, url: URL?) { + errorHandler.didRecievedError(.standardError(url, error)) + } +} diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec index dec497e8..aca8bb71 100644 --- a/TIWebView/TIWebView.podspec +++ b/TIWebView/TIWebView.podspec @@ -15,6 +15,5 @@ Pod::Spec.new do |s| s.dependency 'TIUIKitCore', s.version.to_s s.dependency 'TISwiftUtils', s.version.to_s - s.dependency 'TILogging', s.version.to_s end