现在的位置: 首页 > 综合 > 正文

API Design for ios 译文

2013年09月01日 ⁄ 综合 ⁄ 共 19517字 ⁄ 字号 评论关闭
API Design 译文
 

译文

本文来源于matt gemmell困于自己英语水平问题, 若有错误见谅。

One of the development tasks I do most often is designing the API for a reusable component. The components are usually for iOS (though sometimes they’re for OS X), and are invariably GUI controls or views of some kind.

我最常做的工作之一就是设计可重用的组件,这些组件一般用于iOS(一些时候也用在OS X)的GUI控件或者某些视图上。

I’ve designed literally dozens of component APIs over the years, including for clients like Apple, and I’ve learned quite a bit about the process. I periodically release open source components too, and the feedback I’ve had has helped me put together a set
of guidelines for API design that I’d like to share with you.

多年来我设计了几十个API组件都已经成功出现在了大家的视线里,比如一些苹果风格的客户端,并且我对这个开发过程有了一定的了解。我定期也会发布一些开源的组件,并且从中获得了很多反馈让我受益良多,与此同时我整理了一套API设计的指南,现在我将成果分享给大家。

This is an important topic, whether you’re an open source contributor, or working as part of a team on a large app, or just creating your own software. Just like the first launch experience of an app, your API is part of the first impression that a developer
will have with your code, and will have a huge impact on whether they use it or throw it away.

这是一个很重要的话题,不论你致力于开源,与其他成员一起完成一个大型软件,或者独自创作。就像第一次打开一个应用程序的体验,你的API将是开发者对你的代码的第一印象,他们是否使用它,或者把它扔掉,等都产生巨大的影响。

APIs are UX for developers. I’ve always been surprised that there isn’t more material written about this aspect of our work, in a way that’s specific to the popular platforms.

APIs 是对开发者的交互设计,我时常感到惊讶为什么没有很多资料提到这方面的工作,它是非常受欢迎的。

As we go through some guidelines, I’m going to use my most recently released open source GUI component, MGTileMenu,
as an example where necessary. You can read all about MGTileMenu here first, if you like.

就像我们常看到的指南那样,让我来用我最近发布的开源组件MGTileMenu当做例子来描述,你可以先阅读”MGTileMenu”MGT来了解他,当然如果你愿意的话。

素质(Desirable qualities)

API design is very much like user interface and user experience design. Your target audience has different needs and characteristics, but they’re still humans who are looking to get a job done. As with a friendly, usable app UI, you’re still trying to make
your API:

API设计和用户界面设计以及用户体验设计非常之相像。你的目标用户总是有着不同的需求以及特点,但是他们最终目的依然是完成工作。作为一个友好的可用性高的应用UI,你需要尝试让你的API:

  • 直观(Intuitive)
  • 灵活 Forgiving
  • 松耦合 Frictionless

As with any piece of software designed for use by humans, we have to think about the use cases. We have to make the most commonly-needed stuff easy, without undue configuration. Default behaviours should be useful as-is, and should be sensibly chosen. The software
should be discoverable, and should allow the user to generalise from known paradigms. It’s all exactly the same set of principles as when we’re creating UIs.

以人为本的软件,我们需要思考实际用例,让一些常用的东西不需要过多的配置就可以容易使用。而你设计的默认行为应该是最实用最明智的那个。软件的功能应该是容易发现的,并且可以让用户从已知的行为模式中概括出来。这套准则应该和创建UI的时候完全一致。

开发接口(The developer interface)

Components are interacted with by the developer using four primary explicit means:

Components是通过下四种方式和开发者互动:

  • The class interface: its exposed properties and methods.
  • The delegate protocol, where relevant.
  • The data-source protocol, where appropriate.
  • Any provided notifications.

We have to design each of those, judiciously and deliberately, for human use. There are two key questions when you’re thinking about the API:

以人为本的设计我们需要谨慎的考虑,当你思考API设计的时候有2个关键的问题:

  • What is the control?

This influences the interface and convenience methods. Is it a button? A slider? Your interface is obvious. Your convenience methods will follow the standard semantics of the control.

这个问题会影响界面和接口方法。它是一个按钮吗?一个滑块?您的接口是否可见。您的方法将遵循的标准语义的control。

  • What is the control like?

