一个使用 @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
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 是多么容易。
extension UIWindow {
var nsWindow: NSObject? {
var nsWindow = Dynamic.NSApplication.sharedApplication.delegate.hostWindowForUIWindow(self)
if #available(macOS 11, *) {
nsWindow = nsWindow.attachedWindow
}
return nsWindow.asObject
}
}
// macOS App
window.toggleFullScreen(nil)
// Mac Catalyst (with Dynamic)
window.nsWindow.toggleFullScreen(nil)
// 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
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
访问私有属性和方法:
为了使用 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`()
注意 1:
formatter
是Dynamic
的一个实例,它包装了NSDateFormatter
的新实例
注意 2:
ObjC
只是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
添加到方法选择器。 如果您选择使用较短的形式,请记住您只能从原始初始化器名称中删除前缀initWith
。initWith
之后的内容应该是第一个参数的标签。
访问单例也很简单
// 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 文档,以确保您使用正确的名称。
在包装 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
,但我们可以将其设置为 SwiftString
,库会自动将其转换为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)
,但请不要这样做。
要为代码块参数传递 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
方法和属性默认返回 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
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
Dynamic
使用 Swift 5 中引入的 @dynamicCallable
属性。
请随时贡献 pull request,或为错误和功能请求创建 issue。