ios macos_以样式调用ios和macos隐藏的api

ios macos

Making private API visible in Swift is a tedious job. You have to find the private headers, copy them to your project, create a bridging header file, and import the private headers. Or you can use message sending techniques to perform a method selector on a target object, and extract the returned value and convert it to a Swift type.

在Swift中使私有API可见是一项繁琐的工作。 您必须找到专用头,将它们复制到项目中,创建桥接头文件,然后导入专用头。 或者,您可以使用消息发送技术对目标对象执行方法选择器,然后提取返回的值并将其转换为Swift类型。

In this post, we’ll see how we can use the Swift attributes @dynamicMemberLookup and @dynamicCallable to create a wrapper around Objective-C classes and objects, and then use method invocation to access their properties and methods. By doing so, we’ll be able to call private API in a far more simple and intuitive way, and pure Swift.

在本文中,我们将看到如何使用Swift属性@dynamicMemberLookup@dynamicCallable围绕Objective-C类和对象创建包装,然后使用方法调用来访问它们的属性和方法。 这样,我们将能够以更简单,直观的方式以及纯Swift调用私有API。

背景 (Background)

Assume we have the following private Objective-C class that we want to access in Swift:

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

Image for post
Toolbar class defined in Objective-C
Objective-C中定义的工具栏类

There are three ways to dynamically call the method in this class:

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

1. Using performSelector()

1.使用 performSelector()

Image for post

But it comes with multiple limitations:

但是它具有多个限制:

  • You can’t use it if one of the parameters, or the return type, is a number, boolean, or a struct value. It only works with objects since its parameters and return type are defined as NSObject *.

    如果参数之一或返回类型是数字,布尔值或结构值,则不能使用它。 它仅适用于对象,因为其参数和返回类型定义为NSObject *

  • You also can’t use performSelector() if the method you want to call has more than two parameters.

    如果要调用的方法具有两个以上的参数,则也不能使用performSelector()

2. Using methodForSelector() with @convention(c)

2.将 methodForSelector() @convention(c)

Image for post

This is a more flexible solution, but you have to define the method type and the selector, and cast the method implementation to the defined type for every API call you want to make.

这是一种更灵活的解决方案,但是您必须定义方法类型和选择器,并针对要进行的每个API调用将方法实现转换为已定义的类型。

3. Using NSInvocation

3.使用 NSInvocation

This class is only available in Objective-C and can’t be used from Swift, so the example will be in Objective-C:

此类仅在Objective-C中可用,不能在Swift中使用,因此该示例将在Objective-C中:

Image for post

Every solution mentioned above has its limitations, not to mention how ugly and clunky all of them are. We need a better solution that allows us to write cleaner code with minimal syntax. Something like this:

上面提到的每个解决方案都有其局限性,更不用说它们都是多么丑陋和笨拙。 我们需要一个更好的解决方案,使我们能够以最少的语法编写更简洁的代码。 像这样:

Image for post

But there is no existing solution like that, so let’s create one!

但是,没有像这样的现有解决方案,所以让我们创建一个!

动态解决方案 (The dynamic solution)

What we’re trying to do now is creating a class that will wrap the target object and allows us to call the method directly from it as if the method was actually defined in the wrapper class itself.

现在,我们正在尝试创建一个类,该类将包装目标对象,并允许我们直接从该对象调用该方法,就像该方法实际上是在包装类本身中定义的一样。

But how will the compiler allow us to access a property or call a method that is not defined in the first place?

但是编译器将如何允许我们访问属性或调用最初未定义的方法?

Well, thanks to the latest additions in Swift 4.2 and Swift 5.0, this is now possible with @dynamicMemberLookup and @dynamicCallable attributes.

好吧,多亏了Swift 4.2和Swift 5.0中的最新功能,现在可以通过@dynamicMemberLookup@dynamicCallable属性来实现这一点。

1. @dynamicMemberLookup (1. @dynamicMemberLookup)

This attribute allows using the dot syntax to access arbitrary properties. With this attribute, the access to an undefined property is translated to a call to the dynamic member subscript.

此属性允许使用点语法来访问任意属性。 使用此属性,对未定义属性的访问将转换为对动态成员下标的调用。

Let’s add it to our wrapper class:

让我们将其添加到包装类中:

Image for post

Now, with this attribute, we can write something like dynamic.foo and the compiler will convert this unknown property access into a call to our dynamic subscript getter:

现在,使用此属性,我们可以编写诸如dynamic.foo之类的东西,编译器会将这种未知属性访问转换为对我们的动态下标getter的调用:

dynamic[dynamicMember: "foo"]

dynamic[dynamicMember: "foo"]

Note how we made the subscript returns a Dynamic object which will allow us to chain calls and do things like dynamic.foo.bar.

请注意,如何使下标返回一个Dynamic对象,该对象将允许我们链接调用并执行诸如dynamic.foo.bar

2. @dynamicCallable (2. @dynamicCallable)

This attribute marks a type as being “callable”. Instances of a callable type can be treated as functions that could be “called” directly by adding () after the object name. And the compiler will understand that as a call to a special dynamicallyCall() method we define:

此属性将类型标记为“可调用”。 可以将可调用类型的实例视为可以通过在对象名称后添加()直接“调用”的函数。 编译器将理解,作为对特殊dynamicallyCall()方法的调用,我们定义:

Image for post

But if you look closely, you’ll notice that the compiler only passes the method arguments to dynamicallyCall() method, so how will we know the method name?

但是,如果仔细观察,您会注意到编译器仅将方法参数传递给dynamicallyCall()方法,那么我们如何知道方法名称?

Well, we need to understand first how the compiler translates the complete method call statement.

好吧,我们需要首先了解编译器如何转换完整的方法调用语句。

Assuming that we are calling dynamic.show(item), the compiler will translate this statement to two calls:

假设我们正在调用dynamic.show(item) ,则编译器会将此语句转换为两个调用:

Image for post
How a dynamic method call is translated by the Swift compiler
Swift编译器如何翻译动态方法调用

(1) The dynamic subscript is called first with “show” as the member name.

(1)首先以“ show”作为成员名称调用动态下标。

Remember that our dynamic subscript returns a Dynamic object.

请记住,我们的动态下标返回一个Dynamic对象。

(2) The dynamicallyCall() method is then called from the newDynamic object returned from (1).

(2)然后从(1)返回的新Dynamic对象中调用dynamicallyCall()方法。

Note that we are unable to tell in (1) whether the member name is for a property or a method, and we’ll need to wait until the next dynamic call:

请注意,我们无法在(1)中判断成员名称是用于属性还是用于方法,我们需要等到下一个动态调用:

  • If the dynamic subscript is called again, we know it was a property name.

    如果动态下标再次被调用,我们知道它是一个属性名称。
  • if dynamicallyCall() is called, then it was a method name.

    如果dynamicallyCall()被调用,则它是一个方法名称。

This means we’ll always have to delay accessing the actual property from the wrapped object until we are sure the member name was indeed for a property, not a method.

这意味着我们总是必须延迟从包装的对象访问实际属性,直到我们确定成员名称确实是针对某个属性而非方法的。

Image for post

By adding @dynamicMemberLookup and @dynamicCallable attributes, we can now call arbitrary methods from arbitrary properties: dynamic.foo.bar().