This influences the delegate and/or data-source model and notifications. If it’s a new type of control, is it essentially very similar to something else? An outline view is a linear table. A calendar widget is a date-picker. A collection of commands presented
with a unified presentation is a menu.

这个问题会影响委托和/或数据源模型和通知。如果它是一个新型的control,它本质上是非常相似的别的东西吗?大纲视图是​​一个线性表。日历小工具是一个日期选择器。用一个统一的命令集合就是一个菜单。

Our core goal is consistency with existing components and models, so that we can turn an unfamiliar control into something the developer already understands. Use standard APIs, models, and patterns wherever possible (and that’s almost always). Familiarity and
intuitiveness are just as important at code level as they are for the end user.

我们的核心目标是与现有的组件和模型的保持一致,因此,我们可以把一个陌生的control,变成开发人员已经知道的东西。尽可能的使用标准的API,模型和模式,因为熟悉和直观对用户来说也相当重要。

Let’s look at the four components of the component API mentioned above.

让我们来看下由以上四点构成的API组件。

类接口Class interface

Here’s the interface file for MGTileMenu.

Before we even start talking about the specifics of the interface, we have a couple of over-arching rules:

在我们讨论具体的接口之前,这有一些涵盖范围比较广泛的规则:

Rule 1: 使用方言(Use the local dialect)

One the most common mistakes I see in API design is the use of foreign conventions. APIs belong to a platform and a developer ecosystem. You simply can’t use whatever idioms and architectures you’re used to from a different platform; to do so is to pollute
your current codebase and to damage the productivity of your fellow developers.

我所看到最常见的错误是API的设计利用了外来的约定。APIs 属于固定平台和固定的开发者生态系统。你根本无法使用任何习语和你用过的其他平台的架构,这样做会污染您当前的代码库,并​​对其他开发人员的效率造成损害。

Learn your target platform’s conventions before coding. For example, on iOS or OS X, don’t use exceptions for control flow. Name your methods in an appropriate manner (which usually means sufficiently verbose, but should also of course be sufficiently succinct).

在coding之前要了解你目标平台的约定,比如,在iOS 或者 OS X,不使用异常对待control的流程 。以适当的方式命名你的方法(通常指有足够详细,但也应该有足够的简洁)。

Learn what a protocol is, and a delegate, and a category. Use that terminology throughout your code. Learn the relevant naming schemes for constructors and destructors. Obey native memory management rules. The vocabulary and the grammar are indivisible, and
you’re either developing for a given platform or you’re not.

了解协议,和委托,类别分别是什么。在你的代码中使用他们。学习相关的构造函数和析构函数的​​命名方案。请遵守内存管理规则。词汇和语法是不可分割的,你要么发展为一个固定的的平台,或者你跨平台。

Rule 2: 设计解耦(Design decoupled)

Any component should be designed such that it’s not coupled to the project you created it for, and if it’s a GUI control or view, it should at least display something by default. Use the existing framework classes as a guide, and maintain loose coupling with
delegate protocols, well-designed/named API methods and notifications where appropriate.

任何component的设计应该没有连接到你当前创建的项目,如果他是一个GUI control或者一个视图,它应该默认显示一些东西。使用现有的框架作为一个指南,与委托协议,精心设计的/命名的API方法和通知在适当的地方保持松耦合。

An obvious but very effective way to do this is to create a new project for each component, and develop the component literally in isolation. Force yourself to use your own API. Stay away from the temptation of tying unrelated classes together. Start as you
mean to go on.

一个很明显的,但非常有效的方式,是每次为你的component创建一个项目,并逐渐的隔离开发component。强迫自己使用自己的API。远离无关的类。

With that said, let’s talk about the class interface proper. Initialisation methods are one of the most important parts of the interface, because they’re how people get started with your component. Your class will have certain required settings for initial
configuration. So, an obvious rule:

接下来,让我们来适当谈谈类的接口。初始化方法的接口中最重要的部分之一,因为他们是人们如何开始使用您的组件。你的类将有一定的初始配置所需的设置。所以,一个明显的规律:

Rule 3: 必须设置初始化参数(Required settings should be initializer parameters)

If something needs to be set, don’t wait for it - require it up-front, immediately, and return nil if you don’t get something acceptable.

如果有什么需要设置的,不要等待 -需要它了就去做,如果你没有得到的东西的立即返回nil。

1 - (id)initWithDelegate:(id<MGTileMenuDelegate>)theDelegate; //
required parameter; cannot be nil.

