现在Swift已经弃用了initialize(),在macOS中有替代方法吗?

18

Objective-C声明了一个类函数initialize(),它在每个类被使用之前运行一次。它经常被用作交换方法实现(方法混淆)等操作的入口点。它在Swift 3.1中已被弃用。

这是我以前做的事情:

extension NSView {
    public override class func initialize() {
        // This is called on class init and before `applicationDidFinishLaunching`
    }
}

如何在不使用initialize的情况下实现相同的功能?

我需要用于框架,因此需要在AppDelegate中调用某些内容是不可行的。我需要在applicationDidFinishLaunching之前调用它。

我非常喜欢这个解决方案。 它正是我想要的,但它是针对iOS的。 我需要macOS版本。 有人能提供一个macOS版本吗?

具体而言,我需要类似于以下内容的macOS版本:

extension UIApplication {
    private static let runOnce: Void = {
        // This is called before `applicationDidFinishLaunching`
    }()

    override open var next: UIResponder? {
        UIApplication.runOnce
        return super.next
    }
}

我尝试了覆盖NSApplication的各种属性,但都没有成功。

解决方案必须是纯Swift编写的,不能使用Objective-C。


2
你觉得在你所提到的代码中,将 UIApplication 替换为 NSApplication 怎么样? - Cristik
2
@SindreSorhus,也许你能给我们一些具体的例子,说明你试图实现什么。否则,这个问题就有点宽泛了。 - Cristik
1
你说你需要在 applicationDidFinishLaunching 之前调用它,然而 Objective-C 的 initialized 并不能保证在 applicationDidFinishLaunching 之前被调用,所以也许你正在寻找其他东西。 - Cristik
6
您提供的解决方案不能实现您所要求的功能。它们仅仅是碰巧可行,不够稳健(例如,您在赌注一个 NSView 在应用程序完成启动之前被创建;这是常见的,但并不保证)。如果您可以控制 NIB 文件或 NSApplicationMain,则此问题很容易解决,而无需使用任何诡计。如果使用 ObjC 文件,这个问题也很容易解决(甚至不需要 Objective-C 桥接;您只需将 ObjC 文件直接放入项目中即可)。不清楚为什么您把所有简单的方法都排除在外,并且不清楚您的约束条件是什么。 - Rob Napier
3
@SindreSorhus 快速问题 - 你是否实际需要完全相同的 initialize() 行为,还是只需要在 appDidFinishLaunching 之前执行代码的方法? - Cristik
显示剩余6条评论
3个回答

6
编辑:自从我写下这个答案,OP已经在编辑中添加了“纯Swift”。然而,我仍然将此答案保留在这里,因为它仍然是目前唯一正确的方法。希望在Swift 6、7或8中添加模块初始化钩子,但截至2018年3月,对于这种用例,纯Swift是错误的工具

原始答案如下:

不幸的是,Swift没有任何直接等效于旧的initialize()load()方法,所以这不能在纯Swift AFAIK中完成。然而,如果您不反对在项目中混合少量的Objective-C,那么这并不难做到。只需创建一个完全暴露给Objective-C的Swift类即可:

class MyInitThingy: NSObject {
    @objc static func appWillLaunch(_: Notification) {
        print("App Will Launch")
    }
}

然后将这个简短的 Objective-C 文件添加到项目中:

#import <Cocoa/Cocoa.h>

static void __attribute__ ((constructor)) Initer() {
    // Replace "MyFrameworkName" with your framework's module name.

    // You can also just #import <MyFrameworkName/MyFrameworkName-Swift.h>
    // and then access the class directly, but that requires the class to
    // be public, which pollutes the framework's external interface.
    // If we just look up the class and selector via the Objective-C runtime,
    // we can keep everything internal.

    Class class = NSClassFromString(@"MyFrameworkName.MyInitThingy");
    SEL selector = NSSelectorFromString(@"appWillLaunch:");

    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];

    [center addObserver:class
               selector:selector
                   name:NSApplicationWillFinishLaunchingNotification
                 object:nil];
}

使用这两段代码,链接到您的框架的应用程序应该在调用applicationDidFinishLaunching之前,将“App Will Launch”记录到控制台。
或者,如果您的模块中已经有一个公共的ObjC可见类,则可以通过类别而无需使用运行时函数来执行此操作:
public class SomeClass: NSObject {
    @objc static func appWillLaunch(_: Notification) {
        print("App Will Launch")
    }
}

并且:

#import <Cocoa/Cocoa.h>
#import <MyFrameworkName/MyFrameworkName-Swift.h>

@interface SomeClass (InternalSwiftMethods)

+ (void)appWillLaunch:(NSNotification *)notification;

@end

@implementation SomeClass (FrameworkInitialization)

+ (void)load {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];

    [center addObserver:self
               selector:@selector(appWillLaunch:)
                   name:NSApplicationWillFinishLaunchingNotification
                 object:nil];
}

@end

2
@SindreSorhus 链接解决方案中的黑客行为是不可取的,原因有两个:1)如果两个独立的框架尝试覆盖 NSApplication 上的 nextResponder,其中一个将被覆盖,而且未定义哪一个将被覆盖;2)如果 NSApplication 在 AppKit 的某个未来版本中获得自己的 nextResponder 覆盖,这将覆盖它并导致不知道多少未定义的行为。很抱歉,没有纯 Swift 解决此问题的方法不会对其造成积极的伤害。 - Charles Srstka
@Cristik 我字面上理解了“在applicationDidFinishLaunching之前”的要求;applicationWillFinishLaunching通知应该会在applicationDidFinishLaunching之前不久被触发。如果OP想要在启动时立即调用代码,该方法也可以通过performSelector:直接调用,而无需经过NSNotificationCenter - Charles Srstka
@CharlesSrstka 是的,这就是该短语应该被解释的方式,我的想法只是为什么OP对“initialize”感兴趣?您发布的类似于“load”的解决方案应该提供“before didFinishLaunch”的要求。 - Cristik
@Cristik 只有管理员可以回答这个问题。 - Charles Srstka
@CharlesSrstka,让我们找出来,我已经问过原帖作者了 :) - Cristik
显示剩余2条评论

6
不,Swift没有initialize()的替代方法,主要是因为Swift是静态分派的,所以无法拦截方法调用。而且这个方法已经不再需要了,因为在Objective-C文件中通常使用它来初始化静态变量,在Swift中静态/全局变量始终是惰性的,并且可以使用表达式的结果进行初始化(这在Objective-C中是不可能的)。
即使可能实现类似的功能,我也不建议您在没有框架用户知情的情况下隐式运行代码。我建议在库中添加一个configure方法,并要求库的用户调用它。这样他们就可以控制初始化点。只有少数事情比我链接但不再(或尚未)使用的框架开始执行代码更糟糕。
给框架用户控制框架代码何时启动是更明智的选择。如果确实需要您的代码在应用程序生命周期的最开始运行,请要求框架用户在任何其他调用之前调用您的框架入口点。这对于他们的利益也是如此,因为他们希望您的框架能够得到正确的配置。
例如:
MyFramework.configure(with: ...)
// or
MyManager.start()

2

正如其他人之前指出的那样,在Swift框架中不可能(也不是好的编程实践)实现您所要求的功能。然而,在应用程序本身中实现此类行为非常简单 - 无需处理通知或选择器。您只需覆盖NSApplicationDelegate(或UIApplicationDelegate)的init方法并在其中设置您的类初始化器即可:

class AppDelegate: NSObject, NSApplicationDelegate {

    override init() {
        super.init()
        YourClass.initializeClass()
    }
}

对应的静态函数如下:

class YourClass {
    static func initializeClass() {
        // do stuff
    }
}

这将实现与initialize()相同的功能。

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