最近一直在為公司3.0的app加班加點,前段時間總算完成了,有空坐下來寫寫東西。既然是第一篇關於IOS開發的文章就先寫點自己這些年學到最最基本的經驗吧。一些編程中的小細節很容易被忽略,但是往往細節可以讓自己變得更專業。
主要是想說下Project的結構。由於Project裡的內容是否分組或者整潔,基本不會影響做出來的APP的效果,所以常常被忽視。其實不然,在很多工作項目中(特別是大型項目),我們都會和其他程序員或者設計師合作。而設立一個整潔的項目結構對合作開發非常有幫助,對後期Bug修復或者Code Review也都會事半功倍。Project的結構就猶如家庭裝潢的內部排線,排線的好壞通常是無法從房屋的外觀判斷的。但是一旦需要排查問題或者修改線路時,一個好的內部系統就能讓一切變得井井有條,也可以體現制作者的專業性。
就根據我個人的經驗判斷,好的Project 結構包括了合理的命名規則,Project內容分組,常用代碼的封裝,和使用有效的Comments。下面我們就一條一條的來看。
1. 第一是合理的命名規則。
其實這條還是比較抽象的,因為不一樣的項目可能會有一些不一樣的需求。但是我主要想說一些最最基本和幾乎通用的規則。
非常推薦使用Class prefix name就是Class的前綴名,使用前綴名能夠一定程度的避免你的Class和其他Framework / Library中的內容重名,也能夠讓你在Import或使用Class的時候比較方便尋找。通常都會推薦用幾個大寫字母來作為前綴名,Apple的iOS SDK裡全部都使用了Prefix,比如我們經常會用到的以NS為前綴的Foundation和Application Kit裡的Class,以UI開頭的UIKit,以CL開頭的Core Location 等等。
在命名Class的時候盡量保證它的名字能夠涵蓋他的類別和用途。比如你有一個UIViewController是用來顯示User Profile的, 那你就可以把它命名為在XX(前綴)UserProfile(用途)ViewController(類別)。這樣的話如果在一個比較大型的工程中,你或者你的同事只需要知道用途或者類別就可以很快的鎖定目標。而且我推薦把比如使用在XXUserProfileViewController內部的自定義的subview,都用XXUserProfile來前綴,其實依然是遵守上述的命名規則,XX(前綴)UserProfileAddress(用途)Cell(類別),這樣能夠在後面的文件分組裡面起到比較好的效果,而且可以很快查到所有和UserProfile相關的內容。
在命名Method的時候也應該選擇能包括這個Method功能,所需的參數類別和關系的名字,但是盡量簡潔。當然簡潔只是相對的,更重要的就是能夠讓其他程序員包括你自己在幾個月後甚至一年後一看就能大約明白這個Method的用途和條件。就連Apple提供的SDK裡面都有一些超長的Method,但是重要的是一看就大概知道這個Method能干什麼和需要什麼參數,比如從NSBitmapImageRep裡面可以找到的:
- (id)initWithBitmapDataPlanes:(unsigned char **)planes pixelsWide:(NSInteger)width pixelsHigh:(NSInteger)height bitsPerSample:(NSInteger)bps samplesPerPixel:(NSInteger)spp hasAlpha:(BOOL)alpha isPlanar:(BOOL)isPlanar colorSpaceName:(NSString *)colorSpaceName bitmapFormat:(NSBitmapFormat)bitmapFormat bytesPerRow:(NSInteger)rowBytes bitsPerPixel:(NSInteger)pixelBits;
Apple的程序員手冊裡面也對Method命名規則有不少的敘述,比如說盡量不要用縮寫,盡量用意義明確的詞,等。
在我們3.0的App中也有一些比較長的Method,但是我們盡量保持他的含義可以像讀短文一樣容易理解,比如:
+ (NSMutableArray *)animateNavBarFromColor:(UIColor *)fromColor toColor:(UIColor *)toColor duration:(NSTimeInterval)duration; + (NSMutableAttributedString *)generateAttributedStringFromString:(NSString *)string withFontName:(NSString *)fontName fontSize:(float)fontSize textAlignment:(NSTextAlignment)alignment lineBreakMode:(NSLineBreakMode)lineBreakMode;
這兩個是我們項目中得Public Method的名字,我覺得這樣的Method的名字還是比較可以接受的。哪怕對我們的項目完全不理解的人也應該可以明白這兩個Method的大概功能。而且一些常用的而且意義明確的名字就可以用簡短的方式來表達,比如說NavigationBar,一般稱作NavBar不會有任何的歧義,也遵守了簡潔的規則。
2. 接下來讓我介紹下,如何較好的將一個大型Project其中的內容分組。
其實不論Project的大小,我們都應該將View Controllers, Views, Objects, Frameworks, Resources,等等不同類型的文件,都按照他們的種類和具體用途進行了歸類,這樣在日後修改和添加內容時候都會同樣的事半功倍。因為大中型的Project中勢必使用了大量的View Controllers和自定義的Object Class和Custom View. 如果成百上千個文件無須的羅列在一個項目中,那種混亂程度是可以讓一個原本20分鐘的Bug fix延長到一個小時。當然很多時候,如果你知道部分文件名的話,Cmd+shift+O還是我們最好的朋友(所以合理的命名規則這塊是非常重要)。而且分組的重要性在你不是很確定文件名的時候,比如review你同事的code或者在一位同事離職或休假的時候有緊急的bug fix,明確的分組就好像一張好的地圖一樣,很容易就找到你的目標。
我相信文件分組還是被相對普遍運用的,但代碼的分組常常會被忽略。如同剛才所說的,你在一堆文件中總算找到你的的目標文件了。而有時候一個View Controller可能有幾百上千行的代碼,如果把代碼也按照類型和功能分組的話,這樣能進一步的提升效率,減少不必要的痛苦。?合理的使用#pragma mark - 是代碼分組最簡單而且最有效的方法,好像這張圖片裡的,
我們把代碼按照他們的基本類型和功效使用#pragma mark - 來區分,比如:
#pragma mark - View Controller Life Cycle #pragma mark - API Calls
然後每次你只要知道大概的目標代碼,就可以在導航欄裡點擊代碼這塊,很容易就能找到結果。
3. 常用代碼的封裝。
有些朋友可能不明白什麼時候是需要將一組代碼封裝到一個Method中,其實最簡單的就是當你復制粘貼的時候,你就應該問問自己:"是不是可以把這組代碼封裝到一個或者多個Methods中,以便重復使用。" 這樣主要能讓你的Project看上去清楚簡潔,而且一定意義上也節省了你項目的大小(雖然代碼不會占太大的空間)。就比如我們常用的stringWithFormat或者isEqualToString等等的Methods,在他們背後都是,一大塊被封裝在這個Method裡面的代碼。只要我們理解這個Interface Method的功能,背後的代碼已經不重要了。舉個很簡單的例子,在我們3.0的軟件中,我們幾乎吧所有的用戶頭像就變成圓形,也許大家都知道這個很簡單,只需要兩行代碼,
imageView.clipsToBounds = YES; imageView.layer.cornerRadius = image.frame.size.width/2;
而基於有輕度代碼潔癖的我,只要是超過一行,而且被反復被使用的代碼就應該封裝到一個Method中,所有就變成了這樣
+ (void)roundImage:(UIImageView *)imageView { imageView.clipsToBounds = YES; imageView.layer.cornerRadius = imageView.frame.size.width/2; }
讓我們來看一個比較復雜的例子,在我們的項目中,反復使用了Attributed String, 主要是我們有大量的用戶Review, 酒店餐廳景點的簡介,我們想控制String的行間距,否則會看起來太擁擠。於是我們把所需要用到的代碼都分裝在一個Method中:
+ (NSMutableAttributedString *)generateAttributedStringFromString:(NSString *)string withFontName:(NSString *)fontName fontSize:(float)fontSize textAlignment:(NSTextAlignment)alignment lineBreakMode:(NSLineBreakMode)lineBreakMode { NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:string]; NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; style.lineSpacing = fontSize / 2; style.alignment = alignment; style.lineBreakMode = lineBreakMode; [mutableAttributedString addAttribute:(NSString*)kCTParagraphStyleAttributeName value:style range:NSMakeRange(0, [mutableAttributedString length])]; [mutableAttributedString addAttribute:NSFontAttributeName value:[UIFont fontWithName:fontName size:fontSize] range:NSMakeRange(0, [mutableAttributedString length])]; return mutableAttributedString; }
這樣一來,一個需要7,8行來實現的功能,簡單的封裝就可以縮減變成1,2行。我簡單的在我們的項目裡所搜了一下,我們一共有42處Call了這個Method,粗略一算我們至少節約了230多行的代碼。
4. 最後就是Comments(注釋)的重要性。
其實不同的程序員對Comments有著不同的理解,有些人認為Comments是基本沒有意義的,因為他們覺得好的Code應該是很容易被理解的,包括其作用,類別等等都應該一目了然。這個論調確實也有一定的道理,但我不能說100%贊同。我認為Comments的作用不是讓其他程序員甚至你自己理解你這段代碼的作用究竟是什麼,而更多的應該把這段代碼的邏輯描述出來。除非是非常簡單的一兩行的代碼,否則都應該有當時的編寫的邏輯。這些邏輯在過了幾個月甚至一年,哪怕你是編寫者,你回頭去看,都會需要一段時間才能想起來究竟當時基於什麼原因選擇了這樣做,更不要說其他合作的同事。比如說這段簡單關於某些部分Show,某些Hide的代碼:
if (myPlaceView.currentTripPlan.isTemporary) { myPlaceView.myPlacesCountLabel.hidden = YES; myPlaceView.activityIndicator.hidden = NO; [myPlaceView.activityIndicator startAnimating]; } else { myPlaceView.activityIndicator.hidden = YES; [myPlaceView.activityIndicator stopAnimating]; myPlaceView.myPlacesCountLabel.hidden = NO; myPlaceView.myPlacesCountLabel.text = [myPlaceView.currentTripPlan.trip.placesCount stringValue]; }
如果直接讀的話,很難明白為什麼Trip plan是temporary就需要吧PlaceCountLabel隱藏和等等東西。如果加上這些注釋:
//Since when user creates a new trip plan, we will save a temporary local copy of it //so when they come back to splashboard, they will see the new trip plan before the api responses if (myPlaceView.currentTripPlan.isTemporary) { //in this case, our api is not responded yet //we need to show the loader, and hide the place count myPlaceView.myPlacesCountLabel.hidden = YES; myPlaceView.activityIndicator.hidden = NO; [myPlaceView.activityIndicator startAnimating]; } else { //if current trip plan is not temporary, that means we get the data from the api myPlaceView.activityIndicator.hidden = YES; [myPlaceView.activityIndicator stopAnimating]; myPlaceView.myPlacesCountLabel.hidden = NO; myPlaceView.myPlacesCountLabel.text = [myPlaceView.currentTripPlan.trip.placesCount stringValue]; }
看了Comments之後其實是很容易理解的,Temporary是一個新建立的trip plan所擁有的Flag,而Temporary是一個Local Copy而不是從數據庫讀取到的。當我們獲得了數據庫數據後,將把所有的信息都Update並且顯示。
再比如你的代碼中包含了數學計算或者公式等,和一些Hard coded的數字,添加一些注釋也是非常有幫助的,否則當你過了一段時間回頭來看時會被那些毫無來由的數字弄的暈頭轉向。