image

Swift SwiftPM compatible Build Tests

一个使用 @dynamicMemberLookup@dynamicCallable 以 Swifty 方式访问 Objective-C API 的库。

目录

简介

假设我们有以下私有的 Objective-C 类,我们想要在 Swift 中访问它

@interface Toolbar : NSObject
- (NSString *)titleForItem:(NSString *)item withTag:(NSString *)tag;
@end

有三种方法可以动态调用此类中的方法

1. 使用 performSelector()

let selector = NSSelectorFromString("titleForItem:withTag:")
let unmanaged = toolbar.perform(selector, with: "foo", with: "bar")
let result = unmanaged?.takeRetainedValue() as? String

2. 使用 methodForSelector()@convention(c)

typealias titleForItemMethod = @convention(c)
    (NSObject, Selector, NSString, NSString) -> NSString
  
let selector = NSSelectorFromString("titleForItem:withTag:")
let methodIMP = toolbar.method(for: selector)
let method = unsafeBitCast(methodIMP, to: titleForItemMethod.self)
let result = method(toolbar, selector, "foo", "bar")

3. 使用 NSInvocation

它仅在 Objective-C 中可用。
SEL selector = @selector(titleForItem:withTag:);
NSMethodSignature *signature = [toolbar methodSignatureForSelector:selector];

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = toolbar;
invocation.selector = selector;

NSString *argument1 = @"foo";
NSString *argument2 = @"bar";
[invocation setArgument:&argument1 atIndex:2];
[invocation setArgument:&argument2 atIndex:3];

[invocation invoke];

NSString *result;
[invocation getReturnValue:&result];

或者,我们可以使用 Dynamic 🎉

let result = Dynamic(toolbar)            // Wrap the object with Dynamic
    .titleForItem("foo", withTag: "bar") // Call the method directly!

关于库的设计原理和工作方式的更多详细信息,请点击此处

示例

Dynamic 的主要用例是在 Swift 中访问私有/隐藏的 iOS 和 macOS API。 随着 Mac Catalyst 的引入,访问隐藏 API 的需求随之出现,因为 Apple 仅向 Catalyst 应用程序开放了 macOS AppKit API 的一小部分。

接下来是一些示例,说明在 Mac Catalyst 中借助 Dynamic 访问 AppKit API 是多么容易。

1. 在 Mac Catalyst 应用程序中从 UIWindow 获取 NSWindow

extension UIWindow {
    var nsWindow: NSObject? {
        var nsWindow = Dynamic.NSApplication.sharedApplication.delegate.hostWindowForUIWindow(self)
        if #available(macOS 11, *) {
            nsWindow = nsWindow.attachedWindow
        }
        return nsWindow.asObject
    }
}

2. 在 Mac Catalyst 应用程序中进入全屏

// macOS App
window.toggleFullScreen(nil)

// Mac Catalyst (with Dynamic)
window.nsWindow.toggleFullScreen(nil)

3. 在 Mac Catalyst 应用程序中使用 NSOpenPanel

// macOS App
let panel = NSOpenPanel()
panel.beginSheetModal(for: view.window!, completionHandler: { response in
    if let url: URL = panel.urls.first {
        print("url: ", url)
    }
})

// Mac Catalyst (with Dynamic)
let panel = Dynamic.NSOpenPanel()
panel.beginSheetModalForWindow(self.view.window!.nsWindow, completionHandler: { response in
    if let url: URL = panel.URLs.firstObject {
        print("url: ", url)
    }
} as ResponseBlock)

typealias ResponseBlock = @convention(block) (_ response: Int) -> Void

4. 在 Mac Catalyst 应用程序中更改窗口缩放因子

Mac Catalyst 应用程序中的 iOS 视图会自动缩小到 77%。 要更改缩放因子,我们需要访问一个隐藏属性

override func viewDidAppear(_ animated: Bool) {
  view.window?.scaleFactor = 1.0 // Default value is 0.77
}

extension UIWindow {
  var scaleFactor: CGFloat {
    get {
      Dynamic(view.window?.nsWindow).contentView
        .subviews.firstObject.scaleFactor ?? 1.0
    }
    set {
      Dynamic(view.window?.nsWindow).contentView
        .subviews.firstObject.scaleFactor = newValue
    }
  }
}

安装

您可以使用 Swift Package Manager 来安装 Dynamic,方法是在您的 Package.swift 中添加它

