iOS編碼規(guī)范

概覽

為了減少團隊的大家代碼風格的不一致又跛,降低出錯率,提供代碼的可維護性笋庄,針對App自身特點制定以下規(guī)范效扫。

規(guī)范按照執(zhí)行力度分為

  • 強制遵守 【must】
  • 建議遵守 【suggest】
  • 最佳實踐 【best practice】

強制遵守:為一些非常重要的原則和既定的約定倔监,下面簡稱【M】
建議遵守:為一些建議遵守的原則和代碼風格,下面簡稱【S】
最佳實踐:為一些經(jīng)過實踐驗證比較優(yōu)的寫法菌仁,下面簡稱【BP】

這篇文章內(nèi)容主要包含以下三個部分

  • 核心原則:介紹了這篇代碼規(guī)范所遵循的核心原則
  • 通用規(guī)范:不局限于iOS的通用性的代碼規(guī)范(使用C語言和Swift語言)
  • iOS規(guī)范:僅適用于iOS的代碼規(guī)范(使用Objective-C語言)

每條原則有一個默認執(zhí)行力度南誊,最終執(zhí)行力度將由大家團隊討論,半數(shù)以上的得票為最終方案睡毒,在執(zhí)行過程中更加反饋生巡,后續(xù)可能會有所調(diào)整,直到最佳摹迷。

一. 核心原則【M】

原則一:簡潔性原則--代碼應(yīng)該簡潔易懂疟赊,邏輯清晰

因為軟件是需要人來維護的。這個人在未來很可能是你也可算是你的同事峡碉,簡潔的代碼易于維護近哟。

  • 不要過分追求技巧,降低程序的可讀性鲫寄。
  • 簡潔的代碼可以讓bug無處藏身吉执。要寫出明顯沒有bug的代碼,而不是沒有明顯bug的代碼地来。

原則二:可拓展性--面向變化編程戳玫,而不是面向需求編程。

需求是暫時的未斑,只有變化才是永恒的咕宿。
本次迭代不能僅僅為了當前的需求,寫出擴展性強蜡秽,易修改的程序才是負責任的做法府阀,對自己負責,對公司負責载城。

原則三:效率優(yōu)先--先保證程序的正確性肌似,防止過度工程

過度工程(over-engineering):在正確可用的代碼寫出之前就過度地考慮擴展,重用的問題诉瓦,使得工程過度復(fù)雜川队。
引用《王垠:編程的智慧》里的話:

  1. 先把眼前的問題解決掉,解決好睬澡,再考慮將來的擴展問題固额。
  2. 先寫出可用的代碼,反復(fù)推敲煞聪,再考慮是否需要重用的問題斗躏。
  3. 先寫出可用,簡單昔脯,明顯沒有bug的代碼啄糙,再考慮測試的問題笛臣。

二. 通用規(guī)范

格式【M】


  • 使用4個空格(tab)來縮進
  • 方法- 和 + 和返回值之前為1個空格
  • 方法參數(shù)之間有一個空格,其他地方不出現(xiàn)多余的空格
  • 方法長度不超過80行隧饼,建議不超過50行沈堡。

大括號【S】


  • 控制語句(if,for,while,switch)中,大括號開始與行尾
  • 函數(shù)中燕雁,大括號要開始于行首

推薦這樣寫:

//控制語句
while(someCondition) {

}

if (someCondition) {

} else {

}

for (NSInteger i = 0; i < 5; i++) {

}

//函數(shù)
void function(param1,param2)
{

}

運算符


1. 運算符與變量之間的間隔

1.1 一元運算符與變量之間沒有空格【M】:

!bValue
~iValue
++iCount
*strSource
&fSum

1.2 二元運算符與變量之間必須有空格【M】

fWidth = 5 + 5;
fLength = fWidth * 2;
fHeight = fWidth + fLength;
for (int i = 0; i < 10; i++)

2. 多個不同的運算符同時存在時應(yīng)該使用括號來明確優(yōu)先級【M】

在多個不同的運算符同時存在的時候應(yīng)該合理使用括號诞丽,不要盲目依賴操作符優(yōu)先級。
因為有的時候不能保證閱讀你代碼的人就一定能了解你寫的算式里面所有操作符的優(yōu)先級拐格。

來看一下這個算式:2 << 2 + 1 * 3 - 4

這里的<<是移位操作直觀上卻很容易認為它的優(yōu)先級很高僧免,所以就把這個算式誤認為:(2 << 2) + 1 *3 - 4
但事實上,它的優(yōu)先級是比加減法還要低的捏浊,所以該算式應(yīng)該等同于:2 << (2 + 1 *3 - 4)懂衩。
所以在以后寫這種復(fù)雜一點的算式的時候,盡量多加一點括號呛伴,避免讓其他人誤解(甚至是自己)勃痴。

變量


1. 一個變量有且只有一個功能谒所,盡量不要把一個變量用作多種用途【M】

2. 變量在使用前應(yīng)初始化热康,防止未初始化的變量被引用【M】

3. 局部變量應(yīng)該盡量接近使用它的地方【S】

推薦這樣寫:

func someFunction() {

 let index = ...;
 //Do something With index

 ...
 ...

 let count = ...;
 //Do something With count

}

不推薦這樣寫:

func someFunction() {

 let index = ...;
 let count = ...;
 //Do something With index

 ...
 ...

 //Do something With count
}

if語句


1. 必須列出所有分支(窮舉所有的情況),而且每個分支都必須給出明確的結(jié)果劣领〗憔【S】

推薦這樣寫:

var hintStr;
if (count < 3) {
    hintStr = "Good";
} else {
    hintStr = "";
}

不推薦這樣寫:

var hintStr;
if (count < 3) {
    hintStr = "Good";
}

2. 不要使用過多的分支,要善于使用return來提前返回錯誤的情況【M】

推薦這樣寫:

- (void)someMethod {
    if (!goodCondition) {
        return;
    }
    //Do something
}

不推薦這樣寫:

- (instancetype)initWithDictionary:(NSDictionary *)dict error:(NSError)err
{
 //方法1\. 參數(shù)為nil
 if (!dict) {
 if (err) *err = [JSONModelError errorInputIsNil];
 return nil;
    }

    //方法2\. 參數(shù)不是nil尖淘,但也不是字典
    if (![dict isKindOfClass:[NSDictionary class]]) {
        if (err) *err = [JSONModelError errorInvalidDataWithMessage:@"Attempt to initialize JSONModel object using initWithDictionary:error: but the dictionary parameter was not an 'NSDictionary'."];
        return nil;
    }

    //方法3\. 初始化
    self = [self init];
    if (!self) {
        //初始化失敗
        if (err) *err = [JSONModelError errorModelIsInvalid];
        return nil;
    }

    //方法4\. 檢查用戶定義的模型里的屬性集合是否大于傳入的字典里的key集合(如果大于奕锌,則返回NO)
    if (![self __doesDictionary:dict matchModelWithKeyMapper:self.__keyMapper error:err]) {
        return nil;
    }

    //方法5\. 核心方法:字典的key與模型的屬性的映射
    if (![self __importDictionary:dict withKeyMapper:self.__keyMapper validation:YES error:err]) {
        return nil;
    }

    //方法6\. 可以重寫[self validate:err]方法并返回NO,讓用戶自定義錯誤并阻攔model的返回
    if (![self validate:err]) {
        return nil;
    }

    //方法7\. 終于通過了村生!成功返回model
    return self;
}

可以看到惊暴,在這里,首先判斷出各種錯誤的情況然后提前返回趁桃,把最正確的情況放到最后返回辽话。

3. 條件表達式如果很長,則需要將他們提取出來賦給一個BOOL值【M】

推薦這樣寫:

let nameContainsSwift = sessionName.hasPrefix("Swift")
let isCurrentYear = sessionDateCompontents.year == 2014
let isSwiftSession = nameContainsSwift && isCurrentYear
if (isSwiftSession) { 
 // Do something
}

不推薦這樣寫:

if ( sessionName.hasPrefix("Swift") && (sessionDateCompontents.year == 2014) ) { 
 // Do something
}

4. 條件語句的判斷應(yīng)該是變量在左卫病,常量在右【S】

推薦這樣寫:

if ( count == 6) {
}

或者

if ( object == nil) {
}

或者

if ( !object ) {
}

不推薦這樣寫:

if ( 6 == count) {
}

或者

if ( nil == object ) {
}

5. 每個分支的實現(xiàn)代碼都必須被大括號包圍【S】

推薦這樣寫:

if (!error) {
 return success;
}

不推薦這樣寫:

if (!error)
 return success;

或者

if (!error) return success;

6. 條件過多油啤,過長的時候應(yīng)該換行【S】

推薦這樣寫:

if (condition1() && 
    condition2() && 
    condition3() && 
    condition4()) {
  // Do something
}

不推薦這樣寫:

if (condition1() && condition2() && condition3() && condition4()) {
  // Do something
}

for語句


1. 不可在for循環(huán)內(nèi)修改循環(huán)變量,防止for循環(huán)失去控制蟀苛∫嬉В【M】

for (int index = 0; index < 10; index++){
    ...
    logicToChange(index)
}

2. 避免使用continue和break≈钠剑【S】

continue和break所描述的是“什么時候不做什么”幽告,所以為了讀懂二者所在的代碼梅鹦,我們需要在頭腦里將他們?nèi)》础?/p>

其實最好不要讓這兩個東西出現(xiàn),因為我們的代碼只要體現(xiàn)出“什么時候做什么”就好了冗锁,而且通過適當?shù)姆椒辈t,是可以將這兩個東西消滅掉的:

2.1 如果出現(xiàn)了continue,只需要把continue的條件取反即可

var filteredProducts = Array<String>()
for level in products {
    if level.hasPrefix("bad") {
        continue
    }
    filteredProducts.append(level)
}

我們可以看到蒿讥,通過判斷字符串里是否含有“bad”這個prefix來過濾掉一些值蝶念。其實我們是可以通過取反,來避免使用continue的:

for level in products {
    if !level.hasPrefix("bad") {
        filteredProducts.append(level)
    }
}

2.2 消除while里的break:將break的條件取反芋绸,并合并到主循環(huán)里

在while里的block其實就相當于“不存在”媒殉,既然是不存在的東西就完全可以在最開始的條件語句中將其排除。

while里的break:

while (condition1) {
    ... 
    if (condition2) {
        break;
    }
}

取反并合并到主條件:

while (condition1 && !condition2) {
  ...
}

2.3 在有返回值的方法里消除break:將break轉(zhuǎn)換為return立即返回

有些朋友喜歡這樣做:在有返回值的方法里break之后摔敛,再返回某個值廷蓉。其實完全可以在break的那一行直接返回。

func hasBadProductIn(products : Array<String>) -> Bool {

    var result = false
    for level in products {
        if level.hasPrefix("bad") {
            result = true
        }
    }
    return result
}

遇到錯誤條件直接返回:

func hasBadProductIn(products: Array<String>) -> Bool {
 for level in products {
 if level.hasPrefix("bad") {
 return true
 }
 }
 return false
}

這樣寫的話不用特意聲明一個變量來特意保存需要返回的值马昙,看起來非常簡潔桃犬,可讀性高。

Switch語句


1. 每個分支都必須用大括號括起來行楞,每個case太長建議后面空一行再寫下一個case【S】

推薦這樣寫:

switch (integer) {
    case 1:
        {
            // ...
        }
        break;
    case 2:
        {
            // ...
        }
        break;
    case 3:
        {
            // ...
        }
        break;
    default:
        {
            // ...
        }
        break;
}

或
switch (integer) {
    case 1:{
            // ...
        }
        break;
        
    case 2:{
            // ...
        }
        break;

    case 3:{
            // ...
        }
        break;
        
    default:{
            // ...
        }
        break;
}

2. 使用枚舉類型時攒暇,不能有default分支, 除了使用枚舉類型以外子房,都必須有default分支【M】

RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;
switch (menuType) {
    case RWTLeftMenuTopItemMain:
        {
            // ...
            break;
        }
    case RWTLeftMenuTopItemShows:
        {
            // ...
            break;
        }
    case RWTLeftMenuTopItemSchedule:
        {
            // ...
            break;
        }
}

在Switch語句使用枚舉類型的時候形用,如果使用了default分支,在將來就無法通過編譯器來檢查新增的枚舉類型了证杭。

函數(shù)


1. 一個函數(shù)的長度必須限制在80行以內(nèi)田度,建議50行以內(nèi)【M】

通常來說,在閱讀一個函數(shù)的時候解愤,如果視需要跨過很長的垂直距離會非常影響代碼的閱讀體驗镇饺。如果需要來回滾動眼球或代碼才能看全一個方法,就會很影響思維的連貫性送讲,對閱讀代碼的速度造成比較大的影響奸笤。最好的情況是在不滾動眼球或代碼的情況下一眼就能將該方法的全部代碼映入眼簾。

2. 一個函數(shù)只做一件事(單一原則)【M】

每個函數(shù)的職責都應(yīng)該劃分的很明確(就像類一樣)李茫。

推薦這樣寫:

dataConfiguration()
viewConfiguration()

不推薦這樣寫:

void dataConfiguration()
{ 
    ...
    viewConfiguration()
}

3. 對于有返回值的函數(shù)(方法)揭保,每一個分支都必須有返回值【S】

推薦這樣寫:

int function() {
    if (condition1) {
        return count1
    } else if (condition2) {
        return count2
    } else {
        return defaultCount
    }
}

不推薦這樣寫:

int function() {
    if (condition1) {
        return count1
    } else if (condition2) {
        return count2
    }
}

4. 對輸入?yún)?shù)的正確性和有效性進行檢查,參數(shù)錯誤立即返回【S】

推薦這樣寫:

void function(param1, param2)
{
 if (param1 is unavailable){
    return;
 }

 if (param2 is unavailable){
    return;
 }

 //Do some right thing
}

5. 如果在不同的函數(shù)內(nèi)部有相同的功能魄宏,應(yīng)該把相同的功能抽取出來單獨作為另一個函數(shù)【S】

原來的調(diào)用:

void logic() {
    a();
    b()秸侣; 
    if (logic1 condition) {
        c();
    } else {
        d();
    }
}

將a,b函數(shù)抽取出來作為單獨的函數(shù)

void basicConfig() {
  a();
  b();
}

void logic1() {
  basicConfig();
  c();
}

void logic2() {
  basicConfig();
  d();
}

6. 將函數(shù)內(nèi)部比較復(fù)雜的邏輯提取出來作為單獨的函數(shù)【S】

一個函數(shù)內(nèi)的不清晰(邏輯判斷比較多,行數(shù)較多)的那片代碼味榛,往往可以被提取出去椭坚,構(gòu)成一個新的函數(shù),然后在原來的地方調(diào)用它這樣你就可以使用有意義的函數(shù)名來代替注釋搏色,增加程序的可讀性善茎。

舉一個發(fā)送郵件的例子:

openEmailSite();
login();

writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);

send();

中間的部分稍微長一些,我們可以將它們提取出來:

void writeEmail(title, content,receiver,attachment)
{
 writeTitle(title);
 writeContent(content);
 writeReceiver(receiver);
 addAttachment(attachment); 
}

然后再看一下原來的代碼:

openEmailSite();
login();
writeEmail(title, content,receiver,attachment)
send();

8. 避免使用全局變量频轿,類成員(class member)來傳遞信息垂涯,盡量使用局部變量和參數(shù)『叫希【M】

在一個類里面耕赘,經(jīng)常會有傳遞某些變量的情況。而如果需要傳遞的變量是某個全局變量或者屬性的時候膳殷,有些朋友不喜歡將它們作為參數(shù)操骡,而是在方法內(nèi)部就直接訪問了:

class A {
    var x;

    func updateX() {
        ...x = ...;
    }

    func printX() {
        updateX();
        print(x);
    }
}

我們可以看到,在printX方法里面赚窃,updateX和print方法之間并沒有值的傳遞册招,乍一看我們可能不知道x從哪里來的,導(dǎo)致程序的可讀性降低了勒极。

而如果你使用局部變量而不是類成員來傳遞信息是掰,那么這兩個函數(shù)就不需要依賴于某一個類,而且更加容易理解河质,不易出錯:

func updateX() - > String {
    x = ...;
    return x;
}

func printX() {
    String x = updateX();
    print(x);
}

注釋規(guī)范


參考:Cocoa代碼風格指南之注釋規(guī)范(三)

優(yōu)秀的代碼大部分是可以自描述的冀惭,我們完全可以用程代碼本身來表達它到底在干什么,而不需要注釋的輔助掀鹅。

但并不是說一定不能寫注釋,有以下幾種情況比較適合寫注釋:

  1. 解釋實現(xiàn)文件中復(fù)雜的邏輯
  2. 對代碼進行標注
  3. 對接口進行說明
  4. 容易產(chǎn)生歧義的代碼

除了上述這幾種情況媒楼,如果別人只能依靠注釋才能讀懂你的代碼的時候乐尊,就要反思代碼出現(xiàn)了什么問題。

最后划址,對于注釋的內(nèi)容扔嵌,相對于“做了什么”,更應(yīng)該說明“為什么這么做”夺颤。

解釋實現(xiàn)文件中復(fù)雜的邏輯

在實現(xiàn)文件中痢缎,有時候會有一些復(fù)雜的邏輯,這種情況通過看代碼去理解代價往往很大世澜。如果不寫備注独旷,可能以后連自己都很難維護。所以,就算現(xiàn)在你對這些代碼的邏輯有很清晰的理解嵌洼,也最好寫上備注案疲,因為以后你肯定會忘記。這種注釋由于不需要文檔化麻养,也不需要聯(lián)想褐啡,所以使用 C/C++ 的注釋風格就可以。當然如果你有文檔化的需求鳖昌,那還是按照統(tǒng)一的注釋風格备畦。

// 這是注釋
/* 這是也是注釋 */

對代碼進行標注

在程序中,我們見過最多的注釋莫過于//TODO了许昨。這種注釋如果在后邊加上“:”就能在方法列表中顯示萍恕,效果類似于#pragma mark。Xcode支持類似功能的注釋有如下幾種车要。

//MARK:MARK
//TODO:TODO
//FIXME:FIXME
// !!!:!!!
//???:???

這樣的注釋能夠起到對代碼進行標記的作用允粤。比如項目中如果存在還沒有完成的功能就可以加上//TODO:Response button event。如果某塊代碼需要修改翼岁,可以加上//FIXME:There is a problem on iOS7类垫。如果你需要警告他人// !!!:DO NOT TOUCH MY CODE!±牌拢或者你對哪里的代碼有不解(不滿)//???:What is this shit悉患。如果你想在方法內(nèi)部實現(xiàn)像#pragma mark一樣的標注,就可以使用//MARK:Initialization榆俺,是的售躁,就是這么簡單。如果你很另類茴晋,你還可以這樣來注釋代碼陪捷。

對接口進行說明

當我們封裝庫或在開發(fā)較復(fù)雜的項目時,因為接口需要供多人使用诺擅,所以對接口的注釋說明就十分重要市袖。

先說行尾注釋。生成文檔的話烁涌,只有Docxygen是支持行尾注釋的苍碟。但是所幸 Xcode Quick Help 的支持讓我們有了使用的理由。

行尾注釋有以下幾種

/**< 行尾注釋1. AppleDoc 不支持會變?yōu)橄乱豁椀淖⑨? Quick Help 支持撮执,Doxygen 支持, 根據(jù)英文句號自動切分簡要描述與詳細描述. */
/*!< 行尾注釋2. AppleDoc 不支持會變?yōu)橄乱豁椀淖⑨? Quick Help 支持微峰,Doxygen 支持, 會全部當作詳細描述, 而缺少簡要描述. */
///< 行尾注釋3. AppleDoc 不支持會變?yōu)橄乱豁椀淖⑨? Quick Help 不支持, Doxygen 支持。
//!< 行尾注釋4. AppleDoc 不支持會會忽略, Quick Help 支持, Doxygen 支持抒钱。

推薦

  • 行首單行注釋///
  • 行尾單行注釋//!<
  • 行首多行注釋/** */

通用二級標簽

Xcode 基本支持所有的二級標簽顏色加深的功能蜓肆。二級標簽可以在生成文檔颜凯、聯(lián)想和使用 Quick Help 時顯示。不加任何標簽的情況的下生成文檔症杏,單行注釋默認為簡要描述装获。多行注釋默認為詳細描述。在使用多行注釋的時候也可以在不加標簽的情況下同時生成簡要描述和詳細描述

/** 簡要描述
 * 詳細描述
 */
///簡要描述.詳細描述

多行注釋中的簡要描述也可以用@brief來代替厉颤。除此之外還有很多支持的二級標簽穴豫。這里列舉一些 AppleDoc 和 Doxygen 均支持的常用二級標簽。

/**
 * @brief <title>: 簡要注釋. appledoc中僅對屬性逼友、方法有效精肃,對類、協(xié)議 無效帜乞,會造成后續(xù)內(nèi)容解析失敗.
 * @param <name> <description>: 參數(shù)描述.
 * @return <description>: 返回值描述.
 * @exception <name> <description>: 異常描述.
 * @see <name>: 參見.
 * @sa <name>: 參見. 同@see.
 * @warning <text>: 警告.
 * @bug <text>: 警告.
 * @name <title>: 組名. 用于給成員們分組, 既文檔中Tasks區(qū)的子類別.
 * 代碼塊 `int sum = 0;`
 * 多行代碼塊:
 *     int sum = 0;
 *     for(int i = 1; i <= 10; i++) {
 *         sum += i;
 *     }
 * 無序列表:
 * - first
 * - second
 * - third
 * 有序列表:
 * 1. first
 * 2. second
 * 3. third
 * 多級列表:
 * - abc
 *    - a
 *    - b
 *    - c
 * - rgb
 *    - red
 *        1. first.
 *            1. alpha.
 *            2. beta.
 *        2. second.
 *        3. third.
 *    - green
 *    - blue
 * 鏈接:
 * <http://www.xuyafei.cn>
 * [xuyafei](<http://www.xuyafei.cn>)
 */

下邊是 Quick Help 支持的常用二級標簽司抱。

/**
 * @brief It converts temperature degrees from Celsius to Fahrenheit scale.
 * @param  fromCelcius The celsius degrees value.
 * @return float The degrees in the Fahrenheit scale.
 * @code
 *     float f = [self toCelsius:80];
 * @endcode
 * @remark This is a super-easy method.
 */

Code Review


換行、注釋黎烈、方法長度习柠、代碼重復(fù)等這些是通過機器檢查出來的問題,是無需通過人來做的照棋。

而且除了審查需求的實現(xiàn)的程度资溃,bug是否無處藏身以外,更應(yīng)該關(guān)注代碼的設(shè)計烈炭。比如類與類之間的耦合程度溶锭,設(shè)計的可擴展性,復(fù)用性符隙,是否可以將某些方法抽出來作為接口等等趴捅。

三. iOS規(guī)范

變量


1. 變量名必須使用駝峰格式【M】

類,協(xié)議使用大駝峰:

HomePageViewController<HeaderViewDelegate>

對象等局部變量使用小駝峰:

NSString *personName = @"";
NSUInteger totalCount = 0;

2. 變量的名稱必須同時包含功能與類型【S】

UIButton *addBtn //添加按鈕
UILabel *nameLbl //名字標簽
NSString *addressStr//地址字符串

3. 系統(tǒng)常用類作實例變量聲明時加入后綴【S】

類型 后綴
UIViewController VC
UIView View
UILabel Lbl
UIButton Btn
UIImage Img
UIImageView ImagView
NSArray Array
NSMutableArray Marray
NSDictionary Dict
NSMutableDictionary Mdict
NSString Str
NSMutableString MStr
NSSet Set
NSMutableSet Mset

常量


1. 常量以相關(guān)類名作為前綴【S】

推薦這樣寫:

static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;

不推薦這樣寫:

static const NSTimeInterval fadeOutTime = 0.4;

2. 建議使用類型常量霹疫,不建議使用#define預(yù)處理命令【S】

