总结一些interface声明时的规范,相关宏的介绍,定义方法时有用的修饰符,编写注释的规范,最终写出一个合格的头文件。ios
.h文件里的声明是用于暴露给外部的接口,而类内部的私有方法、私有属性和实例变量,应该放到.m文件的interface extension里。git
这3个关键字用于修饰实例变量,不能用于修饰属性。当错误地使用了实例变量时,Xcode会报错提示。github
关键字 | 说明 |
---|---|
@private | 做用范围只能在自身类 |
@protected | 做用范围在自身类和继承本身的子类,什么都不写,默认是此属性。 |
@public | 做用范围最大,在任何地方。 |
示例代码:web
//SearchManager.h
@interface SearchManager : NSObject {
@public NSInteger *state;
@public NSInteger *timeout;
@protected id *searchAPI;
@private id _privateIvar;
}
@end
复制代码
因为会暴露私有变量,而且没有@property的一些高级关键字,不多在头文件里声明实例变量。优先使用@property。macos
头文件中的属性是用于描述这个对象的一系列特性集合。 声明@property时,在.h里使用readonly,让外部只有读的权限,在.m里使用readwrite,使内部拥有读写权限。swift
示例代码:api
//SearchManager.h
@interface SearchManager : NSObject
@property (nonatomic, readonly) NSInteger * state;
@end
复制代码
//SearchManager.m
@interface SearchManager : NSObject
@property (nonatomic, readwrite) NSInteger * state;
@end
复制代码
当在@interface的接口里用到了其余类,不要在.h里直接导入类的头文件,这样会让使用此头文件的地方也导入这些没必要要的其余头文件。正确的作法是使用关键字@class
进行前向声明。固然,若是是继承了父类,仍是须要import父类的头文件。 示例代码:bash
//SearchManager.h
#import "SearchManagerBase.h"//导入父类的头文件
@class LocationModel;//前向声明LocationModel类
typedef void(^LocationSearchCompletionHandler)(LocationModel *location, NSError *error);
@interface LocationSearchManager : SearchManagerBase
- (void)searchLocationWithKeyword:(NSString *)keyword completionHandler:(LocationSearchCompletionHandler)completionHandler;
@end
复制代码
使用@class会告诉编译器有这么一个类存在,可是如今并不关心这个类的具体实现,等到调用者在.m里使用的时候再import这个类便可。使用@class和@protocol分别声明一个类和一个protocol。 使用前向引用的缘由有两个:app
LocationModel.h
,那么当LocationModel.h
的内容发生变化时,全部import了LocationModel.h
的地方都须要从新编译。若是.m引用了SearchManager.h
,可是并无使用LocationModel
,就会增长没必要要的编译,下降开发效率。头文件里只声明那些给外部使用的公开方法,而且在设计时须要考虑到可测试性,遵循单一职责。 私有方法只定义在类内部,而且为了进行区别,建议在私有方法前加上前缀,例如- (void)p_myPrivateMethod
。 因为Apple在它的编码规范里声明了,Apple公司拥有下划线的方法前缀,就像它拥有NS
,UI
这些类名前缀同样,所以不建议咱们的私有方法直接使用下划线做为前缀。不然,当你在继承Cocoa Touch的类时,有可能会覆盖父类的私有方法,形成难以调试的错误。post
错误的示例代码:
//SearchManager.h
@interface SearchManager : NSObject<NSCoding, UITableViewDelegate>
@property (nonatomic, readonly) NSInteger * state;
@end
复制代码
UITableViewDelegate
是类内部使用时遵循的protocol,没有必要暴露给外部,所以应该放到.m文件里。 而NSCoding
则描述了类的特性,用于告诉外部本类可使用归档,所以应该放在头文件里。
在声明时,可使用下列关键字描述对象是否能够为nil。
关键字 | 说明 |
---|---|
nullable | 可空,用于描述objc对象 |
nonnull | 不可空,用于描述objc对象 |
null_unspecified | 不肯定,用于描述objc对象 |
null_resettable | set可空,get不为空。仅用于property |
_Nullable | 可空,用于描述C指针和block |
_Nonnull | 不可空,用于描述C指针和block |
_Null_unspecified | 不肯定,用于描述C指针和block |
示例代码:
//SearchManager.h
#import "SearchManagerBase.h"
@class LocationModel;
typedef void(^LocationSearchCompletionHandler)(LocationModel *_Nullable location, NSError *_Nullable error);
@interface LocationSearchManager : SearchManagerBase
- (void)searchLocationWithKeyword:(nonnull NSString *)keyword completionHandler:(LocationSearchCompletionHandler _Nonnull)completionHandler;
@end
复制代码
若是向一个使用nonnull修饰的值赋空,编译器会给出警告。 在开发时,大部分时候使用的都是nonnull,所以Apple提供了一对宏NS_ASSUME_NONNULL_BEGIN
和NS_ASSUME_NONNULL_END
来进行快速修饰,写在两个宏之间的属性、方法,均会使用nonnull
修饰。 示例代码:
//LocationSearchManager.h
#import "SearchManagerBase.h"
@class LocationModel;
NS_ASSUME_NONNULL_BEGIN
typedef void(^LocationSearchCompletionHandler)(LocationModel *_Nullable location, NSError *_Nullable error);
@interface LocationSearchManager : SearchManagerBase
- (void)searchLocationWithKeyword:(NSString *)keyword completionHandler:(LocationSearchCompletionHandler)completionHandler;
@end
NS_ASSUME_NONNULL_END
复制代码
关于NS_ENUM和NS_OPTIONS的区别,参考这里。 简单来讲,NS_OPTIONS提供了按位掩码的功能。
示例代码:
typedef NS_ENUM(NSInteger,SearchState) {
SearchStateNotSearch,
SearchStateSearching,
SearchStateSearchFinished,
SearchStateSearchFailed
};
复制代码
示例代码,参考NSKeyValueObserving.h
:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew,
NSKeyValueObservingOptionOld,
NSKeyValueObservingOptionInitial,
NSKeyValueObservingOptionPrior
};
复制代码
在使用时就能够用|
组合多个option:
[_webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL];
复制代码
当使用字典做为参数传递,或者做为返回值时,每每难以直接提供字典的key,如今使用字符串枚举便可解决这个问题。 示例代码,参考NSKeyValueObserving.h
:
//使用NS_STRING_ENUM宏,定义了一个枚举类型
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey;
//使用泛型,声明了change参数用到的key,是在NSKeyValueChangeKey的枚举范围中
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
复制代码
这不关@interface的事,可是和头文件有关,就放在一块儿说明了。
//SearchManager.h
extern NSString *const SearchErrorDomain;
extern NSInteger SearchDefaultTimeout;
@interface SearchManager : NSObject
@end
复制代码
//SearchManager.m
NSString *const SearchErrorDomain = @"SearchErrorDomain";
const NSInteger SearchDefaultTimeout = 20;
@interface SearchManager()
@end
复制代码
因为类的头文件只存放那些暴露给外部的属性和方法,在遇到这些状况时,会遇到障碍:
UIGestureRecognizerSubclass.h
。 示例代码://SearchManager.h
@interface SearchManager : NSObject
///外部访问,只有读权限
@property (nonatomic, readonly) SearchState state;
@end
复制代码
//SearchManager.m
@interface SearchManager()
///内部使用,有读写权限
@property (nonatomic, assign) SearchState state;
///只在内部使用的私有属性
@property (nonatomic, strong) id searchAPI;
@end
复制代码
///暴露给子类和category的私有属性和私有方法
//SearchManagerInternal.h
///限制使用此头文件,防止被别的类误用
#ifdef SEARCHMANAGER_PROTECTED_ACCESS
#import "SearchManager.h"
@interface SearchManager()
///在internal.h里,从新声明为readwrite权限
@property (nonatomic, readwrite, assign) SearchState state;
///暴露私有属性
@property (nonatomic, strong) id searchAPI;
///暴露私有方法
- (void)p_privateMethod;
@end
#else
#error Only be included by SearchManager's subclass or category!
#endif
复制代码
///category的实现文件
//SearchManager+Category.m
///声明私有头文件的使用权限
#define SEARCHMANAGER_PROTECTED_ACCESS
///导入私有头文件
#import "SearchManagerInternal.h"
@implementation SearchManager(Category)
- (void)categoryMethod {
//拥有了读写权限
self.state = SearchStateSearching;
//能够访问私有属性
[self.searchAPI startSearch];
//可使用私有方法
[self p_privateMethod];
}
@end
复制代码
SearchManagerInternal.h
其实也是公开的,其余类也可以导入并使用,只能在开发时进行约定。若是想要限制其余类导入,而且提示错误,Internal.h
可使用以下方式:
#ifdef MYCLASS_PROTECTED_ACCESS
//声明部分
#else
#error Only be included by MYCLASS's subclass or category!
#endif
复制代码
这样在别的类内意外地导入了Internal.h
时就会产生编译警告,而且没法直接使用。缺点是须要在全部使用到Internal.h
的地方都#define MYCLASS_PROTECTED_ACCESS
。
指定初始化方法,即接收参数最多的那个初始化方法,其余初始化方法调用它便可,这样设计的目的是为了保证全部初始化方法都正确地初始化实例变量。 在方法后面加上NS_DESIGNATED_INITIALIZER
宏便可。这样,当你子类化这个类时,在子类的初始化方法里若是没有正确地调用父类的designated initializer,编译器就会给出警告。 实例代码:
@interface WKWebView : UIView
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
@end
复制代码
关于designated initializer更详细的说明,参考:
在更新接口,或者开发framework时,须要标明版本信息,告诉使用者此接口的平台限制、操做系统版本、是否可用、是否已弃用等。 苹果给出了几个自带的宏用于标明版本,Xcode在检测到错误使用时会给出警告。只须要在方法名后面加上对应的宏便可。
声明本接口最低支持的操做系统版本。 当你的接口使用了新系统的API,例如iOS8以上才有的UIAlertController,可是项目的deployment target倒是iOS7时,须要标明此接口的版本信息,让使用者进行兼容。 示例:
//SearchManager.h
typedef NS_ENUM(NSInteger,SearchState) {
SearchStateNotSearch,
SearchStateSearching,
SearchStateSearchFinished,
SearchStateSearchFailed
} NS_ENUM_AVAILABLE_IOS(2_0);//此枚举在iOS2.0以上才能使用
NS_CLASS_AVAILABLE_IOS(2_0) //此类在iOS2.0以上才能使用
@interface SearchManager : NSObject
- (void)reSearch NS_AVAILABLE_IOS(5_0);//此方法在iOS5.0以上才能使用
@end
复制代码
这几个宏有对应平台的版本,例如NS_AVAILABLE_MAC, NS_AVAILABLE_IOS, NS_AVAILABLE_IPHONE。 iOS10开始提供了新的available宏API_AVAILABLE
,用来统一macOS、iOS、watchOS、tvOS几个平台。
API_AVAILABLE(macos(10.10))
API_AVAILABLE(macos(10.9), ios(10.0))
API_AVAILABLE(macos(10.4), ios(8.0), watchos(2.0), tvos(10.0))
复制代码
声明此接口不可用,大多数时候是用于声明所在平台限制。 示例:
@interface SearchManager : NSObject
- (void)searchInWatch NS_UNAVAILABLE;//不能用此接口
- (void)searchInHostApp NS_EXTENSION_UNAVAILABLE_IOS;//extension里不能用此接口
- (void)search __TVOS_PROHIBITED;//tvOS里不能用此接口,可修饰枚举,类,方法,参数
@end
复制代码
iOS10开始提供了新的unavailable宏API_UNAVAILABLE
:
API_UNAVAILABLE(macos)
API_UNAVAILABLE(watchos, tvos)
复制代码
声明此接口已经被弃用,能够同时加注释注明替代接口。 当deployment target版本号设置成大于或等于方法被弃用的版本号时,Xcode会给出警告。 示例:
//注明废弃类
NS_CLASS_DEPRECATED_IOS(2_0, 9_0, "UIAlertView is deprecated. Use UIAlertController with a preferredStyle of UIAlertControllerStyleAlert instead")
@interface UIAlertView : UIView
@end
复制代码
//注明废弃API
@interface UIViewController : UIResponder
- (void)viewDidUnload NS_DEPRECATED_IOS(3_0,6_0);
@end
复制代码
//注明废弃枚举
typedef NS_ENUM(NSInteger, UIStatusBarStyle) {
UIStatusBarStyleDefault = 0, // Dark content, for use on light backgrounds
UIStatusBarStyleLightContent NS_ENUM_AVAILABLE_IOS(7_0) = 1, // Light content, for use on dark backgrounds
UIStatusBarStyleBlackTranslucent NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 1,
UIStatusBarStyleBlackOpaque NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 2,
}
复制代码
iOS10开始提供了新的deprecated宏API_DEPRECATED
和API_DEPRECATED_WITH_REPLACEMENT
。前者能够注明弃用缘由,后者能够注明替代接口。
API_DEPRECATED("No longer supported", macos(10.4, 10.8))
API_DEPRECATED("No longer supported", macos(10.4, 10.8), ios(2.0, 3.0), watchos(2.0, 3.0), tvos(9.0, 10.0))
API_DEPRECATED_WITH_REPLACEMENT("-setName:", tvos(10.0, 10.4), ios(9.0, 10.0))
API_DEPRECATED_WITH_REPLACEMENT("SomeClassName", macos(10.4, 10.6), watchos(2.0, 3.0))
复制代码
在声明时,对集合类型的对象增长泛型的修饰,就能够声明集合内存储的数据类型。 例如:
@property (nonatomic, strong) NSMutableArray<NSString *> *myArray;
复制代码
当你向myArray
里放入一个非NSString *
类型的对象时,编译器会给出警告。
@property(nonatomic, strong) NSMutableArray<__kindof UIView *> * viewArray;
复制代码
_kindof
只限定了存储类型为UIView
,所以也能够存储UIView
的子类,例如UIButton
。 更详细的介绍,参考:Objective—C语言的新魅力——Nullability、泛型集合与类型延拓
NS_REQUIRES_SUPER
宏用于声明子类在重载父类的这个方法时,须要调用父类的方法。例如:
- (void)viewWillAppear:(BOOL)animated NS_REQUIRES_SUPER;
复制代码
NS_NOESCAPE
用于修饰方法中的block类型参数,例如:
@interface NSArray: NSObject
- (NSArray *)sortedArrayUsingComparator:(NSComparator NS_NOESCAPE)cmptr
@end
复制代码
做用是告诉编译器,cmptr
这个block在sortedArrayUsingComparator:
方法返回以前就会执行完毕,而不是被保存起来在以后的某个时候再执行。 相似于这样的实现:
- (void)performWithLock:(NS_NOESCAPE void (^)())block { // exposed as @noescape to Swift
[myLock lock];
block();
[myLock unlock];
}
复制代码
编译器知道以后,就会相应地作一些优化,例如去掉一些多余的对self
的捕获、retain、release操做。由于block的存活范围仅限于本方法内,没有必要再在block内保留self
了。 更详细的介绍,参考这里。
头文件就是文档,须要让使用者快速知道这个类的做用。一个好的方法名可让使用者快速理解,但大部分时候仍是须要相应的注释。 写好格式化注释后,当光标停留在方法名和属性上时,在Xcode右侧的Quick Help栏里会出现注释内容,按住option
并单击,也会弹出注释框。
直接在方法或者属性声明的上一行使用///
,后面加注释,同时兼容Xcode和appleDoc。Xcode也支持//!
,可是appleDoc不支持。
//SearchManagerBase.h
///搜索manager的基类
@interface SearchManagerBase : NSObject
///搜索状态
@property (nonatomic, readonly) NSInteger * state;
@end
复制代码
多行注释使用:
/**
注释内容
*/
复制代码
Xcode8提供了快速生成格式化注释的快捷键:option
+command
+/
。若是方法有参数,会自动添加@param关键字,用于描述对应的参数。 Apple提供了官方的headDoc语法,可是不少都已经在Xcode中失效了,并且有些关键字也和appleDoc不兼容。下面几种列举出了在Xcode中仍然有效的一些关键字:
/**
演示苹果headDoc的语法。这里能够写方法简介
@brief 方法的简介(appleDoc不支持此关键字)
@discussion 方法的详细说明
@code //示例代码(这个在Xcode里经常使用,可是appleDoc不支持此关键字)
UIView *view;
@endcode
@bug 存在的bug的说明
@note 须要注意的提示
@warning 警告
@since iOS7.0
@exception 方法会抛出的异常的说明
@attention 注意,从这里开始往下的关键字,appleDoc都不支持
@author 编写者
@copyright 版权
@date 日期
@invariant 不变量
@post 后置条件
@pre 前置条件
@remarks 备注
@todo todo text
@version 版本
*/
- (void)sampleMethod;
复制代码
在Xcode中,就会显示为这样:
若是要给枚举注释,须要在每一个枚举值前注释,按照以下格式:
///搜索状态
typedef NS_ENUM(NSInteger,SearchState) {
///没有开始搜索
SearchStateNotSearch,
///搜索中
SearchStateSearching,
///搜索结束
SearchStateSearchFinished,
///搜索失败
SearchStateSearchFailed
};
复制代码
须要注释的内容: