iOS代碼規範

配圖來自:nipic.com

這篇規範一共分爲三個部分:

  1. 核心原則:介紹了這篇代碼規範所遵循的核心原則。
  2. 通用規範:不侷限於iOS的通用性的代碼規範(使用C語言和Swift語言)。
  3. iOS規範:僅適用於iOS的代碼規範(使用Objective-C語言)。

一. 核心原則

原則一:代碼應該簡潔易懂,邏輯清晰

因爲軟件是需要人來維護的。這個人在未來很可能不是你。所以首先是爲人編寫程序,其次纔是計算機:

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

原則二:面向變化編程,而不是面向需求編程。

需求是暫時的,只有變化纔是永恆的。 
本次迭代不能僅僅爲了當前的需求,寫出擴展性強,易修改的程序纔是負責任的做法,對自己負責,對公司負責。

原則三:先保證程序的正確性,防止過度工程

過度工程(over-engineering):在正確可用的代碼寫出之前就過度地考慮擴展,重用的問題,使得工程過度複雜。

  1. 先把眼前的問題解決掉,解決好,再考慮將來的擴展問題。
  2. 先寫出可用的代碼,反覆推敲,再考慮是否需要重用的問題。
  3. 先寫出可用,簡單,明顯沒有bug的代碼,再考慮測試的問題。

二. 通用規範

關於大括號


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

推薦這樣寫:

white(someCondition){
}
//函數
void function(param1,param2)
{
}           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

運算符


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

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

!bValue
~iValue
++iCount
*strSource
&fSum
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

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

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

2. 多個不同的運算符同時存在時應該使用括號來明確優先級

在多個不同的運算符同時存在的時候應該合理使用括號,不要盲目依賴操作符優先級。 
因爲有的時候不能保證閱讀你代碼的人就一定能瞭解你寫的算式裏面所有操作符的優先級。

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

這裏的&lt;&lt;是移位操作直觀上卻很容易認爲它的優先級很高,所以就把這個算式誤認爲:(2 << 2) + 1 _ 3 - 4 
但事實上,它的優先級是比加減法還要低的,所以該算式應該等同於:2 << (2 + 1 _ 3 - 4)。 
所以在以後寫這種複雜一點的算式的時候,儘量多加一點括號,避免讓其他人誤解(甚至是自己)。

變量


1. 一個變量有且只有一個功能,儘量不要把一個變量用作多種用途

2. 變量在使用前應初始化,防止未初始化的變量被引用

3. 局部變量應該儘量接近使用它的地方

推薦這樣寫:

func someFunction() {

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

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

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

不推薦這樣寫:

func someFunction() {

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

  //Do something With count
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

if語句


1. 必須列出所有分支(窮舉所有的情況),而且每個分支都必須給出明確的結果。

推薦這樣寫:

var hintStr;
if (count < 3) {
  hintStr = "Good";
} else {
  hintStr = "";
}           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

不推薦這樣寫:

var hintStr;
if (count < 3) {
 hintStr = "Good";
}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

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

推薦這樣寫:

- (void)someMethod { 
  if (!goodCondition) {
    return;
  }
  //Do something
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

不推薦這樣寫:

- (void)someMethod { 
  if (goodCondition) {
    //Do something
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

比較典型的例子我在JSONModel裏遇到過:

-(id)initWithDictionary:(NSDictionary*)dict error:(NSError)err
{
   //方法1. 參數爲nil
   if (!dict) {
     if (err) *err = [JSONModelError errorInputIsNil];
     return nil;
    }
    //方法2. 參數不是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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

可以看到,在這裏,首先判斷出各種錯誤的情況然後提前返回,把最正確的情況放到最後返回。

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

推薦這樣寫:

let nameContainsSwift = sessionName.hasPrefix("Swift")
let isCurrentYear = sessionDateCompontents.year == 2014
let isSwiftSession = nameContainsSwift && isCurrentYear
if (isSwiftSession) { 
   // Do something
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

不推薦這樣寫:

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

4. 條件語句的判斷應該是變量在左,常量在右

推薦這樣寫:

if ( count == 6) {
}
  • 1
  • 2
  • 1
  • 2

或者

if ( object == nil) {
}
  • 1
  • 2
  • 1
  • 2

或者

if ( !object ) {
}
  • 1
  • 2
  • 1
  • 2

不推薦這樣寫:

if ( 6 == count) {
}
  • 1
  • 2
  • 1
  • 2

或者

if ( nil == object ) {
}
  • 1
  • 2
  • 1
  • 2

5. 每個分支的實現代碼都必須被大括號包圍

推薦這樣寫:

if (!error) {
  return success;
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

不推薦這樣寫:

if (!error)
    return success;
  • 1
  • 2
  • 1
  • 2

或者

if (!error) return success;
  • 1
  • 1

6. 條件過多,過長的時候應該換行

推薦這樣寫:

if (condition1() && 
    condition2() && 
    condition3() && 
    condition4()) {
  // Do something
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

不推薦這樣寫:

if (condition1() && condition2() && condition3() && condition4()) {
  // Do something
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

for語句


1. 不可在for循環內修改循環變量,防止for循環失去控制。

for (int index = 0; index < 10; index++){
   ...
   logicToChange(index)
}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

2. 避免使用continue和break。

continue和break所描述的是“什麼時候不做什麼”,所以爲了讀懂二者所在的代碼,我們需要在頭腦裏將他們取反。

其實最好不要讓這兩個東西出現,因爲我們的代碼只要體現出“什麼時候做什麼”就好了,而且通過適當的方法,是可以將這兩個東西消滅掉的:

2.1 如果出現了continue,只需要把continue的條件取反即可

var filteredProducts = Array<String>()
for level in products {
    if level.hasPrefix("bad") {
        continue
    }
    filteredProducts.append(level)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

for level in products {
    if !level.hasPrefix("bad") {
      filteredProducts.append(level)
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

2.2 消除while裏的break:將break的條件取反,併合併到主循環裏

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

while裏的break:

while (condition1) {
  ...
  if (condition2) {
    break;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

取反併合併到主條件:

while (condition1 && !condition2) {
  ...
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

2.3 在有返回值的方法裏消除break:將break轉換爲return立即返回

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

func hasBadProductIn(products: Array<String>) -> Bool {
    var result = false    
    for level in products {
        if level.hasPrefix("bad") {
            result = true
        }
    }
   return result
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

遇到錯誤條件直接返回:

func hasBadProductIn(products: Array<String>) -> Bool {
    for level in products {
        if level.hasPrefix("bad") {
            return true
        }
    }
   return false
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

Switch語句


1. 每個分支都必須用大括號括起來

推薦這樣寫:

switch (integer) {  
  case 1:  {
    // ...  
   }
    break;  
  case 2: {  
    // ...  
    break;  
  }  
  case 3: {
    // ...  
    break; 
  }
  default:{
    // ...  
    break; 
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

2. 使用枚舉類型時,不能有default分支, 除了使用枚舉類型以外,都必須有default分支

RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;  
switch (menuType) {  
  case RWTLeftMenuTopItemMain: {
    // ...  
    break; 
   }
  case RWTLeftMenuTopItemShows: {
    // ...  
    break; 
  }
  case RWTLeftMenuTopItemSchedule: {
    // ...  
    break; 
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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

函數


1. 一個函數的長度必須限制在50行以內

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

2. 一個函數只做一件事(單一原則)

每個函數的職責都應該劃分的很明確(就像類一樣)。

推薦這樣寫:

dataConfiguration()
viewConfiguration()
  • 1
  • 2
  • 1
  • 2

不推薦這樣寫:

void dataConfiguration()
{   
   ...
   viewConfiguration()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

3. 對於有返回值的函數(方法),每一個分支都必須有返回值

推薦這樣寫:

int function()
{
    if(condition1){
        return count1
    }else if(condition2){
        return count2
    }else{
       return defaultCount
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

不推薦這樣寫:

int function()
{
    if(condition1){
        return count1
    }else if(condition2){
        return count2
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4. 對輸入參數的正確性和有效性進行檢查,參數錯誤立即返回

推薦這樣寫:

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

      if(param2 is unavailable){
           return;
      }
     //Do some right thing
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

5. 如果在不同的函數內部有相同的功能,應該把相同的功能抽取出來單獨作爲另一個函數

原來的調用:

void logic() {
  a();
  b();
  if (logic1 condition) {
    c();
  } else {
    d();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

將a,b函數抽取出來作爲單獨的函數

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

void logic1() {
  basicConfig();
  c();
}
void logic2() {
  basicConfig();
  d();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

6. 將函數內部比較複雜的邏輯提取出來作爲單獨的函數

一個函數內的不清晰(邏輯判斷比較多,行數較多)的那片代碼,往往可以被提取出去,構成一個新的函數,然後在原來的地方調用它這樣你就可以使用有意義的函數名來代替註釋,增加程序的可讀性。

舉一個發送郵件的例子:

openEmailSite();
login();
writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);
send();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

void writeEmail(title, content,receiver,attachment)
{
  writeTitle(title);
  writeContent(content);
  writeReceiver(receiver);
  addAttachment(attachment); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

openEmailSite();
login();
writeEmail(title, content,receiver,attachment)
send();
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

8. 避免使用全局變量,類成員(class member)來傳遞信息,儘量使用局部變量和參數。

在一個類裏面,經常會有傳遞某些變量的情況。而如果需要傳遞的變量是某個全局變量或者屬性的時候,有些朋友不喜歡將它們作爲參數,而是在方法內部就直接訪問了:

 class A {
   var x;
   func updateX() {
      ...
      x = ...;
   }
   func printX() {
     updateX();
     print(x);
   }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

我們可以看到,在printX方法裏面,updateX和print方法之間並沒有值的傳遞,乍一看我們可能不知道x從哪裏來的,導致程序的可讀性降低了。

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

func updateX() -> String{
    x = ...;
    return x;
 }
 func printX() {
   String x = updateX();
   print(x);
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

註釋


優秀的代碼大部分是可以自描述的,我們完全可以用程代碼本身來表達它到底在幹什麼,而不需要註釋的輔助。

但並不是說一定不能寫註釋,有以下三種情況比較適合寫註釋:

  1. 公共接口(註釋要告訴閱讀代碼的人,當前類能實現什麼功能)。
  2. 涉及到比較深層專業知識的代碼(註釋要體現出實現原理和思想)。
  3. 容易產生歧義的代碼(但是嚴格來說,容易讓人產生歧義的代碼是不允許存在的)。

除了上述這三種情況,如果別人只能依靠註釋才能讀懂你的代碼的時候,就要反思代碼出現了什麼問題。

最後,對於註釋的內容,相對於“做了什麼”,更應該說明“爲什麼這麼做”。

Code Review


換行、註釋、方法長度、代碼重複等這些是通過機器檢查出來的問題,是無需通過人來做的。

而且除了審查需求的實現的程度,bug是否無處藏身以外,更應該關注代碼的設計。比如類與類之間的耦合程度,設計的可擴展性,複用性,是否可以將某些方法抽出來作爲接口等等。

三. iOS規範

變量


1. 變量名必須使用駝峯格式

類,協議使用大駝峯:

HomePageViewController.h
<HeaderViewDelegate>
  • 1
  • 2
  • 1
  • 2

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

NSString *personName = @"";
NSUInteger totalCount = 0;
  • 1
  • 2
  • 1
  • 2

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

UIButton *addBtn //添加按鈕
UILabel *nameLbl //名字標籤
NSString *addressStr//地址字符串
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

3. 系統常用類作實例變量聲明時加入後綴

類型 後綴
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. 常量以相關類名作爲前綴

推薦這樣寫:

static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
  • 1
  • 2
  • 1
  • 2

不推薦這樣寫:

static const NSTimeInterval fadeOutTime = 0.4;
  • 1
  • 1

2. 建議使用類型常量,不建議使用#define預處理命令

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

  • 預處理命令:簡單的文本替換,不包括類型信息,並且可被任意修改。
  • 類型常量:包括類型信息,並且可以設置其使用範圍,而且不可被修改。

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

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

3. 對外公開某個常量:

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

推薦這樣寫:

//頭文件
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
  • 1
  • 2
  • 1
  • 2

不推薦這樣寫:

#define CompanyName @"Apple Inc." 
#define magicNumber 42
  • 1
  • 2
  • 1
  • 2


1. 宏、常量名都要使用大寫字母,用下劃線‘_’分割單詞。

#define URL_GAIN_QUOTE_LIST @"/v1/quote/list"
#define URL_UPDATE_QUOTE_LIST @"/v1/quote/update"
#define URL_LOGIN  @"/v1/user/login”
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

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

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

CGRect函數


其實iOS內部已經提供了相應的獲取CGRect各個部分的函數了,它們的可讀性比較高,而且簡短,推薦使用:

推薦這樣寫:

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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

而不是

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 };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

範型


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

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

Block


爲常用的Block類型創建typedef

如果我們需要重複創建某種block(相同參數,返回值)的變量,我們就可以通過typedef來給某一種塊定義屬於它自己的新類型

例如:

int (^variableName)(BOOL flag, int value) =^(BOOL flag, int value){
     // Implementation
     return someInt;
}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

這個Block有一個bool參數和一個int參數,並返回int類型。我們可以給它定義類型:

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

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

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

定義作爲參數的Block:

(void)startWithCompletionHandler: (void(^)(NSData _data, NSError _error))completion;
  • 1
  • 1

這裏的Block有一個NSData參數,一個NSError參數並沒有返回值

typedef void(^EOCCompletionHandler)(NSData _data, NSError _error);
  • 1
  • 1
-(void)startWithCompletionHandler:(EOCCompletionHandler)completion;
  • 1
  • 1

通過typedef定義Block簽名的好處是:如果要某種塊增加參數,那麼只修改定義簽名的那行代碼即可。

字面量語法

儘量使用字面量值來創建 NSString , NSDictionary , NSArray , NSNumber 這些不可變對象: 
推薦這樣寫

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

不推薦這樣寫:

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
  • 2
  • 3
  • 1
  • 2
  • 3

屬性


1. 屬性的命名使用小駝峯

推薦這樣寫:

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

2. 屬性的關鍵字推薦按照 原子性,讀寫,內存管理的順序排列

推薦這樣寫:

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

3. Block屬性應該使用copy關鍵字

推薦這樣寫:

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

4. 形容詞性的BOOL屬性的getter應該加上is前綴

推薦這樣寫:

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

5. 使用getter方法做懶加載

實例化一個對象是需要耗費資源的,如果這個對象裏的某個屬性的實例化要調用很多配置和計算,就需要懶加載它,在使用它的前一刻對它進行實例化:

- (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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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

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

使用點語法的好處:

setter:

  1. setter會遵守內存管理語義(strong, copy, weak)。
  2. 通過在內部設置斷點,有助於調試bug。
  3. 可以過濾一些外部傳入的值。
  4. 捕捉KVO通知。

getter:

  1. 允許子類化。
  2. 通過在內部設置斷點,有助於調試bug。
  3. 實現懶加載(lazy initialization)。 
    注意: 

    1. 懶加載的屬性,必須通過點語法來讀取數據。因爲懶加載是通過重寫getter方法來初始化實例變量的,如果不通過屬性來讀取該實例變量,那麼這個實例變量就永遠不會被初始化。 
    2. 在init和dealloc方法裏面使用點語法的後果是:因爲沒有繞過setter和getter,在setter和getter裏面可能會有很多其他的操作。而且如果它的子類重載了它的setter和getter方法,那麼就可能導致該子類調用其他的方法。

7. 不要濫用點語法,要區分好方法調用和屬性訪問

推薦這樣寫:
view.backgroundColor = [UIColor orangeColor]; 
[UIApplication sharedApplication].delegate;
  • 1
  • 2
  • 1
  • 2
不推薦這樣寫:
[view setBackgroundColor:[UIColor orangeColor]]; 
UIApplication.sharedApplication.delegate;
  • 1
  • 2
  • 1
  • 2

8. 儘量使用不可變對象

建議儘量把對外公佈出來的屬性設置爲只讀,在實現文件內部設爲讀寫。具體做法是:
  • 在頭文件中,設置對象屬性爲readwrite。這樣一來,在外部就只能讀取該數據,而不能修改它,使得這個類的實例所持有的數據更加安全。而且,對於集合類的對象,更應該仔細考慮是否可以將其設爲可變的。如果在公開部分只能設置其爲只讀屬性,那麼就在非公開部分存儲一個可變型。所以當在外部獲取這個屬性時,獲取的只是內部可變型的一個不可變版本,例如: 
    在公共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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
在這裏,我們將friends屬性設置爲不可變的set。然後,提供了來增加和刪除這個set裏的元素的公共接口。 在實現文件裏:
@interface EOCPerson ()

@property (nonatomic, copy, readwrite) NSString _firstName;
@property (nonatomic, copy, readwrite) NSString _lastName;

@end

@implementation EOCPerson {
     NSMutableSet *_internalFriends;  //實現文件裏的可變集合
}

-(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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
我們可以看到,在實現文件裏,保存一個可變set來記錄外部的增刪操作。 這裏最重要的代碼是:
-(NSSet*)friends {
return [_internalFriends copy];
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
這個是friends屬性的獲取方法:它將當前保存的可變set複製了一不可變的set並返回。因此,外部讀取到的set都將是不可變的版本。

方法

1. 方法名中不應使用and,而且簽名要與對應的參數名保持高度一致

- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
  • 1
  • 1
不推薦這樣寫:
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
  • 1
  • 1
- (instancetype)initWith:(int)width and:(int)height;
  • 1
  • 1

2. 方法實現時,如果參數過長,則令每個參數佔用一行,以冒號對齊。

- (void)doSomethingWith:(NSString *)theFoo
                   rect:(CGRect)theRect
               interval:(CGFloat)theInterval
{
   //Implementation
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3. 私有方法應該在實現文件中申明。

@interface ViewController ()
- (void)basicConfiguration;
@end
@implementation ViewController
- (void)basicConfiguration
{
   //Do some basic configuration
}
@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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

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

5. 方法名前綴

  • 刷新視圖的方法名要以refresh爲首。
  • 更新數據的方法名要以update爲首。
推薦這樣寫:
- (void)refreshHeaderViewWithCount:(NSUInteger)count;
- (void)updateDataSourceWithViewModel:(ViewModel*)viewModel;
  • 1
  • 2
  • 1
  • 2

面向協議編程


如果某些功能(方法)具備可複用性,我們就需要將它們抽取出來放入一個抽象接口文件中(在iOS中,抽象接口即協議),讓不同類型的對象遵循這個協議,從而擁有相同的功能。 因爲協議是不依賴於某個對象的,所以通過協議,我們可以解開兩個對象之間的耦合。如何理解呢?我們來看一下下面這個例子: 現在有一個需求:在一個`UITableViewController`裏面拉取feed並展示出來。

方案一:

定義一個拉取feed的類`ZOCFeedParser`,這個類有一些代理方法實現feed相關功能:
@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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
然後在`ZOCTableViewController`裏面傳入`ZOCFeedParser`,並遵循其代理方法,實現feed的拉取功能。
@interface ZOCTableViewController : UITableViewController<ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(ZOCFeedParser *)feedParser; 
@end
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
具體應用:
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;
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
OK,現在我們實現了需求:在`ZOCTableViewController`裏面存放了一個`ZOCFeedParser`對象來處理feed的拉取功能。 但這裏有一個嚴重的耦合問題:`ZOCTableViewController`只能通過`ZOCFeedParser`對象來處理feed的拉取功能。 於是我們重新審視一下這個需求:其實我們實際上只需要`ZOCTableViewController`拉取feed就可以了,而具體是由哪個對象來拉取,`ZOCTableViewController`並不需要關心。 也就是說,我們需要提供給`ZOCTableViewController`的是一個更範型的對象,這個對象具備了拉取feed的功能就好了,而不應該僅僅侷限於某個具體的對象(`ZOCFeedParser`)。所以,剛纔的設計需要重新做一次修改:

方案二:

首先需要在一個接口文件`ZOCFeedParserProtocol.h`裏面定義抽象的,具有拉取feed功能的協議:
@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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
而原來的`ZOCFeedParser`僅僅是需要遵循上面這個協議就具備了拉取feed的功能:
@interface ZOCFeedParser : NSObject <ZOCFeedParserProtocol> 
- (id)initWithURL:(NSURL *)url;//僅僅需要通過傳入url即可,其他事情都交給ZOCFeedParserProtocol@end
  • 1
  • 2
  • 1
  • 2
而且,`ZOCTableViewController`也不直接依賴於`ZOCFeedParser`對象,我們只需要傳給它一個遵循`<ZOCFeedParserProtocol>`的對象即可。
@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(id<ZOCFeedParserProtocol>)feedParser;
@end
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
這樣一來,`ZOCTableViewController`和`ZOCFeedParser`之間就沒有直接的關係了。以後,如果我們想:
  • 給這個feed拉取器增加新的功能:僅需要修改ZOCFeedParserProtocol.h文件。
  • 更換一個feed拉取器實例:創建一個新類型來遵循ZOCFeedParserProtocol.h即可。

iOS 中委託的設計


1. 要區分好代理和數據源的區別

在iOS開發中的委託模式包含了delegate(代理)和datasource(數據源)。雖然二者同屬於委託模式,但是這兩者是有區別的。這個區別就是二者的信息流方向是不同的:
  • delegate :事件發生的時候,委託者需要通知代理。(信息流從委託者到代理)
  • datasource:委託者需要從數據源拉取數據。(信息流從數據源到委託者)
然而包括蘋果也沒有做好榜樣,將它們徹底的區分開。就拿UITableView來說,在它的delegate方法中有一個方法:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
  • 1
  • 1
這個方法正確地體現了代理的作用:委託者(tableview)告訴代理(控制器)“我的某個cell被點擊了”。但是,UITableViewDelegate的方法列表裏還有這個方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
  • 1
  • 1
該方法的作用是 由控制器來告訴tabievlew的行高,也就是說,它的信息流是從控制器(數據源)到委託者(tableview)的。準確來講,它應該是一個數據源方法,而不是代理方法。 在UITableViewDataSource中,就有標準的數據源方法:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
  • 1
  • 1
這個方法的作用就是讓tableview向控制器拉取一個section數量的數據。 所以,在我們設計一個視圖控件的代理和數據源時,一定要區分好二者的區別,合理地劃分哪些方法屬於代理方法,哪些方法屬於數據源方法。

2. 代理方法的第一個參數必須爲委託者

代理方法必須以委託者作爲第一個參數(參考UITableViewDelegate)的方法。其目的是爲了區分不同委託着的實例。因爲同一個控制器是可以作爲多個tableview的代理的。若要區分到底是哪個tableview的cell被點擊了,就需要在“
  • (void)tableView:(UITableView )tableView didSelectRowAtIndexPath:(NSIndexPath )indexPath“方法中做個區分。

向代理髮送消息時需要判斷其是否實現該方法

最後,在委託着向代理髮送消息的時候,需要判斷委託着是否實現了這個代理方法:
if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) { 
 [self.delegate signUpViewControllerDidPressSignUpButton:self]; 
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

3. 遵循代理過多的時候,換行對齊顯示

@interface ShopViewController () <UIGestureRecognizerDelegate,
                                  HXSClickEventDelegate,
                                  UITableViewDelegate,
                                  UITableViewDataSource>
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

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

代理方法在默認情況下都是必須執行的,然而在設計一組代理方法的時候,有些方法可以不是必須執行(是因爲存在默認配置),這些方法就需要使用`@optional`關鍵字來修飾:
@protocol ZOCServiceDelegate <NSObject>@optional- (void)generalService:(ZOCGeneralService *)service didRetrieveEntries:(NSArray *)entries; 
@end
  • 1
  • 2
  • 1
  • 2


1. 類的名稱應該以三個大寫字母爲前綴;創建子類的時候,應該把代表子類特點的部分放在前綴和父類名的中間

推薦這樣寫:
//父類
ZOCSalesListViewController
//子類
ZOCDaySalesListViewController
ZOCMonthSalesListViewController
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

2. initializer && dealloc

推薦:
  • 將 dealloc 方法放在實現文件的最前面
  • 將init方法放在dealloc方法後面。如果有多個初始化方法,應該將指定初始化方法放在最前面,其他初始化方法放在其後。

2.1 dealloc方法裏面應該直接訪問實例變量,不應該用點語法訪問

2.2 init方法的寫法:

  • init方法返回類型必須是instancetype,不能是id。
  • 必須先實現[super init]。
- (instancetype)init { 
    self = [super init]; // call the designated initializer 
    if (self) { 
        // Custom initialization 
    } 
    return self; 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2.3 指定初始化方法

指定初始化方法(designated initializer)是提供所有的(最多的)參數的初始化方法,間接初始化方法(secondary initializer)有一個或部分參數的初始化方法。 注意事項1:間接初始化方法必須調用指定初始化方法。
@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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
注意事項2:如果直接父類有指定初始化方法,則必須調用其指定初始化方法
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 
    if (self) {
    }
    return self; 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
注意事項3:如果想在當前類自定義一個新的全能初始化方法,則需要如下幾個步驟
  1. 定義新的指定初始化方法,並確保調用了直接父類的初始化方法。
  2. 重載直接父類的初始化方法,在內部調用新定義的指定初始化方法。
  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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在這裏,重載父類的初始化方法並在內部調用新定義的指定初始化方法的原因是你不能確定調用者調用的就一定是你定義的這個新的指定初始化方法,而不是原來從父類繼承來的指定初始化方法。

假設你沒有重載父類的指定初始化方法,而調用者卻恰恰調用了父類的初始化方法。那麼調用者可能永遠都調用不到你自己定義的新指定初始化方法了。

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

3. 所有返回類對象和實例對象的方法都應該使用instancetype

將instancetype關鍵字作爲返回值的時候,可以讓編譯器進行類型檢查,同時適用於子類的檢查,這樣就保證了返回類型的正確性(一定爲當前的類對象或實例對象)

推薦這樣寫:

@interface ZOCPerson
+ (instancetype)personWithName:(NSString *)name; 
@end
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

不推薦這樣寫:

@interface ZOCPerson
+ (id)personWithName:(NSString *)name; 
@end
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

4. 在類的頭文件中儘量少引用其他頭文件

有時,類A需要將類B的實例變量作爲它公共API的屬性。這個時候,我們不應該引入類B的頭文件,而應該使用向前聲明(forward declaring)使用class關鍵字,並且在A的實現文件引用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"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

這樣做有什麼優點呢:

  • 不在A的頭文件中引入B的頭文件,就不會一併引入B的全部內容,這樣就減少了編譯時間。
  • 可以避免循環引用:因爲如果兩個類在自己的頭文件中都引入了對方的頭文件,那麼就會導致其中一個類無法被正確編譯。

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

主要有兩種情況:

  1. 該類繼承於某個類,則應該引入父類的頭文件。
  2. 該類遵從某個協議,則應該引入該協議的頭文件。而且最好將協議單獨放在一個頭文件中。

5. 類的佈局

#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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

分類


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

推薦這樣寫:

@interface NSDate (ZOCTimeExtensions)
 - (NSString *)zoc_timeAgoShort;
@end
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

不推薦這樣寫:

@interface NSDate (ZOCTimeExtensions) 
- (NSString *)timeAgoShort;
@end
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

2. 把類的實現代碼分散到便於管理的多個分類中

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

舉個��:

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

#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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

分類之後:

#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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

其中,FriendShip分類的實現代碼可以這麼寫:

// 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

通過分類機制,可以把類代碼分成很多個易於管理的功能區,同時也便於調試。因爲分類的方法名稱會包含分類的名稱,可以馬上看到該方法屬於哪個分類中。

利用這一點,我們可以創建名爲Private的分類,將所有私有方法都放在該類裏。這樣一來,我們就可以根據private一詞的出現位置來判斷調用的合理性,這也是一種編寫“自我描述式代碼(self-documenting)”的辦法。

單例


1. 單例不能作爲容器對象來使用

單例對象不應該暴露出任何屬性,也就是說它不能作爲讓外部存放對象的容器。它應該是一個處理某些特定任務的工具,比如在ios中的GPS和加速度傳感器。我們只能從他們那裏得到一些特定的數據。

2. 使用dispatch_once來生成單例

推薦這樣寫:

+ (instancetype)sharedInstance { 
 static id sharedInstance = nil; 
 static dispatch_once_t onceToken = 0;
       dispatch_once(&onceToken, ^{ 
  sharedInstance = [[self alloc] init];
  }); 
 return sharedInstance; 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

不推薦這樣寫:

+ (instancetype)sharedInstance { 
 static id sharedInstance; 
 @synchronized(self) { 
 if (sharedInstance == nil) {  sharedInstance = [[MyClass alloc] init]; 
 } } 
 return sharedInstance; 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

相等性的判斷


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

-  (BOOL)isEqual:(id)object {
     if (self == object) {  return YES; //判斷內存地址
 } 
  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; 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

方法文檔


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

  • 非公開,私有函數。
  • 很短。
  • 顯而易見。

而其餘的,包括公開接口,重要的方法,分類,以及協議,都應該伴隨文檔(註釋):

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

建議這樣寫:

/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.
*/
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

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

/ 
  *  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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

多用隊列,少用同步鎖來避免資源搶奪


多個線程執行同一份代碼時,很可能會造成數據不同步。建議使用GCD來爲代碼加鎖的方式解決這個問題。

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

_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);
//讀取字符串
- (NSString*)someString {
         __block NSString *localSomeString;
         dispatch_sync(_syncQueue, ^{
            localSomeString = _someString;
        });
         return localSomeString;
}
//設置字符串
- (void)setSomeString:(NSString*)someString {
     dispatch_sync(_syncQueue, ^{
        _someString = someString;
    });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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

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

方案二:將寫操作放入柵欄快中,讓他們單獨執行;將讀取操作併發執行。

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//讀取字符串
- (NSString*)someString {
     __block NSString *localSomeString;
     dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
     return localSomeString;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

顯然,數據的正確性主要取決於寫入操作,那麼只要保證寫入時,線程是安全的,那麼即便讀取操作是併發的,也可以保證數據是同步的。 
這裏的dispatch_barrier_async方法使得操作放在了同步隊列裏“有序進行”,保證了寫入操作的任務是在串行隊列裏。

實現description方法打印自定義對象信息


在打印我們自己定義的類的實例對象時,在控制檯輸出的結果往往是這樣的:object = &lt;EOCPerson: 0x7fd9a1600600&gt;

這裏只包含了類名和內存地址,它的信息顯然是不具體的,遠達不到調試的要求。

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

例如:

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

在這裏,顯示了內存地址,還有該類的所有屬性。

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

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

    @{    @"title":_title,
       @"latitude":@(_latitude),
      @"longitude":@(_longitude)}
    ];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

輸出結果:

location = <EOCLocation: 0x7f98f2e01d20, {
    latitude = "51.506";
   longitude = 0;
       title = London;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

我們可以看到,通過重寫description方法可以讓我們更加了解對象的情況,便於後期的調試,節省開發時間。

NSArray& NSMutableArray


1. addObject之前要非空判斷。

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

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

NSCache


1. 構建緩存時選用NSCache 而非NSDictionary

如果我們緩存使用得當,那麼應用程序的響應速度就會提高。只有那種“重新計算起來很費事的數據,才值得放入緩存”,比如那些需要從網絡獲取或從磁盤讀取的數據。

在構建緩存的時候很多人習慣用NSDictionary或者NSMutableDictionary,但是作者建議大家使用NSCache,它作爲管理緩存的類,有很多特點要優於字典,因爲它本來就是爲了管理緩存而設計的。

2. NSCache優於NSDictionary的幾點:

  • 當系統資源將要耗盡時,NSCache具備自動刪減緩衝的功能。並且還會先刪減“最久未使用”的對象。
  • NSCache不拷貝鍵,而是保留鍵。因爲並不是所有的鍵都遵從拷貝協議(字典的鍵是必須要支持拷貝協議的,有侷限性)。
  • NSCache是線程安全的:不編寫加鎖代碼的前提下,多個線程可以同時訪問NSCache。

NSNotification


1. 通知的名稱

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

// Const.h
extern NSString * const ZOCFooDidBecomeBarNotification
// Const.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

2. 通知的移除

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

其他


1. Xcode工程文件的物理路徑要和邏輯路徑保持一致。

2. 忽略沒有使用變量的編譯警告

對於某些暫時不用,以後可能用到的臨時變量,爲了避免警告,我們可以使用如下方法將這個警告消除:

- (NSInteger)giveMeFive { 
 NSString *foo; 
 #pragma unused (foo) 
 return 5; 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

3. 手動標明警告和錯誤

手動明確一個錯誤:

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

手動明確一個警告:

- (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; 
 } 
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章