首先比較一下這兩種聲明常量的區(qū)別:

  • 預(yù)處理命令:簡單的文本替換拱绑,不包括類型信息,并且可被任意修改更米。
  • 類型常量:包括類型信息欺栗,并且可以設(shè)置其使用范圍,而且不可被修改征峦。

使用預(yù)處理雖然能達到替換文本的目的,但是本身還是有局限性的:

  • 不具備類型信息消请。
  • 可以被任意修改栏笆。

3. 對外公開某個常量:【M】

如果我們需要發(fā)送通知,那么就需要在不同的地方拿到通知的“頻道”字符串(通知的名稱)臊泰,那么顯然這個字符串是不能被輕易更改蛉加,而且可以在不同的地方獲取。這個時候就需要定義一個外界可見的字符串常量。

推薦這樣寫:

//頭文件
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
//實現(xiàn)文件
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;

不推薦這樣寫:

#define CompanyName @"Apple Inc." 
#define magicNumber 42


1. 宏针饥、常量名都要使用大寫字母厂抽,用下劃線‘_’分割單詞《⊙郏【S】

// TODO

#define URL_GAIN_QUOTE_LIST @"/v1/quote/list"
#define URL_UPDATE_QUOTE_LIST @"/v1/quote/update"
#define URL_LOGIN  @"/v1/user/login”

2. 宏定義中如果包含表達式或變量筷凤,表達式和變量必須用小括號括起來。

#define MY_MIN(A, B)  ((A)>(B)?(B):(A))

CGRect函數(shù)


其實iOS內(nèi)部已經(jīng)提供了相應(yīng)的獲取CGRect各個部分的函數(shù)了苞七,它們的可讀性比較高藐守,而且簡短,推薦使用:

推薦這樣寫:

CGRect frame = self.view.frame; 
CGFloat x = CGRectGetMinX(frame); 
CGFloat y = CGRectGetMinY(frame); 
CGFloat width = CGRectGetWidth(frame); 
CGFloat height = CGRectGetHeight(frame); 
CGRect frame = CGRectMake(0.0, 0.0, width, height);

而不是

CGRect frame = self.view.frame;  
CGFloat x = frame.origin.x;  
CGFloat y = frame.origin.y;  
CGFloat width = frame.size.width;  
CGFloat height = frame.size.height;  
CGRect frame = (CGRect){ .origin = CGPointZero, .size = frame.size };

范型


建議在定義NSArray和NSDictionary時使用泛型蹂风,可以保證程序的安全性:

NSArray<NSString *> *testArr = [NSArray arrayWithObjects:@"Hello", @"world", nil];
NSDictionary<NSString *, NSNumber *> *dic = @{@"key":@(1), @"age":@(10)};

Block


為常用的Block類型創(chuàng)建typedef 【S】

如果我們需要重復(fù)創(chuàng)建某種block(相同參數(shù)卢厂,返回值)的變量,我們就可以通過typedef來給某一種塊定義屬于它自己的新類型

例如:

int (^variableName)(BOOL flag, int value) =^(BOOL flag, int value){
     // Implementation
     return someInt;
}

這個Block有一個bool參數(shù)和一個int參數(shù)惠啄,并返回int類型慎恒。我們可以給它定義類型:

int(^EOCSomeBlock)(BOOL flag, int value);

再次定義的時候,就可以通過簡單的賦值來實現(xiàn):

EOCSomeBlock block = ^(BOOL flag, int value){
// Implementation
};

定義作為參數(shù)的Block:

- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;

這里的Block有一個NSData參數(shù)撵渡,一個NSError參數(shù)并沒有返回值

typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);

- (void)startWithCompletionHandler:(EOCCompletionHandler)

通過typedef定義Block簽名的好處是:如果要某種塊增加參數(shù)融柬,那么只修改定義簽名的那行代碼

字面量

盡量使用字面量值來創(chuàng)建 NSString , NSDictionary , NSArray , NSNumber 這些不可變【S】

推薦這樣寫:

    NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
    NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"}; 
    NSNumber *shouldUseLiterals = @YES;NSNumber *buildingZIPCode = @10018;

不推薦這樣寫:

NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill" ];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];NSNumber *buildingZIPCode = [NSNumber numberWithInteger:10018];

屬性


1. 屬性的命名使用小駝峰【M】

推薦這樣寫:

@property (nonatomic, readwrite, strong) UIButton *confirmButton;

2. 屬性的關(guān)鍵字推薦按照 原子性,讀寫姥闭,內(nèi)存管理的順序排列【M】

推薦這樣寫:

@property (nonatomic, readwrite,   copy) NSString *name;
@property (nonatomic, readonly,    copy) NSString *gender;
@property (nonatomic, readwrite, strong) UIView *headerView;

3. Block屬性應(yīng)該使用copy關(guān)鍵字 【M】

推薦這樣寫:

typedef void (^ErrorCodeBlock) (id errorCode, NSString *message);
@property (nonatomic, readwrite, copy) ErrorCodeBlock errorBlock;//將block拷貝到堆中

4. 形容詞性的BOOL屬性的getter應(yīng)該加上is前綴【S】

推薦這樣寫:

@property (assign, getter=isEditable) BOOL editable;

5. 使用getter方法做懶加載 【S】

實例化一個對象是需要耗費資源的丹鸿,如果這個對象里的某個屬性的實例化要調(diào)用很多配置和計算,就需要懶加載它棚品,在使用它的前一刻對它進行實例化:

- (NSDateFormatter *)dateFormatter {
    if (!_dateFormatter) {
        _dateFormatter = [[NSDateFormatter alloc] init];
        NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
        [_dateFormatter setLocale:enUSPOSIXLocale];
        [_dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"];
    } 
    return _dateFormatter;
}

但是也有對這種做法的爭議:getter方法可能會產(chǎn)生某些副作用靠欢,例如如果它修改了全局變量,可能會產(chǎn)生難以排查的錯誤铜跑。

6. 除了init和dealloc方法门怪,建議都使用點語法訪問屬性【M】

使用點語法的好處:

setter:

  1. setter會遵守內(nèi)存管理語義(strong, copy, weak)。
  2. 通過在內(nèi)部設(shè)置斷點锅纺,有助于調(diào)試bug掷空。
  3. 可以過濾一些外部傳入的值。
  4. 捕捉KVO通知囤锉。

getter:

  1. 允許子類化坦弟。
  2. 通過在內(nèi)部設(shè)置斷點,有助于調(diào)試bug官地。
  3. 實現(xiàn)懶加載(lazy initialization)酿傍。

注意:

  1. 懶加載的屬性,必須通過點語法來讀取數(shù)據(jù)驱入。因為懶加載是通過重寫getter方法來初始化實例變量的赤炒,如果不通過屬性來讀取該實例變量氯析,那么這個實例變量就永遠不會被初始化。
  2. 在init和dealloc方法里面使用點語法的后果是:因為沒有繞過setter和getter莺褒,在setter和getter里面可能會有很多其他的操作掩缓。而且如果它的子類重載了它的setter和getter方法,那么就可能導(dǎo)致該子類調(diào)用其他的方法遵岩。

7. 不要濫用點語法你辣,要區(qū)分好方法調(diào)用和屬性訪問【S】

推薦這樣寫:

view.backgroundColor = [UIColor orangeColor]; 
[UIApplication sharedApplication].delegate;

不推薦這樣寫:

[view setBackgroundColor:[UIColor orangeColor]]; 
UIApplication.sharedApplication.delegate;

8. 盡量使用不可變對象【S】

建議盡量把對外公布出來的屬性設(shè)置為只讀,在實現(xiàn)文件內(nèi)部設(shè)為讀寫旷余。具體做法是:

  • 在頭文件中绢记,設(shè)置對象屬性為
  • 在實現(xiàn)文件中設(shè)置為readwrite

    這樣一來正卧,在外部就只能讀取該數(shù)據(jù)蠢熄,而不能修改它,使得這個類的實例所持有的數(shù)據(jù)更加安全炉旷。而且签孔,對于集合類的對象,更應(yīng)該仔細考慮是否可以將其設(shè)為可變的窘行。

    如果在公開部分只能設(shè)置其為只讀屬性饥追,那么就在非公開部分存儲一個可變型。所以當在外部獲取這個屬性時罐盔,獲取的只是內(nèi)部可變型的一個不可變版本,例如:

    在公共API中:

    @interface EOCPerson : NSObject
    
    @property (nonatomic, copy, readonly) NSString *firstName;
    @property (nonatomic, copy, readonly) NSString *lastName;
    @property (nonatomic, strong, readonly) NSSet *friends //向外公開的不可變集合
    
    *   (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;
    *   (void)addFriend:(EOCPerson*)person;
    *   (void)removeFriend:(EOCPerson*)person;
    
    @end
    

    在這里但绕,我們將friends屬性設(shè)置為不可變的set杖刷。然后症副,提供了來增加和刪除這個set里的元素的公共接口。

    在實現(xiàn)文件里:

    @interface EOCPerson ()
    
    @property (nonatomic, copy, readwrite) NSString *firstName;
    @property (nonatomic, copy, readwrite) NSString *lastName;
    
    @end
    
    @implementation EOCPerson {
    NSMutableSet *_internalFriends; //實現(xiàn)文件里的可變集合
    }
    
    - (NSSet*)friends {
        return [_internalFriends copy]; //get方法返回的永遠是可變set的不可變型
        }
    
    - (void)addFriend:(EOCPerson*)person {
        [_internalFriends addObject:person]; //在外部增加集合元素的操作
        //do something when add element
        }
    
    - (void)removeFriend:(EOCPerson*)person {
        [_internalFriends removeObject:person]; //在外部移除元素的操作
        //do something when remove element
        }
    
    - (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName {
    
        if ((self = [super init])) {
    
        _firstName = firstName;
        _lastName = lastName;
        _internalFriends = [NSMutableSet new];
    
        }
        return self;
    }
    

    我們可以看到椿浓,在實現(xiàn)文件里纬黎,保存一個可變set來記錄外部的增刪操作幅骄。

    這里最重要的代碼是:

    - (NSSet*)friends {
        return [_internalFriends copy];
        }
    

    這個是friends屬性的獲取方法:它將當前保存的可變set復(fù)制了一不可變的set并返回。因此本今,外部讀取到的set都將是不可變的

方法


1. 方法名中不應(yīng)使用and拆座,而且簽名要與對應(yīng)的參數(shù)名保持高度一致【M】

推薦這樣

- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;

不推薦這樣寫:

- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height;

2. 方法實現(xiàn)時,如果參數(shù)過長冠息,則令每個參數(shù)占用一行挪凑,以冒號對齊」浼瑁【S】

- (void)doSomethingWith:(NSString *)theFoo
                   rect:(CGRect)theRect
               interval:(CGFloat)theInterval
{
 //Implementation
}

3. 私有方法應(yīng)該在實現(xiàn)文件中申明岖赋。【S】

@interface ViewController ()
- (void)basicConfiguration;
@end

@implementation ViewController
- (void)basicConfiguration
{
 //Do some basic configuration
}
@end

4. 方法名用小寫字母開頭的單詞組合而成【M】

- (NSString *)descriptionWithLocale:(id)locale;

5. 方法名前綴【S】

  • 刷新視圖的方法名要以refresh為首瓮孙。
  • 更新數(shù)據(jù)的方法名要以update或者configure為首唐断。

推薦這樣寫:

- (void)refreshHeaderViewWithCount:(NSUInteger)count;
- (void)updateDataSourceWithViewModel:(ViewModel *)viewModel;

面向協(xié)議編程


如果某些功能(方法)具備可復(fù)用性,我們就需要將它們抽取出來放入一個抽象接口文件中(在iOS中杭抠,抽象接口即協(xié)議)脸甘,讓不同類型的對象遵循這個協(xié)議,從而擁有相同的功能偏灿。

因為協(xié)議是不依賴于某個對象的丹诀,所以通過協(xié)議,我們可以解開兩個對象之間的耦合翁垂。如何理解呢铆遭?我們來看一下下面這個例子:

現(xiàn)在有一個需求:在一個UITableViewController里面拉取feed并展示出來。

方案一:

定義一個拉取feed的類ZOCFeedParser沿猜,這個類有一些代理方法實現(xiàn)feed相關(guān)功能:

@protocol ZOCFeedParserDelegate <NSObject>

@optional
- (void)feedParserDidStart:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info; 
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedItem:(ZOCFeedItemDTO *)item; 
- (void)feedParserDidFinish:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didFailWithError:(NSError *)error;@end 

@interface ZOCFeedParser : NSObject

@property (nonatomic, weak  ) id <ZOCFeedParserDelegate> delegate; 
@property (nonatomic, strong) NSURL *url; 

- (id)initWithURL:(NSURL *)url; 
- (BOOL)start; 
- (void)stop; 

@end

然后在ZOCTableViewController里面?zhèn)魅?code>ZOCFeedParser枚荣,并遵循其代理方法,實現(xiàn)feed的拉取功能啼肩。

@interface ZOCTableViewController : UITableViewController<ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(ZOCFeedParser *)feedParser; 
@end

具體應(yīng)用:

NSURL *feedURL = [NSURL URLWithString:@"http://bbc.co.uk/feed.rss"]; 
ZOCFeedParser *feedParser = [[ZOCFeedParser alloc] initWithURL:feedURL]; 
ZOCTableViewController *tableViewController = [[ZOCTableViewController alloc] initWithFeedParser:feedParser]; 
feedParser.delegate = tableViewController;

OK橄妆,現(xiàn)在我們實現(xiàn)了需求:在ZOCTableViewController里面存放了一個ZOCFeedParser對象來處理feed的拉取功能。

但這里有一個嚴重的耦合問題:ZOCTableViewController只能通過ZOCFeedParser對象來處理feed的拉取功能祈坠。
于是我們重新審視一下這個需求:其實我們實際上只需要ZOCTableViewController拉取feed就可以了害碾,而具體是由哪個對象來拉取,ZOCTableViewController并不需要關(guān)心赦拘。

也就是說慌随,我們需要提供給ZOCTableViewController的是一個更范型的對象,這個對象具備了拉取feed的功能就好了躺同,而不應(yīng)該僅僅局限于某個具體的對象(ZOCFeedParser)阁猜。所以,剛才的設(shè)計需要重新做一次修改:

方案二:

首先需要在一個接口文件ZOCFeedParserProtocol.h里面定義抽象的笋籽,具有拉取feed功能的協(xié)議:

@protocol ZOCFeedParserDelegate <NSObject>

@optional
- (void)feedParserDidStart:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info; 
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedItem:(ZOCFeedItemDTO *)item; 
- (void)feedParserDidFinish:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didFailWithError:(NSError *)error;

@end 

@protocol ZOCFeedParserProtocol <NSObject>

@property (nonatomic, weak) id <ZOCFeedParserDelegate> delegate; 
@property (nonatomic, strong) NSURL *url;

- (BOOL)start;
- (void)stop;

@end

而原來的ZOCFeedParser僅僅是需要遵循上面這個協(xié)議就具備了拉取feed的功能:

@interface ZOCFeedParser : NSObject <ZOCFeedParserProtocol> 
- (id)initWithURL:(NSURL *)url;//僅僅需要通過傳入url即可蹦漠,其他事情都交給ZOCFeedParserProtocol@end

而且,ZOCTableViewController也不直接依賴于ZOCFeedParser對象车海,我們只需要傳給它一個遵循<ZOCFeedParserProtocol>的對象即可笛园。

@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(id<ZOCFeedParserProtocol>)feedParser;
@end

這樣一來,ZOCTableViewControllerZOCFeedParser之間就沒有直接的關(guān)系了侍芝。以后研铆,如果我們想:

  • 給這個feed拉取器增加新的功能:僅需要修改ZOCFeedParserProtocol.h文件。
  • 更換一個feed拉取器實例:創(chuàng)建一個新類型來遵循ZOCFeedParserProtocol.h即可州叠。

iOS 中委托的設(shè)計


1. 要區(qū)分好代理和數(shù)據(jù)源的區(qū)別【S】

在iOS開發(fā)中的委托模式包含了delegate(代理)和datasource(數(shù)據(jù)源)棵红。雖然二者同屬于委托模式,但是這兩者是有區(qū)別的咧栗。這個區(qū)別就是二者的信息流方向是不同的:

  • delegate :事件發(fā)生的時候逆甜,委托者需要通知代理虱肄。(信息流從委托者到代理)
  • datasource:委托者需要從數(shù)據(jù)源拉取數(shù)據(jù)。(信息流從數(shù)據(jù)源到委托者)

然而包括蘋果也沒有做好榜樣交煞,將它們徹底的區(qū)分開咏窿。就拿UITableView來說,在它的delegate方法中有一個方法:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

這個方法正確地體現(xiàn)了代理的作用:委托者(tableview)告訴代理(控制器)“我的某個cell被點擊了”素征。但是集嵌,UITableViewDelegate的方法列表里還有這個方法:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;

該方法的作用是 由控制器來告訴tabievlew的行高,也就是說御毅,它的信息流是從控制器(數(shù)據(jù)源)到委托者(tableview)的根欧。準確來講,它應(yīng)該是一個數(shù)據(jù)源方法端蛆,而不是代理方法凤粗。

在UITableViewDataSource中,就有標準的數(shù)據(jù)源方法:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;

這個方法的作用就是讓tableview向控制器拉取一個section數(shù)量的數(shù)據(jù)欺税。

所以侈沪,在我們設(shè)計一個視圖控件的代理和數(shù)據(jù)源時,一定要區(qū)分好二者的區(qū)別晚凿,合理地劃分哪些方法屬于代理方法亭罪,哪些方法屬于數(shù)據(jù)源方法。

2. 代理方法的第一個參數(shù)必須為委托者【S】

代理方法必須以委托者作為第一個參數(shù)(參考UITableViewDelegate)的方法歼秽。其目的是為了區(qū)分不同委托著的實例应役。因為同一個控制器是可以作為多個tableview的代理的。若要區(qū)分到底是哪個tableview的cell被點擊了燥筷,就需要在方法中做個區(qū)分

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath箩祥。

向代理發(fā)送消息時需要判斷其是否實現(xiàn)該方法

最后,在委托著向代理發(fā)送消息的時候肆氓,需要判斷委托著是否實現(xiàn)了這個代理方法:

if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) { 
    [self.delegate signUpViewControllerDidPressSignUpButton:self]; 
}

3. 遵循代理過多的時候袍祖,換行對齊顯示【S】

@interface ShopViewController () <UIGestureRecognizerDelegate,
                                  HXSClickEventDelegate,
                                  UITableViewDelegate,
                                  UITableViewDataSource>

4. 代理的方法需要明確必須執(zhí)行和可不執(zhí)行

代理方法在默認情況下都是必須執(zhí)行的,然而在設(shè)計一組代理方法的時候谢揪,有些方法可以不是必須執(zhí)行(是因為存在默認配置)蕉陋,這些方法就需要使用@optional關(guān)鍵字來修飾:

@protocol ZOCServiceDelegate <NSObject>
@optional
- (void)generalService:(ZOCGeneralService *)service didRetrieveEntries:(NSArray *)entries; 
@end


1. 類的名稱應(yīng)該以三個大或者兩個寫字母為前綴;創(chuàng)建子類的時候拨扶,應(yīng)該把代表子類特點的部分放在前綴和父類名的中間【S】

推薦這樣寫:

//父類
ZOCSalesListViewController

//子類
ZOCDaySalesListViewController
ZOCMonthSalesListViewController

2. initializer && dealloc

推薦:

  • 將 dealloc 方法放在實現(xiàn)文件的最前面
  • 將init方法放在dealloc方法后面凳鬓。如果有多個初始化方法,應(yīng)該將指定初始化方法放在最前面患民,其他初始化方法放在其后缩举。

2.1 dealloc方法里面應(yīng)該直接訪問實例變量,不應(yīng)該用點語法訪問 【M】

2.2 init方法的寫法:【M】

  • init方法返回類型必須是instancetype,不能是id仅孩。
  • 必須先實現(xiàn)[super init]托猩。
- (instancetype)init { 
 self = [super init]; // call the designated initializer 
 if (self) {
 // Custom initialization
 }
 return self;
}

2.3 指定初始化方法【S】

指定初始化方法(designated initializer)是提供所有的(最多的)參數(shù)的初始化方法,間接初始化方法(secondary initializer)有一個或部分參數(shù)的初始化方法杠氢。

注意事項1:間接初始化方法必須調(diào)用指定初始化方法站刑。

@implementation ZOCEvent 

//指定初始化方法
- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date 
location:(CLLocation *)location
{ 
 self = [super init];
 if (self) {
    _title = title;
    _date = date;
    _location = location;
 }
 return self;
} 

//間接初始化方法
- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date
{ 
    return [self initWithTitle:title date:date location:nil];
}

//間接初始化方法
- (instancetype)initWithTitle:(NSString *)title 
{ 
    return [self initWithTitle:title date:[NSDate date] location:nil];
}

@end

注意事項2:如果直接父類有指定初始化方法,則必須調(diào)用其指定初始化方法

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 
    if (self) {
    }
    return self; 
}

