开始使用iPhone和iOS开发






4.87/5 (150投票s)
这是一系列文章中的第一篇,旨在帮助读者快速上手 iPhone 开发。本文将帮助您确定开发所需的硬件,并简要介绍 Objective-C。
引言
这是一系列文章中的第一篇,旨在帮助读者快速上手 iOS 开发。iOS 是运行在 iPhone、iPad 和 iPod Touch 上的操作系统。虽然我将使用 iPhone 作为示例代码的平台,但这些代码同样适用于这三种设备。在您熟悉 iOS 开发后,可以阅读本系列的第二篇文章:iOS 图形 API 介绍(第一部分)。
先决条件
为了充分利用本文,您需要已经理解面向对象的概念,并且需要了解一门 C 系语言(C、C++、C#),同时明白我所使用的术语,如对象、类、函数、方法、变量等的含义。我假设您从未接触过 Objective-C。我将向您介绍 Objective-C 语言,然后通过一些我认为最能帮助您入门的编程场景进行讲解。与任何编程环境一样,完成一项任务可能有多种方法,表达某件事物也可能有多种语法。详尽地列出所有可能的方式是不现实的。所以请记住,我所解释的内容还有其他的实现方式。
选择硬件
开发 iOS 应用的最低配置开发环境仅需一台基于 Intel 的 Mac。拥有一台 iPhone、iPod 或 iPad 是可选的,但非常有帮助。
如果您正在寻找开发 iOS 应用的最佳硬件,那么很容易会说“买一台顶配的 Power Mac,加上所有你能选的配置”。但对于我们大多数人(包括我自己)来说,在选择开发硬件时都会有成本限制。您需要的绝对最低硬件配置是一台运行 Snow Leopard 的基于 Intel 的 Mac。您可以购买到的最便宜的新款 Mac 是 Mac Mini。与市面上最便宜的 PC 相比,Mac Mini 的价格会显得相当昂贵。但我建议多花一点钱买一台便携式电脑。使用 Mac Mini,您只能在它所在的房间里进行开发。而有了一台 Mac Book,您可能会发现在看电视或在其他舒适的地方放松时也能进行开发。
根据您的需求,如果您发现您预期的应用并不需要最新版本的 iPhone,那么在苹果发布新款 iPhone 或新款 Mac 时,四处寻找优惠是个好主意。我从一个正在升级设备的人那里买到了一台便宜的 iPhone,并从一家正在为新款设备清仓的商店里找到了一台打折的 Mac Mini。(后来因为想在房子的其他房间里开发,我换成了一台 Mac Book Pro。)
您不需要拥有 iPhone、iPod 或 iPad 就能开始(下文我将统称这些设备为 iOS 设备)。SDK 附带了模拟器(或者如果您习惯使用 emulator 这个词的话)。但模拟器有其局限性;您会发现,在开发任何使用加速计的应用时,您会希望有一台真实设备。
如果您计划使用真实硬件进行测试,仅有必需的硬件和软件是不够的。您还需要订阅苹果的 iPhone 开发者计划(每年约 99 美元)。
我必须要有 Macintosh 吗?
我的个人网站有很多流量,是因为我评测过一项旨在让没有 Mac 的人也能进行 iPhone 开发的服务。我不想在这里讨论这项服务,所以我就直接告诉您,是的,您必须有一台 Macintosh。唯一官方认可的 iPhone 开发硬件是基于 Intel 的 Macintosh。不要浪费时间试图绕过这一点。
安装 SDK
iPhone SDK 已经包含在 OS X Snow Leopard 的安装盘中。但不要用那个版本。总会有更新的版本可供下载。SDK 大小约为 3GB,下载时间取决于您的网络连接速度。要获取安装程序,请访问 http://developer.apple.com。您会看到一个指向 iPhone 开发中心的链接。点击它将带您进入 SDK 下载页面。苹果有时会提供 SDK 的测试版供下载。但与当前版本的 SDK 不同,测试版并非所有人都能下载。您必须是订阅了开发者计划的注册开发者才能访问它们。
SDK 的 DMG 文件下载后会自动挂载。运行安装程序,安装完成后,您就可以开始开发了。
Objective-C 入门
为 iPhone 创建的程序是用 Objective-C 编写的。Objective-C 通常被描述为 C 语言的严格超集。也有人称之为应用了面向对象概念的 C 语言。我第一次看到它时,不得不说感觉完全看不懂。它看起来不像我所知道的任何其他编程语言。但当您完成前几个程序后,您会发现它容易理解得多。作为 C 语言的严格超集,如果您已经有用 C 语言编写的算法,应该能够将它们移植到 iPhone 上。下表展示了一些在 C 和 Objective-C 中含义相近的表达式。
C/C++ |
Objective-C |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
在 Objective-C 中,您的类定义和从这些类实例化的对象都是一个名为 `id
` 的结构体的实例。`id
` 的定义如下
typedef struct objc_object {
Class* isa;
} *id;
由于这是 Objective-C 中所有对象的基础,您会在每个 Objective-C 对象上找到一个指向其类型数据的“isa
”成员。“id
”的实例都是指针,因此您可以通过比较它们的地址来区分它们。有一个名为 `nil
` 的特殊 id 值,其指针值为零。
类
在 Objective-C 中,您将类的接口与其实现分开声明。您可能在 C++ 程序中见过类似的接口与实现分离,其中类的接口在 *.h 文件中定义,而其在 *.cpp 文件中实现。同样,在 Objective-C 中,接口在 *.h 文件中定义,其在 *.m 文件中实现。
您在 Objective-C 中创建的每个类都应该直接或间接地派生自 `NSObject
` 基类。乍一看,这个基类似乎作用不大,但它包含了运行时与对象交互所需的功能。您肯定不想自己去实现这些功能。
类接口声明
要声明一个新类,您首先要声明类的接口。类接口声明以 `@interface
` 编译器指令开始,以 `@end
` 指令结束。您会在类接口声明中看到一对花括号。请记住,右花括号并不是类接口定义的结束!这是一个通用的类定义
@interface YourClassName: TheSuperClass
{
instance members
}
Message Declarations
@end
实例成员的声明与您在其他 C 系语言中可能看到的并无不同。
BOOL isValid;
float width;
NSString* name;
一个类自动拥有其继承链中所有类的访问权限。因此,您可以引用其任何基类型的类而无需做任何特殊处理。如果您要引用一个不在其继承链中的类,则需要对该类进行前向声明。下面是对 `Employee
` 和 `Accountant
` 这两个类的前向引用。
@class Employee, Accountant;
消息的声明有所不同。消息可以与类关联,也可以与该类的实例关联。类方法前面有一个加号 (+),而实例方法前面有一个减号 (-)。类消息的默认返回类型是 `id
` 实例。要将返回类型更改为其他类型,您需要在类名前面用括号括起返回类型。请注意,这与将变量从一种类型强制转换为另一种类型的语法相同。下面是一个简单消息声明的示例
- (int)GetSize;
消息参数紧跟在消息名称后面。它们以冒号分隔,其类型使用与强制类型转换相同的语法来表示。
-(float) Power:(float)width:(float)height;
类实现
类的实现在一个 *.m 文件中。就像在 C++ 中一样,在编写实现时,您需要包含类的头文件。但您将使用 `#import
` 指令而不是 `#include
` 指令。声明实现的语法与声明接口的语法相似;只是您需要将 `@interface
` 指令替换为 `@implementation
` 指令。下面是一种开始实现的可行语法
#import "YourClass.h"
@implementation YourClass
your message implementations go here
@end
或者,您也可以在这里重新声明实例变量和父类,但这并非必需。消息的实现以与消息声明相同的方式开始。但不是用分号(`;
`)结束声明,而是附加一对包含您实现的花括号。
- (float)square:(float)x
{
return x*x;
}
方法、消息和对象通信
您遇到的大多数面向对象的资料都会提到在对象之间发送消息。大多数时候,您会看到这是通过方法实现的。在大多数情况下,方法和消息这两个词可以互换使用。Objective-C 坚持使用“消息”这个术语。因此,在本文档中我将遵循这一标准。
发送消息(与“调用方法”意思相同)的语法是将目标对象的名称和要传递的消息括在方括号内。因此,在 C++/C# 中,您可能会使用以下语法发送消息
myObject.SomeMethod();
在 Objective-C 中,您将使用以下方式实现相同的功能
[myObject SomeMethod];
向消息传递参数与您习惯的方式略有不同。对于单个参数,情况很明了,不需要太多解释。只需在消息名称后放置一个冒号,后跟参数值即可。
[myObject SomeMethod:20];
当您想传递多个参数时,情况可能会变得复杂。
[myObject SomeMethod:20:40];
随着向消息中添加更多参数,代码可能会变得不易阅读;我不期望开发者能记住一个方法需要哪些参数。即使他/她记住了,也仍然需要费脑筋来识别哪个值与哪个参数相关联。为了清晰起见,您可以这样做
[myObject SomeMethodA:20 B:40];
在上面的例子中,我似乎将参数命名为 'A' 和 'B',并且参数 'A' 很奇怪地与消息名称连接在一起。您可能没有意识到的是,参数名和冒号都是消息名称的一部分。所以,这个消息的全名是 `SomeMethodA:B:
`。
如果一个消息返回一个对象,您可以通过嵌套调用的方式向返回的消息发送消息。在 C/C# 中,您会有类似这样的代码
myObject.SomeMessage().SomeOtherMessage();
而在 Objective-C 中,同样的功能可以通过以下方式实现
[[myObject SomeMessage] SomeOtherMessage];
协议(Prototypes)
协议(Prototypes)很像弱实现的接口(interfaces)。在其他编程语言中,如果一个类实现了一个接口,那么在该类的声明中,会有一条语句表明它正在实现某个特定的接口。而对于协议,类只需要实现某些方法即可遵循一个协议。不需要提及它所遵循的协议定义。
类实例化和初始化
类的实例是使用类的 `alloc
` 方法实例化的。`alloc
` 会为类预留所需的内存。内存分配后,您需要对其进行初始化。每个拥有实例成员的类都需要定义一个 `init
` 方法。
myObject = [[MyClass alloc] init];
我第一次看到上面的模式时,错误地认为它等同于以下代码:
myObject = [MyClass alloc];
[myObject init];
它们并不等价。`init
` 返回的值可能与 `alloc
` 返回的值不同。因此,在进行赋值时,您应该始终将 `alloc
` 和 `init
` 消息结合起来。
当您为类实现自己的初始化器时,初始化器应该返回 `self
` 以表示成功,或返回 `nil
` 表示失败。初始化器的返回类型应始终为 `id
`。这是为了明确表示初始化器返回的类型可能是另一种类型。
如果您需要向初始化器传递参数,那么它通常会以 `init
`作为初始化器名称的前缀。例如,如果我有一个需要日期的初始化器,我可能会将其命名为`initWithNSSDate
`。
内存管理
Objective-C 类会跟踪一个引用计数。这个计数可以随时通过 `-(NSUInteger)retainCount;` 读取(尽管除了满足好奇心,很少有场景需要这样做)。当一个对象的引用计数达到零时,它就会被释放。在大多数情况下,这个引用计数的细节被抽象掉了,与引用计数相关的方法被呈现为获取对象所有权的方法(增加引用计数)或放弃对象所有权的方法(减少引用计数)。
一般的规则是,如果您使用任何以`alloc
`或`new
`为前缀的方法创建了一个对象,或者从一个现有对象复制了该对象,那么您就有责任释放该对象。如果您获取了对象的所有权(使用`retain
`),那么您也必须`release
`它。注意:如果您不拥有它,就不要释放它。对一个对象调用`autorelease
`会将其添加到一个对象池中,以便在将来的某个时间点被释放和回收内存。
复制对象
复制对象时,如果该对象的成员变量包含指向其他对象的指针,有两种复制方式。一种方式是将原始对象的指针复制到正在创建的对象中。这种复制方法(也称为浅拷贝)将导致新对象和原始对象共享其成员对象的相同实例。因此,如果一个类的某个成员对象发生变化,它也会影响另一个类中的成员,因为它们实际上是同一个实例。另一种复制方法是在创建新对象时创建成员对象的新实例。这样做的最终结果是,对一个对象实例中的成员对象的更改对另一个实例没有影响。在这种情况下,原始对象和复制的对象是完全独立的。从内存消耗和潜在副作用的角度来看,这两种复制方法各有优缺点。也可能存在混合使用浅拷贝和深拷贝技术的情况。如果您创建了一个类并实现了深拷贝,但其中一个实例成员实现了浅拷贝,那么您最终可能会得到一个实例成员的实例成员,它与您刚刚复制的对象中一个实例成员的实例成员引用的是同一个对象(这可能有点绕,但我找不到更清晰的表达方式)。
一个类的拷贝方法的工作方式应该与该类的设置方法的工作方式保持一致。如果类上的设置方法在执行赋值时创建新实例,那么拷贝方法应该是深拷贝。如果设置方法保存传递给它的实例,那么拷贝方法应该是浅拷贝。
-(void)SetMyMemberVariable:(id)newValue
{
[myMmeberVariable autorelease];
myMemberVariable = [newValue copy];
}
创建一个新对象实例的 set 方法的实现。
|
-(void)SetMyMemberVariable:(id)newValue
{
[myMemberVariable autorelease];
myMemberVariable = [newValue retain];
}
一个复制对现有对象实例引用的 set 方法的实现。
|
低内存状况
您的 iOS 程序的用户界面将通过一个派生自 `UIViewController
` 的类来控制。该类上有两个方法与低内存处理有关。一个是 `didReceiveMemoryWarning
`,它在 iOS 3 之前就存在了。从 iOS 3 及更高版本开始,有一个名为 `viewDidUnload
` 的方法。当设备内存不足时,如果操作系统知道视图可以被重新加载和重新创建,它就会销毁这些视图。当这个方法被调用时,您的类应该通过释放对视图对象的引用来响应。
构建 Hello World
如果您跳过了 Objective-C 的介绍,直接尝试创建一个程序,您将会迷失在神秘的符号和无意义的字形海洋中。如果您已经阅读了入门指南,那么理解接下来的内容将毫无问题。我们将构建一个“Hello World”程序。该程序将有一个按钮和一个文本字段。当有人点击按钮时,文本字段将显示“Hello World”字样。我也会借此机会介绍一些若无此练习则难以理解的概念。
使用 Finder 或 Spotlight 启动 Xcode。当 Xcode 欢迎屏幕出现时,选择“Create a new Xcode project”选项。
确保在 iPhone OS 下选择了“Application”,以查看您可以创建的 iOS 项目类型列表。选择“View-based Application”,并确保“Product”下拉菜单中选择了 iPhone。完成此操作后,点击“Choose”按钮,并在提示输入项目名称时键入“MyHelloWorld”。
一个新的项目被生成,此时它处于可运行状态。请确保您的 iOS 设备未连接到电脑(如果您现在尝试部署到您自己的个人设备上,将会失败),然后选择“Build and Run”。您的项目将被编译并复制到模拟器中运行。结果看起来相当无聊;它只是一个空白屏幕,因为我们还没有向用户界面添加任何东西。您可以点击 Xcode 中的红色按钮来停止程序。当程序终止时,您会在 iPhone 模拟器中看到它的图标。它只是一个普通的白色圆角正方形。让我们来改变它。
如果您想更改程序的图标,请创建一个新的 57x57 像素的 PNG 图像。不用担心圆角处理或做其他使其看起来像 iPhone 风格的事情;这些都会自动为您完成。一旦您在您选择的编辑器中创建了图像,请在 Finder 中导航到它。将该图标点击并拖动到 Xcode 的 Resources 文件夹中。系统会询问您是要将文件复制到项目文件夹中,还是只复制一个引用。通常最好是复制文件。
在 Resources 文件夹中,找到名为 HelloWorld-Info.plist 的文件并点击它。*.plist 文件是一个属性列表,包含有关应用程序的一些常规信息。点击 plist 文件将打开属性编辑器。其中一个可用属性名为“Icon”。双击该属性开始编辑,并输入您的图标文件名。下次您构建并运行应用程序,关闭后,您将看到您的图标。
Xcode 窗口分为三个窗格。顶部的水平窗格中有一个文件列表。找到名为 MyHelloWorldViewController.XIB 的文件并双击它。这将打开界面构建器。
在众多窗口中,您应该能看到一个空白的 iPhone 界面,准备让您在上面布局控件。如果您没有看到这个界面,请在出现的某个窗口中双击“View”图标以显示布局界面。在“Library”窗口中向下滚动,直到看到 Text-Field 控件。将该控件的一个实例拖动到设计界面的上半部分某处。同时,将一个“Round Rect Button”拖动到设计界面上靠近文本字段的地方。
选择按钮,然后转到属性窗口中的 Identity 选项卡。将按钮的 Name 设置为“MyButton”。选择文本字段,并将其名称设置为“MyText”。保存您的更改,然后返回 Xcode 再次运行您的项目。这次您将在屏幕上看到文本字段和按钮。
我们希望更改程序,以便当用户点击按钮时,文本字段中显示“Hello World”。但如果您查看我们的源代码,会发现按钮和文本字段都无法在代码中直接操作。
IBAction 和 IBOutlet - 响应操作并提供反馈
我们可以让按钮在被点击时向我们的代码发送一条消息。我们需要在代码中声明一个 `IBMethod
`,以便按钮有东西可以发送消息。打开“HelloWorldViewController.h”,在接口定义的右花括号后的那一行,添加以下代码。
- (IBAction) myButtonWasClicked:(id)sender;
正如您可能从阅读 Objective-C 入门指南中了解到的,您还需要在 HelloWorldViewController.m 文件中为这个方法添加一个实现。在 `@implementation` 指令后面的那一行,输入以下内容:
- (IBAction) myButtonWasClicked:(id)sender
{
NSLog(@"The button was clicked");
}
在界面构建器中打开 HelloWorldViewController.xib。按住 Control 键,从您的按钮点击并拖动到“File's Owner”图标。一个子菜单会出现,让您选择一个方法。选择 `myButtonWasClicked
` 方法。
运行项目,然后在 Xcode 中按 Shift-Command-R 打开控制台。每当您点击按钮时,您都会看到文本“The button was clicked”出现。
我们的最终目标是在屏幕上向用户显示一条消息;还有更多工作要做。虽然 `IBAction
` 方法允许我们接收到某事发生的通知,但它们在主动更改文本字段内容方面帮助不大。我们首先需要获取文本字段的实例。但文本字段并未在我们的代码中声明。它是在 XIB 文件中声明的。要获取对文本字段的引用,我们必须声明一个 outlet。outlet 是一个在运行时由 Cocoa 自动赋值的变量。
在视图的头文件中,在您创建的方法下面键入以下内容
@property (retain) IBOutlet UITextField *myTextBox;
将同样的内容复制到实例变量区域,并移除 `@property (retain)` 部分。
在实现中,添加以下内容:
@synthasize myTextBox;
`myButtonWasClicked
` 方法中的代码也需要更改。在此方法内,将 `myTextBox
` 实例的文本设置为“Hello World”。
myTextButton.text=@"Hello World"
下一步是从界面构建器将文本字段与此属性关联起来。在界面构建器中,右键单击“File's owner”模块,找到 `myTextBox
` outlet。按住 Control 键的同时,从 outlet 单击并拖动到文本字段。如果您现在运行程序,当点击按钮时,“Hello World”将出现在文本框中。我们只看了文本框,但与许多其他可视化控件的交互是相同的。如果您向界面添加一个滑块,向我的类添加一个 `IBAction
` 方法,然后将该方法与滑块关联,我就可以在滑块值改变时接收通知。
最后一步是清理任务。如低内存部分所述,如果 iPhone 内存不足,它将开始销毁视图以清理内存。当这种情况发生时,我们需要清除对视图对象的引用。此外,当视图被销毁时,我们需要释放我们在代码中引用的视图对象的所有权。您可以在下面的完整代码中看到这两者是如何在 `-(void)viewDidUnload` 和 `-(void)dealloc` 方法中实现的。
下面的源代码包含了 HelloWorld 的功能以及显示一个滑块的值。
#import <uikit>
@class NSTextField;
@interface HelloWorldViewController : UIViewController {
IBOutlet UITextField * myTextBox;
IBOutlet UISlider * mySlider;
IBOutlet UITextField * mySliderText;
}
@property (retain) IBOutlet UITextField * myTextBox;
@property (retain) IBOutlet UISlider * mySlider;
@property (retain) IBOutlet UITextField *mySliderText;
- (IBAction) myButtonWasClicked:(id)sender;
- (IBAction) mySliderWasMoved:(id)sender;
@end
#import "HelloWorldFinalViewController.h" @implementation HelloWorldFinalViewController @synthesize myTextBox; @synthesize mySliderText; @synthesize mySlider; - (IBAction) myButtonWasClicked:(id)sender { NSLog(@"The button was clicked"); myTextBox.text=@"Hello World"; } - (IBAction) mySliderWasMoved:(id)sender { NSLog(@"The Slider was moved"); UISlider *slider = (UISlider*)sender; float sliderValue = [slider value]; NSString* sliderValueText = [NSString stringWithFormat:@"The slider's current value is %f", sliderValue]; mySliderText.text=sliderValueText; } // Override to allow orientations other than the default portrait orientation. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } //View is being destroyed to free memory. Clear //out our references to view obejcts. - (void)viewDidUnload { self.textBox=nil; self.mySliderText=nil; self.mySlider=nil; } // during a dealloc release our hold on the view objects -(void)dealloc{ [myTextBox release]; [mySliderText release]; [mySlider release]; [super dealloc]; } @end
从这个示例程序中需要掌握的两件事是:如何获取在界面构建器中创建的 UI 对象的引用,以及如何从 UI 对象接收消息。
部署到真实设备
既然您已经编写了第一个程序,那些拥有真实设备的人可能想把它部署到设备上。在您可以将程序复制到真实设备之前,您*必须*完成在苹果的 App Store 注册。在完成此操作之前,您将无法完成配置(provisioning)过程。
将您的 iOS 设备连接到电脑。在 Xcode 中,更改下拉菜单,以便 Xcode 在实际设备上运行程序,而不是在模拟器中运行。当您尝试运行程序时,您会收到一个失败通知,指出设备没有配置文件(provisioning profile)。如果您从错误对话框中选择“Install and Run”,您会遇到另一个关于找不到配置文件的失败。
要为您的设备进行配置,您需要有您的开发者证书。前往 iPhone 开发者门户 (http://developer.apple.com/iPhone) 并用您的 Apple ID 登录。在左侧,有一个链接可以将您带到配置页面。点击该链接并从页面底部选择“Launch Assistant”。当开发配置助手欢迎窗口打开时,点击“Continue”。在下一个屏幕上,您必须确定您正在开发的应用。点击“Create a new App ID”并选择“Continue”。在接下来的字符串中,您为应用程序输入一个描述或名称(要有创意!)。之后,您必须确定您用于开发的苹果设备。选择“Assign a new Apple device”。系统会要求您提供设备的描述和设备 ID。
描述几乎可以是任何名称。如果您只有一个 iOS 设备,那么您输入什么都无所谓。如果您有多个设备,那么您需要在这里输入一些能让您轻松识别当时使用的是哪个设备的内容。设备 ID 可以在 Xcode Organizer 中找到。切换回 Xcode,选择 Window 菜单,然后选择“Organizer”。当 Organizer 打开时,您会在左侧看到您的 iOS 设备。选择它。您可能会看到一个标有“Use this device for development”的按钮。如果看到了,就点击它。当您的设备信息显示在屏幕顶部时,找到标有“Identifier”的值并将其复制到剪贴板。返回到开发配置助手,将其粘贴到 Device ID 中,然后点击 Continue。您会看到一个确认您的证书已创建的提示。当您进入下一个屏幕时,您需要为您刚刚创建的配置文件指定一个名称。再点击两次 Continue 后,您就可以下载您的配置证书了。下载证书后,返回 Xcode Organizer,在配置区域点击“+”按钮。在打开的窗口中,导航到您的配置文件并选择它。完成此操作后,您的设备就配置好了。现在您应该可以在您的设备上运行该项目了。有时,当我在设备上部署配置证书时,它不起作用。当这种情况发生时,我必须断开然后重新连接我的 iDevice 一两次才能成功。
注意,配置文件会过期!
复制到您设备的配置文件有效期仅为大约三个月。过期后,您设备上与该配置文件关联的任何可执行文件都将停止运行。如果您想查看设备的配置文件何时过期,可以在“设置”菜单中查看。导航至“设置”->“通用”->“描述文件”以查看您当前的配置。
字符串解析、格式化及其他文本操作
字符串的基类是 `NSString
` 类。`NSString
` 类是不可变的,所以它包含的字符串不会改变。还有一个名为 `NSMutableString
` 的子类,其内容可以被改变。`NSString
` 类存储 Unicode 字符。字符串的长度可以通过其 `Length
` 方法探测,而字符串中特定位置的字符可以通过 `characterAtIndex
` 方法探测。
初始化字符串有几种方法。一种方法(您现在已经见过了)是将字面量文本用引号括起来,并在前面加上@符号(例如`@"Hello World!"`)。如果您需要从其他数据(例如整数值)创建字符串,可以使用类方法`stringWithFormat`。该方法接受一个格式化字符串,类似于您在C/C++中可能使用过的格式化字符串。以下代码会生成字符串“There are 12 items in a dozen.”(一打有12个物品。)
NSString* myString = [NSString withStringFormat:@"There are %d items in a dozen.", 12];
如果您有一个表示为字符串的数值,您可以使用 `xxxValue` 方法之一轻松地将其转换回数值(其中 xxx 可以是 `int`、`integer`、`double`、`bool`、`float` 或 `long`)。
还有许多其他的字符串操作方法可用,我无法在这里全部介绍。但我鼓励您浏览一下`NSString
`的文档,以了解更多可用的字符串操作方法。在本文档中,我将根据需要使用一些其他可用的方法。
数据持久化
在 iOS 设备上持久化数据有多种方式。如果您想使用传统的 C 语言 IO,您可以使用 `fopen
` 及其相关函数来操作文件。我不会在这里介绍。相反,我将介绍一些 iOS 特有的解决方案。
获取应用程序的存储路径
在 iOS 设备上,您的应用程序是沙盒化的。您的应用程序只能写入少数几个文件夹。有一个 /Documents 文件夹,用于存放您需要持久化并定期备份的信息。有一个 /Cache 文件夹,您可以放置那些应该在应用启动之间保持可用但不由 iTunes 备份的信息。还有一个 /tmp 文件夹,用于存放您仅在应用程序运行时需要的信息。它不能读取或写入设备上的任何其他位置。您可以使用函数 `NSSearchPathForDirectoriesInDomain()
` 来获取您应用程序的 /Documents 文件夹的路径。这个函数在 iOS 和 Mac OS X 上都存在。您会发现它并非支持所有可用选项;这要么是因为该选项在 iOS 上不支持,要么是因为您的应用程序没有权限使用某些选项。
此函数返回一个满足给定条件的目录数组。我们在这里的初始用法是检索单个目录,所以当我们检索到 Documents 文件夹的路径时,我们将得到一个包含一项的数组。我们必须传递给函数的第一个参数是常量 `NSDocumentDirectory
`。顾名思义,这告诉函数我们想要 Documents 文件夹。第二个参数是常量 `NSUserDomainMask
`。这指定了我们希望将搜索范围限制在我们的应用程序文件夹内。
NSArray *myPathList =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
NSString *myPath = [myPathList objectAtIndex:0];
要构建用于读写目的的文件名,我们只需将文件名连接到我们的路径上。`NSString
` 有一个专门用于此目的的方法。
NSString *myFilename = [myPath stringByAppendingPathComponent:@"fileName.txt"];
对于临时文件,有一个不带参数的函数,它返回一个可用于临时文件的路径。它的名字是 `NSTemporaryDirectory();
`
构建好您的文件名后,您就可以将其用于其中一个数据持久化 API。
PList
PList(或属性列表)易于使用。如果您仅使用一组特定的对象在内存中创建数据层次结构,那么该层次结构可以一次性写入文件或从文件中加载。作为开发者,您不必担心序列化您的数据。这是为您完成的。使用此解决方案的一个缺点是,即使您对数据做了很小的更改,也必须重写整个文件;您不能进行增量更改。以下是您在为 PList 创建对象层次结构时可以使用的类:
NSArray
NSData
NSDate
NSDictionary
MutableArray
NSString
NSMutableString
NSNumber
在您构建了对象层次结构之后,您可以使用 `writeToFile
` 方法将该结构及其数据保存到文件中。它需要一个文件名和一个 `Atomically
` 参数的值。如果 `Atomically
` 设置为 yes,那么数据将不会直接保存到目标文件,而是会先存储到一个临时文件,然后再移动到目标文件名。这需要更长的时间,但安全得多。如果设备崩溃,您最终得到一个损坏文件的可能性较小。要加载文件的内容,我们可以只实例化根元素,并使用初始化方法 `initWithContentsOfFile
`。此方法以文件路径作为其唯一参数,并会在填充数据时负责构建整个层次结构。
在下面的代码示例中,我使用 PList 将数据保存到文件中。我创建了一个简单的界面,包含三个文本框(用于数据)和两个按钮来调用保存和加载功能。
虽然不可见,但在按钮下方还有一个标签控件。当数据加载或保存时,文件的路径会显示在那里。当点击保存按钮时,文本框的内容会被复制到一个 `NSArray
` 中,并使用 `writeToFile
` 方法保存到一个文件中。当点击加载按钮时,会创建一个新的 `NSArray
`,并使用 `initWithContentsOfFile
` 方法进行初始化。
如果您想查看文件是否真的保存了,请运行程序,保存一些文本,然后关闭程序。如果您再次运行它,它将能够重新加载数据。
#import <uikit>
@interface DataPersistenceExampleViewController : UIViewController {
IBOutlet UITextField* textBox00;
IBOutlet UITextField* textBox01;
IBOutlet UITextField* textBox02;
IBOutlet UILabel* storageLocationLabel;
NSString* storagePath;
}
@property (retain) IBOutlet UITextField *textBox00;
@property (retain) IBOutlet UITextField *textBox01;
@property (retain) IBOutlet UITextField *textBox02;
@property (retain) IBOutlet UILabel *storageLocationLabel;
@property (retain) NSString *storagePath;
- (IBAction)saveButtonClicked:(id)sender;
- (IBAction)loadButtonClicked:(id)sender;
@end
#import "DataPersistenceExampleViewController.h"
@implementation DataPersistenceExampleViewController
@synthesize textBox00;
@synthesize textBox01;
@synthesize textBox02;
@synthesize storageLocationLabel;
//@synthesize storagePath;
- (IBAction)saveButtonClicked:(id)sender
{
NSArray* valueList = [NSArray arrayWithObjects:
textBox00.text,textBox01.text, textBox02.text, nil];
NSArray *myPathList = NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES);
NSString *myPath = [myPathList objectAtIndex:0];
storagePath = [myPath stringByAppendingPathComponent:@"myData.txt"];
storageLocationLabel.text = storagePath;
[valueList writeToFile:storagePath atomically:true];
}
- (IBAction)loadButtonClicked:(id)sender
{
NSArray *myPathList = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *myPath = [myPathList objectAtIndex:0];
storagePath = [myPath stringByAppendingPathComponent:@"myData.txt"];
NSArray* valueList = [[NSArray alloc] initWithContentsOfFile:storagePath];
textBox00.text = [valueList objectAtIndex:0];
textBox01.text = [valueList objectAtIndex:1];
textBox02.text = [valueList objectAtIndex:2];
}
- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
- (void)viewDidUnload {
self.textBox00=nil;
self.textBox01=nil;
self.textBox02=nil;
self.storageLocationLabel=nil;
}
- (void)dealloc {
[textBox00 release];
[textBox01 release];
[textBox02 release];
[storageLocationLabel release]
[super dealloc];
}
@end
使用归档器写入文件
虽然 PList 易于操作,但您可以在其中存储的数据类型相当基础。只要对象遵循 NSCoding 协议,归档器就可以将任何类型的对象写入文件。NSCoding 定义了用于读取和写入对象状态的消息。当一个对象被保存到文件时,它的 `encodeWithCoder:
` 方法会被调用,以便它可以表达其状态信息。在解档期间(当对象从文件中读回时),`initWithCoder:
` 方法被调用,并且之前由归档器保存的信息被提供,以便对象可以重新初始化自己。
这两个消息都接收一个类型为 `NSCoder
` 的参数。`NSCoder
` 对象有多种方法用于编码不同的数据类型,如布尔值、字节、字符串和其他类型的对象。对于每种编码方法,也都有一个对应的解码方法。
编码方法 |
解码方法 |
|
|
|
|
|
|
使用加速计
设备内的加速计可以检测加速力。地球上的重力等于一个物体以 9.81 米/秒^2 加速时所受的力。因此,一个静止的设备会检测到一个指向地心引力方向的力。如果设备处于自由落体状态,其加速计的读数将接近于零(请相信我的话,不要通过摔设备来测试)。加速计的读数由沿 X、Y 和 Z 轴的测量值组成,因此 iPhone 中的加速计可以检测任何方向的运动。
每个应用程序都可以访问一个单独的 `UIAccelerometer
` 实例来读取加速计数据。您不直接实例化这个实例。相反,您通过 `UIAccelerometer
` 类的类方法 `sharedAccelerometer
` 来获取它。一旦您拥有了这个类的实例,您需要提供一个将接收数据的委托和一个更新间隔。最小的更新间隔是10毫秒,这将导致每秒更新100次。大多数应用程序不需要如此频繁的更新。当您不再需要加速计数据时,可以将委托设置为 `nil
` 来停止接收更新。当您实际上不需要加速计数据时,不应该让更新机制保持活动状态。这样做会导致电池消耗得更快。
您需要实现的委托方法名为 `accelerometer:didAccelerate:
`。当此方法被调用时,它将同时接收到对加速计的引用和加速读数。
根据苹果的文档,以下是推荐的更新频率范围
频率 (Hz) |
用法 |
10-20 |
获取设备的当前方向 |
30-60 |
游戏和其他实时输入应用 |
70-100 |
用于检测高频运动,例如用户敲击设备或快速摇晃设备。 |
如果您只需要知道设备的大致方向而不需要加速计数据,那么您应该从 `UIDevice
` 类中检索设备的方向。要检索设备的方向,您必须首先调用 `UIDevice
` 的 `beginGeneratingDeviceOrientationNotifications
`。一旦调用了这个方法,您就可以从 `Orientation
` 属性中读取当前的方向。您还可以订阅 `UIDeviceOrientationDidChangeNotification
`。在任何情况下,如果您的应用程序不再需要方向事件,它应该调用 `endGeneratingDeviceOrientationNotifications
` 来关闭加速计并节省电量。
为了演示,我正在构建一个程序,它将读取加速计的值并将其显示在屏幕上。
#import <uikit>
@interface AccelerometerDemonstrationViewController : UIViewController {
IBOutlet UITextField* AccelX;
IBOutlet UITextField* AccelY;
IBOutlet UITextField* AccelZ;
UIAccelerometer *myAccelerometer;
}
@property (retain) IBOutlet UITextField *AccelX;
@property (retain) IBOutlet UITextField *AccelY;
@property (retain) IBOutlet UITextField *AccelZ;
-(IBAction)startAccelerometerClicked:(id)sender;
-(IBAction)stopAccelerometerClicked:(id)sender;
-(void)accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration;
@end
#import "AccelerometerDemonstrationViewController.h"
@implementation AccelerometerDemonstrationViewController
@synthesize AccelX;
@synthesize AccelY;
@synthesize AccelZ;
-(IBAction)startAccelerometerClicked:(id)sender {
[[UIAccelerometer sharedAccelerometer] setUpdateInterval:0.20] ;
[[UIAccelerometer sharedAccelerometer] setDelegate:self];
}
-(IBAction)stopAccelerometerClicked:(id)sender {
[[UIAccelerometer sharedAccelerometer] setDelegate:nil];
}
-(void)accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration {
[AccelX.text release];
[AccelY.text release];
[AccelZ.text release];
AccelX.text = [NSString stringWithFormat:@"%f", acceleration.x];
AccelY.text = [NSString stringWithFormat:@"%f", acceleration.y];
AccelZ.text = [NSString stringWithFormat:@"%f", acceleration.z];
}
- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
- (void)viewDidUnload {
self.AccelX = nul;
self.AccelY = nul;
self.AccelZ = nul;
}
- (void)dealloc {
[AccelX dealloc];
[AccelY dealloc];
[AllocZ dealloc]
[super dealloc];
}
@end
我可以单独就处理加速计时有用的数学方程和算法进行完整的讨论。目前,我只提两个我在之前的程序中处理加速计时发现很有用的。第一个方程是获取加速计值的模长。对于一个不动的设备,这个数字应该接近于一(尽管由于加速计不是100%精确,这个数字可能会在一的上下轻微浮动)。对于一个快速加速的物体,它将大于一。对于一个自由落体的设备,它将接近于零。您可以通过计算`(x*x+y*y+z*z)`的平方根来得到加速计读数的模长。另一个值得了解的方程是获取设备沿某个平面倾斜的方向(以弧度为单位)。例如,如果我把一个设备平放在桌子上,然后决定倾斜它,设备倾斜的方向可以用`atan2(y,x)`来计算。
下一步?
虽然本文足以让您开始使用 UI 框架中可用的控件面板,但在某个时候,您也会想要渲染自己的图形。在您对 iOS 开发更加熟悉之后,我建议您看一下我的 iOS 图形 API 介绍。
闭运算
我希望这足以帮助您入门 iOS 开发。我计划随着时间的推移,编辑这篇文章并添加新的文章,专注于 iPhone 开发的各个方面。如果您有任何反馈或建议,请随时在下方留言或给我发消息。
致谢
我要感谢 CraigNicholson 和 Tom the Cat,他们指出了本文及其代码原始版本中的错误或 bug,从而帮助了我。
历史
- 2010年6月21日 - 首次发布。
- 2010年7月19日 - 整合了评论中的反馈,添加了指向图形 API 介绍的链接。