歡迎光臨
每天分享高質量文章

看完這個你們團隊的程式碼也很規範

來自:知識小集(ID:iOS-Tips)

最近重構專案元件,看到專案中存在一些命名和方法分塊方面存在一些問題,結合平時經驗和 Apple官方程式碼規範 在此整理出 iOS 工程規範。提出第一個版本,如果後期覺得有不完善的地方,繼續提出來不斷完善,檔案在此記錄的目的就是為了大家的程式碼可讀性較好,後來的人或者團隊裡面的其他人看到程式碼可以不會因為程式碼風格和可讀性上面造成較大時間的開銷。

軟體的生命週期貫穿產品的開發,測試,生產,使用者使用,版本升級和後期維護等過程,只有易讀,易維護的軟體程式碼才具有生命力。

一些原則

1) 長的,描述性的方法和變數命名是好的。不要使用簡寫,除非是一些大家都知道的場景比如 VIP。不要使用 bgView,推薦使用 backgroundView
2) 見名知意。含義清楚,做好不加註釋程式碼自我表述能力強。(前提是程式碼足夠規範)
3) 不要過分追求技巧,降低程式碼可讀性 
4) 刪除沒必要的程式碼。比如我們新建一個控制器,裡面會有一些不會用到的程式碼,或者註釋起來的程式碼,如果這些程式碼不需要,那就刪除它,留著偷懶嗎?下次需要自己手寫
5) 在方法內部不要重覆計算某個值,適當的情況下可以將計算結果快取起來
6) 儘量減少單例的使用。
7) 提供一個統一的資料管理入口,不管是 MVC、MVVM、MVP 模組內提供一個統一的資料管理入口會使得程式碼變得更容易管理和維護。
8) 除了 .m 檔案中方法,其他的地方”{“不需要另起一行。

– (void)getGooodsList
{
    // …
}

– (void)doHomework
{
    if (self.hungry) {
        return;
    }
    if (self.thirsty) {
        return;
    }
    if (self.tired) {
        return;
    }
    papapa.then.over;
}


變數

1) 一個變數最好只有一個作用,切勿為了節省程式碼行數,覺得一個變數可以做多個用途。(單一原則)
2) 方法內部如果有區域性變數,那麼區域性變數應該靠近在使用的地方,而不是全部在頂部宣告全部的區域性變數。

運運算元

1) 1元運運算元和變數之間不需要空格。例如:++n
2) 2元運運算元與變數之間需要空格隔開。例如: containerWidth = 0.3 * Screen_Width
3) 當有多個運運算元的時候需要使用括號來明確正確的順序,可讀性較好。例如: 2 << (1 + 2 * 3 – 4)

條件運算式

1) 當有條件過多、過長的時候需要換行,為了程式碼看起來整齊些

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


2) 在一個程式碼塊裡面有個可能的情況時善於使用 return 來結束異常的情況。

– (void)doHomework
{
    if (self.hungry) {
        return;
    }
    if (self.thirsty) {
        return;
    }
    if (self.tired) {
        return;
    }
    papapa.then.over;
}


3) 每個分支的實現都必須使用 {} 包含。

// bad
if (self.hungry) self.eat() 
// good
if (self.hungry) {
    self.eat()
}


4) 條件判斷的時候應該是變數在左,條件在右。 if ( currentCursor == 2 ) { //… }
5) switch 陳述句後面的每個分支都需要用大括號括起來。
6) switch 陳述句後面的 default 分支必須存在,除非是在對列舉進行 switch。

switch (menuType) {  
  case menuTypeLeft: {
    // …  
    break
   }
  case menuTypeRight: {
    // …  
    break
  }
  case menuTypeTop: {
    // …  
    break
  }
  case menuTypeBottom: {
    // …  
    break
  }
}


類名

1) 大寫駝峰式命名。每個單詞首字母大寫。比如「申請記錄控制器」ApplyRecordsViewController
2) 每個型別的命名以該型別結尾。

  • ViewController:使用 ViewController 結尾。例子:ApplyRecordsViewController

  • View:使用 View 結尾。例子:分界線:boundaryView

  • NSArray:使用 s 結尾。比如商品分類資料源。categories

  • UITableViewCell:使用 Cell 結尾。比如 MyProfileCell

  • Protocol:使用 Delegate 或者 Datasource 結尾。比如 XQScanViewDelegate

  • Tool:工具類

  • 代理類:Delegate

  • Service 類:Service

類的註釋

有時候我們需要為我們建立的類設定一些註釋。我們可以在類的下麵新增。

列舉

列舉的命名和類的命名相近。

typedef NS_ENUM(NSIntegerUIControlContentVerticalAlignment) {
    UIControlContentVerticalAlignmentCenter  = 0,
    UIControlContentVerticalAlignmentTop     = 1,
    UIControlContentVerticalAlignmentBottom  = 2,
    UIControlContentVerticalAlignmentFill    = 3,
};


1) 全部大寫,單詞與單詞之間用 _ 連線。
2) 以 
K 開頭。後面遵循大寫駝峰命名。「不帶引數」

#define HOME_PAGE_DID_SCROLL @“com.xq.home.page.tableview.did.scroll”
#define KHomePageDidScroll @“com.xq.home.page.tableview.did.scroll”


屬性

書寫規則,基本上就是 @property 之後空一格,括號,裡面的 執行緒修飾詞、記憶體修飾詞、讀寫修飾詞,空一格 類 物件名稱

根據不同的場景選擇合適的修飾符。

@property (nonatomicstrongUITableView *tableView;
@property (nonatomicassignreadonlyBOOL loading;   
@property (nonatomicweakid<#delegate#> delegate;
@property (nonatomiccopy) <#returnType#> (^)();


單例

單例適合全域性管理狀態或者事件的場景。一旦建立,物件的指標儲存在靜態區,單例物件在堆記憶體中分配的記憶體空間只有程式銷毀的時候才會釋放。基於這種特點,那麼我們類似 UIApplication 物件,需要全域性訪問唯一一個物件的情況才適合單例,或者訪問頻次較高的情況。我們的功能模組的生命週期肯定小於 App 的生命週期,如果多個單例物件的話,勢必 App 的開銷會很大,糟糕的情況系統會殺死 App。如果覺得非要用單例比較好,那麼註意需要在合適的場合 tearDown 掉。

單例的使用場景概括如下:

  • 控制資源的使用,透過執行緒同步來控制資源的併發訪問。

  • 控制實體的產生,以達到節約資源的目的。

  • 控制資料的共享,在不建立直接關聯的條件下,讓多個不相關的行程或執行緒之間實現通訊。

+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken;, ^{
        //because has rewrited allocWithZone  use NULL avoid endless loop lol.
        _sharedInstance = [[super allocWithZone:NULL] init];
    });

    return _sharedInstance;
}

+ (id)allocWithZone:(struct _NSZone *)zone
{
    return [TestNSObject sharedInstance];

}

+ (instancetype)alloc
{
    return [TestNSObject sharedInstance];
}

– (id)copy
{
    return self;
}

– (id)mutableCopy
{
    return self;
}

– (id)copyWithZone:(struct _NSZone *)zone
{
    return self;
}


私有變數

推薦以 _ 開頭,寫在 .m 檔案中。例如 NSString * _somePrivateVariable

代理方法

1) 類的實體必須作為方法的引數之一。
2) 對於一些連續的狀態的,可以加一些 will(將要)、did(已經)
3) 以類的名稱開頭

– (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;

– (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;


方法

1) 方法與方法之間間隔一行
2) 大量的方法儘量要以組的形式放在一起,比如生命週期函式、公有方法、私有方法、setter && getter、代理方法..
3) 方法最後面的括號需要另起一行。遵循 Apple 的規範
4) 對於其他場景的括號,括號不需要單獨換行。比如 if 後面的括號。
5) 如果方法引數過多過長,建議多行書寫。用冒號進行對齊。
6) 一個方法內的程式碼最好保持在50行以內,一般經驗來看如果一個方法裡面的程式碼行數過多,程式碼的閱讀體驗就很差(別問為什麼,做過重構程式碼行數很長的人都有類似的心情)
7) 一個函式只做一個事情,做到單一原則。所有的類、方法設計好後就可以類似搭積木一樣實現一個系統。
8) 對於有傳回值的函式,且函式內有分支情況。確保每個分支都有傳回值。
9) 函式如果有多個引數,外部傳入的引數需要檢驗引數的非空、資料型別的合法性,引數錯誤做一些措施:立即傳回、斷言。
10) 多個函式如果有邏輯重覆的程式碼,建議將重覆的部分抽取出來,成為獨立的函式進行呼叫

– (instancetype)init
{
    self = [super init];
    if (self) {
        <#statements#>
    }
    return self;
}

– (void)doHomework:(NSString *)name
            period:(NSInteger)second
            score:(NSInteger)score;


11) 方法如果有多個引數的情況下需要註意是否需要介詞和連詞。很多時候在不知道如何抉擇測時候思考下蘋果的一些 API 的方法命名。

//good
– (instancetype)initWithAge:(NSInteger)age name:(NSString *)name;

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


//bad
– (instancetype)initWithAge:(NSInteger)age andName:(NSString *)name;

– (void)tableView:(UITableView *)tableView 🙁NSIndexPath *)indexPath;


12) .m 檔案中的私有方法需要在頂部進行宣告
13) 方法組之間也有個順序問題。

  • 在檔案最頂部實現屬性的宣告、私有方法的宣告(很多人省去這一步,問題不大,但是蠻多第三方的庫都寫了,看起來還是會很方便,建議書寫)。

  • 在生命週期的方法裡面,比如 viewDidLoad 裡面只做介面的新增,而不是做介面的初始化,所有的 view 初始化建議放在 getter 裡面去做。往往 view 的初始化的程式碼長度會比較長、且一般會有多個 view 所以 getter 和 setter 一般建議放在最下麵,這樣子頂部就可以很清楚的看到程式碼的主要邏輯。

  • 所有button、gestureRecognizer 的響應事件都放在這個區域裡面,不要到處亂放。

檔案基本上就是 

#import “ViewController.h”
/*ViewController*/

/*View&&Util;*/

/*model*/

/*NetWork InterFace*/

/*Vender*/

@interface ViewController ()

@end

@implementation ViewController

#pragma mark – life cycle
– (void)viewWillAppear:(BOOL)animated{
    [super viewDidAppear:animated];
}

– (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

}

– (void)viewDidLoad
{
    [super viewDidLoad];
    self.title = @”標準模版”;
}

– (void)viewWillDisappear:(BOOL)animated
{
    [super viewDidAppear:animated];

}

– (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidAppear:animated];

}

– (void)dealloc
{
    NSLog(@”%s”,__func__);
}

#pragma mark – public Method

#pragma mark – private method

#pragma mark – event response

#pragma mark – UITableViewDelegate

#pragma mark – UITableViewDataSource
//…(多個代理方法依次往下寫)

#pragma mark – getters and setters

@end


圖片資源

1) 單個檔案的命名

檔案資源的命名也需要一定的規範,形式為:功能模組名類別功能_狀態@nx.png,如

Setting_Button_search_selected@2x.png
Setting_Button_search_selected@3x.png
Setting_Button_search_unselected@2x.png
Setting_Button_search_unselected@3x.png


2) 資源的檔案夾命名

最好也參考 App 按照功能模組建立對應的物體檔案夾目錄,最後到對應的目錄下新增相應的資源檔案。

註釋

1) 對於類的註釋寫在當前類檔案的頂部
2) 對於屬性的註釋需要寫在屬性後面的地方。 // userId
3) 對於 .h 檔案中方法的註釋,一律按快捷鍵 
command+option+/。三個快捷鍵解決。按需在旁邊對方法進行說明解釋、傳回值、引數的說明和解釋
4) 對於 .m 檔案中的方法的註釋,在方法的旁邊新增 
//
5) 註釋符和註釋內容需要間隔一個空格。 例如: // fetch goods list

版本規範

採用 A.B.C 三位數字命名,比如:1.0.2,當有更新的情況下按照下麵的依據

版本號

右說明對齊標題

示例

A.b.c

屬於重大內容的更新

1.0.2 -> 2.0.0

a.B.c

屬於小部分內容的更新

1.0.2 -> 1.1.1

a.b.C

屬於補丁更新

1.0.2 -> 1.0.3

改進

我們知道了平時在使用 Xcode 開發的過程中使用的系統提供的程式碼塊所在的地址和新建控制器、模型、view等的檔案模版的存放檔案夾地址後,我們就可以設想下我們是否可以定製自己團隊風格的控制器模版、是否可以打造和維護自己團隊的高頻使用的程式碼塊?

答案是可以的。

Xcode 程式碼塊的存放地址:~/Library/Developer/Xcode/UserData/CodeSnippets
Xcode 檔案模版的存放地址:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/