let package = Package(
    dependencies: [
        .package(url: "https://github.com/mhdhejazi/Dynamic.git", branch: "master")
    ]
)

如何使用

下图显示了我们如何使用 Dynamic 从 Objective-C 对象 obj 访问私有属性和方法: Diagram

1. 包装 Objective-C 对象

为了使用 Objective-C 类和实例,我们需要首先使用 Dynamic 包装它们

包装现有对象

如果我们有对现有 Objective-C 对象的引用,我们可以简单地使用 Dynamic 包装它

let dynamicObject = Dynamic(objcObject)

创建新实例

要从隐藏类创建新实例,我们在其名称前加上 Dynamic(或 ObjC

// Objective-C:
[[NSDateFormatter alloc] init];

// Swift:
let formatter = Dynamic.NSDateFormatter()
// Or maybe:
let formatter = ObjC.NSDateFormatter()
// Or the longer form:
let formatter = ObjC.NSDateFormatter.`init`()

注意 1formatterDynamic 的一个实例,它包装了 NSDateFormatter 的新实例

注意 2ObjC 只是 Dynamic 的类型别名。 无论您选择使用哪个,请保持一致。

如果初始化器接受参数,我们可以直接传递它们

// Objective-C:
[[NSProgress alloc] initWithParent:foo userInfo:bar];

// Swift:
let progress = Dynamic.NSProgress(parent: foo, userInfo: bar)
// Or the longer form:
let progress = Dynamic.NSProgress.initWithParent(foo, userInfo: bar)

两种形式是等效的,因为库在第一种情况下将前缀 initWith 添加到方法选择器。 如果您选择使用较短的形式,请记住您只能从原始初始化器名称中删除前缀 initWithinitWith 之后的内容应该是第一个参数的标签。

单例

访问单例也很简单

// Objective-C:
[NSApplication sharedApplication];

// Swift:
let app = Dynamic.NSApplication.sharedApplication()
// Or we can drop the parenthesizes, as if `sharedApplication` was a static property:
let app = Dynamic.NSApplication.sharedApplication

重要提示: 虽然语法看起来与 Swift API 非常相似,但它并不总是与所用 API 的 Swift 版本相同。 例如,上面单例在 Swift 中的名称是 shared 而不是 sharedApplication,但我们在这里只能使用 sharedApplicaton,因为我们在内部与 Objective-C 类进行交互。 始终参考您尝试调用的方法的 Objective-C 文档,以确保您使用正确的名称。

2. 调用私有 API

在包装 Objective-C 对象之后,我们现在可以直接从 Dynamic 对象访问其属性和方法。

访问属性

// Objective-C:
@interface NSDateFormatter {
  @property(copy) NSString *dateFormat;
}

// Swift:
let formatter = Dynamic.NSDateFormatter()
// Getting the property value:
let format = formatter.dateFormat // `format` is now a Dynamic object
// Setting the property value:
formatter.dateFormat = "yyyy-MM-dd"
// Or the longer version:
formatter.dateFormat = NSString("yyyy-MM-dd")

注意 1: 上面的变量 format 现在是一个 Dynamic 对象,它包装了实际的属性值。 返回 Dynamic 对象而不是实际值的原因是为了允许调用链。 我们稍后将看到如何从 Dynamic 对象中解包实际值。

注意 2: 尽管属性 NSDateFormatter.dataFormat 的类型为 NSString,但我们可以将其设置为 Swift String,库会自动将其转换为 NSString

调用方法

let formatter = Dynamic.NSDateFormatter()
let date = formatter.dateFromString("2020 Mar 30") // `date` is now a Dynamic object
// Objective-C:
[view resizeSubviewsWithOldSize:size];
[view beginPageInRect:rect atPlacement:point];

// Swift:
view.resizeSubviewsWithOldSize(size) // OR ⤸
view.resizeSubviews(withOldSize: size)

view.beginPageInRect(rect, atPlacement: point) // OR ⤸
view.beginPage(inRect: rect, atPlacement: point)

以不同形式调用相同的方法是可能的,因为库将方法名称(例如 resizeSubviews)与第一个参数标签(例如 withOldSize)组合以形成方法选择器(例如 resizeSubviewsWithOldSize:)。 这意味着您也可以调用: view.re(sizeSubviewsWithOldSize: size),但请不要这样做。

Objective-C 代码块参数

要为代码块参数传递 Swift 闭包,我们需要将 @convention(block) 添加到闭包类型,然后将传递的闭包强制转换为此类型。

// Objective-C:
- (void)beginSheetModalForWindow:(NSWindow *)sheetWindow 
               completionHandler:(void (^)(NSModalResponse returnCode))handler;

// Swift:
let panel = Dynamic.NSOpenPanel.openPanel()
panel.beginSheetModal(forWindow: window, completionHandler: { result in
    print("result: ", result)
} as ResultBlock)

typealias ResultBlock = @convention(block) (_ result: Int) -> Void

3. 解包结果

方法和属性默认返回 Dynamic 对象,以便可以进行链式调用。 当需要实际值时,可以通过多种方式解包它

隐式解包

可以通过简单地指定我们将结果分配给的变量的类型来隐式解包值。

let formatter = Dynamic.NSDateFormatter()
let date: Date? = formatter.dateFromString("2020 Mar 30") // Implicitly unwrapped as Date?
let format: String? = formatter.dateFormat // Implicitly unwrapped as String?

let progress = Dynamic.NSProgress()
let total: Int? = progress.totalUnitCount // Implicitly unwrapped as Int?

请注意,我们应始终对变量类型使用可空类型(Optional),否则我们可能会看到编译器错误

let total = progress.totalUnitCount // No unwrapping. `total` is a Dynamic object
let total: Int? = progress.totalUnitCount // Implicit unwrapping as Int?
let total: Int = progress.totalUnitCount // Compiler error
let total: Int = progress.totalUnitCount! // Okay, but dangerous

分配给可选类型的变量不是隐式解包值的唯一方法。 其他方法包括返回方法调用的结果或将其与可选类型的变量进行比较。

请注意,隐式解包仅适用于属性和方法调用,因为编译器可以根据预期类型选择适当的重载方法。 当我们简单地返回 Dynamic 变量或将其分配给另一个变量时,情况并非如此

// This is okay:
let format: Date? = formatter.dateFromString("2020 Mar 30")

// But this is not:
let dynamicObj = formatter.dateFromString("2020 Mar 30")
let format: Date? = dynamicObj // Compiler error

显式解包

我们还可以通过调用 as<Type> 属性之一来显式解包值

Dynamic.NSDateFormatter().asObject // Returns the wrapped value as NSObject?
formatter.dateFormat.asString // Returns the wrapped value as String?
progress.totalUnitCount.asInt // Returns the wrapped value as Int?

并且有许多属性用于不同类型的值

var asAnyObject: AnyObject? { get }
var asValue: NSValue? { get }
var asObject: NSObject? { get }
var asArray: NSArray? { get }
var asDictionary: NSDictionary? { get }
var asString: String? { get }
var asFloat: Float? { get }
var asDouble: Double? { get }
var asBool: Bool? { get }
var asInt: Int? { get }
var asSelector: Selector? { get }
var asCGPoint: CGPoint? { get }
var asCGVector: CGVector? { get }
var asCGSize: CGSize? { get }
var asCGRect: CGRect? { get }
var asCGAffineTransform: CGAffineTransform? { get }
var asUIEdgeInsets: UIEdgeInsets? { get }
var asUIOffset: UIOffset? { get }
var asCATransform3D: CATransform3D? { get }

边缘情况

无法识别的方法和属性

如果您尝试访问未定义的属性或方法,应用程序不会崩溃,但您会获得包装在 Dynamic 对象中的 InvocationError.unrecognizedSelector。 您可以使用 Dynamic.isError 检查此类错误。

let result = Dynamic.NSDateFormatter().undefinedMethod()
result.isError // -> true

您还将在控制台中看到警告

WARNING: Trying to access an unrecognized member: NSDateFormatter.undefinedMethod

请注意,如果您将意外类型的随机参数传递给不期望它们的方法,则可能会发生预期的崩溃。

将属性设置为 nil

您可以使用以下方法之一将属性设置为 nil

formatter.dateFormat = .nil           // The custom Dynamic.nil constant
formatter.dateFormat = nil as String? // A "typed" nil
formatter.dateFormat = String?.none   // The Optional.none case

日志记录

了解幕后发生的事情总是好的 - 无论是为了调试问题还是仅仅出于好奇。 要启用详细日志记录,只需将 loggingEnabled 属性更改为 true

Dynamic.loggingEnabled = true

要求

Swift: 5.0

Dynamic 使用 Swift 5 中引入的 @dynamicCallable 属性。

贡献

请随时贡献 pull request,或为错误和功能请求创建 issue。

作者

Mhd Hejazi