Rule 4: 允许访问初始化参数(Allow access to initializer parameters)

This is a corollary to the previous rule: remember not to just swallow those parameters. Give access to them via properties, and note if they might have been massaged in any way (sanitised, or otherwise modified).

这个前一个结果的必然结果: 记住不要仅仅传入参数,应该可以通过属性或者赋值来访问他们,如果他们可以通过任何方式来一场“按摩”(修改,重写等)

1 @property (nonatomic, weak, readonly) id<MGTileMenuDelegate> delegate; //
must be specified via initializer method.

These previous two examples raise a further general point.

前两个例子阐述了这个观点。

Rule 5: 注释你的header文件 Comment your header files (including defaults)

Realistically, you won’t always provide separate, standalone documentation for a component. If you don’t provide documentation, your .h files (and demo app) are your docs. They should be suitably written, and by ‘suitably’ I mean:

实际上,你不总为component提供单独的文档。如果你不提供文档,你的.h文件(包括demo app)就是你的文档。他们应该适当的描述,我的意思是:

  • Sufficiently detailed, but no more so. Be succinct.
  • For professionals. Assume things that are safe to assume. Don’t waffle.

  • 足以描述,但是不是特别多,要简洁。

  • 一切是提供给专业人士,所以适当的描述别描述无关的事情。

Particularly, you should briefly note default values beside properties or accessors; it’s much easier to scan those in the header file than to try to locate your initialisation code in the implementation.

特别是,你应该简要注释在属性或访问器旁边;头文件扫描比在初始化实例的时候更容易。

123 @property (nonatomic) CGGradientRef tileGradient; //
gradient to apply to tile backgrounds (default: a lovely blue)
@property (nonatomic) NSInteger selectionBorderWidth; //
default: 5 pixels
@property(nonatomic) CGGradientRef selectionGradient; //
default: a subtle white (top) to grey (bottom) gradient

Rule 6: 三行内运行起来 Get up and running in 3 lines

Your class should be designed so that it requires minimal code to integrate (delegate/data-source protocol included, about which more later). Excluding delegate methods, you should aim to make it usable at least for testing purposes with only 3 lines of code.

你的类应该是这样设计的,它需要最少的代码来集成(包括委托/数据源)。你的目标应该是只用3行代码使达到测试目的,当然这一切不包括委托方法。

Those lines are:

这几行应该是:

  • Instantiate it.
  • Basically configure, so it will show and/or do something.
  • Display or otherwise activate it.

  • 实例化

  • 基本配置
  • 显示

That should be it. Anything substantially more onerous is a code smell. Here are the relevant lines from MGTileMenu’s demo app:

就是这样,这里是MGTileMenu中相应的代码:

123456 // Instantiate. 初始化tileController = [[MGTileMenuController alloc] initWithDelegate:self];//
Configure. 配置
tileController.dismissAfterTileActivated = NO; //
to make it easier to play with in the demo app.
// Display. 显示[tileController displayMenuCenteredOnPoint:loc inView:self.view];

Rule 7: 臃肿的demo意味着不合格的component A fat demo usually means a broken component

Another corollary: the size of your demo harness is a quality metric for your component, where smaller is better. Demo harnesses/code should be as small and thin as possible (making suitable allowances for demos that aim to explore all of a component’s customisation
or functionality).

另一个推论:您的demo的大小是衡量你component质量的标准,其值越小越好。Demo/Code 应该尽可能的小而薄的(用于演示,旨在描述所有组件的定制或功能)。

The core required code to turn an empty Xcode app template into a demo of your app should be minimised. It’s not OK to required copy-pasted boilerplate to get your component working, and having an example of it in your demo isn’t an excuse.

核心思想是当你的代码从你的空的xcode项目模板到你的demo中应该保持最小化的修改。这并不是一个好的借口当你需要复制粘贴demo来让你的component运行。

Rule 8: 预测定制化的可能性 Anticipate customisation scenarios

My standard rule for apps is don’t give the user options. Choose sensible defaults to fit the majority, and skip the Preferences window. Good software, after all, is opinionated.

我的标准规则的应用程序是不给用户选择。选择合理的默认值,以适应大多数情况,并跳过“首选项”窗口。我固执己见的认为好的软件应该如此。

The situation is a bit different with components, because the scenarios of use aren’t as clear-cut. You can certainly make a component that only fits one specific situation, but usually we want some flexibility. You never know exactly how another developer
is going to use your component, so you have to build in some generality.

目前的情况是应该有一些不同的components,因为在使用目的不明确的情况下。你当然可以使一个components只适合一个特定的情况,但通常我们需要有一定的灵活性。你永远不知道究竟另一名开发人员将如何使用您的components,所以你必须建立在一些一般性。

It’s important to choose your customisation points carefully. It’s particularly important to consider dependencies - not in the compiling/linking sense, but rather the logical relationships between types of customisation. I approach this by trying not to think
of customisation at the instance-variable level, but rather at the “aspect” level. What aspects of your component do you want to allow customisation of? Then you work out what specific properties to expose.

一个非常重要的事情就是小心的选择可定制的地方。这是特别重要的考虑依赖关系——而不是在编译/链接意义上,而是逻辑关系类型的定制。我的方法通过努力不去想定制的实例变量层面,而是在“方面”水平。您的组件的哪些方面你想让定制的?然后你找出特定的属性来公开。

It’s easy to cripple a certain type of customisation by not exposing sufficient configuration points. Some examples:

这很容易削弱某种类型的自定义的不足。一些例子:

  • Don’t expose width and height without considering corner radius too.
  • Don’t expose background colour without highlighted background colour.
  • Don’t expose size without spacing.

  • 不公开的宽度和高度也没有考虑圆角半径。

  • 不公开背景颜色没有突出显示的背景颜色。
  • 不公开大小没有间距。

The specifics depend on the component, but just try to consider the relationships between properties, from the point of view of either appearance or functionality. Empathise with the developer. Be flexible, without abandoning the identity of the component.

具体细节取决于component,但只尝试考虑属性之间的关系,要么外观或功能。对于开发者。是需要灵活的,没有放弃身份的component。

123456 @property (nonatomic) BOOL dismissAfterTileActivated; //
automatically dismiss menu after a tile is activated (YES; default)
@property (nonatomic) BOOL rightHanded; //
leave gap for right-handed finger (YES; default) or left-handed (NO)
@property (nonatomic) NSInteger tileSide; //
width and height of each tile, in pixels (default 72 pixels)
@property (nonatomic) NSInteger tileGap; //
horizontal and vertical gaps between tiles, in pixels (default: 20 pixels)
@property (nonatomic)CGFloat cornerRadius; //
corner radius for bezel and all tiles, in pixels (default: 12.0 pixels)

Let common sense be your guide. Decide what options will serve 70% or so of the usage situations you can think of, and provide those options. Let your delegate methods and code structure serve the rest.

让常识成为你的向导。这将确定你能想到的70%左右的选项,并提供这些选项。以及您的委托方法和代码结构的其余服务。

Rule 9: 更多的属性,更少的actions More properties, fewer actions

There’s a particular pattern that keeps cropping up in components that I like - some of which are from standard frameworks, some open source from third parties, and some even my own. It’s a ratio of the number of properties (or accessors, or customisation points)
on a component, to the number of “do stuff” methods (i.e. all the other stuff, from initializers to state-updating).

有一个特定的模式,不断出现在我喜欢的组件中 - 其中一些是从标准的框架,也有来自第三方的,有的甚至是我自己的一些开放源码的。在一个组件中属性的数量(或访问器,或定制)和“做东西”的方法(即所有其他的东西,从初始化来状态更新)有特定的比例。

It’s pretty much always more properties, and fewer ‘actions’ (again, that’s not actions in the Interface Builder sense). MGTileMenu has an initializer, and four actual for-public-use methods (one of which is a convenience that calls another). In terms of customisation
points, it has four times as many. I think that’s a good ratio, and leads to components that are both concise in actual functionality, but also flexible in customisation.

它几乎总是更多的属性,和更少的action”(再一次,这不是Interface Builder中的actions)。MGTileMenu有一个初始化器,和四个实际公共使用方法。在定制中,它达到4倍多。我认为这是一个好的比率,很简洁,但也很灵活去定制的component。

12345 - (id)initWithDelegate:(id<MGTileMenuDelegate>)theDelegate; //
required parameter; cannot be nil.
-(CGPoint)displayMenuPage:(NSInteger)pageNum centeredOnPoint:(CGPoint)centerPt inView:(UIView*)parentView; //
zero-based pageNum
- (void)dismissMenu;- (void)switchToPage:(NSInteger)pageNum; //
zero-based pageNum

