1. 程式人生 > >iOS開發-一個例子學習iOS中的常見設計模式

iOS開發-一個例子學習iOS中的常見設計模式

原文iOS Design Patterns
iOS設計模式 ,你可能已經聽說過這個術語,但你知道這意味著什麼嗎?雖然大多數的開發人員認為設計模式是非常重要的,但目前關於這個問題的文章不是很多,我們的開發人員有時寫程式碼有時不注重設計模式。
設計模式是軟體設計常見問題的可重用的解決方案。它們幫助您編寫容易理解和可重用的模組。他們還幫助您建立鬆散耦合的程式碼,這個可以方便的在你的程式碼中更改或替換元件。
如果你剛接觸設計模式,那麼我有一個好訊息告訴你!首先,你在建立iOS工程中Cocoa框架已經使用了大量的iOS設計模式,很容易理解和重用。其次,本教程將帶您快速瞭解Cocoa框架中所有重要的(和不那麼重要)的iOS的設計模式。

本教程分為幾個部分,每個設計模式一個部分。在每一節,你會讀到以下的內容:

  • 設計模式是什麼。
  • 為什麼要使用它。
  • 如何使用它,並在適當情況下,使用該模式常見的陷阱需要提防。

在本教程中,您將建立一個音樂庫的應用程式,可以顯示您的相簿及相關資訊。
在開發這個應用程式的過程中,你會逐漸熟悉最常見的Cocoa設計模式:

  • 物件建立型別:單例模式、抽象工廠模式。
  • 結構型別:MVC、裝飾者模式、介面卡模式、門面模式、合成模式。
  • 行為型別:觀察者模式、備忘錄模式、責任鏈模式、命令模式。

不要誤以為這是關於理論的文章,在你的音樂庫應用中,會使用大多數設計模式,你的應用在結束時是這樣子的。
最終效果

開始

下載這個專案,解壓,用Xcode開啟BlueLibrary.xcodeproj

這裡沒有很多東西,僅僅有一個預設的ViewController和一個簡單的沒有具體實現的HTTP客戶端類。

你知道嗎,只要你建立新的Xcode專案,你的程式碼已經實現了設計模式的。MVC,委託協議,單例 !

在你學習第一種設計模式,必須建立兩個類來儲存並顯示專輯資料。
導航至File->New->File(或者簡單的 Command+N),選擇 iOS > Cocoa Touch 然後選擇Objective-C class點選下一步,選擇Album作為類名,父類是NSObject

,選擇下一步建立。

開啟Album.h,在@interface@end中間新增屬性和方法。

@property (nonatomic, copy, readonly) NSString *title, *artist, *genre, *coverUrl, *year;

- (id)initWithTitle:(NSString*)title artist:(NSString*)artist coverUrl:(NSString*)coverUrl year:(NSString*)year;

現在所有的屬性都是隻讀的,一旦建立Album例項就沒有必要改變。
這個方法是物件初始化函式。當你建立一個新的Albun例項時,你要傳遞title、artist、coverUrl、year屬性。
開啟Album.m,在@implementation和 @end中間新增下列程式碼:

- (id)initWithTitle:(NSString*)title artist:(NSString*)artist coverUrl:(NSString*)coverUrl year:(NSString*)year
{
    self = [super init];
    if (self)
    {
        _title = title;
        _artist = artist;
        _coverUrl = coverUrl;
        _year = year;
        _genre = @"Pop";
    }
    return self;
}

這很無趣,僅僅是一個建立Album例項的初始化方法。
再次,導航至File->New->File(或者簡單的 Command+N),選擇 iOS > Cocoa Touch 然後選擇Objective-C class點選下一步,選擇AlbumView作為類名,父類是UIView,選擇下一步建立。

常見鍵盤快捷鍵
建立新檔案:Command+N
建立一個組:Command+Option+N
編譯: Command+B
執行:Command+R

開啟AlbumView.h,在@interface@end中間新增方法。

- (id)initWithFrame:(CGRect)frame albumCover:(NSString*)albumCover;

開啟AlbumView.m,在@implementation後面新增下列程式碼:

@implementation AlbumView
{
    UIImageView *coverImage;
    UIActivityIndicatorView *indicator;
}

- (id)initWithFrame:(CGRect)frame albumCover:(NSString*)albumCover
{
    self = [super initWithFrame:frame];
    if (self)
    {
        self.backgroundColor = [UIColor blackColor];
        // the coverImage has a 5 pixels margin from its frame
        coverImage = [[UIImageView alloc] initWithFrame:CGRectMake(5, 5, frame.size.width-10, frame.size.height-10)];
        [self addSubview:coverImage];

        indicator = [[UIActivityIndicatorView alloc] init];
        indicator.center = self.center;
        indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
        [indicator startAnimating];
        [self addSubview:indicator];
    }
    return self;
}
@end

你在這裡首先注意到的是,有一個名為coverImage例項變數。此變數代表專輯封面圖片。第二個變數是indicator,代表正在下載的菊花。

