Swift中performSelector的替代方法?

51
performSelector系列方法在Swift中不可用。那么,如果要在运行时选择要调用的方法而不是在编译时知道该如何调用一个@objc对象上的方法,该怎么办?显然,在Swift中也无法使用NSInvocation
我知道在Swift中,您可以将任何方法(只要有一个可见的@objc方法声明)发送到类型AnyObject,类似于Objective-C中的id。但是,这仍然需要您在编译时硬编码方法名。是否有一种方法可以在运行时动态选择它?

8
重新审视你的方法。在我的代码中,一个闭包解决了它。 - matt
你仍然可以做到这一点,找到我的答案在这里:https://dev59.com/n2Af5IYBdhLWcg3w7mb3#31853168 - Anand Suthar
@matt,请帮我重新考虑一下“金字塔式的地狱”问题,谢谢。 - turingtested
@turingtested 我将其重新思考为一系列“guard”语句。 :) - matt
17个回答

18

使用闭包

class A {
    var selectorClosure: (() -> Void)?

    func invoke() {
        self.selectorClosure?()
    }
}

var a = A()
a.selectorClosure = { println("Selector called") }
a.invoke()
注意,即使在 Obj-C 中,新的 API 更喜欢使用块而不是 performSelector(比较使用 respondsToSelector:performSelector: 来调用委托方法的 UIAlertView,与新的 UIAlertController)。始终使用 performSelector: 是不安全的,并且与 ARC 不兼容(因此有关 performSelector: 的 ARC 警告)。

6
看起来不错,但你可以像这样重写selectorClosure属性(请注意,对可选项进行的空值分配是隐式的):var selectorClosure: (() -> Void)? - Jack Lawrence

16

从Xcode 7开始,完整的performSelector方法家族都可以在Swift中使用,包括performSelectorOnMainThread()performSelectorInBackground()。享受吧!


1
@RoiMulia 是的,只要你使用 Xcode 7+ 进行编译。 - Ky -

15

方法A

使用NSThread.detachNewThreadSelector,这种方法的好处是我们可以将对象附加到消息上。示例代码在ViewController中:

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    let delay = 2.0 * Double(NSEC_PER_SEC)
    var time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
    dispatch_after(time, dispatch_get_main_queue(), {
        NSThread.detachNewThreadSelector(Selector("greetings:"), toTarget:self, withObject: "sunshine")
        })
}

func greetings(object: AnyObject?) {
    println("greetings world")
    println("attached object: \(object)")
}

控制台日志:

问候世界

附加对象:阳光

B方法

这个替代方案早些时候已经被发现,我也在设备和模拟器上进行了测试。想法是使用UIControl的以下方法:

func sendAction(_ action: Selector, to target: AnyObject!, forEvent event: UIEvent!)

ViewController中的示例代码:

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    
    var control: UIControl = UIControl()
    control.sendAction(Selector("greetings"), to: self, forEvent: nil) // Use dispatch_after to invoke this line as block if delay is intended 
}

func greetings() {
    println("greetings world")
}

控制台日志:

问候世界

方法 C

NSTimer

class func scheduledTimerWithTimeInterval(_ seconds: NSTimeInterval,
                                      target target: AnyObject!,
                                 selector aSelector: Selector,
                                  userInfo userInfo: AnyObject!,
                                    repeats repeats: Bool) -> NSTimer!

