fix: code review notes

This commit is contained in:
Nikita Semenov 2022-12-30 17:15:46 +07:00
parent b98678b235
commit c75ff4c1d0
20 changed files with 290 additions and 116 deletions

View File

@ -56,7 +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"),
.target(name: "TIWebView", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIWebView/Sources"),
// MARK: - SwiftUI
.target(name: "TISwiftUICore", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TISwiftUICore/Sources"),

View File

@ -20,15 +20,9 @@
// THE SOFTWARE.
//
import TILogging
open class BaseWebViewErrorHandler {
open class BaseWebViewErrorHandler: WebViewErrorHandler {
public init() {
}
open func didRecievedError(_ error: WebViewError) {
// override in subviews
}
}

View File

@ -22,7 +22,6 @@
import Foundation
public enum WebViewError: Error {
case standardError(URL?, Error)
case jsError(URL?, String)
public protocol WebViewError: Error {
var contentURL: URL? { get }
}

View File

@ -0,0 +1,41 @@
//
// 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
public struct WebViewJSError: WebViewError, Codable {
public let contentURL: URL?
public let name: String?
public let message: String?
public let stackTrace: String?
public init(contentURL: URL?,
name: String?,
message: String?,
stackTrace: String?) {
self.contentURL = contentURL
self.name = name
self.message = message
self.stackTrace = stackTrace
}
}

View File

@ -0,0 +1,33 @@
//
// 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
public struct WebViewLoadingError: WebViewError {
public let contentURL: URL?
public let innerError: Error
public init(contentURL: URL?, innerError: Error) {
self.contentURL = contentURL
self.innerError = innerError
}
}

View File

@ -25,7 +25,19 @@ public enum WebViewErrorConstants {
"error"
}
static var errorPropertyName: String {
static var errorMessage: String {
"message"
}
static var errorName: String {
"name"
}
static var errorUrl: String {
"url"
}
static var errorStack: String {
"stack"
}
}

View File

@ -0,0 +1,31 @@
//
// 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 protocol WebViewErrorHandler {
func didRecievedError(_ error: WebViewError)
}
public extension WebViewErrorHandler {
func didRecievedError(_ error: WebViewError) {
// override in subclasses
}
}

View File

@ -20,12 +20,9 @@
// THE SOFTWARE.
//
import Foundation
import enum WebKit.WKNavigationActionPolicy
open class BaseWebViewNavigator: WebViewNavigator {
open class BaseWebViewNavigator {
public let navigationMap: [NavigationPolicy]
public var navigationMap: [NavigationPolicy]
public init(navigationMap: [NavigationPolicy]) {
self.navigationMap = navigationMap
@ -34,13 +31,4 @@ open class BaseWebViewNavigator {
public convenience init() {
self.init(navigationMap: [])
}
open func shouldNavigate(toUrl url: URL) -> WKNavigationActionPolicy {
guard !navigationMap.isEmpty else {
return .cancel
}
let allowPolicy = navigationMap.filter { $0.policy(for: url) == .allow }
return allowPolicy.isEmpty ? .cancel : .allow
}
}

View File

@ -23,9 +23,8 @@
import Foundation
extension URL {
func validate(with regex: NSRegularExpression) -> Bool {
func matches(_ regex: NSRegularExpression) -> Bool {
let range = NSRange(location: 0, length: absoluteString.utf16.count)
return regex.firstMatch(in: absoluteString, range: range) != nil
}
@ -38,7 +37,10 @@ extension URL {
return absoluteString == path
case let .query(query):
return (self.query ?? "").contains(query)
if let urlQuery = self.query {
return urlQuery.contains(query)
}
return false
}
}
}

View File

@ -23,7 +23,7 @@
import Foundation
import enum WebKit.WKNavigationActionPolicy
open class AnyNavigationPolicy: NavigationPolicy {
open class AlwaysAllowNavigationPolicy: NavigationPolicy {
public init() {

View File

@ -23,7 +23,7 @@
import Foundation
import enum WebKit.WKNavigationActionPolicy
open class RegexNavigationPolicy: AnyNavigationPolicy {
open class RegexNavigationPolicy: AlwaysAllowNavigationPolicy {
public var regex: NSRegularExpression
@ -46,6 +46,6 @@ open class RegexNavigationPolicy: AnyNavigationPolicy {
// MARK: - NavigationPolicy
open override func policy(for url: URL) -> WKNavigationActionPolicy {
url.validate(with: regex) ? .allow : .cancel
url.matches(regex) ? .allow : .cancel
}
}

View File

@ -24,7 +24,7 @@ import Foundation
import enum WebKit.WKNavigationActionPolicy
/// Compares URL with combination of URL components.
open class URLComponentsNavigationPolicy: AnyNavigationPolicy {
open class URLComponentsNavigationPolicy: AlwaysAllowNavigationPolicy {
public var components: [URLComponent]

View File

@ -0,0 +1,41 @@
//
// 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
public protocol WebViewNavigator {
var navigationMap: [NavigationPolicy] { get set }
func shouldNavigate(to url: URL) -> WKNavigationActionPolicy
}
public extension WebViewNavigator {
func shouldNavigate(to url: URL) -> WKNavigationActionPolicy {
guard !navigationMap.isEmpty else {
return .cancel
}
let allowPolicy = navigationMap.filter { $0.policy(for: url) == .allow }
return allowPolicy.isEmpty ? .cancel : .allow
}
}

View File

@ -36,16 +36,16 @@ open class BaseWebViewStateHandler: NSObject, WebViewStateHandler {
return decisionHandler(.cancel)
}
let decision = viewModel?.shouldNavigate(toUrl: url) ?? .cancel
let decision = viewModel?.shouldNavigate(to: url) ?? .cancel
decisionHandler(decision)
}
open func webView(_ webView: WKWebView, didCommit navigation: WKNavigation?) {
// override in subviews
// override in subclasses
}
open func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) {
viewModel?.makeUrlInjection(forWebView: webView)
viewModel?.makeUrlInjection(into: webView)
}
open func webView(_ webView: WKWebView,

View File

@ -23,9 +23,7 @@
import Foundation
import class WebKit.WKWebView
open class BaseWebViewUrlInjector {
public typealias URLInjection = [WebViewUrlComparator: [WebViewUrlInjection]]
open class BaseWebViewUrlInjector: WebViewUrlInjector {
public var injection: URLInjection
@ -36,57 +34,4 @@ open class BaseWebViewUrlInjector {
public convenience init() {
self.init(injection: [:])
}
// MARK: - Open methods
open func inject(on 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) }
}
}
// MARK: - Private methods
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(url):
let css = try? String(contentsOf: url)
.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);
"""
}
}

View File

@ -37,11 +37,8 @@ public extension URL {
case let .query(query):
return compare(by: .query(query))
case let .regex(stringRegex):
guard let regex = try? NSRegularExpression(pattern: stringRegex) else {
return false
}
return validate(with: regex)
case let .regex(nsRegex):
return matches(nsRegex)
}
}
}

View File

@ -20,10 +20,12 @@
// THE SOFTWARE.
//
import Foundation
public enum WebViewUrlComparator: Hashable {
case any
case absolutePath(String)
case host(String)
case query(String)
case regex(String)
case regex(NSRegularExpression)
}

View File

@ -0,0 +1,86 @@
//
// 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 class WebKit.WKWebView
public typealias URLInjection = [WebViewUrlComparator: [WebViewUrlInjection]]
public protocol WebViewUrlInjector {
var injection: URLInjection { get set }
func inject(into webView: WKWebView)
}
public extension WebViewUrlInjector {
func inject(into 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) }
}
}
// MARK: - Helper methods
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(url):
let css = try? String(contentsOf: url)
.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);
"""
}
}

View File

@ -24,15 +24,15 @@ import WebKit
open class DefaultWebViewModel: NSObject, WebViewModel {
public var injector: BaseWebViewUrlInjector
public var navigator: BaseWebViewNavigator
public var errorHandler: BaseWebViewErrorHandler
public var injector: WebViewUrlInjector
public var navigator: WebViewNavigator
public var errorHandler: WebViewErrorHandler
// MARK: - Init
public init(injector: BaseWebViewUrlInjector = .init(),
navigator: BaseWebViewNavigator = .init(),
errorHandler: BaseWebViewErrorHandler = .init()) {
public init(injector: WebViewUrlInjector = BaseWebViewUrlInjector(),
navigator: WebViewNavigator = BaseWebViewNavigator(),
errorHandler: WebViewErrorHandler = BaseWebViewErrorHandler()) {
self.injector = injector
self.navigator = navigator
@ -56,7 +56,9 @@ open class DefaultWebViewModel: NSObject, WebViewModel {
private func parseError(_ message: WKScriptMessage) -> WebViewError {
let body = message.body as? [String: Any]
let error = body?[WebViewErrorConstants.errorPropertyName] as? String
return .jsError(message.webView?.url, error ?? "")
return WebViewJSError(contentURL: body?[WebViewErrorConstants.errorUrl] as? URL,
name: body?[WebViewErrorConstants.errorName] as? String,
message: body?[WebViewErrorConstants.errorMessage] as? String,
stackTrace: body?[WebViewErrorConstants.errorStack] as? String)
}
}

View File

@ -23,26 +23,27 @@
import WebKit
public protocol WebViewModel: WKScriptMessageHandler {
var injector: BaseWebViewUrlInjector { get }
var navigator: BaseWebViewNavigator { get }
var errorHandler: BaseWebViewErrorHandler { get }
var injector: WebViewUrlInjector { get }
var navigator: WebViewNavigator { get }
var errorHandler: WebViewErrorHandler { get }
func makeUrlInjection(forWebView webView: WKWebView)
func shouldNavigate(toUrl url: URL) -> WKNavigationActionPolicy
func makeUrlInjection(into webView: WKWebView)
func shouldNavigate(to url: URL) -> WKNavigationActionPolicy
func handleError(_ error: Error, url: URL?)
}
public extension WebViewModel {
func makeUrlInjection(forWebView webView: WKWebView) {
injector.inject(on: webView)
func makeUrlInjection(into webView: WKWebView) {
injector.inject(into: webView)
}
func shouldNavigate(toUrl url: URL) -> WKNavigationActionPolicy {
navigator.shouldNavigate(toUrl: url)
func shouldNavigate(to url: URL) -> WKNavigationActionPolicy {
navigator.shouldNavigate(to: url)
}
func handleError(_ error: Error, url: URL?) {
errorHandler.didRecievedError(.standardError(url, error))
let errorModel = WebViewLoadingError(contentURL: url, innerError: error)
errorHandler.didRecievedError(errorModel)
}
}