iOS 儲存資料與 SQLite 使用範例

資料儲存至硬碟的必要性

在 iOS App 開發時,有許多情況會需要存些東西在硬碟裡:

登入資訊

對於需要登入的App,為避免每次使用都要重新登入一次,就會需要把 Access Token 甚至是帳密存進硬碟裡。當然為了安全性考量,系統應該避免直接將密碼儲存,而應該設計 Access Token 的作法比較理想。

檔案暫存

已經下載過的圖檔或是資料檔,避免重覆下載浪費頻寬,就需要存在硬碟裡。對於使用非吃到飽的資費方案的使用者,這是很實用的功能。

資料庫暫存

對於許多需要作多面向的搜尋的 App 而言,每一個搜尋都要跟 Server 尋問會導致執行速度低落,以及頻寬的浪費。此時即可將問到的資料都先行存在硬碟裡,搜尋時先搜尋硬碟裡的資料,同時向 Server 尋問是否資料有更新。

多步驟程序

有許多時候,有些任務會需要依序執行數個步驟,比如依順序向伺服器發出多次命令,如果能將已執行完成的命令結果存在硬碟裡,可以避免重複命令,因此能減少頻寬,增進執行效率。

頁面位置暫存

記錄使用者上一次結束 App 時的頁面,而在 App 一開始時即跳至該頁面,會是一個不錯的貼心小功能。

背景App記憶體被釋放

當使用者切換至另一個很吃記憶體的App而導致App 進入背景時,不保證記憶體不會被適放。若沒有儲存至硬碟,將會導致重要的資料消失。

App 閃退

理論上,一個測試完整的 App 不應該存在閃退的情況,然而實際上一方面在測試完整之前 App 還是要能給內部外部使用者使用(這也是測試的一環),而另一方面,總是難免會有許多意外的情況出現;而身為一個 App 的設計者,會希望在即始偶爾閃退的情況下,App 還能夠維持它能夠使用的程度。比如說,如果輸入帳密之後,若不幸的會有10%的情況下會閃退,若 App 有將資訊存至硬碟,這10%的人再重開 App 時,並不需要再次輸入帳密,就可以將不適的感覺降至最低,也多少爭取了修正的時間。

iOS App 儲存資料方案

儲存的方案主要有下列幾種:

1. NSUserDefault

適合用來存輕量的 key-value 資料,比如說 token, user account info, app status 等等。

Write 範例

    [[NSUserDefaults standardUserDefaults] setObject:valueString forKey:keyString];
    [[NSUserDefaults standardUserDefaults] synchronize];  // save to disk

Read 範例

    NSString *value=[[NSUserDefaults standardUserDefaults] objectForKey:keyString];

2. 寫入 local file

適合放圖檔或是其它比較大的檔案。要使用這個方法,要先對於 App 的檔案存取限制路徑要有基本的了解,對於本機來說,常會使用到的檔案主要為 NSBundle 裡的檔案,以及 Document folder 裡檔案:

NSBundle

NSBundle 裡的檔案,也就是在 Project 裡有匯入的檔案,App 可以讀取,但是無法寫入。這點在後面要使用 SQLite 資料庫時也需要注意。

Document folder

另一個可以利用的檔案目錄,則是NSDocumentDirectory。App可以對這個目錄底下的檔案作完全的控制。
每個 App 都會有其配置的NSDocumentDirectory,不同 App 之間無法互通。電腦透過 iTunes 程式,可以將檔案放至 App 的 NSDocumentDirectory底下。這個目錄在 App 被移除時也會一併消滅。

一般的作法,是將設定檔案先放在 NSBundle 裡,而在 App 初始化時,檢查 Document folder 裡是否有同名的檔案,若沒有就將 NSBundle 裡的檔案複製至 Document folder 下,接下來就可以存取這個檔案了。

Read 範例

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *filePath= [documentsDirectory stringByAppendingPathComponent:fileName];
    UIImage *image = [[UIImage alloc] initWithContentsOfFile:filePath];

Write 範例

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *filePath= [documentsDirectory stringByAppendingPathComponent:fileName];
    NSData *imageData=UIImagePNGRepresentation(crayImage);
    if(imageData){
        [imageData writeToFile:filePath atomically:YES];
   }

3. Core Data

iOS 自定的一個資料服務,近似物件資料庫不過不完全相同。使用上直接以 Objective-C 物件方式來進行,對新手來說一開始會比較難上手,若能夠熟悉的話,也會有相當大的幫助。

Read 範例

