數據持久化就是將文件保存到硬盤,以便下次運行時可以讀取或永久保存。iOS提供了以下幾種持久化方案:
- NSUserDefaults (偏好設置)
- property list 即Plist (屬性列表)
- NSKeyedArchiver NSKeyedUnarchiver(歸檔、解檔)
- text file
- SQL databases
- Core Data
在這個demo中,我們將使用前面四種方法保存數據、恢復數據。
1. 創建Demo
在Xcode中創建一個Single View Application模板的應用,創建后在storyboard中添加控件,如下圖所示:
segmented control的segments修改為3,其他控件默認屬性即可。在ViewController.m接口部分創建如下連接:
@property (strong, nonatomic) IBOutlet UISegmentedControl *segments;
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *spinner;
@property (strong, nonatomic) IBOutlet UIButton *spinningButton;
@property (strong, nonatomic) IBOutlet UISwitch *cSwitch;
@property (strong, nonatomic) IBOutlet UITextField *textField;
@property (strong, nonatomic) IBOutlet UIProgressView *progressBar;
@property (strong, nonatomic) IBOutlet UISlider *slider1;
@property (strong, nonatomic) IBOutlet UISlider *slider2;
@property (strong, nonatomic) IBOutlet UISlider *slider3;
@property (strong, nonatomic) IBOutlet UITextView *textView;
// 聲明兩個可變詞典類型屬性,用來保存數據。
@property (strong, nonatomic) NSMutableDictionary *controlState;
@property (strong, nonatomic) NSMutableDictionary *sliderValue;
初始化controlState
、sliderValue
屬性,如下所示:
- (NSMutableDictionary *)controlState {
if (!_controlState) {
_controlState = [NSMutableDictionary dictionary];
}
return _controlState;
}
- (NSMutableDictionary *)sliderValue {
if (!_sliderValue) {
_sliderValue = [NSMutableDictionary dictionary];
}
return _sliderValue;
}
這篇文章會用到很多字符串,用來確定保存、讀取key,或路徑,在ViewController.m
文件私有接口后、實現部分前添加以下常量:
@end
static NSString * const selectedSegmentKey = @"selectedSegmentKey";
static NSString * const spinnerAnimatingKey = @"spinnerAnimatingKey";
static NSString * const switchEnabledKey = @"SwitchEnabledKey";
static NSString * const progressBarKey = @"progressBarKey";
static NSString * const textFieldKey = @"textFieldKey";
static NSString * const slider1Key = @"slider1Key";
static NSString * const slider2Key = @"slider2Key";
static NSString * const slider3Key = @"slider3Key";
static NSString * const controlStateComponent = @"controlStateComponent";
static NSString * const archivedDataComponent = @"archivedDataComponent";
static NSString * const sliderValueKey = @"sliderValueKey";
static NSString * const textViewComponent = @"textViewComponent";
@implementation ViewController
2. 使用User Defaults保存設置
- 運行時,NSUserDefaults從用戶的默認數據庫讀取程序設置,NSUserDefaults的緩存避免了每次獲取數據都要讀取數據庫。
- NSUserDefaults可以保存float、double、integer、Boolean、NSURL、NSData、NSString、NSNumber、NSDate、NSArray和NSDictionary類型的數據。
- NSUserDefaults返回的值是不可變的,盡管保存時值是可變的。例如:設定一個可變字符串為MyStringDefault的值,之后用stringForKey:獲取到的字符串將不可變。
- NSUserDefaults是線程安全的。
- 任何保存在偏好設置的數據,如沒有明確刪除會永遠保存在這里。所以,不要使用NSUserDefaults保存偏好設置外其他內容。
我們會把segmented control和spinner保存在NSUserDefaults。代碼如下:
// spinner部分
- (IBAction)toggleSpinner:(id)sender
{
if (self.spinner.isAnimating)
{
[self.spinner stopAnimating];
[self.spinningButton setTitle:@"Start Animating" forState:UIControlStateNormal];
}
else
{
[self.spinner startAnimating];
[self.spinningButton setTitle:@"Stop Animating" forState:UIControlStateNormal];
}
// 使用偏好設置保存設置。
[[NSUserDefaults standardUserDefaults] setBool:[self.spinner isAnimating] forKey:spinnerAnimatingKey];
}
// segmented control部分
- (IBAction)controlValueChanged:(id)sender
{
if (sender == self.segments)
{
// 使用偏好設置保存segment狀態。
NSInteger selectedSegment = ((UISegmentedControl *)sender).selectedSegmentIndex;
[[NSUserDefaults standardUserDefaults] setInteger:selectedSegment forKey:selectedSegmentKey];
}
}
toggleSpinner:
方法是從spinningButton
拖拽出IBAction事件,controlValueChanged:
方法是從cSwitch
、textField
、slider1
、slider2
、slider3
拖拽出IBAction事件。
3. 使用Plist保存數據
- Plist是一個標準的保存文本和設置的方式,Plist的數據可以是XML格式或二進制格式,也可以在這兩種格式間轉換,
- Plist支持數據類型有NSData、NSDate、NSNumber、NSString、NSArray和NSDictionary,writeToFile:atomically: 方法會自動檢測數據類型,如果不是這些類型,會返回false;反之,返回true。
將cSwitch
、textField
的數據保存到constrolState
,如下所示:
- (IBAction)controlValueChanged:(id)sender
{
...
else if (sender == self.cSwitch)
{
[self.controlState setValue:[NSNumber numberWithBool:self.cSwitch.isOn] forKey:switchEnabledKey];
}
else if (sender == self.textField)
{
[self.controlState setValue:self.textField.text forKey:textFieldKey];
}
// 使用plist保存controlState詞典。
NSURL *controlStateURL = [self urlForDocumentDirectoryWithPathComponent:controlStateComponent];
[self.controlState writeToURL:controlStateURL atomically:YES];
}
向空數組發送
firstObject
,返回nil
,程序不會崩潰;向空數組發送objectAtIndex:0
,會導致程序崩潰。
這篇文章中,需要多次獲取NSDocumentDirectory
路徑,創建了urlForDocumentDirectoryWithPathComponent:
方法:
- (NSURL *)urlForDocumentDirectoryWithPathComponent:(NSString *)pathComponent {
NSURL *documentDirectory = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
return [documentDirectory URLByAppendingPathComponent:pathComponent];
}
使用NSSearchPathForDirectoriesInDomains
方法可以創建NSString
類型路徑,但應當優先考慮使用NSURL
類型路徑。NSURL
類在定位目錄、文件上更為強大,且可以同時管理網絡資源和本地文件系統。想要更全面學習iOS文件系統,可以查看使用NSFileManager管理文件系統這篇文章。
4. 使用Archived Objects歸檔數據
- 保存的數據是二進制格式。
- 繼承時,需要調用 [super encodeWithCoder:encoder]和[super initWithCoder:decoder]方法。
- 必須遵守NSCoding協議,實現下面兩個方法。
- (void)encodeWithCoder:(NSCoder *)encoder //歸檔
{
[encoder encodeObject:obj1 forKey:@"obj1Key"];
[encoder encodeInt:anInt forKey:@"IntValueKey"];
[encoder encodeFloat:aFloat forKey:@"FloatValueKey"];
}
-(instancetype)initWithCoder:(NSCoder *)decoder // 解檔
{
if (!(self = [super init]))
return nil;
obj1 = [decoder decodeObjectForKey:@"obj1Key"];
anInt = [decoder decodeObjectForKey:@"IntValueKey"];
aFloat = [decoder decodeObjectForKey:@"FloatValueKey"];
}
在我們這個demo里,沒有自定義數據類型,不需要實現上面兩個方法。
繼續在controlValueChanged:
中,保存slider1
、slider2
和slider3
值到sliderValues
,如下所示:
- (IBAction)controlValueChanged:(id)sender
{
...
else if (sender == self.slider1)
{
[self.sliderValue setValue:[NSNumber numberWithFloat:self.slider1.value] forKey:slider1Key];
// Update progress bar with slider
[self.progressBar setProgress:self.slider1.value];
[self.controlState setValue:[NSNumber numberWithFloat:self.progressBar.progress] forKey:progressBarKey];
}
else if (sender == self.slider2)
[self.sliderValue setValue:[NSNumber numberWithFloat:self.slider2.value] forKey:slider2Key];
else if (sender == self.slider3)
[self.sliderValue setValue:[NSNumber numberWithFloat:self.slider3.value] forKey:slider3Key];
else
return ;
...
// 使用歸檔保存sliderValue詞典。
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:self.sliderValue forKey:sliderValueKey];
[archiver finishEncoding];
NSURL *dataURL = [self urlForDocumentDirectoryWithPathComponent:archivedDataComponent];
if (![data writeToURL:dataURL atomically:YES]) {
NSLog(@"Couldn't write to dataURL");
}
}
5. 保存文本文件
把字符串保存為文本文件很簡單。人們可以讀取文本文件,并且文本文件是夸平臺數據類型,在textView
保存文本文件需要 遵守UITextViewDelegate
協議。另外,使用textField
也需要遵守UITextFieldDelegate
協議,以便點擊return時,可以隱藏鍵盤。在ViewController.m
文件私有接口部分聲明遵守UITextFieldDelegate
和UITextViewDelegate
協議,并設置代理。更新后如下:
// 聲明遵守UITextViewDelegate、UITextFieldDelegate協議
@interface ViewController () <UITextViewDelegate, UITextFieldDelegate>
- (void)viewDidLoad {
[super viewDidLoad];
// 設置代理
self.textField.delegate = self;
self.textView.delegate = self;
// 設置textView背景色,textField占位符。
self.textView.backgroundColor = [UIColor lightGrayColor];
self.textField.placeholder = @"Text Field";
}
- (void)textViewDidEndEditing:(UITextView *)textView {
// 編輯完成后,保存文本文件。
NSString *textViewContents = textView.text;
NSURL *fileURL = [self urlForDocumentDirectoryWithPathComponent:textViewComponent];
[textViewContents writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
-(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
if ([text isEqualToString:@"\n"]) {
[textView resignFirstResponder];
}
return true;
}
// 隱藏鍵盤
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
if ([textField isFirstResponder]) {
[textField resignFirstResponder];
}
return YES;
}
7. 恢復數據
每次啟動app時,在viewWillAppear:
內恢復數據。
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:true];
// 讀取偏好設置中segment設置。
NSInteger selectedSegmentIndex = [[NSUserDefaults standardUserDefaults] integerForKey:selectedSegmentKey];
self.segments.selectedSegmentIndex = selectedSegmentIndex;
// 恢復偏好設置中spinner狀態設置。
if ([[NSUserDefaults standardUserDefaults] boolForKey:spinnerAnimatingKey] == true)
{
[self.spinner startAnimating];
[self.spinningButton setTitle:@"Stop Animating" forState:UIControlStateNormal];
}
else
{
[self.spinner stopAnimating];
[self.spinningButton setTitle:@"Start Animating" forState:UIControlStateNormal];
}
// 從plist恢復controlState詞典。
NSURL *controlStateURL = [self urlForDocumentDirectoryWithPathComponent:controlStateComponent];
if (controlStateURL) {
// 如果url存在,讀取保存的數據。
self.controlState = [NSMutableDictionary dictionaryWithContentsOfURL:controlStateURL];
}
if ([[self.controlState allKeys] count] != 0)
{
// 如果詞典不為空,恢復數據。
[self.cSwitch setOn:[[self.controlState objectForKey:switchEnabledKey] boolValue]];
self.progressBar.progress = [[self.controlState objectForKey:progressBarKey] floatValue];
self.textField.text = [self.controlState objectForKey:textFieldKey];
}
// 讀取歸檔,恢復sliderValue詞典內容。
NSURL *dataURL = [self urlForDocumentDirectoryWithPathComponent:archivedDataComponent];
if (dataURL) {
// 如果url存在,讀取保存的數據。
NSMutableData *data = [[NSMutableData alloc] initWithContentsOfURL:dataURL];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
self.sliderValue = [unarchiver decodeObjectForKey:sliderValueKey];
}
if (self.sliderValue.allKeys.count != 0)
{
// 如果詞典不為空,恢復數據。
self.slider1.value = [[self.sliderValue objectForKey:slider1Key] floatValue];
self.slider2.value = [[self.sliderValue objectForKey:slider2Key] floatValue];
self.slider3.value = [[self.sliderValue objectForKey:slider3Key] floatValue];
}
// 讀取文本文件,恢復textView內容。
NSURL *textViewContentsURL = [self urlForDocumentDirectoryWithPathComponent:textViewComponent];
if (textViewContentsURL) {
NSString *textViewContents = [NSString stringWithContentsOfURL:textViewContentsURL encoding:NSUTF8StringEncoding error:nil];
self.textView.text = textViewContents;
}
}
現在,已經可以通過偏好設置、屬性列表、歸檔解檔保存數據、恢復數據。
文件名稱:Persistence
源碼地址:https://github.com/pro648/BasicDemos-iOS