歡迎使用CSDN-markdown編輯器 -开发者知识库

歡迎使用CSDN-markdown編輯器 -开发者知识库,第1张

IOS設計模式之四(備忘錄模式,命令模式)

本文原文請見:http://www.raywenderlich.com/46988/ios-design-patterns.
由 @krq_tiger(http://weibo.com/xmuzyq)翻譯,如果你發現有什么錯誤,請與我聯系謝謝。
備忘錄(Memento)模式

如何使用備忘錄模式

在ViewController.m中增加下面的方法:

  • (void)saveCurrentState

{

// When the user leaves the app and then comes back again, he wants it to be in the exact same state  

// he left it. In order to do this we need to save the currently displayed album.

// Since it's only one piece of information we can use NSUserDefaults.

[[NSUserDefaultsstandardUserDefaults] setInteger:currentAlbumIndex forKey:@"currentAlbumIndex"];

}

  • (void)loadPreviousState

{

currentAlbumIndex = [[NSUserDefaultsstandardUserDefaults] integerForKey:@"currentAlbumIndex"];  

[self showDataForAlbumAtIndex:currentAlbumIndex];

}

saveCurrentState 保存當前的專輯索引到NSUserDefaults,NSUserDefaults是IOS提供的保存應用設置信息和數據的地方。

loadPreviousState 加載之前保存的索引。這里其實不是備忘錄模式完整的實現,但是你已經了解到它了。
現在,在ViewController.m的viewDidLoad方法中,在scroller初始化之前增加下面的代碼:

[self loadPreviousState];

它將在應用啟動的時候加載原先保存的狀態。但是在什么時候來保存應用的狀態呢?你將使用通知來實現它。當應用進入后台的時候,IOS會發送UIApplicationDidEnterBackgroundNotification通知,你可以使用這個通知去保存狀態,這是不是很方便?

在viewDidLoad中增加下面的代碼:

[[NSNotificationCenterdefaultCenter] addObserver:self selector:@selector(saveCurrentState) name:UIApplicationDidEnterBackgroundNotification object:nil];

現在,當應用進入后台的時候,ViewController將通過saveCurrentState方法自動保存當前的狀態。

現在增加下面的代碼:

  • (void)dealloc

{

[[NSNotificationCenterdefaultCenter] removeObserver:self];  

}

這將確保當ViewController被銷毀的時候移除觀察者。

構建和運行你的應用,導航到一個專輯,然后通過Command Shift H(模擬器的情況下)將app發送到后台,然后關閉app。再一次打開app,檢查原先選擇的專輯是不是被顯示在中間:

看起來專輯數據是正確的,但是中間的視圖卻沒有顯示正確的專輯。出了什么情況?這是可選方法initialViewIndexForHorizontalScroller的目的所在。因為這個方法沒有在委托中實現,這樣的話初始化視圖總是第一個視圖。
為了修正這個問題,在ViewController.m中增加下面的代碼:

  • (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller

{

return currentAlbumIndex;  

}

現在HorizontalScroller的第一個視圖終於設置為了currentAlbumIndex指定的視圖。這使得app在下次使用的時候還保留了上次使用的狀態。

再一次運行你的app,和之前一樣滾動專輯,停止應用,重啟,確保上面的問題已經修復了:

如果你查看PersistencyManager的init方法,你將注意到專輯數據被硬編碼並且每次都要重新創建它們。但是更好的方式是創建專輯列表一次,然后存儲它們到一個文件,你怎么保存專輯數據到一個文件呢?
一個可選的方式就是循環Album的屬性,保存它們到一個plist文件中,當它們需要的時候再重新構建它們。這個不是一個最好的方式,因為你需要去編寫與每個類的屬性關聯的特定的代碼。舉例來說如果過會你要創建一個具有不同屬性的Movie類,保存和加載的代碼需要重新寫。

此外,你也不能保存每個類的私有變量,因為它們在外面的類中是不可見的。這正是蘋果創建了歸檔(Archiving)機制的原因。(譯者注:Java中這里也可以說是序列化)

歸檔(Archiving)

歸檔是蘋果對於備忘錄模式的特定實現之一。這種機制可以轉換一個對象到一個可保存的數據流中,過會可以在不暴漏私有屬性給外部的情況下重建它們。你可以在iOS 6 by Tutorials書的第16章讀取更多關於此功能的信息,或者你也可以參考:Apple’s Archives and Serializations Programming Guide.

如何使用歸檔
首先,你需要聲明Album可以被歸檔的,這需要Album遵循NSCoding協議。打開Album.h文件,改變@interface行為如下所示:

@interfaceAlbum : NSObject

在Album.m中增加如下的兩個方法:

  • (void)encodeWithCoder:(NSCoder *)aCoder

{

[aCoder encodeObject:self.year forKey:@"year"];  

[aCoder encodeObject:self.title forKey:@"album"];

[aCoder encodeObject:self.artist forKey:@"artist"];

[aCoder encodeObject:self.coverUrl forKey:@"cover_url"];

[aCoder encodeObject:self.genre forKey:@"genre"];

}

  • (id)initWithCoder:(NSCoder *)aDecoder

{

self = [super init];  

if (self)

{

_year = [aDecoder decodeObjectForKey:@"year"];

_title = [aDecoder decodeObjectForKey:@"album"];

_artist = [aDecoder decodeObjectForKey:@"artist"];

_coverUrl = [aDecoder decodeObjectForKey:@"cover_url"];

_genre = [aDecoder decodeObjectForKey:@"genre"];

}

return self;

}

你可以在歸檔一個類的實例對象的時候調用encodeWithCoder:,相反的當你要從歸檔中重建Album實例的時候,你可以調用initWithCoder:,這樣做是不是很簡單,但是它是一種強大的機制哦。
在PersistencyManager.h中,增加下面的簽名(方法原型):

  • (void)saveAlbums;

這個正是保存專輯的方法。
現在在PersistencyManager.m中,增加方法的實現:

  • (void)saveAlbums

{

NSString *filename = [NSHomeDirectory() stringByAppendingString:@"/Documents/albums.bin"];  

NSData *data = [NSKeyedArchiverarchivedDataWithRootObject:albums];

[data writeToFile:filename atomically:YES];

}

NSKeyedArchiver歸檔專輯數據到albums.bin文件中。

當你在歸檔一個對象的時候,歸檔器會遞歸的歸檔對象包含的子對象以及子對象的子對象等等。在本例中,歸檔開始自一個名為albums的數組,因為NSArry和Album兩者都支持NSCoding協議,因此數組中每個對象都會被歸檔.
現在用下面的代碼取代PersistencyManager.m中的init方法:

  • (id)init

{

self = [super init];  

if (self) {

NSData *data = [NSDatadataWithContentsOfFile:[NSHomeDirectory() stringByAppendingString:@"/Documents/albums.bin"]];

albums = [NSKeyedUnarchiverunarchiveObjectWithData:data];

if (albums == nil)

{

albums = [NSMutableArrayarrayWithArray:

@[[[Album alloc] initWithTitle:@"Best of Bowie" artist:@"David Bowie" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_david bowie_best of bowie.png" year:@"1992"],

[[Album alloc] initWithTitle:@"It's My Life" artist:@"No Doubt" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_no doubt_its my life bathwater.png" year:@"2003"],

[[Album alloc] initWithTitle:@"Nothing Like The Sun" artist:@"Sting" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_sting_nothing like the sun.png" year:@"1999"],

[[Album alloc] initWithTitle:@"Staring at the Sun" artist:@"U2" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_u2_staring at the sun.png" year:@"2000"],

[[Album alloc] initWithTitle:@"American Pie" artist:@"Madonna" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_madonna_american pie.png" year:@"2000"]]];

[self saveAlbums];

}

}

return self;

}

新的代碼中,如果專輯數據存在,NSKeyedUnarchiver會從文件中加載專輯數據,如果專輯數據不存在,它會創建專輯數據並立即保存它以便下次啟動的時候使用。

你想在每次app進入后台的時候都保存專輯數據。現在這可能看起來不是很必要,但是如果過會你想增加一個修改專輯數據的選項呢?那時候你就想確保所有的改變都會被保存。
在Library.h中增加下面的代碼:
- (void)saveAlbums;
因為主應用通過LibraryAPI訪問所有的服務,這樣就要求PersistencyManager知道它負責保存專輯數據。
現在在LibraryAPI.m實現中增加方法實現:

  • (void)saveAlbums

{

[persistencyManager saveAlbums];  

}

這個方法將調用LibraryAPI保存數據的請求委托給PersistencyManager處理。在ViewController.m中saveCurrentState方法末尾,增加如下的代碼:

[[LibraryAPI sharedInstance] saveAlbums];
無論何時ViewController保存應用狀態的時候,上面的代碼使用LibraryAPI觸發專輯數據的保存。
構建你的應用,檢查每個資源是否被正確編譯。

不幸的是,沒有一個簡單的方式去檢查數據持久化的正確性。你可以通過Finder在應用的Documents目錄查看到專輯數據文件已經被創建,但是為了能看到任何其它的變化,你還需要增加改變專輯數據的功能。
但是並不僅僅是改變數據,如果你需要刪除不想要的專輯數據呢?另外,是不是可以很漂亮的來增加一個撤銷刪除的功能呢?
這就到了我們討論下個設計模式(命令模式)的機會了。

命令模式

命令模式將一個請求封裝為一個對象。封裝以后的請求會比原生的請求更加靈活,因為這些封裝后的請求可以在多個對象之間傳遞,存儲以便以后使用,還可以動態的修改,或者放進一個隊列中。蘋果通過Target-Action機制和Invocation實現命令模式。

 你可以通過蘋果的官方在線文檔閱讀更多關於Target-Action的內容,至於Invocation,它采用了NSInvocation類,這個類包含了一個目標對象,方法選擇器,以及一些參數。這個對象可以動態的修改並且可以按需執行。實踐中它是一個命令模式很好的例子。它解耦了發送對象和接受對象,並且可以保存一個或者多個請求。

如何使用命令模式

在你深入了解invocation之前,你需要首先來設置一個支持撤銷操作的大體骨架。所以你需要定義一個UIToolBar和用作撤銷堆棧的NSMutableArray。
在ViewController.m的擴展中,在你定義其它變量的地方定義如下的變量:
UIToolbar *toolbar;

// We will use this array as a stack to push and pop operation for the undo option  

NSMutableArray *undoStack;

這里我們創建了包含新增按鈕的工具欄,同時還創建了一個用作命令存儲隊列的數組。
在viewDidLoad方法的第二個注釋之前,增加下面的代碼:

toolbar = [[UIToolbar alloc] init];

UIBarButtonItem *undoItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemUndo target:self action:@selector(undoAction)];

undoItem.enabled = NO;

UIBarButtonItem *space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];

UIBarButtonItem *delete = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(deleteAlbum)];

[toolbar setItems:@[undoItem,space,delete]];

[self.view addSubview:toolbar];

undoStack = [[NSMutableArrayalloc] init];
上面的代碼在工具欄上面增加了2個按鈕和一個可變長度組件(flexible space),它還創建了一個空的撤銷操作棧,剛開始撤銷按鈕是不可用的,因為撤銷棧是空的。
另外你可能注意到工具條沒有使用frame來初始化,因為viewDidLoad不是決定frame大小最終的地方。
在ViewController.m中增加如下設置frame大小的代碼:
- (void)viewWillLayoutSubviews

{

toolbar.frame = CGRectMake(0, self.view.frame.size.height-44, self.view.frame.size.width, 44);  

dataTable.frame = CGRectMake(0, 130, self.view.frame.size.width, self.view.frame.size.height - 200);

}

你將還需要在ViewController.m中增加三個方法來管理專輯:增加,刪除,撤銷。

第一個方法是增加一個新的專輯:

  • (void)addAlbum:(Album*)album atIndex:(int)index

{

[[LibraryAPI sharedInstance] addAlbum:album atIndex:index];  

currentAlbumIndex = index;

[self reloadScroller];

}

在這里你增加專輯,並設置當前專輯索引,然后重新加載滾動視圖。
接下來是刪除方法:

  • (void)deleteAlbum

{

// 1  

Album *deletedAlbum = allAlbums[currentAlbumIndex];



// 2

NSMethodSignature *sig = [self methodSignatureForSelector:@selector(addAlbum:atIndex:)];

NSInvocation *undoAction = [NSInvocationinvocationWithMethodSignature:sig];

[undoAction setTarget:self];

[undoAction setSelector:@selector(addAlbum:atIndex:)];

[undoAction setArgument:&deletedAlbum atIndex:2];

[undoAction setArgument:&currentAlbumIndex atIndex:3];

[undoAction retainArguments];



// 3

[undoStack addObject:undoAction];



// 4

[[LibraryAPI sharedInstance] deleteAlbumAtIndex:currentAlbumIndex];

[self reloadScroller];



// 5

[toolbar.items[0] setEnabled:YES];

}

上面的代碼中有一些新的激動人心的特性,所以下面我們就來考慮每個被標注了注釋的地方:
1. 獲取需要刪除的專輯

2. 定義了一個類型為NSMethodSignature的對象去創建NSInvocation,它將用來撤銷刪除操作。NSInvocation需要知道三件事情:選擇器(發送什么消息),目標對象(發送消息的對象),還有就是消息所需要的參數。在上面的例子中,消息是與刪除方法相反的操作,因為當你想撤銷刪除的時候,你需要將剛刪除的數據回加回去。


3. 創建了undoAction以后,你需要將其增加到undoStack中。撤銷操作將被增加在數組的末尾。


4. 使用LibraryAPI刪除專輯,然后重新加載滾動視圖。


5. 因為在撤銷棧中已經有了操作,你需要使得撤銷按鈕可用。

注意:使用NSInvocation,你需要記住下面的幾點:
1.參數必須以指針的形式傳遞.

2.參數從索引2開始,索引0,1為目標(target)和選擇器(selector)保留。

3.如果參數有可能會被銷毀,你需要調用retainArguments.

最后,增加下面的撤銷方法:
- (void)undoAction

{

if (undoStack.count > 0)  

{

NSInvocation *undoAction = [undoStack lastObject];

[undoStack removeLastObject];

[undoAction invoke];

}



if (undoStack.count == 0)

{

[toolbar.items[0] setEnabled:NO];

}

}

撤銷操作彈出棧頂的NSInvocation對象,然后通過invoke調用它。這將調用你在原先刪除專輯的時候創建的命令,將刪除的專輯加回專輯列表。因為你已經刪除了一個棧中的對象,所以你需要去檢查棧是否為空,如果為空,也就意味着不需要進行撤銷操作了,你這時候需要將撤銷按鈕設置為不可用。

最佳答案:

本文经用户投稿或网站收集转载,如有侵权请联系本站。

发表评论

0条回复