在實現檔案中,初始背景顏色為黑色。
在你設定的背景為黑色初始化的實施,建立並新增影象和菊花。

注:想知道為什麼私有變數在實現檔案中定義,而不是在介面檔案?這是因為AlbumView類外沒有類需要知道這些變數的存在,因為他們只用在類的內部功能的實現。如果你正在建立一個庫或框架,其他開發者使用這種約定是非常重要的。

Command+B 編譯專案,一切正常,學習第一個設計模式。

MVC-設計模式之王

MVC
模型 - 檢視 - 控制器(Model View Controller)是Cocoa框架的基石之一,無疑是所有設計模式最常用的。它在你的應用中根據物件的作用進行分類,也鼓勵基於型別角色的程式碼解耦。

  • Model:儲存您的應用程式資料和定義瞭如何操縱它的物件。例如,在應用程式中的模型是你的相簿Album類。
  • View:是負責模型資料的視覺化和使用者可以互動控制元件的物件,包括了所有的UIViews及其子類。在應用程式中View是AlbumView類。
  • Controller:控制器是協調所有工作的中樞。它從模型中獲得資料並操縱它、用View展示、監聽事件。在應用程式中是ViewController類。

一個很好的實現在應用這種設計模式意味著每個物件屬於其中一個組。控制器到模型檢視之間的通訊可以用下面圖來最好的描述:
MVC圖

模型通知控制器更改資料,反過來,控制器更新檢視中的資料。然後檢視可以通知控制器使用者執行的操作,並且控制器將在必要時更新模型或檢索任何請求資料。

你可能想知道為什麼你不能拋棄控制器,並實現檢視和模型在同一個類,因為這似乎更容易。

這一切都歸結於程式碼分離和可重用性。理想情況下,檢視應與模型完全分離。如果檢視不依賴於模型的特定實現,那麼它可以被重用於不同的模型以呈現一些其他資料。

例如,如果將來您還想要將電影或書籍新增到您的圖書館,您仍然可以使用相同的AlbumView來顯示您的電影和書籍物件。此外,如果你想建立一個與專輯有關的新專案,你可以簡單地重用你的Album類,因為它不依賴於任何檢視。這是MVC的力量!

怎樣使用MVC模式

首先,需要確保專案中的每個類都是Controller,Model或View; 不要在一個類中組合兩個角色的功能。 通過建立一個Album類和一個AlbumView類,你已經做了很好的工作。
其次,為了確保您符合這種工作方法,您應該建立三個資料夾來儲存您的程式碼,每個類別一個。
導航到File\New\Group(或按Command + Option + N),並命名組模型。 重複相同的過程以建立檢視和控制器組。

現在將Album.h和Album.m拖動到模型組。 將AlbumView.h和AlbumView.m拖動到View組,最後將ViewController.h和ViewController.m拖動到Controller組。

此時專案結構應如下所示:
專案結構圖

你的專案已經看起來好多了,沒有所有這些雜亂的檔案。 顯然你可以有其他組和類,但應用程式的核心包含在這三個資料夾。

現在你的元件是有組織的,你需要從某個地方獲取專輯資料。 您將建立一個API類,以在您的程式碼中使用來管理資料 - 這提供了一個機會來討論您的下一個設計模式 - 單例模式。

單例模式

單例模式確保對於給定類只存在一個例項,並且存在到該例項的全域性訪問點。 當第一次需要時,它通常使用延遲載入來建立單個例項。

蘋果使用這種方法很多。 例如:[NSUserDefaults standardUserDefaults],[UIApplication sharedApplication],[UIScreen mainScreen],[NSFileManager defaultManager]全部返回一個單例物件。

你可能想知道為什麼你關心一個類的超過多個例項的情況,因為感覺程式碼和記憶體很便宜,對吧?

在某些情況下,只有一個類的一個例項是有意義的。 例如,不需要有多個Logger例項,除非您想要一次寫入多個日誌檔案。 或者,使用全域性配置處理程式類:更容易實現對單個共享資源(例如配置檔案)的執行緒安全訪問,而不是允許許多類同時修改配置檔案。

怎樣使用單例模式

看下圖:
單例模式

上圖顯示了一個具有單個屬性(它是單個例項)的Logger類,以及兩個方法:sharedInstance和init。
第一次傳送sharedInstance訊息時,屬性例項尚未初始化,因此您建立一個類的新例項並返回一個引用。

下一次呼叫sharedInstance時,立即返回例項而不進行任何初始化。 這個邏輯承諾只有一個例項總是存在。

您將通過建立一個單例類來管理所有相簿資料來實現此模式。

你會注意到在專案中有一個名為API的組; 這裡是你將所有的類,將提供服務到你的應用程式。 在這個組裡面建立一個新的類與iOS \ Cocoa Touch \ Objective-C類模板。 將類命名為LibraryAPI,並將其作為NSObject的子類。