通过添加@dynamicMemberLookup@dynamicCallable属性,我们现在可以从任意属性( dynamic.foo.bar()调用任意方法。

3.方法调用 (3. Method invocation)

To complete our solution, we only still have to invoke the actual method from the wrapped object. To do so, we need a solution that allows us to call arbitrary methods with an arbitrary number of arguments.

为了完成我们的解决方案,我们只需要从包装的对象中调用实际方法。 为此,我们需要一个解决方案,允许我们使用任意数量的arguments调用任意方法

The first option described above using performSelector() is rejected already since it only allows us to call methods that take no more than two object arguments.

上面描述的使用performSelector()的第一个选项已经被拒绝,因为它只允许我们调用不超过两个对象参数的方法。

The second option was using methodForSelector() with @convention(c). But to use this solution, one must know in advance the method signature and define its type in code. But this is exactly the opposite of what we're trying to do here — that is calling a method with no prior knowledge of its signature. So this option is also rejected.

第二个选项是将methodForSelector()@convention(c) 。 但是要使用这种解决方案,必须事先知道方法签名并在代码中定义其类型。 但这恰恰与我们在此试图做的相反–调用的方法没有其签名的先验知识。 因此,该选项也被拒绝。

This leaves us with the third and last option, NSInvocation. But, as I mentioned above, the class is not even available in Swift. So, how are we going to use it?

这给我们提供了第三个也是最后一个选项NSInvocation 。 但是,如上所述,该类甚至在Swift中都不可用。 那么,我们将如何使用它呢?

Well, we’ll “port” it to Swift ourselves! We can do that by dynamically creating instances of this class, and dynamically calling its methods which will eventually allow us to perform the actual method we are trying to call.

好吧,我们将其“移植”到Swift自己! 我们可以通过动态创建此类的实例并动态调用其方法来实现,最终将使我们能够执行我们尝试调用的实际方法。

But how can we dynamically call its methods if that's what we're trying to do here in the first place? Well, we can use the second option to define the already known methods of NSInvocation, and call those methods from the dynamically created instance.

但是,如果这是我们首先要在此处进行的操作,我们如何动态调用其方法? 好了,我们可以使用第二个选项来定义NSInvocation已知方法,并从动态创建的实例中调用这些方法。

I will spare you the details here, but you can find the source code of Invocation, the Swift version of NSInvoation in the GitHub repo mentioned at the end of this post.

我将在这里为您NSInvoation详细信息,但是您可以在本文结尾处提到的GitHub存储库中找到Invocation的源代码,即NSInvoation的Swift版本。

And now that we have the Invocation class, we can write the remaining methods:

现在有了Invocation类,我们可以编写其余方法:

Image for post

动态-库 (Dynamic — the library)

I improved the Dynamic class and added many features to simplify working with hidden Objective-C classes in Swift furthermore, and converted all of that into a standalone library I published on GitHub under the same name.

我改进了Dynamic类,并添加了许多功能以进一步简化在Swift中使用隐藏的Objective-C类的工作,并将所有这些都转换为我在GitHub上以相同名称发布的独立库。

Image for post

用例和范例 (Use cases & examples)

The main use cases for Dynamic is accessing private iOS and macOS API in Swift. And with the introduction of Mac Catalyst, the need to access hidden API arose as Apple only made a very small portion of the macOS AppKit API visible to Catalyst apps.

Dynamic的主要用例是在Swift中访问私有的iOS和macOS API。 随着Mac Catalyst的引入,对访问隐藏API的需求增加了,因为Apple仅使Catalyst应用程序可以看到macOS AppKit API的一小部分。

What follows are examples of how easy it is to access AppKit API in a Mac Catalyst with the help of Dynamic.

以下是在Dynamic的帮助下在Mac Catalyst中访问AppKit API多么容易的示例。

1. Enter fullscreen

1.进入全屏

Note the difference in some member names (shared vs sharedApplication). The reason is that in Dynamic we’re accessing the Objective-C API, not the Swift version of it. So one has to check the Objective-C method signature from Apple Docs.

注意某些成员名称( sharedsharedApplication )之间的差异。 原因是在Dynamic中,我们正在访问Objective-C API,而不是它的Swift版本。 因此,必须检查一下Apple Docs的Objective-C方法签名。

Image for post
Swift method signature
Swift方法签名
Image for post
Objective-C method signature
Objective-C方法签名

Another difference we can spot is the missing optional chaining operator ? in the second code snippet. The reason is that Dynamic always returns a value when a property is accessed or a method is called. This eliminates the need for dealing with nils as they will always be wrapped with a Dynamic object.

我们可以发现的另一个区别是缺少可选的链接运算符? 在第二个代码段中。 原因是当访问属性或调用方法时,Dynamic总是返回一个值。 这消除了处理nil的需要,因为它们总是被Dynamic对象包装。

2. Get the NSWindow from a UIWindow

2. UIWindow 获取 NSWindow

3. Using NSOpenPanel

3.使用 NSOpenPanel

4. Change the window scale factor

4.更改窗口比例因子

iOS views in Mac Catalyst apps are automatically scaled down to 77%. To change the scale factor we need to access a hidden property.

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

奖励:元调用! (Bonus: Meta Invocation!)

Now that we have theDynamic library, we can create instances of the hidden class NSInvocation and call its methods directly in Swift:

现在有了Dynamic库,我们可以创建隐藏类NSInvocation实例,并直接在Swift中调用其方法:

Image for post

We’re creating an instance of NSInvocation and calling its methods with the help of Dynamic class that uses Invocation, the Swift version of NSInvocation!

我们正在创建NSInvocation的实例,并在使用Invocation ( NSInvocation的Swift版本)的Dynamic类的帮助下调用其方法!

I hope this article was helpful. If you have any questions or feedback, feel free to leave a response.

希望本文对您有所帮助。 如果您有任何问题或反馈,请随时回复。

翻译自: https://medium.com/swlh/calling-ios-and-macos-hidden-api-in-style-1a924f244ad1

ios macos

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值