Rule 10: 在你的控件中使用控件 Use controls in your controls

A great way to simplify both the API and implementation of your component is to use existing controls in your implementation. A unified presentation doesn’t meant that you can’t build something out of pre-existing components (indeed, that’s one of the basic
principles of good software engineering).

一个很重要的方式来简化API和实现你的component是里用已有的controls在你的实现方法。标准的表现并不意味着你不能在已有的components上创造东西。(事实上,这是一个好的软件工程师的基本准则)

Consider how UITableViewCell and UIButton have simple APIs because they use sub-controls such as UIImageViews and UILabels. You can, and should, do that too - and if appropriate, expose the corresponding sub-controls to keep your class interface concise and
consistent.

仔细想想UITableViewCell 和 UIButton的简化API的方式,是因为他们使用了sub-controls 例如 UIImageViews和 UILabels。你可以也应该这么做。在适当的时候,使一些相对应的sub-controls暴露在外,这样让你的class接口简洁,一致。

In MGTileMenu, for example, the tiles are regular UIButtons (not even subclasses). This drastically simplified the implementation compared to drawing the tiles within a single custom view, tracking input events, and supporting accessibility.

在MGTileMenu中,例如,贴图是标准的UIButtons(不是子类)。这极大的简化了在单一的自定义视图中绘制块,跟踪输入事件,以及支持辅助功能。

Rule 11: 方便你我 Convenient for you is convenient for me

You’ll naturally add convenience methods during implementation, and the instinct is to keep them private. Instead, consider whether you can expose them for use by those who integrate your component into their own apps.

在实施过程中你会很自然的想到增加一些便利的方法,并且本能的让他私有化。相反的,应该考虑如何公开的你的component 让别人用在他们的项目中。

Whatever made it more convenient for you to add a method or function may apply to those developers too.

无论是什么使它更便于您添加一个方法或函数应该同样适用于其他的开发者。

For example, in MGTileMenu I created these convenience functions:

例如, 在MGTileMenu中 我创建了一些便捷的函数:

123 CGRect MGMinimallyOverlapRects(CGRect inner, CGRect outer, CGFloat padding);CGGradientRefMGCreateGradientWithColors(UIColor *topColorRGB, UIColor *bottomColorRGB); //
assumes colors in RGB colorspace

The first helps me shift a tile menu so that it’s fully visible within its parent view (which might be handy for another developer, if they’re providing ancillary UI related to the menu), and the second returns a Core Graphics gradient from two UIColors, which
I used when setting a default background for the tiles (and another developer may find handy when implementing MGTileMenu’s delegate protocol, to give tiles custom gradients).

这第一个帮助我转换一个tile菜单让他可以完完全全显示在它的父视图中(这可能方便其他开发人员如果他们相关配套的UI菜单),第二个是从2个颜色中返回一个图形渐变,用在了设置默认背景的tile(另一名开发人员可能会发现方便的时候实现MGTileMenu的委托协议,并自定义渐变tile)

Rule 12: 魔法可以,数字就算了 Magic is OK. Numbers aren’t.

Sooner or later, you’ll put magic into your component. Hopefully there’ll be plenty of the Steve Jobs type of intuitive, delightful, empowering magic, but what I’m talking about is things like numbers and other values that have special meaning in your code.
A common example is -1, to indicate a unique thing in a set, or a special situation.

迟早,你将把魔法加入到你的component中。他们将会是大量乔布斯风格的直觉,令人愉快的魔法,但是我要说的事情是在你的代码中数值或者其他的值他们都特殊的意义。一个简单的例子是-1, 在集合中是一个特殊的事情,或者一个特殊的情况。

It’s fine. It’s genuinely OK to do that. What’s not OK, though, is needlessly putting mysterious raw values throughout your code, and . If you’re exposing magic, dress them up for consumption. Use #defines or a constant or something. Just make them presentable
and understandable.

这个可以,诚实的来说也仅仅是OK,那什么是不OK,把一些不必要的神秘的原始值贯穿于你的代码中,尤其不正常的是把它暴露在API中。如果你想施展一些魔法,把他们包装起来再使用,用#defines 或者一个常量或者其他一些什么东西。让他们更像样,更容易理解。