程式碼塊的改造

我們可以將屬性、控制器生命週期方法、單例構造一個物件的方法、代理方法、block、GCD、UITableView 懶載入、UITableViewCell 註冊、UITableView 代理方法的實現、UICollectionVIew 懶載入、UICollectionVIewCell 註冊、UICollectionView 的代理方法實現等等組織為 codesnippets

思考

  • 封裝好 codesnippets 之後團隊除了你編寫這個專案的人如何使用?如何知道是否有這個程式碼塊?

    方案:先在團隊內召開程式碼規範會議,大家都統一知道這個事情在。之後大家共同維護 codesnippets。用法見下

    屬性:透過 Property_型別 開頭,回車鍵自動補全。比如 Strong 型別,編寫程式碼透過 Property_Strong 回車鍵自動補全成如下格式

@property (nonatomicstrong) <#Class#> *;


方法:以 Method_關鍵詞 回車鍵確認,自動補全。比如 Method_UIScrollViewDelegate 回車鍵自動補全成 如下格式

#pragma mark – UIScrollViewDelegate
– (void)scrollViewDidScroll:(UIScrollView *)scrollView {

}


各種常見的 Mark:以 Mark_關鍵詞 回車確認,自動補全。比如 Method_MethodsGroup 回車鍵自動補全成 如下格式

#pragma mark – life cycle
#pragma mark – public Method
#pragma mark – private method
#pragma mark – event response
#pragma mark – UITableViewDelegate
#pragma mark – UITableViewDataSource
#pragma mark – getters and setters


  • 封裝好 codesnippets 之後團隊內如何統一?想到一個方案,可以將團隊內的 codesnippets 共享到 git,團隊內的其他成員再從雲端拉取同步。這樣的話團隊內的每個成員都可以使用最新的 codesnippets 來編碼。

    編寫 shell 指令碼。幾個關鍵步驟:

    1) 給系統檔案夾授權
    2) 在指令碼所在檔案夾新建存放程式碼塊的檔案夾
    3) 將系統檔案夾下麵的程式碼塊複製到步驟2建立的檔案夾下麵
    4) 將當前的所有檔案提交到 Git 倉庫

檔案模版的改造

我們觀察系統檔案模版的特點,和在 Xcode 新建檔案模版對應。

 

Xcode file template存放地址

 

所以我們新建 Custom 檔案夾,將系統 Source 檔案夾下麵的 Cocoa Touch Class.xctemplate 複製到 Custom 檔案夾下。重新命名為我們需要的名字,我這裡以“Power”為例

 

自定義檔案模版示例

 

進入 PowerViewController.xctemplate/PowerViewControllerObjective-C

修改 ___FILEBASENAME___.h 和 ___FILEBASENAME___.m 檔案內容

 

註意點1

 

在替換 .h 檔案內容的時候後面改為 UIViewController,不然其他開發者新建檔案模版的時候出現的不是 UIViewController 而是我們的 PowerViewController 

 

.m檔案內容

修改 TemplateInfo.plist

plist註意點

思考:

  • 如何使用

    商量好一個標識(“Power”)。比如我新建了單例、控制器、Model、UIView4個模版,都以為 Power 開頭。

    模版用法

  • 如何共享

    以 shell 指令碼為工具。使用指令碼將 git 雲端的程式碼模版同步到本地 Xcode 檔案夾對應的位置就可以使用了。關鍵步驟:

  1. git clone 程式碼到指令碼所在檔案夾

  2. 進入存放 codesnippets 的檔案夾將內容複製到系統存放 codesnippets 的地方

  3. 進入存放 file template 的檔案夾將內容複製到系統存放 file template 的地方

使用

./syncSnippets.sh // 同步git雲端程式碼塊和檔案模版到本地
./uploadMySnippets.sh //將本地的程式碼塊和檔案模版同步到雲端


PS

目前新建了大概30個程式碼段和4個類檔案模版(UIViewController控制器帶有方法組、模型、執行緒安全的單例模版、帶有佈局方法的UIView模版)

shell 指令碼基本有每個函式和關鍵步驟的程式碼註釋,想學習 shell 的人可以看看程式碼。程式碼傳送門(https://github.com/FantasticLBP/codesnippets) 

    已同步到看一看
    贊(0)

    分享創造快樂