谢谢,看起来 A (dispatch_after(DISPATCH_TIME_NOW, ...) 是从 Swift 动态调用带参数的 ObjC 方法的唯一方法。 - Cfr
顺便问一下,是否真的有必要在主队列上进行调度?也就是说,当您更改UIView的属性时,它可能会通知等待主线程事件的观察者?此外,当您在主线程上进行调度时,它仍将在不同的线程上执行。 - Cfr

13

Swift 3.1
对于标准的Swift项目,闭包已经是一种优雅的解决方案,这在Sulthan's answer中已经涵盖了。 如果依赖于旧的Objective-C代码/库或想要调用私有API,则使用选择器字符串名称动态调用方法是有意义的。

只有NSObject子类可以接收消息,尝试向纯Swift类发送消息将导致崩溃。

#selector(mySelectorName)只能在其类源文件中解析类型化选择器名称。
通过牺牲类型检查,可以使用NSSelectorFromString(...)检索选择器
(Selector("selectorName:arg:")相比,并没有更安全的方式,只是它不会产生警告).

调用NSObject子类实例方法

let instance : NSObject = fooReturningObjectInstance() as! NSObject
instance.perform(#selector(NSSelectorFromString("selector"))
instance.perform(#selector(NSSelectorFromString("selectorArg:"), with: arg)
instance.perform(#selector(NSSelectorFromString("selectorArg:Arg2:"), with: arg, with: arg2)

同时也有主线程变体:

instance.performSelector(onMainThread: NSSelectorFromString("selectorArg:"), with: arg, waitUntilDone: false)

正如iOS_MIB在https://stackoverflow.com/a/48644264/5329717中指出的那样,这与不相等

DispatchQueue.main.async {
   //perform selector
}

和后台线程变体:

instance.performSelector(inBackground: NSSelectorFromString("selectorArg:"), with: arg)

但是,它有一些限制:

  • 它只能使用0-2个参数
  • 值类型参数如整数和选择器不起作用
  • 无法处理返回的值类型
  • 将对象作为Unmanaged<AnyObject>返回

因此,当不需要返回结果和值类型参数时,这种低成本方法非常方便。

获取NSObject运行时方法IMP允许使用正确的参数和返回类型进行类型化调用。 @convention(c)(types)->type允许将IMP结果转换为兼容的Swift闭包函数。

@convention(c)中,并不是所有类型都被允许

  • 对于类,请使用Any或AnyClass
  • 对于对象,请使用Any或完全匹配的类类型(如果其符号可用)
  • 对于值类型,请使用相关类型
  • 对于void*请使用OpaquePointer

这是根据定义不安全的,如果操作不正确,会导致崩溃和副作用。

C级别上的每个Objective-C方法都包含两个隐藏参数,以符合objc_msgSend(id self, SEL op, ...),需要将其包含在函数类型中,如@convention(c)(Any?,Selector, ... )

let instance : NSObject = fooReturningObjectInstance() as! NSObject
let selector : Selector = NSSelectorFromString("selectorArg:")
let methodIMP : IMP! = instance.method(for: selector)
unsafeBitCast(methodIMP,to:(@convention(c)(Any?,Selector,Any?)->Void).self)(instance,selector,arg) 

这些是perform(...)静态等效项。

NSObject.perform(NSSelectorFromString("selector"))
NSObject.perform(NSSelectorFromString("selectorArg:"), with: arg)
NSObject.perform(NSSelectorFromString("selectorArg:Arg2:"), with: arg, with: arg2)
NSObject.performSelector(onMainThread: NSSelectorFromString("selectorArg:"), with: arg, waitUntilDone: false)
NSObject.performSelector(inBackground: NSSelectorFromString("selectorArg:"), with: arg)

限制:

  • 前面提到的所有类型问题
  • 接收类需要有一个已定义的符号

获取运行时静态方法IMP并处理类型,应用@convention(c)

let receiverClass = NSClassFromString("MyClass")
let selector : Selector = NSSelectorFromString("selectorArg:")
let methodIMP : IMP! = method_getImplementation(class_getClassMethod(receiverClass, selector))
let result : NSObject = unsafeBitCast(methodIMP,to:(@convention(c)(AnyClass?,Selector,Any?)->Any).self)(receiverClass,selector,arg) as! NSObject

没有实际的理由去做这件事,但是objc_msgSend可以动态使用。

let instance : NSObject = fooReturningObjectInstance() as! NSObject
let handle : UnsafeMutableRawPointer! = dlopen("/usr/lib/libobjc.A.dylib", RTLD_NOW)
let selector : Selector = NSSelectorFromString("selectorArg:")
unsafeBitCast(dlsym(handle, "objc_msgSend"), to:(@convention(c)(Any?,Selector!,Any?)->Void).self)(instance,selector,arg)
dlclose(handle)

对于NSInvocation同样适用(这只是有趣的练习,不要这样做)

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

    func invocationTest() {
        let invocation : NSObject = unsafeBitCast(method_getImplementation(class_getClassMethod(NSClassFromString("NSInvocation"), NSSelectorFromString("invocationWithMethodSignature:"))),to:(@convention(c)(AnyClass?,Selector,Any?)->Any).self)(NSClassFromString("NSInvocation"),NSSelectorFromString("invocationWithMethodSignature:"),unsafeBitCast(method(for: NSSelectorFromString("methodSignatureForSelector:"))!,to:(@convention(c)(Any?,Selector,Selector)->Any).self)(self,NSSelectorFromString("methodSignatureForSelector:"),#selector(setter:name))) as! NSObject
        unsafeBitCast(class_getMethodImplementation(NSClassFromString("NSInvocation"), NSSelectorFromString("setSelector:")),to:(@convention(c)(Any,Selector,Selector)->Void).self)(invocation,NSSelectorFromString("setSelector:"),#selector(setter:name))
        var localName = name
        withUnsafePointer(to: &localName) { unsafeBitCast(class_getMethodImplementation(NSClassFromString("NSInvocation"), NSSelectorFromString("setArgument:atIndex:")),to:(@convention(c)(Any,Selector,OpaquePointer,NSInteger)->Void).self)(invocation,NSSelectorFromString("setArgument:atIndex:"), OpaquePointer($0),2) }
        invocation.perform(NSSelectorFromString("invokeWithTarget:"), with: self)
    }
}

8

Swift 3

perform(#selector(someSelector), with: nil, afterDelay: 1.0, inModes: [.commonModes])

在 Swift 3 中,使用上述代码可以延迟 1 秒后执行名为 "someSelector" 的方法,并在通用模式下运行。

7
根据@JTerry的答案,"你不需要在Swift中使用选择器",你可以将实际方法分配给变量。我的解决方案如下(我需要一个参数在方法中):
class SettingsMenuItem: NSObject {
    ...
    var tapFunction: ((sender: AnyObject?) -> ())?
}

然后在视图控制器中,我以这种方式声明、分配和运行函数:

class SettingsViewController: UITableViewController {

    func editProfile(sender: AnyObject?) {
        ...
    }

    ...

    menuItem.tapFunction = editProfile

    ...

    if let tapFunction = menuItem.tapFunction {
        tapFunction(sender: self)
    }


}

你的回答仅限于选择器是否在Swift中仍然需要使用。但问题是关于performSelector的替代方案,它被用于异步操作、延迟执行、等待完成或轻松快速地线程控制(在主线程或后台线程运行)。 - auco
感谢这个示例,这是我找到的第一个概述如何在Swift中调用具有已知签名的“未知”方法的实例(如果我使用动态方法对事物进行建模,这是偶尔想要做的)。比NSInvocation更有用,也更少出错。 - Patru

6
你可以在Swift中使用这个。
var timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector:  Selector("someSelector"), userInfo: nil, repeats: false)


func someSelector() {
// Something after a delay
}

通过这种方式,你可以做到Objective-C中performSelector所执行的功能。

7
这并不完全相同,因为1)它是异步执行的,而performSelector:(不带afterDelay)是同步执行的;2)它只在我们正在运行run loop的线程上工作。 - user102008
嗨@user102008,对我来说它很好用,现在,如果我将重复设置为true,我该如何停止计时器? - Josh
@Josh timer.invalidate() - Hadi Sharghi

4

我也曾为此苦恼。最终我意识到,我不需要使用目标或选择器。对我来说,解决方案是将函数分配给变量并调用该变量。即使从其他类中调用它,它也可以正常工作。以下是一个快速示例:

func Apple() ->Int
{
    let b = 45;
    return b;
}

func Orange()->Int
{
    let i = 5;
    return i;
}

func Peach()
{
    var a = Apple; // assign the var a the Apple function
    var b = Orange; // assisgn the var b to the Orange function

    let c = a(); // assign the return value of calling the 'a' or Apple function to c
    let d = b(); // assign the return value of calling the 'b' or Orange function d

    Pear(a, b)
}

func Pear(x:()->Int, y:()->Int)->Int
{
    let w = (x()+y()); // call the x function, then the y function and add the return values of each function.
    return w; // return the sum
}

Peach();

1
这对我有用。我需要一个带有一个参数的函数,所以我声明了“selector”属性,如下所示--- var tapFunction: ((sender: AnyObject?) -> ())? --- 然后声明一个函数,如下所示--- func actionMethod(sender: AnyObject?) - Matej Ukmar

3
哇,我们可以使用swizzling来实现所需的方法!只需要添加这个扩展并在所有调用之前加上符号即可。
import Foundation

private var dispatchOnceToken: dispatch_once_t = 0

private var selectors: [Selector] = [
    "performSelector:",
    "performSelector:withObject:",
    "performSelector:withObject:withObject:",
    "performSelector:withObject:afterDelay:inModes:",
    "performSelector:withObject:afterDelay:",
]

private func swizzle() {
    dispatch_once(&dispatchOnceToken) {
        for selector: Selector in selectors {
            let selector = Selector("\(selector)")
            let method = class_getInstanceMethod(NSObject.self, selector)

            class_replaceMethod(
                NSObject.self,
                selector,
                method_getImplementation(method),
                method_getTypeEncoding(method)
            )
        }
    }
}

extension NSObject {

    func performSelector(selector: Selector) -> AnyObject? {
        swizzle()
        return self.performSelector(selector)
    }

    func performSelector(selector: Selector, withObject object: AnyObject?) -> AnyObject? {
        swizzle()
        return self.performSelector(selector, withObject: object)
    }

    func performSelector(selector: Selector, withObject object1: AnyObject?, withObject object2: AnyObject?) -> AnyObject? {
        swizzle()
        return self.performSelector(selector, withObject: object1, withObject: object2)
    }

    func performSelector(selector: Selector, withObject object: AnyObject?, afterDelay delay: NSTimeInterval, inModes modes: [AnyObject?]?) {
        swizzle()
        self.performSelector(selector, withObject: object, afterDelay: delay, inModes: modes)
    }

    func performSelector(selector: Selector, withObject object: AnyObject?, afterDelay delay: NSTimeInterval) {
        swizzle()
        self.performSelector(selector, withObject: object, afterDelay: delay)
    }

}

1
嘿..我刚刚看到了一个火箭..!!! :) 开玩笑的..工作得非常好..谢谢。 - Vish_iOS
是的...谢谢。 - Valentin Shergin

2
有时候(特别是在使用target/action模式时),您可能需要使用-[UIApplication sendAction:to:from:forEvent:]方法(适用于iOS),所以在Swift中,可以像这样编写代码:
UIApplication.sharedApplication()
    .sendAction(someSelector, to: someObject, from: antotherObject, forEvent: someEvent)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接