12 // Used for the page-switching tile in methods expecting a tile-number.#define
MG_PAGE_SWITCHING_TILE_INDEX -1

委托和数据源 Delegate and data-source protocols

Delegate protocols are fantastic. They’re an easy, familiar and flexible way to embrace the MVC pattern, and they reinforce good habits of loose coupling and judicious API design.

委托协议是非常令人难以置信的功能,非常的简单,用一种常见并且灵活的方式来实现MVC 模式,同时他也是巩固了松散耦合的良好习惯以及明智了api 设计。

Here’s MGTileMenu’s delegate protocol.

这里是[MGTileMenu’s 的委托][MGTileMenu’s delegate protocol]

There are classic delegate and data-source protocols that we can draw on for almost any component. If you’re displaying data, the One True Data-Source Protocol is likely to be something very close to:

1.How many things do I have? 2.What’s the value for property Y of thing X?

它们是经典的委托和数据源让我们利用在几乎所有的component中。如果你想显示数据,一个真正的数据源很可能类似这样: 1.我有多少东西? 2.X的Y属性的值是多少

Similarly, in almost any situation, the One True Delegate Protocol is likely to take the form:

Should this thing do that? This thing is about to do that. This thing just did that.
This is also known as the 
ShouldWill,Did protocol pattern, and it ties neatly in with the Will-Did** notification
pattern too, about which more later.

同时,几乎在任何情况下,一个真正的委托需要如下这样的一个表单: 这事应该这样做? 这事要做。这事情是这样。这个也是已知的Should,Will,Did协议模式以及与Will-Did通知模式紧密联系在一起,

Let me mention something you might find controversial: I find it perfectly acceptable to conflate the delegate with the data-source (i.e. combine them into a single protocol). I do it with MGTileMenu and several other components, for example.

让我提出一些事情可能你会提出争议:我觉得把委托和数据源混合在一起是完全可以接受的(就是组合成一个协议)

I fully accept the principle of separating them, and I can think of many cases where you’d want to keep them separate. Apple keeps them separate too, generally. That’s fine.

我完全接受他们分开的原则,我能想到的许多情况下,您想要将它们区分开来。苹果一般也让他们分开。这个无所谓。

In my experience, though, in most cases it’s fine to combine them. Most people handle data-source methods and delegate methods in the same place. I’ve never had a complaint about unifying those protocols, and I can scarcely remember a situation where even existing
separate protocols were handled in different places.

在我的经验中,尽管,在大多数情况下可以组合它们。大多数人处理数据源方法和委托方法在相同的地方。我从来没有收到投诉关于统一这些协议,我几乎不记得哪里有分开的protocols在不同的地方处理。

If you care about purity, or have a need to separate delegate from data-source, then obviously you should do so. I just don’t think you need to feel bad if you combine them.

如果你很关心纯粹,或者需要一个独立的于数据源的委托,那么很明显你应该这么做。我只是认为你不需要感到沮丧在你组合他们的时候。

Rule 13: 限制’required‘ 委托 Limit ‘required’ delegate methods

Be very careful when choosing which of your delegate methods are required. Too many required methods tends to indicate:

请小心的选择哪些委托是必须的,大多数的’required‘方法往往:

  • Poor choice of default behaviour.
  • Too much of your own politics are in your code.

  • 默认行为不够不充分

  • 加入了太多你的主观意见

A well-designed component should need very, very few required delegate methods - just the bare minimum to do whatever it does. Choose carefully. Equally, remember that it’s easy to add optional methods later, but it’s hard to turn optional ones into required
ones (people will complain, and rightly so).

一个非常好的component 应该需要非常非常少的’required‘委托方法 - 最小化无论做什么。认真公平的去选择,记得以后会容易增加’optional‘的方法,但是以后很难从’optional‘转变成’required‘方法。

MGTileMenu has five required methods, four of which are data-source methods: MGTileMenu 有5个’required‘ 方法,其中4个数据源方法:

1234 - (NSInteger)numberOfTilesInMenu:(MGTileMenuController *)tileMenu; //
in total (will be shown in groups of up to 5 per page)
- (UIImage *)imageForTile:(NSInteger)tileNumber inMenu:(MGTileMenuController *)tileMenu; //
zero-based tileNumber
- (NSString *)labelForTile:(NSInteger)

抱歉!评论已关闭.