ECMASwift
这个库的目标是将容易出错的路钱功能封装成更强的类型组合部件
要求
- Swift 5.3+
- iOS 13.0 + / macOS 10.15 +
安装
Swift Package Manager
创建一个Package.swift
文件。
import PackageDescription
let package = Package(
name: "TestProject",
dependencies: [
.package(url: "https://github.com/hiimtmac/ECMASwift.git", from: "1.0.0")
]
)
Cocoapods
target 'MyApp' do
pod 'ECMASwift', '~> 1.0'
end
WKWebView
Before
webView.evaluateJavaScript(javaScriptString, completionHandler: { (any, error) in
if let error = error {
print(error)
} else if let any = any as? MyType {
print(any)
} else {
// what do??
}
})
// Can't reuse this!
After
webView.evaluateJavaScript("string;", as: String.self) // -> returns AnyPublisher<String, Error>
存在两个重载版本,一个返回类型,另一个返回void。
func evaluateJavaScript(_ javaScriptString: String) -> AnyPublisher<Void, Error>
func evaluateJavaScript<T: Decodable>(_ javaScriptString: String, as: T.Type) -> AnyPublisher<T, Error>
这些可以串联在一起,完成更复杂的功能。
<script>
var string = "hello";
</script>
let _ = webView
.evaluateJavaScript("string;", as: String.self)
.flatMap { self.webView.evaluateJavaScript("string = \"hi there!\";") }
.flatMap { self.webView.evaluateJavaScript("string;", as: String.self) }
.sink(receiveCompletion: { result in
switch result {
case .finished: print("done")
case .failure(let error): print(error) // error if there was
}
}, receiveValue: {
print($0) // hi there!
})
这以前可能需要通过大量嵌套的完成处理器来创建一个“末日金字塔”!
辅助方法
辅助方法,用于常见任务
- 获取变量
getVariable<T: Decodable>(...)
- 设置变量
setVariable(...)
- 无返回值的调用方法
runVoid(...)
- 有返回值的调用方法
runReturning<T: Decodable>
func getVariable<T: Decodable>(named: String, as type: T.Type) -> AnyPublisher<T, Error>
func setVariable(named: String, value: JavaScriptParameterEncodable) -> AnyPublisher<Void, Error>
func runVoid(named: String, args: [JavaScriptParameterEncodable] = []) -> AnyPublisher<Void, Error>
func runReturning<T: Decodable>(named: String, args: [JavaScriptParameterEncodable] = [], as type: T.Type) -> AnyPublisher<T, Error>
这些方法可以将上述示例大大简化
let _ = webView
.getVariable(named: "string", as: String.self)
.flatMap { self.webView.setVariable(named: "string", value: "hi there!") }
.flatMap { self.webView.getVariable(named: "string", as: String.self) }
.sink(receiveCompletion: { result in
switch result {
case .finished: print("done")
case .failure(let error): print(error) // error if there was
}
}, receiveValue: {
print($0) // hi there!
})
这非常有用,因为你不必担心那些讨厌的分号"{"位置
JavaScriptParameterEncodable
任何遵循JavaScriptParameterEncodable
的值都可以用作设置变量或带有参数调用函数的参数。那些也遵循Encodable
的类型会自动获得这种编码行为。你不必担心转义符等问题。结合使用更强类型的辅助方法和JavaScriptParameterEncodable
协议将使以下内容变得可能:
<script>
function returnContents(contents) {
return contents;
}
</script>
struct JSON: Codable {
let name: String
let age: Int
}
显然,这是一个被设计出来的例子,因为我们期望返回的类型和输入的类型相同。
之前
// simple types
let javaScriptString = "returnContent(\"tmac\");" // cant just shove "tmac" in there
webView.evaluateJavaScript(javaScriptString, completionHandler: { (any, error) in
if let error = error {
print(error) // error if there was
} else if let any = any as? String {
print(any) // "tmac"
} else {
// what do??
}
})
// Can't reuse this!
let javaScriptString = "returnContent({\"name\":\"tmac\",\"age\":28});" // cant just shove JSON(name: "tmac", age: 28) in there
webView.evaluateJavaScript(javaScriptString, completionHandler: { (any, error) in
if let error = error {
print(error) // error if there was
} else if let any = any {
let encoded = try JSONSerialization.data(withJSONObject: any, options: []) // Any -> Data
let decoded = try JSONDecoder().decode(JSON.self, from: encoded) // Data -> Object
print(decoded) // { name: "tmac", age: 28 }
} else {
// what do??
}
})
// Can't reuse this!
之后
let _ = webView
.runReturning(named: "returnContents", args: ["tmac"], as: String.self)
.sink(receiveCompletion: { result in
switch result {
case .finished: print("done")
case .failure(let error): print(error) // error if there was
}
}, receiveValue: {
print($0) // "tmac"
})
// more complex types
extension JSON: JavaScriptParameterEncodable {}
let _ = webView
.runReturning(named: "returnContents", args: [JSON(name: "tmac", age: 28)], as: JSON.self)
.sink(receiveCompletion: { result in
switch result {
case .finished: print("done")
case .failure(let error): print(error) // error if there was
}
}, receiveValue: {
print($0) // { name: "tmac", age: 28 }
})
ESWebView
ESWebView
是 WKWebView
的一个子类,具有一些扩展功能。在 JavaScript 中,你可以通过以下方式连接到 Swift 世界:
window.webkit.messageHandlers.MyMessageHandler.postMessage({message: "hello"});
这需要你通过以下方法订阅 WebView 的消息
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.name) // MyMessageHandler
print(message.body) // Any 🤮
}
ESWebView
监听 3 个消息处理器(并执行所有订阅消息的工作),寻找 3 种格式中的数据
private let kMessage = "ECMASwiftMessage"
public struct Message: Codable {
public let message: String
}
// used for sending a message to swift-land
private let kPrompt = "ECMASwiftPrompt"
public struct Prompt: Codable {
public let name: String
public let type: HandlerType
}
// used to prompt swift-land to reach in and grab a variable or run a function
private let kRequest = "ECMASwiftRequest"
public struct Request: Codable {
public let object: String
public let predicate: String?
public let toHandler: String
public let type: HandlerType
}
// used for requesting a type or types from swift-land
// handler type
public enum HandlerType: String, Codable {
case variable
case function
}
这些可以在 JavaScript 中使用,发送的消息将被发布到 iOS 的 NotificationCenter
。
window.webkit.messageHandlers.ECMASwiftMessage.postMessage({message: "hello"});
window.webkit.messageHandlers.ECMASwiftPrompt.postMessage({name: "name", type: "variable"});
window.webkit.messageHandlers.ECMASwiftRequest.postMessage({object: "JSON", toHandler: "setPeople", type: "function"});
在你的视图控制器中订阅这些 3 个通知(以及第 4 个错误通知)
示例
import UIKit
import ECMASwift
import Combine
class ViewController: UIViewController {
var webView: ESWebView!
override func viewDidLoad() {
super.viewDidLoad()
// setup webView
// something in webView triggers window.webkit.messageHandlers.ECMASwiftXXXXXXX.postMessage(...)
let _ = NotificationCenter.default
.publisher(for: ESWebView.error, object: nil)
.setFailureType(to: ProxyError.self)
.tryMap { notification -> T in
let error = notification.userInfo!["error"] as! String
let attempting = notification.userInfo!["attempting"] as! String
throw ProxyError.error("Attempting: \(attempting) -> Error: \(error)")
}
.eraseToAnyPublisher() // AnyPublisher<T, Error>
let messageNotification = NotificationCenter.default
.publisher(for: ESWebView.message, object: nil)
.tryMap { notification -> ESWebView.Message in
if let message = notification.userInfo?["message"] as? ESWebView.Message {
return message
} else {
throw ProxyError.userInfo
}
}
...
let promptNotification = NotificationCenter.default
.publisher(for: ESWebView.prompt, object: nil)
.tryMap { notification -> ESWebView.Prompt in
if let prompt = notification.userInfo?["prompt"] as? ESWebView.Prompt {
return prompt
} else {
throw ProxyError.userInfo
}
}
.flatMap { self.webView.getVariable(named: $0.name, as: String.self) }
...
let requestNotification = NotificationCenter.default
.publisher(for: ESWebView.request, object: nil)
.tryMap { notification -> ESWebView.Request in
if let request = notification.userInfo?["request"] as? ESWebView.Request {
return request
} else {
throw ProxyError.userInfo
}
}
.flatMap { self.webView.runVoid(named: $0.toHandler, args: [jsons]) }
...
}
}
WKUIDelegate
ESWebView
在初始化器中将其 uiDelegate
设置为 self
。这允许我们捕获来自 JavaScript 的事件,例如警告、确认和文本输入面板。
public var handleAlertPanel: ((_ message: String, _ completionHandler: @escaping () -> Void) -> Void)?
public var handleConfirmPanel: ((_ message: String, _ completionHandler: @escaping (Bool) -> Void) -> Void)?
public var handleTextInputPanel: ((_ prompt: String, _ defaultText: String?, _ completionHandler: @escaping (String?) -> Void) -> Void)?
将上述任何属性设置在你的 WebView 上以处理这些事件。不设置它们(或将它们设置为 nil)将导致系统采取默认行为,如在未设置 uiDelegate
属性时。
webView.handleAlertPanel = { [weak self] message, completion in
let ac = UIAlertController(title: "Alert!", message: message, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completion()
})
self?.present(ac, animated: true)
}
webView.handleConfirmPanel = { [weak self] message, completion in
let ac = UIAlertController(title: "Alert!", message: message, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Yes", style: .default) { _ in
completion(true)
})
ac.addAction(UIAlertAction(title: "NO", style: .default) { _ in
completion(false)
})
self?.present(ac, animated: true)
}
webView.handleTextInputPanel = { [weak self] message, defaultText, completion in
let ac = UIAlertController(title: "Alert!", message: message, preferredStyle: .alert)
ac.addTextField(configurationHandler: { (tf) in
tf.placeholder = "hello"
tf.text = defaultText
})
ac.addAction(UIAlertAction(title: "Yes", style: .default) { [unowned ac] _ in
completion(ac.textFields?.first?.text)
})
ac.addAction(UIAlertAction(title: "NO", style: .default) { _ in
completion(nil)
})
self?.present(ac, animated: true)
}
字符串插值
使用以下方法可以对 JavaScriptParameterEncodable
对象执行字符串插值:
struct Object: Codable, JavaScriptParameterEncodable {
let name: String
}
let javaScriptString = "var myVar = \(asJS: Object(name: "tmac")"
// "var myVar = {\"name\":\"tmac\"}"