终极版本的Objective-C教程备忘单帮助你进行iOS开发。
想开始创建你的第一个iOS应用程序么?那么看一下这篇很棒的教程吧:Create your first iOS 7 Hello World Application
注:这篇文章我写了三天,可能在一些必要的地方使用了编辑和说明,所以如果有任何疑问和修改建议请在下方评论。
这不是一个初学者指南,也不是关于Objective-C的详细讨论,这是关于常见的和高水平的论题的快速索引。
如果这里有些问题没有涉及到,你也可以查阅以下文章:
Objective-C: A Comprehensive Guide for Beginners
The Objective-C Programming Language
内容目录
Commenting
Data Types
Constants
Operators
Declaring Classes
Preprocessor Directives
Compiler Directives
Literals
Methods
Properties and Variables
Naming Conventions
Blocks
Control Statements
Enumeration
Extending Classes
Error Handling
Passing Information
User Defaults
Common Patterns
注释
Objective-C中注释用来组织代码,并为将来的代码重构或者那些可能阅读你代码的iOS开发者们提供重要的额外信息。注释通常会被编译器忽视,因此它们不会增加编译器程序的大小。
有两种方式添加注释:
- // This is an inline comment(内嵌注释)
- /* This is a block comment and it can span multiple lines. */(块注释,可用于多行注释)
- // You can also use it to comment out code(也可以用来注释掉代码)
- /*
- - (SomeOtherClass *)doWork
- {
- // Implement this
- }
- */
使用pragma mark来组织代码:
- #pragma mark - Use pragma mark to logically organize your code(使用#pragma mark可以对代码进行逻辑组织)
- // Declare some methods or variables here(在此处声明方法和变量)
- #pragma mark - They also show up nicely in the properties/methods list in Xcode(在Xcode中#pragma mark还可以用来很好地显示属性及方法列表)
- // Declare some more methods or variables here(在此处声明方法和变量)
数据类型
数据类型的大小
无论是在32位还是64位的系统环境中,允许的数据类型大小是由为具体的类型分配了多少内存字节决定的。在32位的系统环境中,长整型(long)被分配4字节,相当于2^(4*8)(每个字节有8位)或者4294967295的内存范围。在64为的系统环境中,长整型(long)被分配8字节,相当于2^(8*8)或者1.84467440737096e19的范围。
64位系统环境的变化的完全指南,请参考:transition document。
C语言基础
注:Objective-C继承了所有的C语言基本类型,然后又新增了一些其他的类型。
Void(空类型)
Void是C语言的空数据类型。通常用于指定无返回值的函数的返回值类型(即,函数类型为无返回值类型)。
整数
整数既可以是signed(有符号)的也可以是unsigned(无符号)的。signed(有符号)代表是正整数或者负整数,unsigned(无符号)代表只能是正整数。
整数类型以及它们的字节大小:
- // Char (1 byte for both 32-bit and 64-bit)
- unsigned char anUnsignedChar = 255;
- NSLog(@"char size: %zu", sizeof(char));
- // Short (2 bytes for both 32-bit and 64-bit)
- short aShort = -32768;
- unsigned short anUnsignedShort = 65535;
- NSLog(@"short size: %zu", sizeof(short));
- // Integer (4 bytes for both 32-bit and 64-bit)
- int anInt = -2147483648;
- unsigned int anUnsignedInt = 4294967295;
- NSLog(@"int size: %zu", sizeof(int));
- // Long (4 bytes for 32-bit, 8 bytes for 64-bit)
- long aLong = -9223372036854775808; // 32-bit
- unsigned long anUnsignedLong = 18446744073709551615; // 32-bit
- NSLog(@"long size: %zu", sizeof(long));
- // Long Long (8 bytes for both 32-bit and 64-bit)
- long long aLongLong = -9223372036854775808;
- unsigned long long anUnsignedLongLong = 18446744073709551615;
- NSLog(@"long long size: %zu", sizeof(long long));
固定宽度的整数类型和字节大小来作为变量名:
- // Exact integer types
- int8_t aOneByteInt = 127;
- uint8_t aOneByteUnsignedInt = 255;
- int16_t aTwoByteInt = 32767;
- uint16_t aTwoByteUnsignedInt = 65535;
- int32_t aFourByteInt = 2147483647;
- uint32_t aFourByteUnsignedInt = 4294967295;
- int64_t anEightByteInt = 9223372036854775807;
- uint64_t anEightByteUnsignedInt = 18446744073709551615;
- // Minimum integer types
- int_least8_t aTinyInt = 127;
- uint_least8_t aTinyUnsignedInt = 255;
- int_least16_t aMediumInt = 32767;
- uint_least16_t aMediumUnsignedInt = 65535;
- int_least32_t aNormalInt = 2147483647;
- uint_least32_t aNormalUnsignedInt = 4294967295;
- int_least64_t aBigInt = 9223372036854775807;
- uint_least64_t aBigUnsignedInt = 18446744073709551615;
- // The largest supported integer type
- intmax_t theBiggestInt = 9223372036854775807;
- uintmax_t theBiggestUnsignedInt = 18446744073709551615;
浮点类型
浮点没有signed或者unsigned
- // Single precision floating-point (4 bytes for both 32-bit and 64-bit)单精度浮点float aFloat = 72.0345f;
- NSLog(@"float size: %zu", sizeof(float));
- // Double precision floating-point (8 bytes for both 32-bit and 64-bit)双精度浮点
- double aDouble = -72.0345f;
- NSLog(@"double size: %zu", sizeof(double));
- // Extended precision floating-point (16 bytes for both 32-bit and 64-bit)扩展精度浮点
- long double aLongDouble = 72.0345e7L;
- NSLog(@"long double size: %zu", sizeof(long double));
Objective-C基础
id:被定义为匿名或者动态对象类型,它可以存储任何类型对象的引用,不需要指定指针符号。
- id delegate = self.delegate;
Class:用来表示对象的类,并能用于对象的内省。
- Class aClass = [UIView class];
Method:用来表示一个方法,并可用于swizzling方法。
- Method aMethod = class_getInstanceMethod(aClass, aSelector);
SEL:用于指定一个selector,它是编译器指定的代码,用于识别方法的名称。
- SEL someSelector = @selector(someMethodName);
IMP:用于在方法开始时指向内存地址。你可能不会用到这个。
- IMP theImplementation = [self methodForSelector:someSelector];
BOOL:用来指定一个布尔类型,布尔类型中0值被认为是NO(false),任何非零值被认为是YES(true)。任何零值对象都被认为是NO,因此不需要对零值进行同样的验证(例如,只要写if(someObject),不需要写if (someObject !=nil))
- // Boolean
- BOOL isBool = YES; // Or NO
nil:用来指定一个空对象指针。当类第一次被初始化时,类中所有的属性被设置为0,这意味着都指向空指针。
Objective-C中还有很多其他类型,如NSInteger, NSUInteger, CGRect,CGFloat, CGSize, CGPoint等。
Enum(枚举)和Bitmask(位掩码)类型
Objective-C的枚举类型可以用以下多个不同方式定义:
- // Specifying a typed enum with a name (recommended way)用一个别名来指定一个带有typedef关键字的枚举类型(推荐方法)
- typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
- UITableViewCellStyleDefault,
- UITableViewCellStyleValue1,
- UITableViewCellStyleValue2,
- UITableViewCellStyleSubtitle
- };
- // Specify a bitmask with a name (recommended way)用一个别名来指定一个bitmask(推荐方法)
- typedef NS_OPTIONS(NSUInteger, RPBitMask) {
- RPOptionNone = 0,
- RPOptionRight = 1 << 0,
- RPOptionBottom = 1 << 1,
- RPOptionLeft = 1 << 2,
- RPOptionTop = 1 << 3
- };
- // Other methods:(其他方法)
- // Untyped(无类型)
- enum {
- UITableViewCellStyleDefault,
- UITableViewCellStyleValue1,
- UITableViewCellStyleValue2,
- UITableViewCellStyleSubtitle
- };
- // Untyped with a name 用一个别名来定义一个带有typedef关键字将枚举类型
- typedef enum {
- UITableViewCellStyleDefault,
- UITableViewCellStyleValue1,
- UITableViewCellStyleValue2,
- UITableViewCellStyleSubtitle
- } UITableViewCellStyle;
构造数据类型
有时为一个特定的类或者数据类型构造一个id或者不同的类型时很有必要的。例如,从一个浮点型构造成整数或者从一个UITableViewCell构造成一个如RPTableViewCell的子类。
构造非对象数据类型:(cast)
- // Format: nonObjectType variableName = (nonObjectType)
- variableNameToCastFrom;
- int anInt = (int)anAnonymouslyTypedNonObjectOrDifferentDataType;
构造对象数据类型:
- // Format: ClassNameOrObjectType *variableName =(ClassNameOrObjectType *)variableNameToCastFrom;
- UIViewController *aViewController = (UIViewController *)
- anAnonymouslyTypedObjectOrDifferentDataType;
常量
使用常量通常是一个更好的方法,因为常量为代码中的任何对象的引用指向相同的内存地址。#define定义了一个宏。在编译开始前,宏用实际常量值来代替所有的引用,而不是作为指向常量值的内存指针。
Objective-C常量可以这样定义:
- // Format: type const constantName = value;
- NSString *const kRPShortDateFormat = @"MM/dd/yyyy";
- // Format: #define constantName value
- #define kRPShortDateFormat @"MM/dd/yyyy"
要是在扩展类中能使用常量,你必须也将它添加在头文件(.h)中。
- extern NSString *const kRPShortDateFormat;
如果你知道一个常量只可用于它包含的.m文件中,可以这样指定它:
- static NSString *const kRPShortDateFormat = @"MM/dd/yyyy";
在方法中声明一个静态变量在调用时值不会改变。当为一个属性声明一个singleton(单例)或者创建setter和getter器时这将很有用处。
运算符
算术运算符
关系运算符
逻辑运算符
复合赋值运算符
增值或减值运算符
位运算符
其他运算符
声明类
声明类需要两个文件:一个头文件(.h)和一个实现文件(.m)
头文件应包含(按如下顺序):
1. 所有需要#import的语句或者在前面@class声明;
2. 任何协议声明;
3. @interface声明指定继承自哪个类;
4. 所有可访问的公共变量、属性以及方法;
实现文件应包含(按如下顺序):
1. 所有需要的#import语句;
2. 所有的私有变量或属性的种类或者类扩展;
3. @implementation声明指定一个类;
4. 所有的公共或私有方法;
如下例子:
MyClass.h
- #import "SomeClass.h"
- // Used instead of #import to forward declare a class in property return types, etc.
- @class SomeOtherClass;
- // Place all global constants at the top extern NSString *const kRPErrorDomain;
- // Format: YourClassName : ClassThatYouAreInheritingFrom
- @interface MyClass : SomeClass
- // Public properties first
- @property (readonly, nonatomic, strong) SomeClass *someProperty;
- // Then class methods
- + (id)someClassMethod;
- // Then instance methods
- - (SomeOtherClass *)doWork;
- @end
MyClass.m
- #import "MyClass.h"
- #import "SomeOtherClass.h"
- // Declare any constants at the top
- NSString *const kRPErrorDomain = @"com.myIncredibleApp.errors";
- static NSString *const kRPShortDateFormat = @"MM/dd/yyyy";
- // Class extensions for private variables / properties
- @interface MyClass ()
- {
- int somePrivateInt;
- // Re-declare as a private read-write version of the public read-only property
- @property (readwrite, nonatomic, strong) SomeClass
- *someProperty;
- }
- @end
- @implementation MyClass
- // Use #pragma mark - statements to logically organize your code
- #pragma mark - Class Methods
- + (id)someClassMethod
- {
- return [[MyClass alloc] init];
- }
- #pragma mark - Init & Dealloc methods
- - (id)init
- {
- if (self = [super init]) {
- // Initialize any properties or setup code here
- }
- return self;
- }
- // Dealloc method should always follow init method
- - (void)dealloc
- {
- // Remove any observers or free any necessary cache, etc.
- [super dealloc];
- }
- #pragma mark - Instance Methods
- - (SomeOtherClass *)doWork
- {
- // Implement this
- }
- @end
实例化
当想要创建类的新实例时,你需要使用如下语法:
- MyClass *myClass = [[MyClass alloc] init];
alloc类方法返回一个指针指向一个新分配的内存块,这个内存块空间足够大可以储存这个类的一个实例。这个分配的内存中包含了所有Objective-C对象都必须有的实例变量和isa指针。isa指针变量自动初始化指向类对象,类对象分配内存并使实例能够接收消息,比如用来完成初始化的init。
预处理器指令
本节仍有需要改进的地方
编译器指令
请参考literal(字面)章节
类和协议
属性
错误
实例变量的可变性
默认的是@protected类型,所以不用明确地指定此类型。
其他
Literals(字面语法)
字面语法是的编译器指令,它提供简化符号来创建对象。
NSArray访问语法:
- NSArray *example = @[ @"hi", @"there", @23, @YES ];
- NSLog(@"item at index 0: %@", example[0]);
NSDictionary访问语法:
- NSDictionary *example = @{ @"hi" : @"there", @"iOS" : @"people" };
- NSLog(@"hi %@", example[@"hi"]);
注意事项:与NSString类似,通过常量数组和字典收集对象是不可变的。相反,你必须在创建这个不可变的字典或者数组后创建一个可变的副本。此外,你不能像使用NSString那样做静态初始化。
方法
声明语法
方法没有返回值类型时,用void定义:
- // Does not return anything or take any arguments
- - (void)someMethod;
用“+”调用之前声明的类方法:
- // Call on a class (e.g. [MyClass someClassMethod]);
- + (void)someClassMethod;
用“-”调用之前声明的类的实例方法:
- // Called on an instance of a class (e.g. [[NSString alloc] init]);
- - (void)someClassInstanceMethod;
在“:”后面声明方法参数,方法签名应该描述参数类型:
- // Does something with an NSObject argument
- - (void)doWorkWithObject:(NSObject *)object;
使用强制转换语法声明参数和返回值类型:
- // Returns an NSString object for the given NSObject arguments
- - (NSString *)stringFromObject:(NSObject *)objectandSomeOtherObject:(NSObject *)otherObject;
方法调用
使用方括号语法调用方法: [self someMethod]或者[selfsometMethodWithObject:object];
self是对包含类的方法的一个引用。self变量存在于所有Objective-C方法中。它是传递给代码用以执行方法的两个隐藏参数之一。另外一个是_cmd,用于识别接收到的消息。
有时,很有必要使用[super someMethod];在父类中调用一个方法。
在高级选项下,方法通过消息传递实现,并且转换成了这两个C函数其中一个的:
- id objc_msgSend(id self, SEL op, ...);
- id objc_msgSendSuper(struct objc_super *super, SEL op, ...);
这有一个很棒的Objective-C教程系列的初学者指南,涵盖了更多关于方法调用的细节。请访问:Calling Methods in Objective-C。(http://ios-blog.co.uk/tutorials/objective-c-guide-for-developers-part-2/#methods)
测试选择器
如果你想要测试一个类是否在被发送(或者可能会崩溃)之前响应一个特定的选择器,你可以这样做:
- if ([someClassOrInstance respondsToSelector:@selector (someMethodName)])
- {
- // Call the selector or do something here
- }
当你实现一个委托,并且在委托对象上调用这些声明之前,你需要对声明为@optional的方法进行测试,这种模式很常见。
属性和变量
声明一个Objective-C属性允许你保留类中对象的一个引用或者在类间传递对象。
在头文件 ( .h)中声明公共属性:
- @interface MyClass : NSObject
- @property (readonly, nonatomic, strong) NSString *fullName;
- @end
在实现文件 ( .m)的匿名类或者扩展类中声明私有属性:
- #import "MyClass.h"
- // Class extension for private variables / properties
- @interface MyClass ()
- {
- // Instance variable
- int somePrivateInteger;
- // Private properties
- @property (nonatomic, strong) NSString *firstName;
- @property (nonatomic, strong) NSString *lastName;
- @property (readwrite, nonatomic, strong) NSString *fullName;
- }
- @end
- @implementation MyClass
- // Class implementation goes here
- @end
LLVM编译器自动综合所有属性,因此不再需要为属性写明@synthesize语句。当一个属性被综合时,就会创建accessors,允许你设置或者获得属性的值。尽管你可能不会看到它们,因为它们是在构建时创建的,但是一对getter/setter可以显示为:
- - (BOOL)finished
- {
- return _finished;
- }
- - (void)setFinished:(BOOL)aValue
- {
- _finished = aValue;
- }
你可以重写属性的getter和seeter来创建自定义的行为,或者甚至是使用这个模式来创建瞬态属性,如下:
- - (NSString *)fullName
- {
- return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
- }
属性后面通常会有一个带有前导下划线的实例变量,因此创建一个名为fistName的属性会带有一个名为_firstName的实例变量。如果你重写getter/setter或者你需要在类的init方法中设置ivar,你只需要访问私有实例变量。
属性特性
当指定一个属性时,使用如下语法:
- @property SomeClass *someProperty;
- // Or
- @property (xxx) SomeClass *someProperty;
xxx可以与什么组合:
访问属性
使用括号或者点操作都可以访问属性,点操作读起来更清晰:
- [self myProperty];
- // Or
- self.myProperty
局部变量
局部变量只存在于方法的范围内。
- - (void)doWork
- {
- NSString *localStringVariable = @"Some local string variable.";
- [self doSomethingWithString:localStringVariable];
- }
命名约定
一般的经验法则: 清晰和简洁都是重要的,但是清晰更重要。
方法和属性
都是用驼峰式拼写法,第一个单词的首字母为小写,其他单词的首字母都大写。
类名和协议
都是用大写字母,每个单词的首字母都大写。
方法
如果执行一些动作,那么应该使用动词(如performInBackground)。你应该推断出知道发生了什么,方法需要什么参数,或者只通过阅读一个方法签名就知道返回了什么。例如:
- // Correct
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- // Code
- }
- // Incorrect (not expressive enough)
- - (UITableViewCell *)table:(UITableView *)tableView cell:(NSIndexPath *)indexPath
- {
- // Code
- }
属性和局部变量
使用属性时,在内部创建一个带有前导下划线的实例变量,因此创建myVariableName为_myVariableName。然而,Objective-C现在为你综合这些属性,因此你不再需要直接访问带有下划线的实例变量,除非在一个自定义setter中。
相反,通常用selfl访问和变异(mutated)实例变量。
局部变量不能有下划线。
常量
常量通常以k和 XXX命名方式, XXX是一个前缀,或许你的首字母可以避免命名冲突。你不应该害怕你要表达的常量名称,尤其是全局变量时。使用kRPNavigationFadeOutAnimationDuration比fadeOutTiming好的多。
Blocks
在Objective-C中,块(block)实际上是一个匿名函数,用来在方法间传递代码或者在函数回调时执行代码。由于Block实现为闭包,周围的语句也被捕获(有时会导致retain cycles)。
语法
- // As a local variable
- returnType (^blockName)(parameterTypes) = ^returnType(parameters) {
- // Block code here
- };
- // As a property
- @property (nonatomic, copy) returnType (^blockName)(parameterTypes);
- // As a method parameter
- - (void)someMethodThatTakesABlock:(returnType (^)(parameterTypes))blockName {
- // Block code here
- };
- // As an argument to a method call
- [someObject someMethodThatTakesABlock: ^returnType (parameters) {
- // Block code here
- }];
- // As a typedef
- typedef returnType (^TypeName)(parameterTypes);
- TypeName blockName = ^(parameters) {
- // Block code here
- };
查看我们专门的文章了解更多细节Objective-C Programming with blocks
变异的block变量
由于block中的变量正是它们在block区域外部的一个快照,你必须在block内将你的变量用_block变异,例如:
- __block int someIncrementer = 0;
- [someObject someMethodThatTakesABlock:^{
- someIncrementer++;
- }];
Retain cycles
由于block有力地捕捉区域内的所有变量,你必须谨慎设置block代码,以下为retain cycle的两个例子:
- [someObject someMethodThatTakesABlock:^{
- [someObject performSomeAction]; // Will raise a warning
- }];
- [self someMethodThatTakesABlock:^{
- [self performSomeAction]; // Will raise a warning
- }];
这两个例子中,执行block的对象都拥有block,block也拥有对象。这行形成一个回路,或者一个retain cycle,这就意味着内存最终将会泄露。
为了解决这个警告你可以重构代码:
- [self someMethodThatTakesABlock:^{
- [object performSomeAction]; // No retain cycle here
- }];
或者你可以用一个_weak对象:
- __weak typeof(self) weakSelf = self;
- [self someMethodThatTakesABlock:^{
- [weakSelf performSomeAction]; // No retain cycle here
- }];
控制语句
Objective-C使用其他语言所有相同的控制语句:
If-Else If-Else:
- if (someTestCondition) {
- // Code to execute if the condition is true
- } else if (someOtherTestCondition) {
- // Code to execute if the other test condition is true
- } else {
- // Code to execute if the prior conditions are false
- }
三元运算符
if-else的一个速记语句就是一个三元运算符形式:someTestCondition ? doIfTrue : doIfFalse;
例如:
- - (NSString *)stringForTrueOrFalse:(BOOL)trueOrFalse
- {
- return trueOrFalse ? @"True" : @"False";
- }
这还有一个不太常用的形式:A ?: B,基本上是指如果A是正确的或者非空的,就返回A;反之,返回B。
For循环
更多详解,请看Objective-C: Loops
- for (int i = 0; i < totalCount; i++) {
- // Code to execute while i < totalCount
- }
快速枚举
- for (Person *person in arrayOfPeople) {
- // Code to execute each time
- }
arrayOfPeople可以是符合NSFastEnumeration协议的任何对象. NSArray和NSSet列举它们的对象,NSDictionary列举关键词,NSManagedObjectModel列举实体。
While循环
- while (someTextCondition) {
- // Code to execute while the condition is true
- }
Do While循环
- do {
- // Code to execute while the condition is true
- } while (someTestCondition);
Switch(转换)
switch语句通常用来代替if语句,如果需要测试一个指定的变量的值是否匹配另一个常量或者变量值。例如,你或许想要测试你接收到的error代码整数是否匹配现有的常数值,或者如果它是一个新的错误代码。
- switch (errorStatusCode)
- {
- case kRPNetworkErrorCode:
- // Code to execute if it matches
- break;
- case kRPWifiErrorCode:
- // Code to execute if it matches
- break;
- default:
- // Code to execute if nothing else matched
- break;
- }
退出循环
1. return:停止执行,返回到调用的函数。可以用于返回一个方法的值。
2. break:用来停止执行一个循环。
新的枚举方法有一个特别的BOOL变量(如BOOL *stop)用来停止执行循环。在循环内将变量设置为YES,类似于调用break。
枚举
在statements章节已经提高了快速枚举,但是很多集合类也有他们自己的基于block的方法来枚举一个集合。
基于block的方法与快捷枚举执行方法几乎相同,但是他们为枚举集合提供了额外的选择。在NSArray上基于block的枚举示例如下:
- NSArray *people = @[ @"Bob", @"Joe", @"Penelope", @"Jane" ];
- [people enumerateObjectsUsingBlock:^(NSString *nameOfPerson, NSUInteger idx, BOOL *stop) {
- NSLog(@"Person‘s name is: %@", nameOfPerson);
- }];
扩展类
在Objective-C中有几个不同方法可以扩展类,其中一些方法更简单。
继承
继承本质上允许你创建特定的子类,这些子类继承父类头文件中所有的指定为@ public或@protected的方法和属性时,通常有指定的用法。
通过任何框架或者开源项目,你可以看到使用继承不仅获得开放的用法,也整合代码使之很容易重用。可在任何可变框架类中看到这种方法的例子,如NSMutableString就是NSString的一个子类。
在Objective-C中,通过类的继承,所有对象都有很多NSObject class指定的行为。没有继承,你需要自己实现基本的方法,如检查对象或者类对等。最终你将会在类中会有很多重复性的代码。
- // MyClass inherits all behavior from the NSObject class
- @interface MyClass : NSObject
- // Class implementation
- @end
继承本质上创建了类之间的耦合,因此要仔细想想这个用法。
类别
Objective-C类别是一个非常有用并且简单的扩展类的方法,尤其是当你没有访问源码(例如Cocoa Touch框架和类)时。可以为任何一个类或者方法声明一个category,并且在类别中声明的方法对原始类及其子类中所有实例都可用。在运行时,通过类别添加一个方法与通过原始类实现一个方法没有什么不同。
类别也可用于:
1.声明非正式协议;
2.为相关的方法分类,似于有多个类
3.将大型类的实现分解成多个类别,这有助于增量编译
4.很容易为不同的应用程序配置不同的类
实现
类别的命名格式为:ClassYouAreExtending + DescriptorForWhatYouAreAdding
例如,假设我们需要为UIImage类(以及所有子类)中添加一个新的方法,以便我们可以很容易地调整实例大小或者获得实例 在这个类中很容易的重调或者拷贝实例。那么你需要用如下实现方法来创建一个名为UIImage+ResizeCrop的头文件和实现文件。
UIImage+ResizeCrop.h
- @interface UIImage (ResizeCrop)
- - (UIImage *)croppedImageWithSize:(CGSize)size;
- - (UIImage *)resizedImageWithSize:(CGSize)size;
- @end
UIImage+ResizeCrop.m
- #import "UIImage+ResizeCrop.h"
- @implementation UIImage (ResizeCrop)
- - (UIImage *)croppedImageWithSize:(CGSize)size
- {
- // Implementation code here
- }
- - (UIImage *)resizedImageWithSize:(CGSize)size
- {
- // Implementation code here
- }
然后你可以在任何UIImage类或者其子类上调用这些方法:
- UIImage *croppedImage = [userPhoto croppedImageWithSize:photoSize];
关联引用
除非你在编译时访问类的源码,否则不太可能通过使用category为那个class添加实例变量和属性。相反,你必须通过使用Objective-C运行时的特性,即关联引用。
例如,假设我们想要在UIScrollView类中添加一个公共属性来存储一个UIView对象的引用,但是我们不能访问UISCrollView的源码。我们就需要在UIScrollView中创建一个类别,然后为这个新属性创建一对getter/setter方法来存储这个引用,如下:
UIScrollView+UIViewAdditions.h
- #import <UIKit/UIKit.h>
- @interface UIScrollView (UIViewAdditions)
- @property (nonatomic, strong) UIView *myCustomView;
- @end
UIScrollView+UIViewAdditions.m
- #import "UIScrollView+UIViewAdditions.h"
- #import <objc/runtime.h>
- static char UIScrollViewMyCustomView;
- @implementation UIScrollView (UIViewAdditions)
- @dynamic myCustomView;
- - (void)setMyCustomView:(UIView *)customView
- {
- [self willChangeValueForKey:@"myCustomView"];
- objc_setAssociatedObject(self, &UIScrollViewMyCustomView, customView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- [self didChangeValueForKey:@"myCustomView"];
- }
- - (UIView *)myCustomView {
- return objc_getAssociatedObject(self, &UIScrollViewMyCustomView);
- }
- @end
让我们解释一下这里发生了什么:
1.我们创建了一个静态密钥UIScrollViewMyCustomView,可用于获得和设置一个关联的对象。之所以把它声明为静态是为了确保它始终指向相同的内存地址。
2.接下来,我们声明一个带有@dynamic的属性,这告诉了编译器getter/setter不是UIScrollView类自己实现的。
3.在setter中,我们用willChangeValueForKey紧跟didChangeValueForKey,以确保在属性改变时通知了任意的key-value observers。
4.在setter中,我们使用objc_setAssociatedObject来存储一个我们真正需要的对象引用,在静态密钥下我们创建了customView。&是用来表示一个指向UIScrollViewMyCustomView的指针(给UIScrollViewMyCustomView的指针获取地址)。
5.在setter中,我们使用objc_getAssociatedObject和指向静态密钥的指针来检索对象引用。
我们可以像使用其他属性一样使用这个属性:
- UIScrollView *scrollView = [[UIScrollView alloc] init];
- scrollView.myCustomView = [[UIView alloc] init];
- NSLog(@"Custom view is %@", scrollView.myCustomView);
- // Custom view is <UIView: 0x8e4dfb0; frame = (0 0; 0 0); layer = <CALayer: 0x8e4e010>>
文档中更多的信息:
- The [objc_setAssociatedObject] function takes four parameters: the source object, a key, the value, and an association policy constant. The key is a void pointer.
The key for each association must be unique. A typical pattern is to use a static variable. The policy specifies whether the associated object is assigned, retained, or copied, and whether the association is be made atomically or non-atomically. This pattern is similar to that of the attributes of a declared property |
可能的属性声明中的属性特征:
1. OBJC_ASSOCIATION_RETAIN_NONATOMIC,
2. OBJC_ASSOCIATION_ASSIGN,
3. OBJC_ASSOCIATION_COPY_NONATOMIC,
4. OBJC_ASSOCIATION_RETAIN,
5. OBJC_ASSOCIATION_COPY
类扩展类别
在声明类的小节中,它展示了类中的私有实例变量和属性如何通过匿名类别(也称类扩展)被添加到一个class中。
- // Class extensions for private variables / properties
- @interface MyClass ()
- {
- int somePrivateInt;
- // Re-declare as a private read-write version of the public read-only property
- @property (readwrite, nonatomic, strong) SomeClass *someProperty;
- }
- @end
类扩展是使用类别添加变量和属性的唯一方法。为了使用这个方法,你必须在编译时访问源码。
核心数据类别
当你使用核心数据模型并想要添加额外的方法到NSManagedObject子类中时,并且不想担心每次迁移一个模型时Xcode覆盖模型类时,类别是非常有用的。
模型类应该保持在最低限度,只包含属性模型和核心数据生成的访问方法。其他的方法(如瞬态方法)应该在模型类的类别中实现。
命名冲突
苹果文档的建议:
Because the methods declared in a category are added to an existing class, you need to be very careful about method names.
If the name of a method declared in a category is the same as a method in the original class, or a method in another category on the same class (or even a superclass), the behavior is undefined as to which method implementation is used at runtime. This is less likely to be an issue if you’re using categories with your own classes, but can cause problems when using categories to add methods to standard Cocoa or Cocoa Touch classes. |
委托
Objective-C中委托是一个基本方法,这个方法允许一个类对另一个类的改变做出反应,或者在在两个类之间最小化耦合时影响另个一类的用法。
iOS中最常见的已知的委托模式的例子是UITableViewDelegate和UITableViewDataSource。当你告诉编译器你的类符合这些协议,你实际上是同意在你的类中实现特定的方法,UITableView需要这个类发挥适当的作用。
符合现有的协议
为了符合现有的协议,导入包含协议声明(对框架类没有必要)的头文件。然后用<>符号插入协议名,用逗号分开多个协议。下面的两种方法都行得通,但是我更喜欢把他们写在头文件中,因为这对我来讲更清晰。
方法一: 在 .h文件中:
- #import "RPLocationManager.h"
- @interface MyViewController : UIViewController <RPLocationManagerDelegate, UITableViewDelegate, UITableViewDataSource>
- @end
方法二 : 在 .m 文件中:
- #import "MyViewController.h"
- @interface MyViewController () <RPLocationManagerDelegate, UITableViewDelegate, UITableViewDataSource>
- {
- // Class extension implementation
- }
- @end
创建你自己的协议
创建自己的协议使其他类遵守,如下语法:
RPLocationManager.h
- #import <SomeFrameworksYouNeed.h>
- // Declare your protocol and decide which methods are required/optional
- // for the delegate class to implement
- @protocol RPLocationManagerDelegate <NSObject>
- - (void)didAcquireLocation:(CLLocation *)location;
- - (void)didFailToAcquireLocationWithError:(NSError *)error;
- @optional
- - (void)didFindLocationName:(NSString *)locationName;
- @end
- @interface RPLocationManager : NSObject
- // Create a weak, anonymous object to store a reference to the delegate class
- @property (nonatomic, weak) id <RPLocationManagerDelegate>delegate;
- // Implement any other methods here
- @end
当我们声明名@protocol命名RPLocationManagerDelegate时,所有的方法都默认成为@required,因此不需要明确的声明。然而,如果你想要@optional特定的方法来符合类的实现,你必须声明它。
此外,有必要弱声明一个名为delegate的匿名类型的属性,这个属性也引用RPLocationManagerDelegate协议。
发送委托消息
在上面的例子中,RPLocationManager.h声明了一些这个类作为委托方必须要实现的方法。在RPLocationManager.m中,你可以以不同的方法实现,这里我们只举两个例子:
1. @required方法
- - (void)updateLocation
- {
- // Perform some work
- // When ready, notify the delegate method
- [self.delegate didAcquireLocation:locationObject];
- }
[email protected]方法
- - (void)reverseGeoCode
- {
- // Perform some work
- // When ready, notify the delegate method
- if ([self.delegate respondsToSelector:@selector(didFindLocationName:)]) {
- [self.delegate didFindLocationName:locationName];
- }
- }
@required与@optional方法唯一的区别是:你经常需要检查下引用委托是否实现了一个optional方法,在调用该方法之前。
实现委托方法
要实现一个委托方法,只要遵守之前讨论的协议,然后想一个正常的方法去定义就可以:
- MyViewController.m
- - (void)didFindLocationName:(NSString *)locationName
- {
- NSLog(@"We found a location with the name %@", locationName);
- }
子类化
子类本质上和继承相同,但是你通常以以下两种方式创建子类:
1.在父类中重载方法或者属性实现;
2.为子类创建特定的用法(例如,Toyota是Car的子类,但它仍然有轮胎、发动机等,但是它还有额外的独特的属性)。
存在很多设计模式,如类别和委托,因此你不需要创建一个类的子类。例如,UITableViewDelegate协议允许你在自己的类中提供方法的实现,例如tableView:didSelectRowAtIndexPath:,而不需要创建UITableView的子类来重载方法。
另外,类似NSManagedObject的类很容易派生子类。一般的经验法则是从另外一个类派生一个子类,只要你能满足Liskov代换原则 :
If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program. |
示例
假设我们需要汽车模型,所有的车有相同的功能和属性,所以让我们在名为Car的子类中添加一些。
Car.h
- #import <Foundation/Foundation.h>
- @interface Car : NSObject
- @property (nonatomic, strong) NSString *make;
- @property (nonatomic, strong) NSString *model;
- @property (nonatomic, assign) NSInteger year;
- - (void)startEngine;
- - (void)pressGasPedal;
- - (void)pressBrakePedal;
- @end
Car.m
- #import "Car.h"
- @implementation Car
- - (void)startEngine
- {
- NSLog(@"Starting the engine.");
- }
- - (void)pressGasPedal
- {
- NSLog(@"Accelerating...");
- }
- - (void)pressBrakePedal
- {
- NSLog(@"Decelerating...");
- }
- @end
现在,当我们想要生产一个全新的,具有独一无二特性的汽车模型时,我们用Car父类作为切入点,然后在子类中添加自定义行为。
Toyota.h
- #import "Car.h"
- @interface Toyota : Car
- - (void)preventAccident;
- @end
Toyota.m
- #import "Toyota.h"
- @implementation Toyota
- - (void)startEngine
- {
- // Perform custom start sequence, different from the superclass
- NSLog(@"Starting the engine.");
- }
- - (void)preventAccident
- {
- [self pressBrakePedal];
- [self deployAirbags];
- }
- - (void)deployAirbags
- {
- NSLog(@"Deploying the airbags.");
- }
- @end
即使pressBrakePedal是在Car类中声明的,由于继承关系,仍能在Toyota类中访问该方法。
指派初始值
通常,为了简单的实例化,类允许指定的类初始化。如果你为类重载了一个主要的指派初始值,或者提供了一个指派初始值,你需要确保也重载了其他指派初始值,以便他们可以使用新的实现而不是父类的实现。如果你忘记这么做了,并在子类上调用二级指派初始值之一,那么他们将得到父类的行为。
- // The new designated initializer for this class
- - (instancetype)initWithFullName:(NSString *)fullName
- {
- if (self = [super init]) {
- _fullName = fullName;
- [self commonSetup];
- }
- return self;
- }
- // Provide a sensible default for other initializers
- - (instancetype)init
- {
- return [self initWithFullName:@"Default User"];
- }
在比较特别的用例中,如果你不使用默认的初始值,你应该抛出一个异常并为他们提供一个替代的办法:
- - (instancetype)init {
- [NSException raise:NSInvalidArgumentException
- format:@"%s Using the %@ initializer directly is not supported. Use %@ instead.", __PRETTY_FUNCTION__,
- NSStringFromSelector(@selector(init)), NSStringFromSelector(@selector(initWithFrame:))];
- return nil;
- }
重载方法
如果你创建了另一类的子类来重载函数,你必须要谨慎。如果你想要保留父类的用法但是只做稍稍的修改,你可以在重载内调用父类,如下:
- - (void)myMethod
- {
- [super myMethod];
- // Provide your additional custom behavior here
- }
如果你不希望任何重载超类的方法,只要不调用父类即可。但是注意没有任何内存或对象生命周期的影响来这样做。
此外,如果父类有原始的方法,其他派生类在原始方法上实现,你必须确保你重载了所有必需的原始方法,保证派生的方法能正常工作。
注意事项
某些类不能轻易地派生出子类,因此在这种情况下,不鼓励用子类。例如,派生类似NSString和NSNumber的类簇。
类簇中有很多私有类,所以很难确保你已经重载了所有基本方法,并在类簇中适当地指派初始值。
Swizzling
通常来讲头脑清晰比聪明更重要。作为一个一般规则,在方法实现中处理一个bug比用method swizzling代替这种方法更好一些。原因是别人使用你的代码时可能不会意识到你替换了方法实现,然后他们会一直想不通为什么这个方法不响应默认的属性。
因此,我们不在这里讨论method swizzling,但是你可以在这里查阅(link)。
错误处理
通常有三个方式处理错误:断言、异常和可恢复错误。断言和异常的情况通常只用在很少的用例上,因为你的应用程序崩溃的话显然不是一个很好的用户体验。
断言
断言通常用于当你想要确定是什么值的时候。如果不是正确的值,你就会被迫突出应用程序。
- NSAssert(someCondition, @"The condition was false, so we are exiting the app.");
Important: Do not call functions with side effects in the condition parameter of this macro. The condition parameter is not evaluated when assertions are disabled, so if you call functions with side effects, those functions may never get called when you build the project in a non-debug configuration. |
异常
异常通常用于编程或者不能预料的运行错误。例如,试图调用一个有五个元素的数组的第六个元素(越界访问)、试图改变不可变对象、给对象发送一个无效消息。通常你会在创建应用程序而不是运行时来处理异常错误。
正如这样一个例子:你需要使用API密钥才能使用库。
- // Check for an empty API key
- - (void)checkForAPIKey
- {
- if (!self.apiKey || !self.apiKey.length) {
- [NSException raise:@"Forecastr" format:@"Your Forecast.io API key must be populated before you can access the API.", nil];
- }
- }
Try-Catch语句
如果你担心一个代码块会抛出异常,你可以将它放在一个Try-Catch语句块中,但是一定要记住这会稍微影响性能。
- @try {
- // The code to try here
- }
- @catch (NSException *exception) {
- // Handle the caught exception
- }
- @finally {
- // Execute code here that would run after both the @try and @catch blocks
- }
可恢复错误
很多时候,方法会在一个故障代码块中返回一个NSError对象或者是指针的指针(NSFileManager情况)。这通常返回的是一个可恢复的错误,并提供一个更好地用户交互,因为他们可以提示用户哪里出错了。
- [forecastr getForecastForLocation:location success:^(id JSON) {
- NSLog(@"JSON response was: %@", JSON);
- } failure:^(NSError *error, id response) {
- NSLog(@"Error while retrieving forecast: %@", error.localizedDescription);
- }];
创建自己的错误
创建自己的NSError对象返回方法也很有可能。
- // Error domain & enums
- NSString *const kFCErrorDomain = @"com.forecastr.errors";
- typedef NS_ENUM(NSInteger, ForecastErrorType) {
- kFCCachedItemNotFound,
- kFCCacheNotEnabled
- };
- @implementation Forecastr
- - (void)checkForecastCacheForURLString:(NSString *)urlString
- success:(void (^)(id cachedForecast))success
- failure:(void (^)(NSError *error))failure
- {
- // Check cache for a forecast
- id cachedItem = [forecastCache objectForKey:urlString];
- if (cachedItem) {
- success(cachedItem);
- } else {
- // Return an error since it wasn‘t found
- failure([NSError errorWithDomain:kFCErrorDomain code:kFCCachedItemNotFound userInfo:nil]);
- }
- }
- @end
信息传递
我们已经讨论很多类间信息传递的方法了,例如通过方法或者委托,但是我们在这里也稍作讨论,并再举一个委托的例子。
通过委托传递信息
将数据从一个视图控制器传递到另一个视图控制器的常见方法是使用委托方法。例如,如果你有一个带有表格的模态视图显示在你的视图控制器之上,那么你需要知道用户点击的是哪个表格。
AddPersonViewController.h (the modal view)
- #import <UIKit/UIKit.h>
- #import "Person.h"
- @protocol AddPersonTableViewControllerDelegate <NSObject>
- - (void)didSelectPerson:(Person *)person;
- @end
- @interface AddPersonTableViewController : UITableViewController
- @property (nonatomic, weak) id <AddPersonTableViewControllerDelegate>delegate;
- @end
AddPersonViewController.m
- // Other implementation details left out
- - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
- {
- Person *person = [people objectAtIndex:indexPath.row];
- [self.delegate didSelectPerson:person];
- }
GroupViewController.m (the normal view)
- // Other implementation details left out, such as showing the modal view
- // and setting the delegate to self
- #pragma mark - AddPersonTableViewControllerDelegate
- - (void)didSelectPerson:(Person *)person
- {
- [self dismissViewControllerAnimated:YES completion:nil];
- NSLog(@"Selected person: %@", person.fullName);
- }
我们忽略了一些实现的细节。例如,AddPersonTableViewControllerDelegate的规则,但是你可以在委托章节学习到。
另外,注意我们没有考虑到相同类中的最初显示它的模态视图控制器(AddPersonViewController)。这是苹果推荐使用的方法。
NSNotificationCenter(通知中心)
通知是广播消息,用于在运行时分离类间耦合并在对象之间建立匿名通信。通知可以由任何数量的对象发布和接受,因此在对象间可以建立一对多和多对多的关系。
注:通知是同步发送的,所以如果你的观察方法需要很长时间才能返回,你实际上是阻止了给其他观察对象传递通知。
注册观察者
为了获得某一事件发生的通知,你可以先注册。事件包括系统通知,例如UITextField开始编辑。
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldDidBeginEditing:)
- name:UITextFieldTextDidBeginEditingNotification object:self];
当OS系统框架广播UITextFieldTextDidBeginEditingNotification通知时,NSNotificationCenter将调用textFieldDidBeginEditing:并且对象与包含数据的通知一起发送。
textFieldDidBeginEditing:方法的一种可能实现是:
- #pragma mark - Text Field Observers
- - (void)textFieldDidBeginEditing:(NSNotification *)notification
- {
- // Optional check to make sure the method was called from the notification
- if ([notification.name isEqualToString:UITextFieldTextDidBeginEditingNotification])
- {
- // Do something
- }
- }
删除观察者
释放类的同时删除观察者是很重要的,否则NSNotificationCenter将试图在已经释放掉的类中调用方法,这将会引起崩溃。
- - (void)dealloc
- {
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- }
发布通知
你也可以创建和发送自己的通知。最好将通知名称保存在一个常量文件中,这样你就不会因为一不小心拼错了通知名称而坐在那里试图找出为什么不能发送或者接收通知。
通知的命名
通知是由NSString对象的识别的,通知的名称是有以下方式组成的:
[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification
声明一个字符串常量,将通知的名称作为字符串的值:
- // Remember to put the extern of this in the header file
- NSString *const kRPAppDidResumeFromBackgroundNotification = @"RPAppDidResumeFromBackgroundNotification";
发布通知
- [[NSNotificationCenter defaultCenter] postNotificationName:kRPAppDidResumeFromBackgroundNotification object:self];
视图控制器属性
当你准备显示一个新的视图控制器时,你可以在展示之前为属性分配数据:
- MyViewController *myVC = [[MyViewController alloc] init];
- myVC.someProperty = self.someProperty;
- [self presentViewController:myVC animated:YES completion:nil];
Storyboard Segue
当你在storyboard中在两个视图控制器之间切换时,使用prepareForSegue:sender:方法可以简单的实现数据的传递,如下:
- #pragma mark - Segue Handler
- - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
- {
- if ([segue.identifier isEqualToString:@"showLocationSearch"] {
- [segue.destinationViewController setDelegate:self];
- } else if ([segue.identifier isEqualToString:@"showDetailView"]) {
- DetailViewController *detailView = segue.destinationViewController;
- detailView.location = self.location;
- }
- }
用户默认值
用户默认值是存储简单偏好的基本方法,在应用程序启动时可以保存和还原这些偏好值。这并不意味着它被用作像Core Data或者sqlite那样的数据存储层。
存储值
- NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
- [userDefaults setValue:@"Some value" forKey:@"RPSomeUserPreference"];
- [userDefaults synchronize];
记住一定要在默认实例上调用synchronize以确保正确保存值。
检索值
- NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
- id someValue = [userDefaults valueForKey:@"RPSomeUserPreference"];
NSUserDefaults实例中也有其他简单的方法,例如boolForKey:, stringForKey:等
常见模式
单例模式
单例模式是一种特殊的类,在当前进程中只能一个类包含一个实例。单例模式对于在一个应用程序的不同部分中共享数据很便利,并且不需要创建全局变量或者手动传递数据。但是,因为它们经常创建类之间的紧耦合,所以尽量少使用单例模式。
将一个类转换成单例模式,你需要将下面的方法写到实现文件(.m)中,方法名由shared加上一个单词组成,这样可以很好的描述你的类。例如,如果这个类是一个网络或位置管理器,你可以将这个方法命名为sharedManager,而不是sharedInstance。
- + (instancetype)sharedInstance
- {
- static id sharedInstance = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- sharedInstance = [[self alloc] init];
- });
- return sharedInstance;
- }
使用dispatch_once可以保证这个方法只被执行一次,即使它在很多类或者进程之间调用很多次。
如果在MyClass中替换上面的代码,那么用下面的代码你将会在另一个类中得到一个单例模式类的引用:
- MyClass *myClass = [MyClass sharedInstance];
- [myClass doSomething];
- NSLog(@"Property value is %@", myClass.someProperty);
推荐阅读:
iOS应用开发最佳实践:编写高质量的Objective-C代码
http://www.cocoachina.com/industry/20140428/8255.html