Есть ли альтернатива NSInvocation в Swift?

Я пытаюсь вызвать селектор с несколькими (2+) аргументами (количество аргументов можно определить). Однако селектор неизвестен во время компиляции (фактически сгенерирован с помощью NSSelectorFromString).

В Objective-C я мог бы создать вызов, установить для него аргументы и вызвать его. Но это недоступно в Swift. Есть ли способ обойти это? Нравиться:

let obj = SomeClass()
let selector = NSSelectorFromString("arg1:arg2:arg3:") //selector, arguments known only at runtime
//invoke selector

person Aswath    schedule 10.11.2016    source источник
comment
Какую именно проблему вы пытаетесь решить?   -  person Alexander    schedule 10.11.2016
comment
Извините, но я работаю на клиента. Но я постараюсь описать это как можно лучше. Мне нужно настроить представление с помощью файла конфигурации, который можно настроить по мере необходимости. Тем не менее, список возможных конфигураций (каждая из которых пытается получить доступ к множеству различных методов) велик. Да, ими можно управлять в каждом конкретном случае (сопоставление случая с методом), но я пытаюсь использовать решение общего назначения, чтобы попытаться управлять этим.   -  person Aswath    schedule 10.11.2016
comment
Такая динамичность кода обычно является результатом плохой архитектуры (без обид). Есть несколько очень специфических исключений, но в 95% вам не нужно использовать NSInvocation, и вы не должны этого делать, даже в Objective-C. Одним из возможных решений является замыкание по имени в словаре, но даже это немного пахнет кодом.   -  person Sulthan    schedule 10.11.2016
comment
Никто не взял, но почему вы считаете, что такое использование этой динамичности плохо? Для меня это снижает нагрузку на кодирование, и при надлежащих проверках я думаю, что это могло бы быть очень универсальным. К вашему сведению, я не говорю, что все, что я описал в своем предыдущем комментарии, было испорчено. Это очень :-|   -  person Aswath    schedule 10.11.2016
comment
Это NSInvocation. Вам просто нужно убедиться, что все классы и селекторы отмечены @objc.   -  person Bryan Chen    schedule 10.11.2016
comment
@Sulthan, я бы так не сказал, почему плохая архитектура?   -  person    schedule 03.08.2017
comment
@3000 Селекторы и вызовы не являются типобезопасными.   -  person Sulthan    schedule 03.08.2017
comment
@Sulthan: это нормально, но вы можете добавить необходимую безопасность, приведя результат к желаемому типу (или вы имели в виду что-то другое?)   -  person    schedule 03.08.2017
comment
@3000 Вы не можете. Проблема в том, что имя метода является строкой, поэтому компилятор не может проверить, есть ли вообще такой метод. Это частично решено для селекторов с синтаксисом #selector, но такого синтаксиса нет для NSInvocation. Закрытие просто безопаснее. Есть и другие аспекты, связанные с управлением памятью, поскольку на это влияет название метода в Obj-C. Таким образом, это старый способ ведения дел, теперь у нас есть лучшие и более безопасные альтернативы.   -  person Sulthan    schedule 03.08.2017
comment
@Sulthan: хорошо, спасибо за добрый ответ   -  person    schedule 03.08.2017


Ответы (2)


Свифт 3.1

NSInvocation можно использовать динамически, но только как забавное упражнение, определенно не для серьезных приложений. Есть лучшие альтернативы.

import Foundation

class Test: NSObject {
    @objc var name: String? {
        didSet {
            NSLog("didSetCalled")
        }
    }

    func invocationTest() {
        // This is the selector we want our Invocation to send
        let namePropertySetterSelector = #selector(setter:name)
        
        // Look up a bunch of methods/impls on NSInvocation
        let nsInvocationClass: AnyClass = NSClassFromString("NSInvocation")!
        
        // Look up the "invocationWithMethodSignature:" method
        let nsInvocationInitializer = unsafeBitCast(
            method_getImplementation(
                class_getClassMethod(nsInvocationClass, NSSelectorFromString("invocationWithMethodSignature:"))!
            ),
            to: (@convention(c) (AnyClass?, Selector, Any?) -> Any).self
        )
        
        // Look up the "setSelector:" method
        let nsInvocationSetSelector = unsafeBitCast(
            class_getMethodImplementation(nsInvocationClass, NSSelectorFromString("setSelector:")),
            to:(@convention(c) (Any, Selector, Selector) -> Void).self
        )
        
        // Look up the "setArgument:atIndex:" method
        let nsInvocationSetArgAtIndex = unsafeBitCast(
            class_getMethodImplementation(nsInvocationClass, NSSelectorFromString("setArgument:atIndex:")),
            to:(@convention(c)(Any, Selector, OpaquePointer, NSInteger) -> Void).self
        )
        
        // Get the method signiture for our the setter method for our "name" property.
        let methodSignatureForSelector = NSSelectorFromString("methodSignatureForSelector:")
        let getMethodSigniatureForSelector = unsafeBitCast(
            method(for: methodSignatureForSelector)!,
            to: (@convention(c) (Any?, Selector, Selector) -> Any).self
        )
        
        // ObjC:
        // 1. NSMethodSignature *mySignature = [self methodSignatureForSelector: @selector(setName:)];
        // 2. NSInvocation *myInvocation = [NSInvocation invocationWithMethodSignature: mySignature];
        // 3. [myInvocation setSelector: @selector(setName:)];
        // 4. [myInvocation setArgument: @"new name", atIndex: 2];
        // 5. [myInvocation invokeWithTarget: self];
        
        // 1.
        let namyPropertyMethodSigniature = getMethodSigniatureForSelector(self, methodSignatureForSelector, namePropertySetterSelector)

        // 2.
        let invocation = nsInvocationInitializer(
            nsInvocationClass,
            NSSelectorFromString("invocationWithMethodSignature:"),
            namyPropertyMethodSigniature
        ) as! NSObject // Really it's an NSInvocation, but that can't be expressed in Swift.
        
        // 3.
        nsInvocationSetSelector(
            invocation,
            NSSelectorFromString("setSelector:"),
            namePropertySetterSelector
        )
        
        var localName = "New name" as NSString
        
        // 4.
        withUnsafePointer(to: &localName) { stringPointer in
            nsInvocationSetArgAtIndex(
                invocation,
                NSSelectorFromString("setArgument:atIndex:"),
                OpaquePointer(stringPointer),
                2
            )
        }
        
        // 5.
        invocation.perform(NSSelectorFromString("invokeWithTarget:"), with: self)
    }
}

let object = Test()
object.invocationTest()
person Kamil.S    schedule 29.04.2017

Боюсь, в Swift нет возможности сделать это.

Однако у вас может быть класс Objective-C для управления вашими динамическими вызовами. Вы можете использовать NSInvocation там.

person Marcos Crispino    schedule 10.11.2016