ECMASwift 1.3.0

ECMASwift 1.3.0

hiimtmac维护。



ECMASwift 1.3.0

  • 作者:io
  • hiimtmac

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

ESWebViewWKWebView 的一个子类,具有一些扩展功能。在 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\"}"