開啟LibraryAPI.h並將其內容替換為以下內容:

@interface LibraryAPI : NSObject

+ (LibraryAPI*)sharedInstance;

@end

現在轉到Library API.m並在@implentation行後面插入此方法:

+ (LibraryAPI*)sharedInstance
{
    // 1
    static LibraryAPI *_sharedInstance = nil;

    // 2
    static dispatch_once_t oncePredicate;

    // 3
    dispatch_once(&oncePredicate, ^{
        _sharedInstance = [[LibraryAPI alloc] init];
    });
    return _sharedInstance;
}

在這個簡短的方法有很多事情:

  1. 宣告一個靜態變數來儲存你的類的例項,確保它可以在類中全域性使用。
  2. 宣告靜態變數dispatch_once_t,它確保初始化程式碼只執行一次。
  3. 使用Grand Central Dispatch(GCD)執行初始化LibraryAPI例項的塊。 這是單例設計模式的本質:初始化器從來不會在類被例項化時再次呼叫。

下一次呼叫sharedInstance時,dispatch_once塊中的程式碼將不會被執行(因為它已經執行過一次),並且您接收到之前建立的LibraryAPI例項的引用。

您現在有一個單例物件作為管理專輯的入口點。 再進一步,建立一個類來處理你的永續性資料。
使用iOS \ Cocoa Touch \ Objective-C類模板在API組中建立一個新類。 將類命名為PersistencyManager,並將其作為NSObject的子類。

開啟PersistencyManager.h。 將以下匯入新增到檔案頂部:

#import "Album.h"

接下來,在@interface行後面新增以下程式碼到PersistenceManager.h:

- (NSArray*)getAlbums;
- (void)addAlbum:(Album*)album atIndex:(int)index;
- (void)deleteAlbumAtIndex:(int)index;

以上是處理相簿資料所需的三種方法的介面。
開啟PersistenceManager.m並在@implementation行正上方新增以下程式碼:

@interface PersistencyManager () {
    // an array of all albums
    NSMutableArray *albums;
}
@end

上面添加了一個類擴充套件,這是另一種方式來新增私有方法和變數到類,以便外部類不會知道它們。 在這裡,你宣告一個NSMutableArray來儲存相簿資料。 這個陣列是可變的,以便您可以輕鬆新增和刪除相簿。

現在在@implementation行後面新增以下程式碼實現到PersistencyManager.m:

- (id)init
{
    self = [super init];
    if (self) {
        // a dummy list of albums
        albums = [NSMutableArray arrayWithArray:
                 @[[[Album alloc] initWithTitle:@"Best of Bowie" artist:@"David Bowie" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png" year:@"1992"],
                 [[Album alloc] initWithTitle:@"It's My Life" artist:@"No Doubt" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png" year:@"2003"],
                 [[Album alloc] initWithTitle:@"Nothing Like The Sun" artist:@"Sting" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png" year:@"1999"],
                 [[Album alloc] initWithTitle:@"Staring at the Sun" artist:@"U2" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png" year:@"2000"],
                 [[Album alloc] initWithTitle:@"American Pie" artist:@"Madonna" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png" year:@"2000"]]];
    }
    return self;
}

在init中,用五個示例專輯填充陣列。 如果上述專輯不符合您的喜好,請隨意用您喜歡的音樂取代。
現在將以下三個方法新增到PersistencyManager.m:

- (NSArray*)getAlbums
{
    return albums;
}

- (void)addAlbum:(Album*)album atIndex:(int)index
{
    if (albums.count >= index)
        [albums insertObject:album atIndex:index];
    else
        [albums addObject:album];
}

- (void)deleteAlbumAtIndex:(int)index
{
    [albums removeObjectAtIndex:index];
}

這些方法允許您獲取,新增和刪除相簿。

構建你的專案只是為了確保一切仍然正確編譯。

在這一點上,你可能想知道PersistencyManager類在哪裡,因為它不是一個單例。 LibraryAPI和PersistencyManager之間的關係將在下一節中進一步探討,您將在其中檢視門面設計模式。

門面設計模式

門面設計模式

門面設計模式為複雜子系統提供單一介面。 不是將使用者暴露給一組類及其API,而是隻展示一個簡單的統一API。

下圖說明了這個概念:
[門面設計模式

API的使用者完全不知道下面的複雜性。 這種模式是理想的,當使用大量的類,特別是當它們使用複雜或難以理解。

門面設計模式將使用系統的程式碼與您隱藏的類的介面和實現分離; 它還減少了外部程式碼對子系統內部工作的依賴性。 這對底層類的更改也是有用的,因為當類在後臺改變時,門面上層類可以保留相同的API。
例如,如果您想要替換後端服務,那麼您不必更改使用API的程式碼,因為它不會更改。

怎樣使用門面模式

目前您有PersistencyManager在本地儲存相簿資料和HTTPClient來處理遠端通訊。 你專案中的其他類不應該意識到這個邏輯。

要實現此模式,只有LibraryAPI應該持有PersistencyManager和HTTPClient的例項。 然後,LibraryAPI將暴露一個簡單的API來訪問這些服務。

注意:通常,應用程式的生命週期中存在單例。 你不應該將單例中的太多強指標儲存到其他物件,因為它們在應用程式關閉之前不會被釋放。

門面模式

LibraryAPI將暴露給其他程式碼,但會從應用程式的其餘部分隱藏HTTPClient和PersistencyManager複雜性。

開啟LibraryAPI.h並將以下匯入新增到檔案的頂部:

#import "Album.h"

接下來,將以下方法定義新增到Library API.h:

- (NSArray*)getAlbums;
- (void)addAlbum:(Album*)album atIndex:(int)index;
- (void)deleteAlbumAtIndex:(int)index;

現在,這些是你將暴露給其他類的方法。轉到Library API.m並新增以下兩個匯入:

#import "PersistencyManager.h"
#import "HTTPClient.h"

這將是您匯入這些類的唯一的地方。 記住:您的API將是您的“複雜”系統的唯一訪問點。

現在,通過類擴充套件(在@implementation行上面)新增一些私有變數:

@interface LibraryAPI () {
    PersistencyManager *persistencyManager;
    HTTPClient *httpClient;
    BOOL isOnline;
}
@end

isOnline確定是否應該更新伺服器對相簿列表所做的任何更改,例如新增或刪除的相簿。
現在需要通過init初始化這些變數。 將以下程式碼新增到LibraryAPI.m:

- (id)init
{
    self = [super init];
    if (self) {
        persistencyManager = [[PersistencyManager alloc] init];
        httpClient = [[HTTPClient alloc] init];
        isOnline = NO;
    }
    return self;
}

HTTP客戶端實際上不與真實的伺服器一起工作,並且僅在這裡展示門面模式的用法,因此isOnline將始終為NO。
接下來,將以下三個方法新增到LibraryAPI.m:

- (NSArray*)getAlbums
{
    return [persistencyManager getAlbums];
}

- (void)addAlbum:(Album*)album atIndex:(int)index
{
    [persistencyManager addAlbum:album atIndex:index];
    if (isOnline)
    {
        [httpClient postRequest:@"/api/addAlbum" body:[album description]];
    }
}

- (void)deleteAlbumAtIndex:(int)index
{
    [persistencyManager deleteAlbumAtIndex:index];
    if (isOnline)
    {
        [httpClient postRequest:@"/api/deleteAlbum" body:[@(index) description]];
    }
}

看看addAlbum:atIndex :。 該類首先在本地更新資料,然後如果有網際網路連線,它會更新遠端伺服器。 這是門面的真正好處; 當你的系統外的一些類添加了一個新的專輯,它不知道 、並且不需要知道下面的複雜性。

注意:在為子系統中的類設計門面時,請記住,沒有任何東西阻止客戶端直接訪問這些“隱藏”類。 不要吝嗇防禦性程式碼,不要假設所有的客戶端都必須使用你的類,就像門面使用它們一樣。

構建並執行您的應用程式。 你會看到一個令人難以置信的令人興奮的空黑屏,像這樣:
黑屏
你需要一些東西在螢幕上顯示相簿資料,這是你下一個設計模式的完美使用:裝飾模式。

裝飾模式

裝飾器模式動態地將行為和責任新增到物件而不修改其程式碼。 這是一個替代子類化的地方,你通過用另一個物件包裝它來修改類的行為。

在Objective-C中,有兩種非常常見的模式實現:類(Category)和委託(Delegation)。

分類(Category)

分類是一種非常強大的機制,允許您在沒有子類化的情況下將方法新增到現有類。 新方法在編譯時新增,可以像擴充套件類的普通方法那樣執行。 它與裝飾器的經典定義略有不同,因為分類不包含它擴充套件的類的例項。

注意:除了擴充套件自己的類,你也可以新增任何Cocoa框架中類的方法!

怎樣使用分類

想象一下,你想要在表檢視中顯示一個Album物件的情況:
Album物件
專輯名稱從哪裡來? Album是一個Model物件,所以它不在乎你如何呈現資料。 您將需要一些外部程式碼將此功能新增到Album類,但不直接修改類。
您將建立一個分類,它是Album的副檔名; 它將定義一個新的方法,返回一個可以很容易地與UITableViews一起使用的資料結構。

資料結構將如下所示:
資料結構

要將分類別新增到相簿,導航到File \ New \ File …並選擇Objective-C category template - 不要選擇Objective-C類! 在分類欄位中輸入TableRepresentation,並在類別欄位中輸入Album。

注意:您注意到新檔案的名稱嗎? Album + TableRepresentation表示您要擴充套件Album類。 這個約定很重要,因為它更容易閱讀,並防止與您或其他人可能建立的其他類別的衝突。
轉到Album + Table Representation.h並新增以下方法原型:

- (NSDictionary*)tr_tableRepresentation;

注意在方法名的開頭有一個tr_,作為類別名稱的縮寫:TableRepresentation。 再次,這樣的約定將有助於防止與其他方法的衝突!
轉到Album + Table Representation.m並新增以下方法:

- (NSDictionary*)tr_tableRepresentation
{
    return @{@"titles":@[@"Artist", @"Album", @"Genre", @"Year"],
             @"values":@[self.artist, self.title, self.genre, self.year]};
}

考慮一下這個模式有多麼強大:

  • 您直接從相簿使用屬性。
  • 您已新增到Album類,但您尚未對其進行子類化。 如果你需要子類別的相簿,你仍然可以這樣做。
  • 這個簡單的新增允許你返回一個相簿的UITableView-ish表示,而不修改相簿的程式碼。

蘋果在基礎類中使用了很多類別。 要看看他們如何做到這一點,開啟NSString.h。 查詢@interface NSString,您將看到類的定義以及三個類別:NSStringExtensionMethods,NSExtendedStringPropertyListParsing和NSStringDeprecated。 分類有助於保持方法的組織和分離。

委託(Delegation)

另一個裝飾器設計模式,委託,是一個物件代表另一個物件或與另一個物件協同工作的機制。 例如,當您使用UITableView時,必須實現的方法之一是tableView:numberOfRowsInSection :

您不能期望UITableView知道您希望在每個部分中有多少行,因為這是應用程式特定的。 因此,計算每個部分中的行數的任務被傳遞給UITableView delegate。 這允許UITableView類獨立於它顯示的資料。

這裡是一個大概的解釋當你建立一個新的UITableView時發生了什麼:

delegate

UITableView物件執行其顯示錶檢視的任務。 然而,最終它將需要一些它沒有的資訊。 然後,它轉向其代理,併發送一條訊息,要求提供其他資訊。 在Objective-C的委託模式實現中,類可以通過協議宣告可選和必需的方法。 本教程稍後將介紹協議。

看起來更簡單的是子類化一個物件並覆蓋必要的方法,但考慮你只能基於一個類進行子類化。 如果你想讓一個物件成為兩個或多個其他物件的委託,你將不能通過子類化實現這一點。

注意:這是一個重要的模式。 Apple在大多數UIKit類中使用這種方法:UITableView,UITextView,UITextField,UIWebView,UIAlert,UIActionSheet,UICollectionView,UIPickerView,UIGestureRecognizer,UIScrollView等等。

怎樣使用委託模式

轉到ViewController.m並將以下匯入新增到檔案的頂部:

#import "LibraryAPI.h"
#import "Album+TableRepresentation.h"

現在,將這些私有變數新增到類擴充套件,以便類擴充套件看起來像這樣:

@interface ViewController () {
    UITableView *dataTable;
    NSArray *allAlbums;
    NSDictionary *currentAlbumData;
    int currentAlbumIndex;
}
@end

然後,將類擴充套件中的@interface行替換為這一行:

@interface ViewController () <UITableViewDataSource, UITableViewDelegate> {

這就是你如何使你的代理符合協議 - 認為它是一個承諾,由代表完成方法的合同。 在這裡,您指示ViewController將符合UITableViewDataSource和UITableViewDelegate協議。 這樣,UITableView可以絕對確定所需的方法是由其委託實現的。

接下來,將viewDidLoad:替換為此程式碼:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // 1
    self.view.backgroundColor = [UIColor colorWithRed:0.76f green:0.81f blue:0.87f alpha:1];
    currentAlbumIndex = 0;

    //2
    allAlbums = [[LibraryAPI sharedInstance] getAlbums];

    // 3
    // the uitableview that presents the album data
    dataTable = [[UITableView alloc] initWithFrame:CGRectMake(0, 120, self.view.frame.size.width, self.view.frame.size.height-120) style:UITableViewStyleGrouped];
    dataTable.delegate = self;
    dataTable.dataSource = self;
    dataTable.backgroundView = nil;
    [self.view addSubview:dataTable];
}

以下是上述程式碼的細分:
1. 將背景顏色更改為漂亮的海軍藍色。
2. 通過API獲取所有相簿的列表。 您不要直接使用PersistencyManager!
3. 這是您建立UITableView的位置。 您宣告檢視控制器是UITableView delegate/data source; 因此,UITableView所需的所有資訊將由檢視控制器提供。

現在,新增以下方法到ViewController.m:

- (void)showDataForAlbumAtIndex:(int)albumIndex
{
    // defensive code: make sure the requested index is lower than the amount of albums
    if (albumIndex < allAlbums.count)
    {
        // fetch the album
        Album *album = allAlbums[albumIndex];
        // save the albums data to present it later in the tableview
        currentAlbumData = [album tr_tableRepresentation];
    }
    else
    {
        currentAlbumData = nil;
    }

    // we have the data we need, let's refresh our tableview    
    [dataTable reloadData];
}

showDataForAlbumAtIndex:從相簿陣列中提取所需的相簿資料。 當你想呈現新的資料,你只需要呼叫reloadData。 這會導致UITableView詢問其委託,例如表檢視中應顯示多少節,每節中有多少行以及每個單元格的外觀。

將以下行新增到viewDidLoad的末尾

   [self showDataForAlbumAtIndex:currentAlbumIndex];

這會在應用啟動時載入當前相簿。 由於currentAlbumIndex之前設定為0,這將顯示集合中的第一張專輯。
構建和執行您的專案; 您將遇到崩潰,並在除錯控制檯中顯示以下異常:
異常

這裡發生了什麼? 您宣告ViewController作為UITableView的委託和資料來源。 但在這樣做,你必須符合所有必需的方法tableView:numberOfRowsInSection:, 你還沒有做到。

將以下程式碼新增到ViewController.m @implementation和@end行之間的任何地方:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [currentAlbumData[@"titles"] count];
}

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"];
    }

    cell.textLabel.text = currentAlbumData[@"titles"][indexPath.row];
    cell.detailTextLabel.text = currentAlbumData[@"values"][indexPath.row];

    return cell;
}

tableView:numberOfRowsInSection:返回在表檢視中顯示的行數,它與資料結構中的標題數量相匹配。
tableView:cellForRowAtIndexPath:建立並返回具有標題及其值的單元格。
構建並執行您的專案。 您的應用程式應該開始並顯示以下畫面:

tableView影象
這看起來相當不錯。 但如果你回想起第一個影象顯示完成的應用程式,在螢幕的頂部有一個水平卷軸在相簿之間切換。 而不是編碼一個單一目的的水平滾動,為什麼不讓它可重用於任何檢視?

為了使這個檢視可重用,關於它的內容的所有決定應該留給另一個物件:委託。 水平滾動器應該宣告其委託實現的方法,以便使用滾動器,類似於UITableView委託方法的工作方式。 當我們討論下一個設計模式時,我們將實現這一點。

介面卡模式

介面卡允許具有不相容介面的類一起工作。 它將自身包裹在一個物件周圍,並暴露一個標準介面來與該物件互動。

如果你熟悉介面卡模式,那麼你會注意到蘋果以略有不同的方式實現它 - 蘋果使用協議來完成這項工作。 您可能熟悉像UITableViewDelegate,UIScrollViewDelegate,NSCoding和NSCopying的協議。 作為示例,使用NSCopying協議,任何類都可以提供標準複製方法。

怎樣使用介面卡模式

前面提到的水平滾動條將如下所示:
水平滾動條

要開始實現它,右鍵單擊Project Navigator中的View組,選擇New File …並使用iOS \ Cocoa Touch \ Objective-C類模板建立一個類。 命名新類HorizontalScroller並使其從UIView子類。

開啟HorizontalScroller.h並在@end行後插入以下程式碼:

@protocol HorizontalScrollerDelegate <NSObject>
// methods declaration goes in here
@end

這定義了一個名為HorizontalScrollerDelegate的協議,它繼承自NSObject協議,與Objective-C類從其父類繼承的方式相同。 遵循NSObject協議或符合自身符合NSObject協議的協議是一個好的習慣。 這允許您將由NSObject定義的訊息傳送到HorizontalScroller的委託。 你很快就會明白為什麼這很重要。

您定義了委託將在@protocol和@end行之間實現的必需和可選方法。 所以新增以下協議方法:

@required
// ask the delegate how many views he wants to present inside the horizontal scroller
- (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller*)scroller;

// ask the delegate to return the view that should appear at <index>
- (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index;

// inform the delegate what the view at <index> has been clicked
- (void)horizontalScroller:(HorizontalScroller*)scroller clickedViewAtIndex:(int)index;

@optional
// ask the delegate for the index of the initial view to display. this method is optional
// and defaults to 0 if it's not implemented by the delegate
- (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller*)scroller;

這裡有必要的和可選的方法。 必需的方法必須由委託來實現,並且通常包含該類絕對需要的一些資料。 在這種情況下,所需的詳細資訊是檢視的數量,特定索引處的檢視以及輕擊檢視時的行為。 這裡的可選方法是初始檢視; 如果沒有實現,那麼HorizontalScroller將預設使用第一個索引。

接下來,您需要從HorizontalScroller類定義中引用您的新委託。 但協議定義低於類定義,因此在這一點不可見。 那麼你能怎麼做呢?

解決方案是轉發宣告協議,以便編譯器(和Xcode)知道這樣的協議將可用。 為此,請在@interface行上面新增以下程式碼:

@protocol HorizontalScrollerDelegate;

仍然在HorizontalScroller.h中,在@interface和@end語句之間新增以下程式碼:

@property (weak) id<HorizontalScrollerDelegate> delegate;
 - (void)reload;

上面建立的屬性的屬性定義為weak。 這是必要的,以防止迴圈引用。 如果一個類保持一個強的指標到它的委託,並且委託保持一個強的指標回到合格的類,你的應用程式將洩漏記憶體,因為這兩個類都不會釋放分配給另一個的記憶體。
id意味著代理只能被賦予符合HorizontalScrollerDelegate的類,給你一些型別的安全性。
reload方法在UITableView中的reloadData之後呼叫; 它重新載入用於構造水平滾動條的所有資料。
使用以下程式碼替換HorizontalScroller.m的內容:

#import "HorizontalScroller.h"

// 1
#define VIEW_PADDING 10
#define VIEW_DIMENSIONS 100
#define VIEWS_OFFSET 100

// 2
@interface HorizontalScroller () <UIScrollViewDelegate>
@end

// 3
@implementation HorizontalScroller
{
    UIScrollView *scroller;
}

@end

依次考慮下面幾個方面:
定義常量以便於在設計時修改佈局。 檢視在滾動器內的尺寸將為100 x 100,其包圍矩形的邊距為10。
HorizontalScroller符合UIScrollViewDelegate協議。 由於HorizontalScroller使用UIScrollView滾動相簿封面,它需要知道使用者事件,例如使用者停止滾動時。
建立包含檢視的滾動檢視。

接下來,您需要實現初始化程式。 新增以下方法:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        scroller = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
        scroller.delegate = self;
        [self addSubview:scroller];
        UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollerTapped:)];
        [scroller addGestureRecognizer:tapRecognizer];
    }
    return self;
}

滾動檢視完全填充HorizontalScrollerUITapGestureRecognizer檢測滾動檢視上的觸控,並檢查是否已經點選了專輯封面。 如果是,它通知HorizontalScroller委託。
現在新增這個方法:

- (void)scrollerTapped:(UITapGestureRecognizer*)gesture
{
    CGPoint location = [gesture locationInView:gesture.view];
    // we can't use an enumerator here, because we don't want to enumerate over ALL of the UIScrollView subviews.
    // we want to enumerate only the subviews that we added
    for (int index=0; index<[self.delegate numberOfViewsForHorizontalScroller:self]; index++)
    {
        UIView *view = scroller.subviews[index];
        if (CGRectContainsPoint(view.frame, location))
        {
            [self.delegate horizontalScroller:self clickedViewAtIndex:index];
            [scroller setContentOffset:CGPointMake(view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2, 0) animated:YES];
            break;
        }
    }
}

作為引數傳遞的手勢可讓您通過locationInView提取位置。

接下來,呼叫delegate上的numberOfViewsForHorizontalScroller:。 HorizontalScroller例項沒有有關委託的資訊,除了知道它可以安全地傳送此訊息,因為委託必須符合HorizontalScrollerDelegate協議。

對於滾動檢視中的每個檢視,使用CGRectContainsPoint執行命中測試以查詢已輕敲的檢視。 當找到檢視時,傳送代理horizontalScroller:clickedViewAtIndex:訊息。 在您跳出for迴圈之前,將滾動檢視中的輕觸檢視居中。

現在新增以下程式碼重新載入滾動器:

- (void)reload
{
    // 1 - nothing to load if there's no delegate
    if (self.delegate == nil) return;

    // 2 - remove all subviews
    [scroller.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [obj removeFromSuperview];
    }];

    // 3 - xValue is the starting point of the views inside the scroller
    CGFloat xValue = VIEWS_OFFSET;
    for (int i=0; i<[self.delegate numberOfViewsForHorizontalScroller:self]; i++)
    {
        // 4 - add a view at the right position
        xValue += VIEW_PADDING;
        UIView *view = [self.delegate horizontalScroller:self viewAtIndex:i];
        view.frame = CGRectMake(xValue, VIEW_PADDING, VIEW_DIMENSIONS, VIEW_DIMENSIONS);
        [scroller addSubview:view];
        xValue += VIEW_DIMENSIONS+VIEW_PADDING;
    }

    // 5
    [scroller setContentSize:CGSizeMake(xValue+VIEWS_OFFSET, self.frame.size.height)];

    // 6 - if an initial view is defined, center the scroller on it
    if ([self.delegate respondsToSelector:@selector(initialViewIndexForHorizontalScroller:)])
    {
        int initialView = [self.delegate initialViewIndexForHorizontalScroller:self];
        [scroller setContentOffset:CGPointMake(initialView*(VIEW_DIMENSIONS+(2*VIEW_PADDING)), 0) animated:YES];
    }
}

逐步對程式碼進行解釋:

  1. 如果沒有委託,那麼沒有什麼可做,你可以返回。
  2. 刪除先前新增到滾動檢視的所有子檢視。
  3. 所有檢視都從給定的偏移量開始定位。目前它是100,但它可以很容易調整通過更改檔案的頂部的#DEFINE 值。
  4. Horizo​​ntalScroller一次請求一個檢視的代理,並且它們用前面定義的填充水平地彼此相鄰放置。
  5. 一旦所有檢視都就位,設定滾動檢視的內容偏移,以允許使用者滾動瀏覽所有專輯封面。
  6. Horizo​​ntalScroller檢查其委託是否響應initialViewIndexForHorizo​​ntalScroller:selector。這種檢查是必要的,因為特定的協議方法是可選的。如果代理沒有實現此方法,則使用0作為預設值。最後,這段程式碼設定滾動檢視以居中由委託定義的初始檢視。

當資料更改時執行重新載入。當您將Horizo​​ntalScroller新增到另一個檢視時,還需要呼叫此方法。將以下程式碼新增到Horizo​​ntalScroller.m以包括後一種情況:

- (void)didMoveToSuperview
{
    [self reload];
}

當它作為子檢視新增到另一個檢視時,didMoveToSuperview訊息被髮送到檢視。 這是重新載入滾動條內容的正確時間。
HorizontalScroller拼圖的最後一個部分是確保您正在檢視的相簿始終居中在滾動檢視中。 為此,您需要在使用者用手指拖動滾動檢視時執行一些計算。

新增以下方法(再次到HorizontalScroller.m):

- (void)centerCurrentView
{
    int xFinal = scroller.contentOffset.x + (VIEWS_OFFSET/2) + VIEW_PADDING;
    int viewIndex = xFinal / (VIEW_DIMENSIONS+(2*VIEW_PADDING));
    xFinal = viewIndex * (VIEW_DIMENSIONS+(2*VIEW_PADDING));
    [scroller setContentOffset:CGPointMake(xFinal,0) animated:YES];
    [self.delegate horizontalScroller:self clickedViewAtIndex:viewIndex];
}

上述程式碼考慮了滾動檢視的當前偏移以及檢視的尺寸和填充,以便計算當前檢視與中心的距離。 最後一行很重要:一旦檢視居中,您就可以通知代理所選檢視已更改。

要檢測使用者是否完成在滾動檢視內的拖動,必須新增以下UIScrollViewDelegate方法:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    if (!decelerate)
    {
        [self centerCurrentView];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self centerCurrentView];
}

