我有以下枚举:

@dynamicMemberLookup
enum JSCall: ExpressibleByStringInterpolation {
    case getElement(String)
    case getInnerHTML(id: String)
    case removeAttribute(id: String, attributeName: String)
    case customScript(String)
    case document
    indirect case chainedCall(JSCall, String)

    var value: String {
        switch self {
        case .document:
            return "document"
        case .getElement(let id):
            return JSCall.chainedCall(.document, "getElementById('\(id)')").value
        case .getInnerHTML(let id):
            return JSCall.getElement(id).innerHTML.value // Using dynamic member lookup here
        case .removeAttribute(let id, let attribute):
            return JSCall.chainedCall(.getElement(id), "removeAttribute('\(attribute)')").value
        case .chainedCall(let prefixJs, let suffix):
            return "\(prefixJs.scriptString)\(STRING_DOT)\(suffix)"
        case .customScript(let script):
            return script
        }
    }

    init(stringLiteral value: String) {
        self = .customScript(value)
    }
    
    subscript(dynamicMember suffix: String) -> JSCall {
        return .chainedCall(.customScript(self.value), suffix)
    }
}

注意,在计算(computed)属性value中,对于getInnerHTML种情况,我可以使用自然语法通过点语法链接.innerHTML,这要归功于@dynamicMemberLookup.但是,如果我try 链接插入的字符串,如下所示:

let call = JSCall.document.getElementById('\(id)').value

我收到了类似Cannot call value of non-function type 'JSCall'的编译器错误.

我以为符合ExpressibleByStringInterpolation标准就能解决问题,但事实并非如此.

有没有办法在这个枚举中实现链接方法调用的自然语法?

推荐答案

dynamicMemberLookup不处理函数调用.为了能够呼叫查询到的成员,您可以使用@dynamicCallable.你也可以用callAsFunction做类似的事情.

我认为对此建模的一种更简单的方法是只有更多的"基本"用例,对JS语法树建模.

@dynamicMemberLookup
@dynamicCallable
enum JSCall {
    case stringLiteral(String)
    case integerLiteral(Int)
    // add more literal cases if you like
    case call(String, args: [JSCall])
    case property(String)
    indirect case chain(JSCall, JSCall)

    var value: String {
        switch self {
        case let .call(functionName, args):
            let argList = args.map(\.value)
                .joined(separator: ", ")
            return "\(functionName)(\(argList))"
        case .property(let name):
            return name
        case .chain(let first, let second):
            return "\(first.value).\(second.value)"
        case .stringLiteral(let s):
            return "'\(s)'" // note that this does not escape quotes in the JS string!
        case .integerLiteral(let i):
            return "\(i)"
        }
    }

    // ...
}

当你动态查找一个成员时,创建一个chain,第二件事是property.这与foo.bar这样的 case 相对应

subscript(dynamicMember propertyName: String) -> JSCall {
    return .chain(self, .property(propertyName))
}

然后,在类似foo.bar("bar")的情况下,您将隐式调用(foo.bar).dynamicallyCall(withArguments: [.stringLiteral("baz")]).这应该返回.chain(foo, .call("bar", [.stringLiteral("baz")])).

func dynamicallyCall(withArguments args: JSCall...) -> JSCall {
    switch self {
    case .property(let name):
        return .call(name, args: args)
    case .chain(let first, let second):
        // note the recursive call here:
        return .chain(first, second.dynamicallyCall(withArguments: args))
    default:
        fatalError("Cannot call this!")
    }
}

字面上的大小写可以符合ExpressibleByXXXLiteral:

extension JSCall: ExpressibleByStringLiteral, ExpressibleByIntegerLiteral {
    init(stringLiteral value: String) {
        self = .stringLiteral(value)
    }
    
    init(integerLiteral value: Int) {
        self = .integerLiteral(value)
    }
}

这就是所有的基础设施.现在,您可以添加一些方便的静态工厂和属性:

static let document = JSCall.property("document")

static func getElement(_ id: String) -> JSCall {
    document.getElementById(.stringLiteral(id))
}

static func getInnerHTML(_ id: String) -> JSCall {
    getElement(id).innerHTML
}

static func removeAttribute(id: String, attributeName: String) -> JSCall {
    getElement(id).removeAttribute(.stringLiteral(attributeName))
}

用途:

let one = JSCall.document.getElementById("foo").value
let two = JSCall.removeAttribute(id: "bar", attributeName: "attr").somethingElse.value
print(one) // document.getElementById('foo')
print(two) // document.getElementById('bar').removeAttribute('attr').somethingElse

Ios相关问答推荐

如何加密字符串使用AES. GCM在Dart和解密相同的Swift?

SwiftUI中的视频时间轴

避免从CoreData加载图像列表时出现SwiftUI挂起

使用SWIFT传输表示时的条件文件表示格式

RxSwift:如何使用另外两个可观测对象(其中一个依赖于另一个)创建可观测对象?

如何从Windows PC在iPhone上安装Flutter 应用程序

SwiftUI-工作表不显示导航标题

如何链式设置 AttributeContainer 的 UIKit 属性?

如何从单独的视图更改修饰符的值

为什么在Actor内部使用withTaskGroupwork并行运行?

SwiftUI 故障/错误 - 在 LazyVStack 和 ScrollView 中使用 AVPlayer 时状态栏不显示

如何在 SwiftUI 中只显示一次随机数组的项目?

.onChange(of: ) 一次用于多个 @State 属性?

Swift 共享数据模型在页面之间进行通信.这个怎么运作

在 iOS 编程中使用 Storyboard 代替 xib 文件有什么好处?

如何关闭情节提要弹出框

在 Xcode 4 中将静态库链接到 iOS 项目

iOS 应用程序中的手动语言 Select (iPhone 和 iPad)

NSURLSession:如何增加 URL 请求的超时时间?

UITableView 中的圆形 UIImageView 没有性能影响?