注意事項3:如果想在當前類自定義一個新的全能初始化方法鼻百,則需要如下幾個步驟

  1. 定義新的指定初始化方法,并確保調(diào)用了直接父類的初始化方法摆尝。
  2. 重載直接父類的初始化方法温艇,在內(nèi)部調(diào)用新定義的指定初始化方法。
  3. 為新的指定初始化方法寫文檔堕汞。

看一個標準的例子:

@implementation ZOCNewsViewController

//新的指定初始化方法
- (id)initWithNews:(ZOCNews *)news {
    self = [super initWithNibName:nil bundle:nil]; 
    if (self) {
    _news = news;
    }
    return self;
} 

// 重載父類的初始化方法
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
    return [self initWithNews:nil]; 
}
@end

在這里勺爱,重載父類的初始化方法并在內(nèi)部調(diào)用新定義的指定初始化方法的原因是你不能確定調(diào)用者調(diào)用的就一定是你定義的這個新的指定初始化方法,而不是原來從父類繼承來的指定初始化方法讯检。

假設(shè)你沒有重載父類的指定初始化方法琐鲁,而調(diào)用者卻恰恰調(diào)用了父類的初始化方法。那么調(diào)用者可能永遠都調(diào)用不到你自己定義的新指定初始化方法了人灼。

而如果你成功定義了一個新的指定初始化方法并能保證調(diào)用者一定能調(diào)用它围段,你最好要在文檔中明確寫出哪一個才是你定義的新初始化方法⊥斗牛或者你也可以使用編譯器指令__attribute__((objc_designated_initializer))來標記它奈泪。

3. 所有返回類對象和實例對象的方法都應(yīng)該使用instancetype【S】

將instancetype關(guān)鍵字作為返回值的時候,可以讓編譯器進行類型檢查灸芳,同時適用于子類的檢查涝桅,這樣就保證了返回類型的正確性(一定為當前的類對象或?qū)嵗龑ο螅?/p>

推薦這樣寫:

@interface ZOCPerson
+ (instancetype)personWithName:(NSString *)name; 
@end

不推薦這樣寫:

@interface ZOCPerson
+ (id)personWithName:(NSString *)name; 
@end

4. 在類的頭文件中盡量少引用其他頭文件【S】

有時,類A需要將類B的實例變量作為它公共API的屬性烙样。這個時候冯遂,我們不應(yīng)該引入類B的頭文件,而應(yīng)該使用向前聲明(forward declaring)使用class關(guān)鍵字谒获,并且在A的實現(xiàn)文件引用B的頭文件蛤肌。

// EOCPerson.h
#import <Foundation/Foundation.h>

@class EOCEmployer;

@interface EOCPerson : NSObject

@property (nonatomic, copy  ) NSString *firstName;
@property (nonatomic, copy  ) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;//將EOCEmployer作為屬性

@end

// EOCPerson.m
#import "EOCEmployer.h"

這樣做有什么優(yōu)點呢:

  • 不在A的頭文件中引入B的頭文件,就不會一并引入B的全部內(nèi)容究反,這樣就減少了編譯時間寻定。
  • 可以避免循環(huán)引用:因為如果兩個類在自己的頭文件中都引入了對方的頭文件,那么就會導(dǎo)致其中一個類無法被正確編譯精耐。

