【问题标题】:Is there an alternative to initialize() in macOS now that Swift has deprecated it?现在 Swift 已经弃用了 macOS 中的 initialize() ,是否有替代方法?
【发布时间】:2018-02-28 12:23:18
【问题描述】:

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

这是我以前做的:

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

如果没有initialize,我如何实现同样的目标?

我需要它作为一个框架,所以要求在 AppDelegate 中调用某些东西是不行的。我需要在applicationDidFinishLaunching 之前调用它。

我真的很喜欢this 解决方案。这正是我正在寻找的,但它适用于 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。

【问题讨论】:

  • 在您引用的代码中将UIApplication 替换为NSApplication 怎么样?
  • @SindreSorhus 也许你可以给我们一些你试图实现的具体例子。否则问题有点宽泛。
  • 你说你需要在applicationDidFinishLaunching之前调用它,但是不能保证在applicationDidFinishLaunching之前调用Objective-C的initialized,所以也许你正在寻找别的东西。
  • 您提供的解决方案并不完全符合您的要求。它们碰巧是靠运气工作的,而且并不健壮(例如,您打赌在应用程序完成启动之前创建了一个NSView;很常见,但没有承诺)。如果您可以控制 NIB 文件或NSApplicationMain,则无需任何技巧即可轻松解决此问题。使用 ObjC 文件很容易解决(甚至不需要 Objective-C 桥;您可以直接在项目中放置一个 ObjC 文件)。目前尚不清楚您为什么要放弃所有琐碎的方法以及您的限制是什么。
  • @SindreSorhus 快速提问 - 你真的需要相同的 initialize() 行为,还是只需要一种在 appDidFinishLaunching 之前执行代码的方法?

标签: swift macos swizzling


【解决方案1】:

编辑:自从我写了这个答案后,OP 在编辑中添加了“纯 Swift”到问题中。但是,我将这个答案留在这里,因为在撰写本文时它仍然是唯一正确的方法。希望在 Swift 6 或 7 或 8 中添加模块初始化挂钩,但截至 2018 年 3 月,纯 Swift 是 the wrong tool 对于这个用例。

原答案如下:

不幸的是,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 之前的某个时间将“应用程序将启动”记录到控制台。

或者,如果您的模块中已经有一个公共 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

【讨论】:

  • 感谢您的努力,但我正在寻找一个纯粹的 Swift 解决方案。我已经在问题中链接到一个纯 Swift 解决方案,但它只适用于 iOS,所以我正在寻找一个 macOS 适应它。
  • @SindreSorhus 链接解决方案中的 hack 是不可取的,原因有两个:1)如果两个单独的框架试图覆盖 NSApplication 上的 nextResponder,其中一个会被踩到,它的undefined 它将是哪一个,以及 2) 如果 NSApplication 在 AppKit 的某个未来版本中获得自己的 nextResponder 覆盖,这将破坏它并导致谁知道有多少未定义的行为。很抱歉,但对于这个问题,没有没有积极有害的纯 Swift 解决方案。
  • @CharlesSrstka 您的解决方案与initialize 行为之间存在细微差别:initialized 在第一条消息发送到类之前被调用,而构造函数/加载方法执行代码一旦二进制文件加载到内存中。不过,差异可能并不重要,因为 OP 似乎希望在启动时执行一些代码。
  • @Cristik 我从字面上解释了“applicationDidFinishLaunching之前”的要求; applicationWillFinishLaunching 通知应该在applicationDidFinishLaunching 之前不久触发。如果 OP 想在启动时直接调用代码,也可以通过performSelector: 立即调用该方法,而不是通过NSNotificationCenter
  • @CharlesSrstka 是的,这句话应该如何解释,我唯一的想法是为什么 OP 对initialize 感兴趣?您发布的load-like 解决方案应提供“before didFinishLaunch”要求。
【解决方案2】:

不,不存在initialize() 的 Swift 替代方案,主要是因为 Swift 是静态调度的,因此无法拦截方法调用。而且真的不再需要了,因为该方法通常用于从 Objective-C 文件中初始化静态变量,而在 Swift 中,静态/全局变量总是惰性的,可以使用表达式的结果进行初始化(这在 Objective- 中是不可能的) C)。

即使有可能实现类似的东西,我也不鼓励你在框架用户不知情的情况下隐式运行东西。我建议在库中的某处添加一个configure 方法,并要求您的库的用户调用它。这样他们就可以控制初始化点。只有几件事比拥有一个我链接但不再(或尚未)使用的框架在未经我同意的情况下开始执行代码更糟糕。

让框架用户控制他们希望框架代码何时启动更为明智。如果确实您的代码必须在应用程序生命周期的最开始运行,那么请框架用户在任何其他调用之前调用您的框架入口点。正确配置您的框架也符合他们的利益。

例如:

MyFramework.configure(with: ...)
// or
MyManager.start()

【讨论】:

    【解决方案3】:

    正如其他人之前指出的那样,在 Swift 框架中完成您要求的事情是不可能的(也不是好的编程)。不过,从应用程序本身(这种行为所属的地方)实现功能相当简单 - 无需弄乱通知或选择器。您只需覆盖NSApplicationDelegate(或UIApplicationDelegate)的init 并在那里设置您的类初始化程序:

    class AppDelegate: NSObject, NSApplicationDelegate {
    
        override init() {
            super.init()
            YourClass.initializeClass()
        }
    }
    

    以及对应的静态函数:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-09-04
      • 2021-08-01
      • 2022-08-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多