NSCoding 的常见用途
我们会在以下这些场合用到NSCoding:
XIB/Storyboard
我们在实现一个UIView的subclass的时候,会注意到,如果我们希望在initialize这个view的时候,就要做一些事情,不但要override掉initWithFrame:
,也要overrideinitWithCoder:
。
如果这个view是我们用code建立的,那么就会调用到initWithFrame:
,但,如果我们是在Interface Builder里头,用图形化工具建立了一个view,那个,当这样的view在执行的时候,则会走进initWithCoder:
这一段的实现。
我们在开始接触iOS 开发的时候,大概就会先学习如何使用Interface Builder 拉出想要的介面,产生出XIB 或是Storyboard 档案。XIB 与Storyboard 在我们撰写程式的期间,是XML 格式的档案,当我们编译App 的时候,Xcode 会将XIB 与Storyboard 编译成binary 格式的data,分别是NIB 与storyboardc 档案,而这些data 其实就是序列化过的Objective-C view 实例。
在一些其他开发平台上(像是使用Visual Studio拉出Windows Form应用程式)使用视觉化开发工具的时候,这些工具在做的事情,是把从拉出来的介面产生程式码;不过在Xcode中编辑XIB档案做的事情不一样,是先产生出序列化后的档案,然后再执行的时候,读取这些档案,将data转成view实例。这个流程正是使用NSCoding protocol,于是从NIB/storyboardc读出我们的view的时候,所调用的便是initWithCoder:
—我们可以从UIView的interface中,看到UIView实现了NSCoding protocol。
我们在Xcode 2 左右的年代(大概是Mac OS X 10.4 左右)开发Mac App 时,我们在Xcode 中其实是直接编辑NIB 档案,到了Xcode 3 与Mac OS X 10.5 之后才出现使用XML 格式的XIB 档案。这个转变跟当时SVN 等版本管理系统的出现有关,在版本管理系统中编辑binary 格式的档案,会难以diff、merge 以及处理版本冲突,所以苹果便从binary 格式换成文字格式的档案。
NSUserDefaults
如果在我们的App 中,我们想要储存一些偏好设定,那么最好用的选择莫过于Cocoa/Cocoa Touch Framework 本身就提供的NSUserDefaults 实例。操作NSUserDefaults 与操作NSDictionary 差不多,我们只要指定特定的key,就可以将设定值存入NSUserDefaults 中。
NSUserDefaults 支持NSString、NSArray、NSDictionary、 NSData 以及int、 double、float 等类型的资料。但,如果是我们自己定义的Class,或是许多其他的Class,会无法存入NSUserDefaults 中,我们会需要先透过NSCoding 转换成NSData 后存入,在取出的时候,也要多做一次unarchive。
比方说,如果我们的App 的某个地方可以设定颜色,我们想把UIColor 变成设定值,UIColor 就是一种无法直接存入NSUserDefaults 的实例。所以我们想把UIColor 存入NSUserDefaults,就得这么写:
UIColor *color = [UIColor colorWithHue:1.0 saturation:0.5 brightness:0.5 alpha:1.0];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:color];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"color"];
Copy and Paste
我们在行动装置上面会比较少实作Copy and Paste 剪贴功能,原因大概是我们比较少在行动装置上使用与开发比较复杂的编辑工作,而将这些工作留在desktop 环境。如果我们要开发一套Mac App,如何实现Copy and Paste,以及Drag and Drop,就会是不可不学的知识了。
无论是实作Copy and Paste 与Drag and Drop,都是透过pasteboard 实例,实作Drag and Drop 其实只是在开始Drag 的时候,先把想要拖曳的资料先放在另外一个专属的pasteboard 中,到了要放开的时候再从pasteboard 中取出资料。在Mac 上,pasteboard 物件叫做NSPasteboard,在iOS 上叫做UIPasteboard。
除了像NSString、NSData 之类的基础实例之外,许多我们想要可以被复制或拖拉的资料,如果想要存入到pasteboard 中,还是得先透过NSCoding 转换成NSData 才有办法。
像我们之前定义了KKSongTrack 实例,想要写入剪贴簿,可以这么做:
NSString *const KKBOXSongTrackPasteboardType = @"song_track";
KKSongTrack *song = [[KKSongTrack alloc] init];
song.songName = @"orz 之歌";
song.albumName = @"orz 專輯";
song.artistName = @"orz";
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:song];
[[UIPasteboard generalPasteboard] setData:data
forPasteboardType:KKBOXSongTrackPasteboardType];
读出来:
NSData *pasteData = [[UIPasteboard generalPasteboard] dataForPasteboardType:KKBOXSongTrackPasteboardType];
KKSongTrack *pasteSongTrack = [NSKeyedUnarchiver unarchiveObjectWithData:pasteData];
NSLog(@"pasteSongTrack:%@", pasteSongTrack);
Document-based App
如果我们开发的App 种类属于生产力工具,那么我们很有可能开发的就是一套Document-based App。
所谓Document-based App 包括苹果自己的iWork 系列,如Keynote、Numbers、 Pages 等等,主要功能就是让你浏览及编辑特定种类的档案,像iWork 系列的每一个App,功能就是编辑特定种类的简报、试算表与文书档案。
在Cocoa/Cocoa Touch Framework 中,便使用document-Mac 上叫做NSDocument、iOS 上面叫做UIDocument,对前面提到的各种不同类型文件做抽象描述,包括负责开启档案、储存档案、自动存档以及iCloud 备份同步等工作,以及描述档案所在位置与目前状态等。
在iOS 上要写一个Document-based App,我们会建立一个UIDocument 的subclass,而这个subclass 最重要的就是实现开档与读档两个method。比方说,我们建立了一份叫做KKPlaylist 的document,里头有一个array,里头是我们的KKSongTrack 实例,这个document 大概会写成这样:
KKPlaylist.h
@import UIKit;
@interface KKPlaylist : UIDocument
@end
KKPlaylist.m
#import "KKPlaylist.h"
@interface KKPlaylist()
@property (nonatomic, strong) NSMutableArray *songtracks;
@end
@implementation KKPlaylist
- (instancetype)initWithFileURL:(NSURL *)url
{
self = [super initWithFileURL:url];
if (self) {
self.songtracks = [NSMutableArray array];
}
return self;
}
- (id)contentsForType:(NSString *)typeName error:(NSError **)outError
{
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.songtracks];
return data;
}
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError
{
NSArray *songtracks = [NSKeyedUnarchiver unarchiveObjectWithData:contents];
[self.songtracks setArray:songtracks];
return YES;
}
@end
contentsForType:error:
与loadFromContents:ofType:error:
里头的contents参数是id型别,不过其实只接受NSFileWrapper与NSData,如果在我们的document中有不少已经实现了NSCoding protocol的实例,我们就可以轻松将实例转成NSData之后存档,或读取档案转回物件。我们通常对UIDocument做三件事情:
- 开启档案,调用
-openWithCompletionHandler:
- 关闭档案,调用
-closeWithCompletionHandler:
- 存档,调用
-saveToURL:forSaveOperation:completionHandler:
State Preservation and Restoration
State Preservation and Restoration 是苹果在iOS 6 加入的API,用途是让iOS App 可以在开启的的时候,可以立刻回复到上一次关闭App 时的状况,方便用户回复到之前的动作,而不受到因为App关闭/开启而打断。像Mail 这个App,当你在写一封写到一半的时候关闭App,下次打开,就会看到之前写到一半的那封信,避免用户找不到上次写到一半的信在哪里。
原理是,在应用程式关闭的时候,我们可以先把目前App 的状态—像是目前所有的view controller 物件,统统保存起来,下一次应用程式开启的时候,如果发现存在之前所保存的状态,就读取出来,重建上次存起来的view controller。
要实作State Preservation and Restoration,首先,要能够被保存的view controller,要实现两个method:
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder:
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
在App Delegate 则要实现:
-application:shouldSaveApplicationState:
-application:shouldRestoreApplicationState:
-application:willEncodeRestorableStateWithCoder:
-application:didDecodeRestorableStateWithCoder:
-application:willFinishLaunchingWithOptions:
流程是:
一、在App关闭的时候,首先系统会透过-application:shouldSaveApplicationState:
询问我们是否要保存状态,如果要的话,我们就回传YES。
二、前一步回传YES之后,系统就会透过-application:shouldRestoreApplicationState:
,提供我们一个NSCoder,让我们把必要的状态透过这个NSCoder archive起来。如果我们的App里头有一个navigation controller,而我们想把整个navigation controller保存起来,可以这么写:
- (void)application:(UIApplication *)application
willEncodeRestorableStateWithCoder:(NSCoder *)coder
{
NSMutableArray *viewControllers = [self.navigationControllers.viewControllers copy];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:viewControllers];
[coder encodeObject:data forKey:@"viewControllers"];
}
三、在重新开启App的时候,如果系统发现之前我们已经透过NSCoder保存状态了,那么,就会向我们透过-application:shouldRestoreApplicationState:
,询问是否要使用上次的状态,如果要的话,我们就回传YES。
四、接下来-application:didDecodeRestorableStateWithCoder:
就会被呼叫到,如果我们想还原上次存起来的navigation controller,可以这么写:
- (void)application:(UIApplication *)application
didDecodeRestorableStateWithCoder:(NSCoder *)coder
{
NSData *data = [coder decodeObjectForKey:@"viewControllers"];
NSArray *viewControllers = [NSKeyedUnarchiver unarchiveObjectWithData:data];
self.navigationController.viewControllers = viewControllers;
}