但是個別的時候狼速,必須在頭文件中引入其他類的頭文件:

主要有兩種情況:

  1. 該類繼承于某個類,則應(yīng)該引入父類的頭文件卦停。
  2. 該類遵從某個協(xié)議向胡,則應(yīng)該引入該協(xié)議的頭文件恼蓬。而且最好將協(xié)議單獨放在一個頭文件中。

5. 類的布局【S】

#pragma mark - Life Cycle Methods
- (instancetype)init
- (void)dealloc

- (void)viewWillAppear:(BOOL)animated
- (void)viewDidAppear:(BOOL)animated
- (void)viewWillDisappear:(BOOL)animated
- (void)viewDidDisappear:(BOOL)animated

#pragma mark - Override Methods

#pragma mark - Intial Methods

#pragma mark - Network Methods

#pragma mark - Target Methods

#pragma mark - Public Methods

#pragma mark - Private Methods

#pragma mark - UITableViewDataSource  
#pragma mark - UITableViewDelegate  

#pragma mark - Lazy Loads

#pragma mark - NSCopying  

#pragma mark - NSObject  Methods

分類


1. 分類添加的方法需要添加前綴和下劃線【S】

推薦這樣寫:

@interface NSDate (ZOCTimeExtensions)
 - (NSString *)zoc_timeAgoShort;
@end

不推薦這樣寫:

@interface NSDate (ZOCTimeExtensions) 
- (NSString *)timeAgoShort;
@end

2. 把類的實現(xiàn)代碼分散到便于管理的多個分類中【S】

一個類可能會有很多公共方法僵芹,而且這些方法往往可以用某種特有的邏輯來分組处硬。我們可以利用Objecctive-C的分類機制,將類的這些方法按一定的邏輯劃入幾個分區(qū)中拇派。

舉個??:

先看一個沒有使用無分類的類:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy,   readonly) NSString *firstName;
@property (nonatomic, copy,   readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName;

/* Friendship methods */
- (void)addFriend:(EOCPerson *)person;
- (void)removeFriend:(EOCPerson *)person;
- (BOOL)isFriendsWith:(EOCPerson *)person;

/* Work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;

/* Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;

@end

分類之后:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy,   readonly) NSString *firstName;
@property (nonatomic, copy,   readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName;

@end

@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson *)person;
- (void)removeFriend:(EOCPerson *)person;
- (BOOL)isFriendsWith:(EOCPerson *)person;

@end

@interface EOCPerson (Work)

- (void)performDaysWork;
- (void)takeVacationFromWork;

@end

@interface EOCPerson (Play)

- (void)goToTheCinema;
- (void)goToSportsGame;

@end

其中荷辕,F(xiàn)riendShip分類的實現(xiàn)代碼可以這么寫:

// EOCPerson+Friendship.h
#import "EOCPerson.h"

@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson *)person;
- (void)removeFriend:(EOCPerson *)person;
- (BOOL)isFriendsWith:(EOCPerson *)person;

@end

// EOCPerson+Friendship.m
#import "EOCPerson+Friendship.h"

@implementation EOCPerson (Friendship)

- (void)addFriend:(EOCPerson *)person {
 /* ... */
}

- (void)removeFriend:(EOCPerson *)person {
 /* ... */
}

- (BOOL)isFriendsWith:(EOCPerson *)person {
 /* ... */
}

@end

注意:在新建分類文件時,一定要引入被分類的類文件件豌。

通過分類機制疮方,可以把類代碼分成很多個易于管理的功能區(qū),同時也便于調(diào)試茧彤。因為分類的方法名稱會包含分類的名稱骡显,可以馬上看到該方法屬于哪個分類中。

利用這一點曾掂,我們可以創(chuàng)建名為Private的分類惫谤,將所有私有方法都放在該類里。這樣一來珠洗,我們就可以根據(jù)private一詞的出現(xiàn)位置來判斷調(diào)用的合理性溜歪,這也是一種編寫“自我描述式代碼(self-documenting)”的辦法。

單例


1. 單例不能作為容器對象來使用【M】

單例對象不應(yīng)該暴露出任何屬性险污,也就是說它不能作為讓外部存放對象的容器痹愚。它應(yīng)該是一個處理某些特定任務(wù)的工具,比如在iOS中的GPS和加速度傳感器蛔糯。我們只能從他們那里得到一些特定的數(shù)據(jù)拯腮。

2. 使用dispatch_once來生成單例【M】

推薦這樣寫:

+ (instancetype)sharedInstance { 
     static id sharedInstance = nil; 
     static dispatch_once_t onceToken = 0;
     dispatch_once(&onceToken, ^{ 
        sharedInstance = [[self alloc] init];
     }); 
     return sharedInstance; 
}

不推薦這樣寫:

+ (instancetype)sharedInstance { 
    static id sharedInstance; 
    @synchronized(self) { 
        if (sharedInstance == nil) {  
            sharedInstance = [[MyClass alloc] init]; 
        } 
    } 
    return sharedInstance; 
}

相等性的判斷【S】


判斷兩個person類是否相等的合理做法:

-  (BOOL)isEqual:(id)object {

    if (self == object) {  
        return YES; //判斷內(nèi)存地址
    } 
    if (![object isKindOfClass:[ZOCPerson class]]) { 
        return NO; //是否為當前類或派生類 
    } 
    
    return [self isEqualToPerson:(ZOCPerson *)object]; 
}

//自定義的判斷相等性的方法
-  (BOOL)isEqualToPerson:(Person *)person { 
    if (!person) {  
        return NO;
    } 

    BOOL namesMatch = (!self.name && !person.name) || [self.name isEqualToString:person.name]; 
    BOOL birthdaysMatch = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];
    
    return haveEqualNames && haveEqualBirthdays; 
}

方法文檔


一個函數(shù)(方法)必須有一個字符串文檔來解釋,除非它:

  • 非公開蚁飒,私有函數(shù)动壤。
  • 很短。
  • 顯而易見淮逻。

而其余的琼懊,包括公開接口,重要的方法爬早,分類哼丈,以及協(xié)議,都應(yīng)該伴隨文檔(注釋):

  • 以/開始
  • 第二行識總結(jié)性的語句
  • 第三行永遠是空行
  • 在與第二行開頭對齊的位置寫剩下的注釋筛严。

建議這樣寫:

/* This comment serves to demonstrate the format of a doc string.

Note that the summary line is always at most one line long, and after the opening block comment,
and each line of text is preceded by a single space.
*/

看一個指定初始化方法的注釋:

/ *
  *  Designated initializer. *
  *  @param store The store for CRUD operations.
  *  @param searchService The search service used to query the store. 
  *  @return A ZOCCRUDOperationsStore object.
  */ 
- (instancetype)initWithOperationsStore:(id<ZOCGenericStoreProtocol>)store searchService:(id<ZOCGenericSearchServiceProtocol>)searchService;

多用隊列醉旦,少用同步鎖來避免資源搶奪【S】


多個線程執(zhí)行同一份代碼時,很可能會造成數(shù)據(jù)不同步。建議使用GCD來為代碼加鎖的方式解決這個問題车胡。

方案一:使用串行同步隊列來將讀寫操作都安排到同一個隊列里:

_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);

//讀取字符串
- (NSString*)someString {

    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });

    return localSomeString;

}

//設(shè)置字符串
- (void)setSomeString:(NSString *)someString {

    dispatch_sync(_syncQueue, ^{
        _someString = someString;
    });
}

這樣一來檬输,讀寫操作都在串行隊列進行,就不容易出錯匈棘。

但是丧慈,還有一種方法可以讓性能更高:

方案二:將寫操作放入柵欄快中,讓他們單獨執(zhí)行主卫;將讀取操作并發(fā)執(zhí)行逃默。

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//讀取字符串
- (NSString*)someString {

     __block NSString *localSomeString;
     dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
     return localSomeString;
}
//設(shè)置字符串
- (void)setSomeString:(NSString*)someString {

     dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}

顯然,數(shù)據(jù)的正確性主要取決于寫入操作队秩,那么只要保證寫入時笑旺,線程是安全的,那么即便讀取操作是并發(fā)的馍资,也可以保證數(shù)據(jù)是同步的。
這里的dispatch_barrier_async方法使得操作放在了同步隊列里“有序進行”关噪,保證了寫入操作的任務(wù)是在串行隊列里鸟蟹。

