绑定 Objective-C 库
使用 Xamarin.iOS 或 Xamarin.Mac 时,可能会遇到想要使用第三方 Objective-C 库的情况。 在这些情况下,可以使用 Xamarin 绑定项目创建本机 Objective-C 库的 C# 绑定。 项目使用将 iOS 和 Mac API 引入 C# 的相同工具。
本文档介绍如何绑定 Objective-C API,如果仅绑定 C API,则应为此 P/Invoke 框架使用标准 .NET 机制。 链接本机库页上提供了有关如何静态链接 C 库的详细信息。
请参阅我们的配套绑定类型参考指南。 此外,如果要详细了解后台发生的情况,请查看绑定概述页。
可为 iOS 和 Mac 库生成绑定。 本页介绍如何处理 iOS 绑定,但 Mac 绑定非常相似。
适用于 iOS 的示例代码
可以使用 iOS 绑定示例项目来试验绑定。
使用入门
创建绑定的最简单方法是创建 Xamarin.iOS 绑定项目。 可以通过选择项目类型 iOS > 库 > 绑定库,从 Visual Studio for Mac 执行此操作:
生成的项目包含可以编辑的小模板,其中包含两个文件:ApiDefinition.cs
和 StructsAndEnums.cs
。
ApiDefinition.cs
是定义 API 协定的位置,这是描述如何将基础 Objective-C API 投影到 C# 的文件。 此文件的语法和内容是讨论本文档的主要主题,其内容仅限于 C# 接口和 C# 委托声明。 StructsAndEnums.cs
文件是你将在其中输入接口和委托所需的任何定义的文件。 这包括代码可能使用的枚举值和结构。
绑定 API
若要执行全面的绑定,需要了解 Objective-C API 定义并熟悉 .NET Framework 设计指南。
若要绑定库,通常从 API 定义文件开始。 API 定义文件只是一个 C# 源文件,其中包含已被注释的 C# 接口,这些注释包含一些有助于驱动绑定的特性。 此文件定义 C# 与 Objective-C 之间的协定。
例如,这是库的一个简单 API 文件:
using Foundation;
namespace Cocos2D {
[BaseType (typeof (NSObject))]
interface Camera {
[Static, Export ("getZEye")]
nfloat ZEye { get; }
[Export ("restore")]
void Restore ();
[Export ("locate")]
void Locate ();
[Export ("setEyeX:eyeY:eyeZ:")]
void SetEyeXYZ (nfloat x, nfloat y, nfloat z);
[Export ("setMode:")]
void SetMode (CameraMode mode);
}
}
上面的示例定义了一个名为 Cocos2D.Camera
、派生自 NSObject
基类型(此类型来自 Foundation.NSObject
)的类,并定义了一个静态属性 (ZEye
)、两个无参数的方法和一个带三个参数的方法。
下面的 API 定义文件部分详细介绍了 API 文件格式和可以使用的特性。
若要生成完整的绑定,通常需处理四个组件:
- API 定义文件(模板中的
ApiDefinition.cs
)。 - 可选:API 定义文件(模板中的
StructsAndEnums.cs
)所需的任何枚举、类型、结构。 - 可选:可能扩展生成的绑定的额外源,或提供更友好的 C# API(添加到项目的任何 C# 文件)。
- 要绑定的本机库。
此图表显示文件之间的关系:
API 定义文件将仅包含命名空间和接口定义(包含接口可以包含的任何成员),不应包含类、枚举、委托或结构。 API 定义文件只是将用于生成 API 的协定。
任何所需的额外代码(如枚举或支持类)都应托管在单独的文件上,在上面的示例中,"CameraMode" 是 CS 文件中不存在的枚举值,应托管在单独的文件中,例如 StructsAndEnums.cs
:
public enum CameraMode {
FlyOver, Back, Follow
}
APIDefinition.cs
文件与 StructsAndEnum
类结合使用,用于生成库的核心绑定。 你可以直接使用生成的库,但通常,你希望调整生成的库,为用户添加一些 C# 功能。 一些示例包括实现 ToString()
方法、提供 C# 索引器、向某些本机类型添加隐式转换或提供某些方法的强类型版本。 这些改进存储在额外的 C# 文件中。 只需将 C# 文件添加到项目,它们将包含在此生成过程中。
下面显示如何在 Extra.cs
文件中实现代码。 请注意,你将使用分部类,因为这些类将扩充从 ApiDefinition.cs
和 StructsAndEnums.cs
核心绑定组合生成的分部类:
public partial class Camera {
// Provide a ToString method
public override string ToString ()
{
return String.Format ("ZEye: {0}", ZEye);
}
}
生成库将生成本机绑定。
若要完成此绑定,应将本机库添加到项目。 为此,可以将本机库添加到项目,方法是将本机库从 Finder 拖放到解决方案资源管理器中的项目,或者右键单击项目并选择“添加”>“添加文件”以选择本机库。 本机库按约定以 "lib" 开头,以扩展名 ".a" 结尾。 执行此操作时,Visual Studio for Mac 将添加两个文件:.a 文件和自动填充的 C# 文件,其中包含有关本机库包含的信息:
libMagicChord.linkwith.cs
文件的内容包含有关如何使用此库的信息,并指示 IDE 将此二进制文件打包到生成的 DLL 文件中:
using System;
using ObjCRuntime;
[assembly: LinkWith ("libMagicChord.a", SmartLink = true, ForceLoad = true)]
有关如何使用 [LinkWith]
特性的完整详细信息请参阅绑定类型参考指南。
现在,生成项目时,最终会得到一个包含绑定和本机库的 MagicChords.dll
文件。 可以将此项目或生成的 DLL 分发给其他开发人员供他们自己使用。
有时你可能会发现需要几个枚举值、委托定义或其他类型。 不要将这些项放在 API 定义文件中,因为这只是协定
API 定义文件
API 定义文件由多个接口组成。 API 定义中的接口将转换为类声明,并且必须使用 [BaseType]
特性进行修饰才能指定类的基类。
你可能想知道为什么我们没有使用类而不是协定定义的接口。 我们选择接口是因为它允许我们为方法编写协定,而无需在 API 定义文件中提供方法主体,也无需提供一个必须抛出异常或返回有意义的值的方法主体。
但是,由于我们将接口用作主干来生成类,因此必须使用特性修饰协定的各个部分来驱动绑定。
绑定方法
最简单的绑定是绑定方法。 只需使用 C# 命名约定在接口中声明一个方法,并修饰方法 [Export]
属性。 [Export]
特性是将 C# 名称与 Xamarin.iOS 运行时中的 Objective-C 名称链接在一起的内容。 该 [Export]
特性的参数是 Objective-C 选择器的名称。 以下是一些示例:
// A method, that takes no arguments
[Export ("refresh")]
void Refresh ();
// A method that takes two arguments and return the result
[Export ("add:and:")]
nint Add (nint a, nint b);
// A method that takes a string
[Export ("draw:atColumn:andRow:")]
void Draw (string text, nint column, nint row);
上面的示例演示如何绑定实例方法。 若要绑定静态方法,必须使用 [Static]
特性,如下所示:
// A static method, that takes no arguments
[Static, Export ("beep")]
void Beep ();
这是必需的,因为协定是接口的一部分,并且接口没有静态声明与实例声明的概念,因此必须再次求助于特性。 如果要从绑定中隐藏特定方法,可以使用 [Internal]
特性修饰方法。
btouch-native
命令将引入对引用参数的检查,以确保它们不为 null。 如果要允许特定参数的 null 值,请对参数使用 [NullAllowed]
特性,如下所示:
[Export ("setText:")]
string SetText ([NullAllowed] string text);
导出引用类型时,使用 [Export]
关键字,还可以指定分配语义。 这是确保不会泄露任何数据所必需的。
绑定属性
与方法一样,Objective-C 属性使用 [Export]
特性绑定并直接映射到 C# 属性。 与方法一样,可以使用 [Static]
和 [Internal]
特性修饰。
对 btouch-native 下的属性使用 [Export]
特性时,实际上绑定了两种方法:getter 和 setter。 要导出的名称是 basename,而 setter 是通过前面附加 "set" 来计算的,将 basename 的第一个字母成大写,并使选择器采用参数。 这意味着属性上应用的 [Export ("label")]
实际上绑定了 "label" and "setLabel:" Objective-C 方法。
有时,Objective-C 属性不遵循上述模式,并且名称将被手动覆盖。 在这些情况下,可以在 getter 或 setter 上使用 [Bind]
特性控制绑定的生成方式,例如:
[Export ("menuVisible")]
bool MenuVisible { [Bind ("isMenuVisible")] get; set; }
然后,这会绑定 "isMenuVisible" 和 "setMenuVisible:"。 (可选)可以使用以下语法绑定属性:
[Category, BaseType(typeof(UIView))]
interface UIView_MyIn
{
[Export ("name")]
string Name();
[Export("setName:")]
void SetName(string name);
}
在上述 name
和 setName
绑定中显式定义 getter 和 setter 的位置。
除了支持使用 [Static]
的静态属性外,还可以使用 [IsThreadStatic]
修饰线程静态属性,例如:
[Export ("currentRunLoop")][Static][IsThreadStatic]
NSRunLoop Current { get; }
就像方法允许使用 [NullAllowed]
标记某些参数一样,可以应用 [NullAllowed]
到属性,以指示 null 是属性的有效值,例如:
[Export ("text"), NullAllowed]
string Text { get; set; }
也可以直接在 setter 上指定 [NullAllowed]
参数:
[Export ("text")]
string Text { get; [NullAllowed] set; }
绑定自定义控件的注意事项
为自定义控件设置绑定时,应考虑以下注意事项:
- 绑定属性必须是静态的 - 定义属性的绑定时,必须使用
[Static]
特性。 - 属性名称必须完全匹配 - 用于绑定属性的名称必须与自定义控件中的属性名称完全匹配。
- 属性类型必须完全匹配 - 用于绑定属性的变量类型必须与自定义控件中的属性类型完全匹配。
- 断点和 getter/setter - 将永远不会命中属性的 getter 或 setter 方法中的断点。
- 观察回叫 - 需要使用观察回叫来通知自定义控件属性值的更改。
如果无法观察到上述任何警告,可能会导致绑定在运行时以无提示方式失败。
Objective-C 可变模式和属性
Objective-C 框架使用惯用法,其中某些类与可变子类不可变。 例如,NSString
是不可变的版本,而 NSMutableString
是允许突变的子类。
在这些类中,通常可以看到不可变基类包含具有 getter 但没有 setter 的属性。 还有用于引入 setter 的可变版本。 由于 C# 这并非真正可行,因此我们必须将此惯用法映射到可与 C# 一起使用的惯用法。
映射到 C# 的方式是同时在基类上添加 getter 和 setter,但会标记 setter [NotImplemented]
属性。
然后,在可变子类上,对属性使用 [Override]
特性,以确保该属性实际覆盖父级的行为。
示例:
[BaseType (typeof (NSObject))]
interface MyTree {
string Name { get; [NotImplemented] set; }
}
[BaseType (typeof (MyTree))]
interface MyMutableTree {
[Override]
string Name { get; set; }
}
绑定构造函数
对于给定类 Foo
, btouch-native
工具将自动生成类中的四个构造函数,它将生成:
Foo ()
:默认构造函数(映射到 Objective-C的 "init" 构造函数)Foo (NSCoder)
:NIB 文件反序列化期间使用的构造函数(映射到 Objective-C的 "initWithCoder:" 构造函数)。Foo (IntPtr handle)
:基于句柄的创建构造函数,当运行时需要从非托管对象公开托管对象时,运行时会调用此构造函数。Foo (NSEmptyFlag)
:派生类使用此类型来防止双重初始化。
对于定义的构造函数,需要使用接口定义中的以下签名声明它们:它们必须返回 IntPtr
值,并且方法的名称应为构造函数。 例如,若要绑定 initWithFrame:
构造函数,可以使用以下命令:
[Export ("initWithFrame:")]
IntPtr Constructor (CGRect frame);
绑定协议
如 API 设计文档中所述,在讨论模型和协议部分中,Xamarin.iOS 将 Objective-C 协议映射到已标记的类 [Model]
属性。 这通常在实现 Objective-C 委托类时使用。
常规绑定类和委托类之间的很大区别在于委托类可能具有一个或多个可选方法。
例如,考虑 UIKit
类 UIAccelerometerDelegate
,这就是它在 Xamarin.iOS 中绑定的方式:
[BaseType (typeof (NSObject))]
[Model][Protocol]
interface UIAccelerometerDelegate {
[Export ("accelerometer:didAccelerate:")]
void DidAccelerate (UIAccelerometer accelerometer, UIAcceleration acceleration);
}
由于这是 UIAccelerometerDelegate
定义的可选方法,因此没有其他需要做的。 但是,如果协议上存在必需的方法,则应添加 [Abstract]
特性到方法。 这将强制实现的用户实际为该方法提供正文。
一般情况下,协议在响应消息的类中使用。 这通常通过在 Objective-C 中向 "delegate" 属性分配响应协议中方法的对象实例来完成。
Xamarin.iOS 中的约定是支持 Objective-C 松散耦合样式,其中任何 NSObject
实例都可以分配给委托,并公开它的强类型版本。 因此,我们通常同时提供强类型的 Delegate
属性和松散类型的 WeakDelegate
。 我们通常将松散类型版本与 [Export]
绑定,并使用 [Wrap]
特性来提供强类型版本。
下面显示如何绑定 UIAccelerometer
类:
[BaseType (typeof (NSObject))]
interface UIAccelerometer {
[Static] [Export ("sharedAccelerometer")]
UIAccelerometer SharedAccelerometer { get; }
[Export ("updateInterval")]
double UpdateInterval { get; set; }
[Wrap ("WeakDelegate")]
UIAccelerometerDelegate Delegate { get; set; }
[Export ("delegate", ArgumentSemantic.Assign)][NullAllowed]
NSObject WeakDelegate { get; set; }
}
MonoTouch 7.0 中的新增功能
从 MonoTouch 7.0 开始,已合并新的和改进的协议绑定功能。 这一新的支持使得在给定类中采用一个或多个协议时,使用 Objective-C 惯用法更为简单。
对于 Objective-C 中的每个协议定义 MyProtocol
,现在有一个 IMyProtocol
接口列出协议中的所有必需方法,以及提供所有可选方法的扩展类。 与 Xamarin Studio 编辑器中的新支持相结合,开发人员无需使用以前的抽象模型类的单独子类即可实现协议方法。
包含 [Protocol]
特性的任何定义实际上都会生成三个支持类,这些类极大地改善了使用协议的方式:
// Full method implementation, contains all methods
class MyProtocol : IMyProtocol {
public void Say (string msg);
public void Listen (string msg);
}
// Interface that contains only the required methods
interface IMyProtocol: INativeObject, IDisposable {
[Export ("say:")]
void Say (string msg);
}
// Extension methods
static class IMyProtocol_Extensions {
public static void Optional (this IMyProtocol this, string msg);
}
}
类实现 提供了一个完整的抽象类,可以替代各个方法并获取完整类型安全性。 但是,由于 C# 不支持多个继承,因此在某些情况下,可能需要使用不同的基类,但仍希望实现接口,即
生成的接口定义传入。 它是一个接口,其中包含协议中的所有必需方法。 这使想要实现协议的开发人员只需实现该接口即可。 运行时会自动将该类型注册为采用协议。
请注意,接口仅列出所需的方法,并公开可选方法。 这意味着采用协议的类将获取所需方法的完整类型检查,但必须求助于弱类型化(手动使用 [Export]
特性和匹配签名)作为可选协议方法。
为了方便使用使用协议的 API,绑定工具还将生成一个扩展方法类,该类公开所有可选方法。 这意味着,只要使用 API,就可以将协议视为具有所有方法。
如果要在 API 中使用协议定义,则需要在 API 定义中编写主干空接口。 如果要在 API 中使用 MyProtocol,则需要执行以下操作:
[BaseType (typeof (NSObject))]
[Model, Protocol]
interface MyProtocol {
// Use [Abstract] when the method is defined in the @required section
// of the protocol definition in Objective-C
[Abstract]
[Export ("say:")]
void Say (string msg);
[Export ("listen")]
void Listen ();
}
interface IMyProtocol {}
[BaseType (typeof(NSObject))]
interface MyTool {
[Export ("getProtocol")]
IMyProtocol GetProtocol ();
}
之所以需要上述内容,是因为在绑定时 IMyProtocol
不存在,这就是需要提供空接口的原因。
采用协议生成的接口
每当你实现为协议生成的某一接口(如下所示)时:
class MyDelegate : NSObject, IUITableViewDelegate {
nint IUITableViewDelegate.GetRowHeight (nint row) {
return 1;
}
}
必需接口方法的实现将使用正确的名称导出,因此它等效于:
class MyDelegate : NSObject, IUITableViewDelegate {
[Export ("getRowHeight:")]
nint IUITableViewDelegate.GetRowHeight (nint row) {
return 1;
}
}
这将适用于所有必需的协议成员,但有一种特殊情况需要注意:存在可选的选择器。 使用基类时,可选协议成员的处理方式相同:
public class UrlSessionDelegate : NSUrlSessionDownloadDelegate {
public override void DidWriteData (NSUrlSession session, NSUrlSessionDownloadTask downloadTask, long bytesWritten, long totalBytesWritten, long totalBytesExpectedToWrite)
但在使用协议接口时,需要添加 [Export]。 当你从替代开始添加它时,IDE 会通过自动完成添加它。
public class UrlSessionDelegate : NSObject, INSUrlSessionDownloadDelegate {
[Export ("URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:")]
public void DidWriteData (NSUrlSession session, NSUrlSessionDownloadTask downloadTask, long bytesWritten, long totalBytesWritten, long totalBytesExpectedToWrite)
两者在运行时存在轻微的行为差异。
- 基类(例如 NSUrlSessionDownloadDelegate)的用户提供所有必需和可选的选择器,并返回合理的默认值。
- 接口(例如 INSUrlSessionDownloadDelegate)的用户仅响应已提供的确切选择器。
一些罕见的类在这里的行为可能有所不同。 但几乎所有情况下都可以安全地使用。
绑定类扩展
在 Objective-C ,可以使用新方法扩展类,这与 C# 的扩展方法类似。 当其中一种方法存在时,可以使用 [BaseType]
特性将该方法标记为 Objective-C 消息的接收方。
例如,在 Xamarin.iOS 中,当导入 UIKit
时,我们将在 NSString
上定义的扩展方法绑定为 NSStringDrawingExtensions
中的方法,如下所示:
[Category, BaseType (typeof (NSString))]
interface NSStringDrawingExtensions {
[Export ("drawAtPoint:withFont:")]
CGSize DrawString (CGPoint point, UIFont font);
}
绑定 Objective-C 参数列表
Objective-C 支持可变参数。 例如:
- (void) appendWorkers:(XWorker *) firstWorker, ...
NS_REQUIRES_NIL_TERMINATION ;
若要从 C# 调用此方法,需要创建签名,如下所示:
[Export ("appendWorkers"), Internal]
void AppendWorkers (Worker firstWorker, IntPtr workersPtr)
这会将该方法声明为内部方法,将上述 API 隐藏给用户,但将其公开给库。 然后,可以编写如下所示的方法:
public void AppendWorkers(params Worker[] workers)
{
if (workers is null)
throw new ArgumentNullException ("workers");
var pNativeArr = Marshal.AllocHGlobal(workers.Length * IntPtr.Size);
for (int i = 1; i < workers.Length; ++i)
Marshal.WriteIntPtr (pNativeArr, (i - 1) * IntPtr.Size, workers[i].Handle);
// Null termination
Marshal.WriteIntPtr (pNativeArr, (workers.Length - 1) * IntPtr.Size, IntPtr.Zero);
// the signature for this method has gone from (IntPtr, IntPtr) to (Worker, IntPtr)
WorkerManager.AppendWorkers(workers[0], pNativeArr);
Marshal.FreeHGlobal(pNativeArr);
}
绑定字段
有时,你需要访问在库中声明的公共字段。
通常,这些字段包含必须引用的字符串或整数值。 它们通常用作表示特定通知和字典中的键的字符串。
若要绑定字段,请将属性添加到接口定义文件中,并使用 [Field]
特性修饰该属性。 此特性采用一个参数:要查找的符号的 C 名称。 例如:
[Field ("NSSomeEventNotification")]
NSString NSSomeEventNotification { get; }
如果要在并非派生自 NSObject
的静态类中包装各种字段,可以在类上使用 [Static]
特性,如下所示:
[Static]
interface LonelyClass {
[Field ("NSSomeEventNotification")]
NSString NSSomeEventNotification { get; }
}
上述内容将生成一个并非派生自 NSObject
的 LonelyClass
,并将包含对公开为 NSString
的 NSSomeEventNotification
NSString
的绑定。
[Field]
特性可以应用于以下数据类型:
NSString
引用(只读属性)NSArray
引用(只读属性)- 32 位整数 (
System.Int32
) - 64 位整数 (
System.Int64
) - 32 位浮点 (
System.Single
) - 64 位浮点 (
System.Double
) System.Drawing.SizeF
CGSize
除了本机字段名称,还可以通过传递库名称来指定字段所在的库名称:
[Static]
interface LonelyClass {
[Field ("SomeSharedLibrarySymbol", "SomeSharedLibrary")]
NSString SomeSharedLibrarySymbol { get; }
}
如果要静态链接,则没有要绑定到的库,因此需要使用 __Internal
名称:
[Static]
interface LonelyClass {
[Field ("MyFieldFromALibrary", "__Internal")]
NSString MyFieldFromALibrary { get; }
}
绑定枚举
可以直接在绑定文件中添加 enum
,以便更轻松地在 API 定义中使用它们 - 无需使用不同的源文件(需要在绑定和最终项目中编译)。
示例:
[Native] // needed for enums defined as NSInteger in ObjC
enum MyEnum {}
interface MyType {
[Export ("initWithEnum:")]
IntPtr Constructor (MyEnum value);
}
还可以创建自己的枚举来替换 NSString
常量。 在这种情况下,生成器将自动创建方法来转换枚举值和 NSString 常量。
示例:
enum NSRunLoopMode {
[DefaultEnumValue]
[Field ("NSDefaultRunLoopMode")]
Default,
[Field ("NSRunLoopCommonModes")]
Common,
[Field (null)]
Other = 1000
}
interface MyType {
[Export ("performForMode:")]
void Perform (NSString mode);
[Wrap ("Perform (mode.GetConstant ())")]
void Perform (NSRunLoopMode mode);
}
在上面的示例中,你可能决定使用 [Internal]
特性修饰 void Perform (NSString mode);
。 这将隐藏基于常量的API,使其不被绑定使用者看到。
但这会限制类型子类,因为更好的 API 替代项会使用 [Wrap]
特性。 这些生成的方法不是 virtual
,即你将无法替代它们 - 这可能是一个合适的选择。
另一种选择是将原始的基于 NSString
的定义标记为 [Protected]
。 这将允许子类在需要时正常工作,包装的版本仍将有效并调用替代方法。
将 NSValue
、NSNumber
和 NSString
绑定到更好的类型
[BindAs]
属性允许将 NSNumber
、NSValue
和 NSString
(枚举)绑定到更准确的 C# 类型。 该属性可用于通过本机 API 创建更好、更准确的 .NET API。
可以使用 [BindAs]
修饰方法(返回值)、参数和属性。
唯一的限制是成员不得位于 [Protocol]
或 [Model]
接口内。
例如:
[return: BindAs (typeof (bool?))]
[Export ("shouldDrawAt:")]
NSNumber ShouldDraw ([BindAs (typeof (CGRect))] NSValue rect);
将会输出:
[Export ("shouldDrawAt:")]
bool? ShouldDraw (CGRect rect) { ... }
在内部,我们将执行 bool?
<->NSNumber
和 CGRect
<->NSValue
转换。
[BindAs]
还支持NSNumber
NSValue
数组和NSString
(枚举)。
例如:
[BindAs (typeof (CAScroll []))]
[Export ("supportedScrollModes")]
NSString [] SupportedScrollModes { get; set; }
将输出:
[Export ("supportedScrollModes")]
CAScroll [] SupportedScrollModes { get; set; }
CAScroll
是 NSString
支持的枚举,我们将提取正确的 NSString
值并处理类型转换。
请参阅 [BindAs]
文档以查看支持的转换类型。
绑定通知
通知是发布到 NSNotificationCenter.DefaultCenter
的消息,用作将消息从一个应用程序部分广播到另一个部分的机制。 开发人员通常使用 NSNotificationCenter 的 AddObserver 方法订阅通知。 当应用程序将消息发布到通知中心时,它通常包含存储在 NSNotification.UserInfo 字典中的有效负载。 此字典类型较弱,并且从中获取信息容易出错,因为用户通常需要在文档中阅读字典中可用的键以及可在字典中存储的值的类型。 有时,键的存在也用作布尔值。
Xamarin.iOS 绑定生成器为开发人员提供绑定通知的支持。 为此,你在一个已使用 [Notification]
属性标记的属性上设置 [Field]
特性(它可以是公共的或私有的)。
对于不带有效负载的通知,该属性可以不带参数使用;或者,你可以指定引用 API 定义中另一个接口的 System.Type
,通常其名称以 "EventArgs" 结尾。 生成器会将接口转换为子类 EventArgs
的类,并将包含其中列出的所有属性。 [Export]
属性应在 EventArgs 类中使用,列出用于查找 Objective-C 字典以提取值的键的名称。
例如:
interface MyClass {
[Notification]
[Field ("MyClassDidStartNotification")]
NSString DidStartNotification { get; }
}
上述代码将使用以下方法生成嵌套类 MyClass.Notifications
:
public class MyClass {
[..]
public Notifications {
public static NSObject ObserveDidStart (EventHandler<NSNotificationEventArgs> handler)
}
}
然后,代码的用户可以使用如下所示的代码轻松订阅发布到 NSDefaultCenter 的通知:
var token = MyClass.Notifications.ObserverDidStart ((notification) => {
Console.WriteLine ("Observed the 'DidStart' event!");
});
从 ObserveDidStart
返回的值可用于轻松停止接收通知,如下所示:
token.Dispose ();
或者可以调用 NSNotification.DefaultCenter.RemoveObserver 并传递令牌。 如果通知包含参数,则应指定帮助程序 EventArgs
接口,如下所示:
interface MyClass {
[Notification (typeof (MyScreenChangedEventArgs)]
[Field ("MyClassScreenChangedNotification")]
NSString ScreenChangedNotification { get; }
}
// The helper EventArgs declaration
interface MyScreenChangedEventArgs {
[Export ("ScreenXKey")]
nint ScreenX { get; set; }
[Export ("ScreenYKey")]
nint ScreenY { get; set; }
[Export ("DidGoOffKey")]
[ProbePresence]
bool DidGoOff { get; }
}
上述代码将生成一个具有 ScreenX
和 ScreenY
属性的 MyScreenChangedEventArgs
类,这些属性将分别使用键 "ScreenXKey" 和 "ScreenYKey" 从 NSNotification.UserInfo 字典中提取数据,并应用适当的转换。 [ProbePresence]
特性用于让生成器探测键是否在 UserInfo
中设置,而不是尝试提取值。 这用于键的存在就是值(通常是布尔值)的情况。
这样可以编写如下所示的代码:
var token = MyClass.NotificationsObserveScreenChanged ((notification) => {
Console.WriteLine ("The new screen dimensions are {0},{1}", notification.ScreenX, notification.ScreenY);
});
绑定类别
类别是一种 Objective-C 机制,用于扩展类中可用的方法和属性集。 实际上,它们用来在链接特定框架(例如 UIKit
)时扩展基类(例如 NSObject
)的功能,使其方法可用,但前提是链接了该新框架。 在其他情况下,它们用于按功能组织类中的特征。 它们与 C# 扩展方法类似。以下是类别在 Objective-C 中的样子:
@interface UIView (MyUIViewExtension)
-(void) makeBackgroundRed;
@end
如果在库中找到上述示例,则会使用方法 makeBackgroundRed
扩展 UIView
的实例。
若要绑定这些属性,可以在接口定义上使用 [Category]
属性。 使用 [Category]
特性时,[BaseType]
特性从用于指定要扩展的基类更改为要扩展的类型。
下面展示了如何绑定 UIView
扩展并将其转换为 C# 扩展方法:
[BaseType (typeof (UIView))]
[Category]
interface MyUIViewExtension {
[Export ("makeBackgroundRed")]
void MakeBackgroundRed ();
}
上述方法将创建包含 MakeBackgroundRed
扩展方法的 MyUIViewExtension
类。 这意味着你现在可以在任何 UIView
子类上调用 "MakeBackgroundRed",从而获得与在 Objective-C 上相同的功能。 在另外一些情况下,类别用于不扩展系统类,而是为了组织功能,纯粹是为了修饰目的。 类似于下面这样:
@interface SocialNetworking (Twitter)
- (void) postToTwitter:(Message *) message;
@end
@interface SocialNetworking (Facebook)
- (void) postToFacebook:(Message *) message andPicture: (UIImage*)
picture;
@end
虽然可以使用 [Category]
特性来实现这种声明修饰样式,但你也可以将它们全部添加到类定义中。 这两者达到的目的是一样的:
[BaseType (typeof (NSObject))]
interface SocialNetworking {
}
[Category]
[BaseType (typeof (SocialNetworking))]
interface Twitter {
[Export ("postToTwitter:")]
void PostToTwitter (Message message);
}
[Category]
[BaseType (typeof (SocialNetworking))]
interface Facebook {
[Export ("postToFacebook:andPicture:")]
void PostToFacebook (Message message, UIImage picture);
}
在这些情况下,将类别合并更为简洁:
[BaseType (typeof (NSObject))]
interface SocialNetworking {
[Export ("postToTwitter:")]
void PostToTwitter (Message message);
[Export ("postToFacebook:andPicture:")]
void PostToFacebook (Message message, UIImage picture);
}
绑定块
块是 Apple 引入的一种新构造,用于将C#匿名方法的功能等效项带到 Objective-C 中。 例如,NSSet
类现在公开此方法:
- (void) enumerateObjectsUsingBlock:(void (^)(id obj, BOOL *stop) block
上述说明声明一个名为 enumerateObjectsUsingBlock:
的方法,该方法采用名为 block
的参数。 此块类似于 C# 匿名方法,因为它支持捕获当前环境("this" 指针,访问局部变量和参数)。 在 NSSet
中的上述方法使用两个参数调用块:一个 NSObject
(id obj
部分)和一个指向布尔值的指针(BOOL *stop
部分)。
若要将此类 API 与 btouch 绑定,首先需要将块类型签名声明为 C# 委托,然后从 API 入口点引用它,如下所示:
// This declares the callback signature for the block:
delegate void NSSetEnumerator (NSObject obj, ref bool stop)
// Later, inside your definition, do this:
[Export ("enumerateObjectUsingBlock:")]
void Enumerate (NSSetEnumerator enum)
现在,代码可以从 C# 调用函数:
var myset = new NSMutableSet ();
myset.Add (new NSString ("Foo"));
s.Enumerate (delegate (NSObject obj, ref bool stop){
Console.WriteLine ("The first object is: {0} and stop is: {1}", obj, stop);
});
如果愿意,还可以使用 lambda:
var myset = new NSMutableSet ();
mySet.Add (new NSString ("Foo"));
s.Enumerate ((obj, stop) => {
Console.WriteLine ("The first object is: {0} and stop is: {1}", obj, stop);
});
异步方法
绑定生成器可以将特定类的方法转换为异步友好方法(返回 Task 或 Task<T> 的方法)。
使用 [Async]
返回 void 的方法的属性,其最后一个参数是回叫。 将此方法应用于方法时,绑定生成器将生成带有后缀 Async
的该方法版本。 如果回叫不采用任何参数,返回值将是 Task
,如果回叫采用参数,则结果是 Task<T>
。 如果回叫采用多个参数,则应设置 ResultType
或 ResultTypeName
以指定生成类型所需的名称,该名称将保存所有属性。
示例:
[Export ("loadfile:completed:")]
[Async]
void LoadFile (string file, Action<string> completed);
上述代码将生成 LoadFile 方法,以及:
[Export ("loadfile:completed:")]
Task<string> LoadFileAsync (string file);
显示弱 NSDictionary 参数的强类型
在 Objective-C API 的许多地方,参数都作为弱类型化的 NSDictionary
API 传递,具有特定的键和值,但这些方法容易出错(你可以传递无效的键而不会收到警告;也可以传递无效的值而不会收到警告),而且使用起来很令人沮丧,因为它们需要多次查阅文档来查找可能的键名和值。
解决方案是提供强类型化的版本,该版本提供 API 的强类型版本,并在后台映射各种基础键和值。
例如,如果 Objective-C API 接受 NSDictionary
,并且文档中记载它接受采用 0.0 至 1.0 间音量值的 NSNumber
的键 XyzVolumeKey
,以及采用字符串的 XyzCaptionKey
,则你希望用户拥有一个如下所示的漂亮 API:
public class XyzOptions {
public nfloat? Volume { get; set; }
public string Caption { get; set; }
}
Volume
属性定义为可为 null 的浮点数,因为 Objective-C 中的约定不需要这些字典具有值,因此在某些情况下,可能无法设置该值。
为此,需要执行一些操作:
- 创建一个强类型类,该类继承自 DictionaryContainer,并为每个属性提供不同的 getter 和 setter。
- 为采用
NSDictionary
的方法声明重载,以接受新的强类型版本。
可以手动创建强类型类,也可以使用生成器为你完成工作。 我们首先探讨如何手动执行此操作,以便了解发生了什么,然后了解自动方法。
需要为此创建支持文件,它不会进入协定 API。 这是创建 XyzOptions 类时必须写入的内容:
public class XyzOptions : DictionaryContainer {
# if !COREBUILD
public XyzOptions () : base (new NSMutableDictionary ()) {}
public XyzOptions (NSDictionary dictionary) : base (dictionary){}
public nfloat? Volume {
get { return GetFloatValue (XyzOptionsKeys.VolumeKey); }
set { SetNumberValue (XyzOptionsKeys.VolumeKey, value); }
}
public string Caption {
get { return GetStringValue (XyzOptionsKeys.CaptionKey); }
set { SetStringValue (XyzOptionsKeys.CaptionKey, value); }
}
# endif
}
然后,应提供一个包装器方法,用于在低级别 API 的基础上显示高级 API。
[BaseType (typeof (NSObject))]
interface XyzPanel {
[Export ("playback:withOptions:")]
void Playback (string fileName, [NullAllowed] NSDictionary options);
[Wrap ("Playback (fileName, options?.Dictionary")]
void Playback (string fileName, XyzOptions options);
}
如果不需要替代 API,则可以安全地隐藏基于 NSDictionary 的 API,方法是使用 [Internal]
属性。
如你所见,我们使用 [Wrap]
特性来呈现新的 API 入口点,并使用强类型化的 XyzOptions
类来呈现它。 包装器方法还允许传递 null。
现在,我们没有提及的一件事就是 XyzOptionsKeys
值来自何处。 通常会将 API 在静态类(如 XyzOptionsKeys
)中的键分组,如下所示:
[Static]
class XyzOptionKeys {
[Field ("kXyzVolumeKey")]
NSString VolumeKey { get; }
[Field ("kXyzCaptionKey")]
NSString CaptionKey { get; }
}
让我们看看创建这些强类型字典的自动支持。 这样可以避免大量样本,并且可以直接在 API 协定中定义字典,而不是使用外部文件。
若要创建强类型字典,请在 API 中引入一个接口,并使用 StrongDictionary 特性对其进行修饰。 这会告知生成器,它应创建一个与接口同名的派生自 DictionaryContainer
的类,并为它提供强类型访问器。
[StrongDictionary]
属性采用一个参数,即包含字典键的静态类的名称。 然后,接口的每个属性将成为强类型访问器。 默认情况下,代码将使用静态类中后缀为 "Key"的属性的名称来创建访问器。
这意味着,创建强类型访问器不再需要外部文件,不需要手动为每个属性创建 getter 和 setter,也不需要动查找键。
这就是整个绑定的外观:
[Static]
class XyzOptionKeys {
[Field ("kXyzVolumeKey")]
NSString VolumeKey { get; }
[Field ("kXyzCaptionKey")]
NSString CaptionKey { get; }
}
[StrongDictionary ("XyzOptionKeys")]
interface XyzOptions {
nfloat Volume { get; set; }
string Caption { get; set; }
}
[BaseType (typeof (NSObject))]
interface XyzPanel {
[Export ("playback:withOptions:")]
void Playback (string fileName, [NullAllowed] NSDictionary options);
[Wrap ("Playback (fileName, options?.Dictionary")]
void Playback (string fileName, XyzOptions options);
}
如果需要在 XyzOption
成员中引用其他字段(该字段不是后缀为 Key
的属性的名称),可以使用 具有你要使用的名称的 [Export]
特性来修饰属性。
类型映射
本部分介绍如何将 Objective-C 类型映射到 C# 类型。
简单类型
下表显示了如何将类型从 Objective-C 和 CocoaTouch 世界映射到 Xamarin.iOS 世界:
Objective-C 类型名称 | Xamarin.iOS 统一 API 类型 |
---|---|
BOOL , GLboolean |
bool |
NSInteger |
nint |
NSUInteger |
nuint |
CFTimeInterval / NSTimeInterval |
double |
NSString (有关绑定 NSString 的详细信息) |
string |
char * |
string (另请参阅:[PlainString] ) |
CGRect |
CGRect |
CGPoint |
CGPoint |
CGSize |
CGSize |
CGFloat , GLfloat |
nfloat |
CoreFoundation 类型 (CF* ) |
CoreFoundation.CF* |
GLint |
nint |
GLfloat |
nfloat |
基础类型 (NS* ) |
Foundation.NS* |
id |
Foundation .NSObject |
NSGlyph |
nint |
NSSize |
CGSize |
NSTextAlignment |
UITextAlignment |
SEL |
ObjCRuntime.Selector |
dispatch_queue_t |
CoreFoundation.DispatchQueue |
CFTimeInterval |
double |
CFIndex |
nint |
NSGlyph |
nuint |
阵 列
Xamarin.iOS 运行时会自动处理将 C# 数组转换为 NSArrays
以及执行反向转换,例如,返回 NSArray
的 UIViews
的虚拟 Objective-C 方法:
// Get the peer views - untyped
- (NSArray *)getPeerViews ();
// Set the views for this container
- (void) setViews:(NSArray *) views
绑定如下:
[Export ("getPeerViews")]
UIView [] GetPeerViews ();
[Export ("setViews:")]
void SetViews (UIView [] views);
其思路是使用强类型 C# 数组,因为这样,IDE 就可以使用实际类型提供适当的代码完成,而无需强制用户猜测,或查找文档来找出数组中包含的对象的实际类型。
如果无法跟踪数组中包含的实际派生类型,可以使用 NSObject []
作为返回值。
选择器
选择器在 Objective-C API 上显示为特殊类型 SEL
。 绑定选择器时,将类型映射到 ObjCRuntime.Selector
。 通常,选择器在 API 中公开,其中包含对象、目标对象和在目标对象中调用的选择器。 提供这两者基本上都对应于 C# 委托:封装要调用的方法以及调用方法的对象。
绑定如下所示:
interface Button {
[Export ("setTarget:selector:")]
void SetTarget (NSObject target, Selector sel);
}
这就是方法通常在应用程序中使用的方式:
class DialogPrint : UIViewController {
void HookPrintButton (Button b)
{
b.SetTarget (this, new Selector ("print"));
}
[Export ("print")]
void ThePrintMethod ()
{
// This does the printing
}
}
为了使绑定对 C# 开发人员更友好,你通常会提供一个采用 NSAction
参数的方法,这允许使用 C# 委托和 Lambda 表达式,而不是使用 Target+Selector
。 要实现这一点,您通常会隐藏 SetTarget
方法 (使用 [Internal]
特性标记它),然后公开一个新的辅助方法,如下所示:
// API.cs
interface Button {
[Export ("setTarget:selector:"), Internal]
void SetTarget (NSObject target, Selector sel);
}
// Extensions.cs
public partial class Button {
public void SetTarget (NSAction callback)
{
SetTarget (new NSActionDispatcher (callback), NSActionDispatcher.Selector);
}
}
因此,现在用户代码可以如下所示编写:
class DialogPrint : UIViewController {
void HookPrintButton (Button b)
{
// First Style
b.SetTarget (ThePrintMethod);
// Lambda style
b.SetTarget (() => { /* print here */ });
}
void ThePrintMethod ()
{
// This does the printing
}
}
字符串
绑定采用 NSString
的方法时,可以在返回类型和参数上将其替换为 C# 字符串类型。
仅当想要直接使用 NSString
时才使用字符串作为令牌。 有关字符串和 NSString
的详细信息,请阅读 NSString 的 API 设计文档。
在某些情况下,API 可能会公开类似 C 的字符串 (char *
),而不是 Objective-C 字符串 (NSString *
)。 在这些情况下,可以使用 out/ref 对参数 [PlainString]
属性。
进行批注
某些 API 在其参数中返回值,或按引用传递参数。
签名通常如下所示:
- (void) someting:(int) foo withError:(NSError **) retError
- (void) someString:(NSObject **)byref
第一个示例展示了返回错误代码的常见 Objective-C 惯用法,传递一个指向 NSError
指针的指针,并在返回时设置该值。 第二种方法显示 Objective-C 方法如何获取对象并修改其内容。 这是按引用传递的,而不是纯输出值。
绑定如下所示:
[Export ("something:withError:")]
void Something (nint foo, out NSError error);
[Export ("someString:")]
void SomeString (ref NSObject byref);
内存管理属性
使用 [Export]
特性并传递将由调用的方法保留的数据时,可以通过将参数语义作为第二个参数传递来指定参数语义,例如:
[Export ("method", ArgumentSemantic.Retain)]
上面将该值标记为具有“保留”语义。 可用的语义包括:
- 分配
- 复制
- 保留
样式指南
使用 [Internal]
使用 [Internal]
特性来隐藏方法,使其不会出现在公共 API 中。 如果公开的 API 太低,并且想要基于此方法在单独的文件中提供高级实现,则可能需要执行此操作。
在绑定生成器中遇到限制时,也可以使用此方法,例如,某些高级场景可能会公开未绑定的类型,而你希望以自己的方式进行绑定,并希望以自己的方式包装这些类型。
事件处理程序和回叫
Objective-C 类通常通过向委托类(Objective-C 委托)发送消息来广播通知或请求信息。
此模型虽然完全受支持,但 Xamarin.iOS 可能有时很麻烦。 Xamarin.iOS 公开了在这些情况下可在类上使用的 C# 事件模式和方法回叫系统。 这允许如下所示的代码运行:
button.Clicked += delegate {
Console.WriteLine ("I was clicked");
};
绑定生成器能够减少将 Objective-C 模式映射到 C# 模式所需的键入量。
从 Xamarin.iOS 1.4 开始,还可以指示生成器为特定的 Objective-C 委托生成绑定,并将委托作为主机类型上的 C# 事件和属性公开。
此过程涉及两个类,一个是主机类,该类当前发出事件并将这些事件发送到 Delegate
或 WeakDelegate
,另一个是实际的委托类。
请考虑以下设置:
[BaseType (typeof (NSObject))]
interface MyClass {
[Export ("delegate", ArgumentSemantic.Assign)][NullAllowed]
NSObject WeakDelegate { get; set; }
[Wrap ("WeakDelegate")][NullAllowed]
MyClassDelegate Delegate { get; set; }
}
[BaseType (typeof (NSObject))]
interface MyClassDelegate {
[Export ("loaded:bytes:")]
void Loaded (MyClass sender, int bytes);
}
若要包装类,必须:
- 在主机类中,添加到
[BaseType]
声明充当其委托的类型和公开的 C# 名称。 在上面的示例中,这些分别是typeof (MyClassDelegate)
和WeakDelegate
。 - 在委托类中,对于具有两个以上参数的每个方法,需要指定要用于自动生成的 EventArgs 类的类型。
绑定生成器不限于仅包装单个事件目标,某些 Objective-C 类可能会向多个委托发出消息,因此必须提供数组来支持此设置。 大多数设置都不需要它,但生成器可以支持这些情况。
生成的代码为:
[BaseType (typeof (NSObject),
Delegates=new string [] {"WeakDelegate"},
Events=new Type [] { typeof (MyClassDelegate) })]
interface MyClass {
[Export ("delegate", ArgumentSemantic.Assign)][NullAllowed]
NSObject WeakDelegate { get; set; }
[Wrap ("WeakDelegate")][NullAllowed]
MyClassDelegate Delegate { get; set; }
}
[BaseType (typeof (NSObject))]
interface MyClassDelegate {
[Export ("loaded:bytes:"), EventArgs ("MyClassLoaded")]
void Loaded (MyClass sender, int bytes);
}
EventArgs
用于指定要生成的 EventArgs
类的名称。 应每个签名使用一个(在此示例中,EventArgs
将包含 nint 类型的 With
属性)。
使用上述定义,生成器将在生成的 MyClass 中生成以下事件:
public MyClassLoadedEventArgs : EventArgs {
public MyClassLoadedEventArgs (nint bytes);
public nint Bytes { get; set; }
}
public event EventHandler<MyClassLoadedEventArgs> Loaded {
add; remove;
}
因此,现在可以使用如下所示的代码:
MyClass c = new MyClass ();
c.Loaded += delegate (sender, args){
Console.WriteLine ("Loaded event with {0} bytes", args.Bytes);
};
回叫就像事件调用一样,区别在于没有多个潜在订阅者(例如,多个方法可以挂钩到 Clicked
事件或 DownloadFinished
事件),回叫只能有单个订阅者。
此过程是相同的,唯一的区别是,除了公开将生成的 EventArgs
类的名称外,EventArgs 实际上用于命名生成的 C# 委托名称。
如果委托类中的方法返回一个值,绑定生成器会将其映射到父类中的委托方法,而不是事件。 在这些情况下,需要提供方法应返回的默认值(如果用户未连接到委托)。 为此,请使用 [DefaultValue]
或 [DefaultValueFromArgument]
特性。
[DefaultValue]
将对返回值进行硬编码,而 [DefaultValueFromArgument]
用于指定将返回哪些输入参数。
枚举和基类型
还可以引用 btouch 接口定义系统不支持的枚举或基类型。 为此,请将枚举和核心类型放入单独的文件中,并将其作为为 btouch 提供的额外文件的一部分包含在内。
链接依赖项
如果要绑定不属于应用程序的 API,则需要确保可执行文件已链接到这些库。
你需要通知 Xamarin.iOS 如何链接你的库,这可以通过更改构建配置来调用 mtouch
命令并附加一些额外的构建参数来完成,这些参数使用 "-gcc_flags" 选项指定如何与新库进行链接,后面是一个包含程序所需的所有额外库的带引号字符串,如下所示:
-gcc_flags "-L$(MSBuildProjectDirectory) -lMylibrary -force_load -lSystemLibrary -framework CFNetwork -ObjC"
上面的示例将 libMyLibrary.a
、 libSystemLibrary.dylib
和 CFNetwork
框架库链接到最终可执行文件。
或者,可以利用程序集级别的 [LinkWithAttribute]
,你可以将其嵌入到协定文件(如 AssemblyInfo.cs
)中。
使用 [LinkWithAttribute]
时,需要在绑定时提供本机库,因为这会在应用程序中嵌入本机库。 例如:
// Specify only the library name as a constructor argument and specify everything else with properties:
[assembly: LinkWith ("libMyLibrary.a", LinkTarget = LinkTarget.ArmV6 | LinkTarget.ArmV7 | LinkTarget.Simulator, ForceLoad = true, IsCxx = true)]
// Or you can specify library name *and* link target as constructor arguments:
[assembly: LinkWith ("libMyLibrary.a", LinkTarget.ArmV6 | LinkTarget.ArmV7 | LinkTarget.Simulator, ForceLoad = true, IsCxx = true)]
你可能会想,为什么需要 -force_load
命令,原因是虽然 -ObjC 标志会编译代码,但它不会保留支持类别所需的元数据(链接器/编译器会删除死代码),而在 Xamarin.iOS 运行时需要这些元数据。
辅助参考
对于开发人员来说,一些临时对象(如操作表和警告框)难以跟踪,而绑定生成器可以在此方面提供一些帮助。
例如,如果有一个类显示消息,然后生成 Done
事件,则处理此事件的传统方法是:
class Demo {
MessageBox box;
void ShowError (string msg)
{
box = new MessageBox (msg);
box.Done += { box = null; ... };
}
}
在上述方案中,开发者需要自己保留对对象本身的引用,并自己决定是泄露还是主动清除框的引用。 绑定代码时,生成器支持跟踪引用,并在调用特殊方法时将其清除,然后上述代码将变为:
class Demo {
void ShowError (string msg)
{
var box = new MessageBox (msg);
box.Done += { ... };
}
}
请注意,现在不再需要在实例中保留变量,它可以与局部变量一起使用,而且在对象消失时无需清除引用。
若要利用这一优势,你的类应该在 [BaseType]
声明中设置 Events 属性,并将 KeepUntilRef
变量设置为当对象完成其工作时调用的方法的名称,如下所示:
[BaseType (typeof (NSObject), KeepUntilRef="Dismiss"), Delegates=new string [] { "WeakDelegate" }, Events=new Type [] { typeof (SomeDelegate) }) ]
class Demo {
[Export ("show")]
void Show (string message);
}
继承协议
从 Xamarin.iOS v3.2 开始,我们支持从标记有 [Model]
属性的协议继承。 这在某些 API 模式中很有用,例如在 MapKit
中,MKOverlay
协议继承自 MKAnnotation
协议,并被许多继承自 NSObject
的类所采用。
过去,我们需要在每个实现中复制协议,但现在,在这些情况下我们可以让 MKShape
类继承自 MKOverlay
协议,并自动生成所有所需的方法。