scrollViewDidEndDragging:willDecelerate:當用戶完成拖動時通知代理。 如果滾動檢視尚未完全停止,則decelerationate引數為true。 當滾動操作結束時,系統呼叫scrollViewDidEndDecelerating。 在這兩種情況下,我們應該呼叫新方法來使當前檢視居中,因為使用者拖動滾動檢視後當前檢視可能已更改。
您的HorizontalScroller已準備就緒! 瀏覽你剛才寫的程式碼; 你會看到沒有一個提到的Album或AlbumView類。 這是非常好的,因為這意味著新的滾動器是真正獨立和可重用的。
構建您的專案,以確保一切正常編譯。
現在,HorizontalScroller完成,是時候在你的應用程式中使用它。 開啟ViewController.m並新增以下匯入:

#import "HorizontalScroller.h"
#import "AlbumView.h"

新增HorizontalScrollerDelegate到ViewController遵循的協議:

@interface ViewController ()<UITableViewDataSource, UITableViewDelegate, HorizontalScrollerDelegate>

將水平滾動條的以下例項變數新增到類擴充套件:

HorizontalScroller *scroller;

現在你可以實現委託方法; 你會驚訝於幾行程式碼如何實現很多功能。
將以下程式碼新增到ViewController.m:

#pragma mark - HorizontalScrollerDelegate methods
- (void)horizontalScroller:(HorizontalScroller *)scroller clickedViewAtIndex:(int)index
{
    currentAlbumIndex = index;
    [self showDataForAlbumAtIndex:index];
}

這將設定儲存當前相簿的變數,然後呼叫showDataForAlbumAtIndex:顯示新相簿的資料。

注意:通常的做法是放置在#pragma mark偽指令後合併在一起的方法。 編譯器會忽略這行,但是如果你通過Xcode的跳轉欄下拉當前檔案中的方法列表,你會看到一個分隔符和一個粗體標題的指令。 這有助於您組織程式碼,以便在Xcode中輕鬆導航。

下一步,新增這個方法

- (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller*)scroller
{
    return allAlbums.count;
}

這個,你會認識到,是協議方法返回的滾動檢視的數量。 由於滾動檢視將顯示所有相簿資料的封面,所以計數是專輯記錄的數量。

現在,新增以下程式碼:

- (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index
{
    Album *album = allAlbums[index];
    return [[AlbumView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) albumCover:album.coverUrl];
}

在這裡你建立一個新的AlbumView並將其傳遞給HorizontalScroller
只有三個短的方法來顯示一個漂亮的水平卷軸。
是的,您仍然需要實際建立滾動條並將其新增到您的主檢視,但在這之前,新增以下方法:

- (void)reloadScroller
{
    allAlbums = [[Lib