-(NSArray *) getMemberNamed:(NSString *) name
{
    AppDelegate *appDelegate=(AppDelegate *)[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *managedObjectContext=[appDelegate managedObjectContext];
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Member" inManagedObjectContext: managedObjectContext];
    [fetchRequest setEntity:entity];
    
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name=%@", name];
    [fetchRequest setPredicate:predicate];
    
    NSError *error = nil;
    NSArray *fetchedObjects = [ managedObjectContext executeFetchRequest:fetchRequest error:&error];
    if (fetchedObjects == nil) {
        NSLog(@"Fetch error, something's wrong. %@",error);
    }
    return fetchedObjects;
    
}

Write 範例

NSArray *orangeMemberList=[self getMemberNamed:@"Orange"];
if(orangeMemberList){
    for (Member *member in orangeMemberList) {
        member.name=@"Apple";
    }
}
NSError *error = nil;

if (![managedObjectContext save:&error]) {
    NSLog(@"[ERROR] Saving managed context error: %@",error);
}else{
    NSLog(@"Managed context saved.");
}

4. SQLite 資料庫

一個輕量方便好用的關連式資料庫。對於熟悉關聯式資料庫與SQL語法的開發者們來說,這是個不錯的選擇。

本文中介紹 SQLite 在 iOS App 中的用法與範例。

SQLite簡介

SQLite是一個簡易的關聯式資料庫(Relational Database)。在這波 App 開發的浪潮中,SQLite受到廣大的歡迎。它使用單一的檔案來運作,以函式庫的方式直在內建在 App 裡,所需增加的空間不大,由於不需開連線所以運作快速。在iOS, Android, Symbian, WebOS 等都可以使用 SQLite。SQLite 在 iOS 中已有內建函式庫,使用上十分方便。

使用步驟

1. 加入 Framework

Framework 裡加入 libsqlite3.tbd

疑問一:為何這裡有 libsqlite3.tbd 與 libsqlite3.0.tbd?二者有何不同?
答:libsqlite3.tbd是連結至「最新的sqlite library」,而目前最新的是libsqlite3.0.tbd,所以雖然加二者的效果相同,但是為了維護性,應該加 libsqlite3.tbd

疑問二:為何早先的 XCode 版本是使用 libsqlite3.dylib 而現在則是 libsqlite3.tbd?
答:新版的 XCode 改使用 tbd 檔來減少檔案大小

2. 匯入header 檔

法一:在要使用的.h檔裡加上 #import <sqlite3.h>
法二:在<你的專案名稱>-prefix.pch檔裡加上#import <sqlite3.h>
若只有一二個檔案會用到 SQLite,則使用法一;否則就使用法二

3. 建立sqlite檔案法一:預先建立再複製法

3.1 先建立好 sqlite 檔案

打開終端機,然後輸入

sqlite3 member.db

在 sqlite3> 下,輸入

CREATE TABLE IF NOT EXISTS MEMBER (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, address TEXT, phone TEXT);

然後打下面的指令離開

.quit

然後就可以看到 member.db 這個檔案

3.2 將它加入 project Bundle 裡

3.3 在一APP開啟時,將這個檔案複製到 App 目錄下

        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSString *bundlePath = [[NSBundle mainBundle] pathForResource:databaseName ofType:NULL];
        [fileManager copyItemAtPath:bundlePath toPath:databasePath error:&error];

3. 建立sqlite檔案法二:程式從頭建立法

在程式一開始檢查有沒有 sqlite 檔案,若沒有的話進行初始化(Create Database Tables…)

-(BOOL) createDatabase:(NSString *)databaseName{
    NSString *docsDir;
    NSArray *dirPath;
    sqlite3 *db;
    
    // Get the documents directory
    dirPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    docsDir = [dirPath objectAtIndex:0];
    
    // Build the path to the database file
    NSString *databasePath = [[NSString alloc] initWithString: [docsDir stringByAppendingPathComponent: databaseName]];
    
    NSFileManager *filemgr = [NSFileManager defaultManager];
    
    if ([filemgr fileExistsAtPath: databasePath ] == NO) {
        const char *dbpath = [databasePath UTF8String];
        
        if (sqlite3_open(dbpath, &db) == SQLITE_OK) {
            char *errMsg;
            // create SQL statements
            const char *sql = "CREATE TABLE IF NOT EXISTS MEMBER (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, address TEXT, phone TEXT)";
            
            if (sqlite3_exec(db, sql, NULL, NULL, &errMsg) != SQLITE_OK) {
                NSLog( @"Failed to create table");
                return NO;
            }
            sqlite3_close(db);
            return YES;
        }
        else {
            NSLog( @"Failed to open/create database");
            return NO;
        }
    }else{
        NSLog(@"Database already created.");
        return YES;
    }
}

4. 使用範例

4.1 新增

            char *errorMsg;
            const char *insertSql="insert into member(name,company) values('Orange','iOTEC Systems')";
            if (sqlite3_exec(db, insertSql, NULL, NULL, &errorMsg)==SQLITE_OK) {
                NSLog(@"INSERT OK");
            }else{
                NSLog(@"Insert error: %s",errorMsg);
            }

4.2 查詢

            const char *sql = "select * from member";
            sqlite3_stmt *statement =nil;
            if (sqlite3_prepare_v2(db, sql, -1, &statement, NULL) == SQLITE_OK) {
                while (sqlite3_step(statement) == SQLITE_ROW) {
                    
                    NSString *_id,*name, *company;
                    
                    _id = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 0)];
                    name = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 1)];
                    company = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 2)];
                    
                    NSLog(@"Record: %@> %@ , %@",_id, name, company);
                }
                
                sqlite3_finalize(statement);
            }

4.3 修改

            char *errorMsg;
            const char *sql = "UPDATE member SET name='Apple' WHERE name='Orange'";
            
            if (sqlite3_exec(db, sql, NULL, NULL, &errorMsg)==SQLITE_OK) {
                NSLog(@"UPDATE OK");
            }else{
                NSLog(@"UPDATE error: %s",errorMsg);
            }

4.4 刪除

            char *errorMsg;
            const char *sql = "DELETE FROM member WHERE name='Apple'";
            
            if (sqlite3_exec(db, sql, NULL, NULL, &errorMsg)==SQLITE_OK) {
                NSLog(@"DELETE OK");
            }else{
                NSLog(@"DELETE error: %s",errorMsg);
            }

範例程式請參考SQLite example on GitHub

Leave a Reply

Please rate*

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.