使用偏好設置、屬性列表、歸檔解檔保存數據、恢復數據

數據持久化就是將文件保存到硬盤,以便下次運行時可以讀取或永久保存。iOS提供了以下幾種持久化方案:

  • NSUserDefaults (偏好設置)
  • property list 即Plist (屬性列表)
  • NSKeyedArchiver NSKeyedUnarchiver(歸檔、解檔)
  • text file
  • SQL databases
  • Core Data

在這個demo中,我們將使用前面四種方法保存數據、恢復數據。

1. 創建Demo

在Xcode中創建一個Single View Application模板的應用,創建后在storyboard中添加控件,如下圖所示:

DataPersistence.png

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;

初始化controlStatesliderValue屬性,如下所示:

- (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:方法是從cSwitchtextFieldslider1slider2slider3拖拽出IBAction事件。

3. 使用Plist保存數據

  • Plist是一個標準的保存文本和設置的方式,Plist的數據可以是XML格式或二進制格式,也可以在這兩種格式間轉換,
  • Plist支持數據類型有NSData、NSDate、NSNumber、NSString、NSArray和NSDictionary,writeToFile:atomically: 方法會自動檢測數據類型,如果不是這些類型,會返回false;反之,返回true。

cSwitchtextField的數據保存到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:中,保存slider1slider2slider3值到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 文件私有接口部分聲明遵守UITextFieldDelegateUITextViewDelegate協議,并設置代理。更新后如下:

// 聲明遵守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

歡迎更多指正:https://github.com/pro648/tips/wiki

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • 目錄 沙盒及其結構 Preference(偏好設置)-Plist 屬性列表-Plist NSKeyedArchiv...
    Ryan___閱讀 500評論 0 0
  • 1、 沙盒概念基本介紹 iOS 應用程序只能在該 app 的文件系統中讀取。這個默認的 app 文件系統就是我們說...
    Laughingg閱讀 2,731評論 2 10
  • 前言: 在程序開發中,數據層永遠是程序的核心結構之一。對這些數據的加工處理是代碼中能體現技術水平的一大模塊,比如數...
    麥穗0615閱讀 1,469評論 3 22
  • 一回首, 竟是你。 我便再也不能平靜。 不敢讀你的眼睛, 又不忍你忽略了我。 因我, 已心生漣漪。
    湍河故事閱讀 359評論 1 2
  • 之前太年輕,潛意識里總認為所有的事都原是可以一蹴而就的,為此著實吃了不少妄自菲薄的苦楚。近來已深明自己并非聰...
    黃義達閱讀 490評論 0 1