實現(xiàn)description方法打印自定義對象信息【S】


在打印我們自己定義的類的實例對象時,在控制臺輸出的結(jié)果往往是這樣的:object = <EOCPerson: 0x7fd9a1600600>

這里只包含了類名和內(nèi)存地址使兔,它的信息顯然是不具體的,遠達不到調(diào)試的要求建钥。

但是!如果在我們自己定義的類覆寫description方法虐沥,我們就可以在打印這個類的實例時輸出我們想要的信息熊经。

例如:

- (NSString*)description {
     return [NSString stringWithFormat:@"<%@: %p, %@ %@>", [self class], self, firstName, lastName];
}

在這里,顯示了內(nèi)存地址欲险,還有該類的所有屬性镐依。

而且,如果我們將這些屬性值放在字典里打印天试,則更具有可讀性:

- (NSString*)description {

     return [NSString stringWithFormat:@"<%@: %p, {\n%@\n}",[self class],self,
    @{ @"title":_title,
       @"latitude":@(_latitude),
       @"longitude":@(_longitude)}
    ];
}

輸出結(jié)果:

location = <EOCLocation: 0x7f98f2e01d20, {
   latitude = "51.506";
   longitude = 0;
   title = London;
}

我們可以看到槐壳,通過重寫description方法可以讓我們更加了解對象的情況,便于后期的調(diào)試喜每,節(jié)省開發(fā)時間务唐。

Tips:在lldb處于斷點狀態(tài)時,可以重寫debugDescription方法改變debug時輸出樣式

NSArray& NSMutableArray


1. addObject之前要非空判斷带兜。

2. 取下標的時候要判斷是否越界枫笛。

3. 取第一個元素或最后一個元素的時候使用firtstObject和lastObject

NSCache


1. 構(gòu)建緩存時選用NSCache 而非NSDictionary【S】

如果我們緩存使用得當,那么應(yīng)用程序的響應(yīng)速度就會提高刚照。只有那種“重新計算起來很費事的數(shù)據(jù)刑巧,才值得放入緩存”,比如那些需要從網(wǎng)絡(luò)獲取或從磁盤讀取的數(shù)據(jù)。

在構(gòu)建緩存的時候很多人習(xí)慣用NSDictionary或者NSMutableDictionary海诲,但是作者建議大家使用NSCache繁莹,它作為管理緩存的類,有很多特點要優(yōu)于字典特幔,因為它本來就是為了管理緩存而設(shè)計的咨演。

2. NSCache優(yōu)于NSDictionary的幾點:

  • 當系統(tǒng)資源將要耗盡時,NSCache具備自動刪減緩沖的功能蚯斯。并且還會先刪減“最久未使用”的對象薄风。
  • NSCache不拷貝鍵,而是保留鍵拍嵌。因為并不是所有的鍵都遵從拷貝協(xié)議(字典的鍵是必須要支持拷貝協(xié)議的遭赂,有局限性)。
  • NSCache是線程安全的:不編寫加鎖代碼的前提下横辆,多個線程可以同時訪問NSCache撇他。

iOS 圖片命名規(guī)范【S】


參考: iOS 圖片命名規(guī)范

git提交


  • 提交的commit必須能通過編譯不能有error和多余的警告【M】
  • commit message 應(yīng)該清晰明了,說明本次提交的目的【M】

NSNotification


1. 通知的名稱【M】

建議將通知的名字作為常量狈蚤,保存在一個專門的類中:

// Const.h
extern NSString * const ZOCFooDidBecomeBarNotification

// Const.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";

2. 通知的注冊【M】

注冊通知建議是在 - (void)viewDidLoaded進行困肩,特殊情況可以在- (void)viewWillAppear:,此時必須在- (void)viewWillDisappear:成對調(diào)用

3. 通知的移除【M】

通知必須要在對象銷毀之前移除掉。

其他


1. 盡量采用MVC架構(gòu)脆侮,使用fram或者Masonry布局锌畸。【S】

文件按照Model View Controller Cell Other來組織

2. Xcode工程文件的物理路徑要和邏輯路徑保持一致靖避√对妫【S】

3. 忽略沒有使用變量的編譯警告【S】

對于某些暫時不用,以后可能用到的臨時變量幻捏,為了避免警告盆犁,我們可以使用如下方法將這個警告消除:

- (NSInteger)giveMeFive { 
 NSString *foo; 
 #pragma unused (foo) 
 return 5; 
}

4. 手動標明警告和錯誤【S】

手動明確一個錯誤:

- (NSInteger)divide:(NSInteger)dividend by:(NSInteger)divisor { 
 #error Whoa, buddy, you need to check for zero here! 
 return (dividend / divisor); 
}

手動明確一個警告:

- (float)divide:(float)dividend by:(float)divisor { 
 //#warning Dude, don't compare floating point numbers like this! 
    if (divisor != 0.0) { 
        return (dividend / divisor); 
    } else {  
        return NAN; 
    } 
}

參考文獻:

  1. 王垠:編程的智慧
  2. 美團點評技術(shù)團隊:聊聊clean code
  3. 禪與 Objective-C 編程藝術(shù)
  4. J_Knight 的文集:iOS - 《Effective Objective-C 2.0》
  5. 蝴蝶之夢天使:iOS代碼編程規(guī)范-根據(jù)項目經(jīng)驗匯總
  6. 高家二少爺:Objective-C高質(zhì)量代碼參考規(guī)范
  7. J_Knight:iOS 代碼規(guī)范
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市粘咖,隨后出現(xiàn)的幾起案子蚣抗,更是在濱河造成了極大的恐慌,老刑警劉巖瓮下,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翰铡,死亡現(xiàn)場離奇詭異,居然都是意外死亡讽坏,警方通過查閱死者的電腦和手機锭魔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來路呜,“玉大人迷捧,你說我怎么就攤上這事织咧。” “怎么了漠秋?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵笙蒙,是天一觀的道長。 經(jīng)常有香客問我庆锦,道長捅位,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任搂抒,我火速辦了婚禮艇搀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘求晶。我一直安慰自己焰雕,他們只是感情好,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布芳杏。 她就那樣靜靜地躺著矩屁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪爵赵。 梳的紋絲不亂的頭發(fā)上档插,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機與錄音亚再,去河邊找鬼。 笑死晨抡,一個胖子當著我的面吹牛氛悬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播耘柱,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼如捅,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了调煎?” 一聲冷哼從身側(cè)響起镜遣,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎士袄,沒想到半個月后悲关,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡娄柳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年寓辱,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赤拒。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡秫筏,死狀恐怖诱鞠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情这敬,我是刑警寧澤航夺,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站崔涂,受9級特大地震影響阳掐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜堪伍,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一锚烦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧帝雇,春花似錦涮俄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吮廉,卻和暖如春苞尝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背宦芦。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工宙址, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人调卑。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓抡砂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親恬涧。 傳聞我的和親對象是個殘疾皇子注益,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 約定 在我看來,開發(fā)規(guī)范像是一條可供參考的標準線溯捆。不同開發(fā)者可以根據(jù)這條標準線來規(guī)范自己的開發(fā)行為丑搔,尤其是在大的項...
    xxzsxxzs閱讀 625評論 1 0
  • 本文檔旨在總結(jié)出一份通用的編碼規(guī)范,歡迎隨時探討補充提揍。 喜歡我的可以關(guān)注收藏我的個人博客:Ro.bber 代碼格式...
    Ro_bber閱讀 876評論 0 1
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,416評論 8 265
  • 她出生在五十年代啤月,三歲時母親去世;父親便娶了她人碳锈;小小年紀的她受盡了磨難顽冶;自打記事起就開始圍著灶臺打轉(zhuǎn);母...
    虞美人與羊毛閱讀 195評論 0 0
  • 最近我讀完了一本書绞呈,叫《老人與海》间景,這本書有些人說這是諾貝爾文學(xué)獎上的一顆明珠佃声,而有的人說這本書曾經(jīng)激勵了當時無...
    期待待續(xù)閱讀 188評論 0 1