首页 关于 微信公众号
欢迎关注我的微信公众号

iOS 入门(5):在本地存储数据

本文主要讲解如何使用 SQLite + FMDB 来存储数据到本地数据库。本文的内容只包含最基本的知识点。

教程

目录:

1、引用 FMDB 库

修改 Podfile 如下来增加对 FMDB 库的引用:

source 'https://github.com/CocoaPods/Specs.git'

platform :ios, "8.0"
target "iOSStartDemo" do
    pod 'SVProgressHUD', '1.1.3'
    pod 'Masonry', '0.6.3'
    pod 'AFNetworking', '3.0.4'
    pod 'SDWebImage', '3.7.5'
    pod 'FMDB', '2.6'
end

然后在项目目录下执行下列命令来安装新增的库:

$ pod install

代码解释:FMDB 是一个帮助我们更方便的使用 SQLite 数据库的库。

2、使用 SQLite 数据库来存储本地数据

在上一节教程中,我们实现了从网络请求数据。在这一节中,我们的需求是每次从网络请求数据成功后,将本次请求到的数据存储到本地数据库,并清理掉上一次存储的数据。在每次加载页面时,首先从本地数据库加载数据进行展示,再从网络请求数据对页面进行刷新。

在项目中添加新代码文件:

项目代码如下:

image

2.1、实现数据库管理代码 STDBManager

STDBManager.h 代码如下:

#import <Foundation/Foundation.h>

@interface STDBManager : NSObject

@property (strong, readonly, nonatomic) NSString *dbFilePath;

+ (instancetype)sharedInstance;
- (void)setupDB;

@end

STDBManager.m 代码如下:

#import "STDBManager.h"
#import <FMDB/FMDB.h>

static NSString * const STDBFileName = @"stdb.sqlite";

@interface STDBManager ()

@property (strong, readwrite, nonatomic) NSString *dbFilePath;

@end

@implementation STDBManager

#pragma mark - Property
- (NSString *)dbFilePath {
    if (!_dbFilePath) {
        NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *documentFolderPath = searchPaths[0];
        _dbFilePath = [documentFolderPath stringByAppendingPathComponent:STDBFileName];
    }
    
    return _dbFilePath;
}

#pragma mark - Lifecycle
+ (instancetype)sharedInstance {
    static STDBManager *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    
    return sharedInstance;
}

#pragma mark - Utility
- (void)setupDB {
    if ([self isDBFileExist]) {
        return;
    }
    
    BOOL isSuccess = [self copyDBFileFromMainBundle];
    if (isSuccess) {
        return;
    }
    
    if ([self createDB]) {
        [self updateDB];
    }
}

- (BOOL)isDBFileExist {
    return [[NSFileManager defaultManager] fileExistsAtPath:self.dbFilePath];
}

- (BOOL)copyDBFileFromMainBundle {
    BOOL result = NO;
    
    // Not find db file, need to copy backup db file.
    NSString *backupDBPath = [[NSBundle mainBundle] pathForResource:@"stdb" ofType:@"sqlite"];
    if (!backupDBPath) {
        // Not find backup db file.
        result = NO;
    } else {
        BOOL isSuccess = [[NSFileManager defaultManager] copyItemAtPath:backupDBPath toPath:self.dbFilePath error:nil];
        if (!isSuccess) {
            // Copy backup db file failed.
            result = NO;
        } else {
            result = YES;
        }
    }
    
    return result;
}

- (BOOL)createDB {
    if ([FMDatabase databaseWithPath:self.dbFilePath]) {
        return YES;
    }
    
    return NO;
}

- (void)updateDB {
    FMDatabaseQueue *dbQ = [FMDatabaseQueue databaseQueueWithPath:self.dbFilePath];
    [dbQ inTransaction:^(FMDatabase *db, BOOL *rollback) {
        [db executeUpdate:[NSString stringWithFormat:@"Create Table If Not Exists st_movie (rowid integer primary key not null, name text, year text, synopsis text, thumbnail_url text)"]];
    }];
}

@end

代码解释:STDBManager 主要是提供对本地数据库文件的管理功能,包括:创建数据库文件、初始化数据库表结构等等。

2.2、实现 Model/Entity 层代码

这里我们复用上一节教程中的 STMovie 类。

2.2、实现 Model/Service/LocalService 层代码

STMovieLocalService.h 代码如下:

#import <Foundation/Foundation.h>
#import "STMovie.h"

@interface STMovieLocalService : NSObject

#pragma mark - STMovieLocalService
+ (NSArray *)getAllMovies;
+ (int64_t)addOrUpdateMovie:(STMovie *)movie;
+ (BOOL)removeAllMovies;

@end

STMovieLocalService.m 代码如下:

#import "STMovieLocalService.h"
#import <FMDB/FMDB.h>
#import "STDBManager.h"

@implementation STMovieLocalService

#pragma mark - STMovieLocalService
+ (NSArray *)getAllMovies {
    NSMutableArray * __block objs = [[NSMutableArray alloc] init];
    
    NSString *sql = @"Select * From st_movie";
    FMDatabaseQueue *dbQ = [FMDatabaseQueue databaseQueueWithPath:[STDBManager sharedInstance].dbFilePath];
    [dbQ inDatabase:^(FMDatabase *db) {
        FMResultSet *resultSet = [db executeQuery:sql];
        while ([resultSet next]) {
            [objs addObject:[STMovieLocalService getObjectFromResultSet:resultSet]];
        }
        [resultSet close];
    }];
    
    return [objs copy];
}

+ (int64_t)addOrUpdateMovie:(STMovie *)movie {
    int64_t __block result = -1;
    
    NSString *sql = [NSString stringWithFormat:@"Replace Into st_movie (rowid, name, year, synopsis, thumbnail_url) Values (?, ?, ?, ?, ?)"];
    FMDatabaseQueue *dbQ = [FMDatabaseQueue databaseQueueWithPath:[STDBManager sharedInstance].dbFilePath];
    [dbQ inDatabase:^(FMDatabase *db) {
        if ([db executeUpdate:sql, [NSNumber numberWithLongLong:movie.rowid], movie.name, movie.year, movie.synopsis, movie.thumbnailImageURLString]) {
            result = (int64_t) [db lastInsertRowId];
        }
    }];
    
    return result;
}

+ (BOOL)removeAllMovies {
    BOOL __block result = NO;
    
    NSString *sql = @"Delete From st_movie";
    FMDatabaseQueue *dbQ = [FMDatabaseQueue databaseQueueWithPath:[STDBManager sharedInstance].dbFilePath];
    [dbQ inDatabase:^(FMDatabase *db) {
        result = [db executeUpdate:sql];
    }];
    
    return result;
}

#pragma mark - Utility
+ (STMovie *)getObjectFromResultSet:(FMResultSet *)resultSet {
    STMovie *movie = [[STMovie alloc] init];
    
    movie.rowid = [resultSet longLongIntForColumn:@"rowid"];
    movie.name = [resultSet stringForColumn:@"name"];
    movie.year = [resultSet stringForColumn:@"year"];
    movie.synopsis = [resultSet stringForColumn:@"synopsis"];
    movie.thumbnailImageURLString = [resultSet stringForColumn:@"thumbnail_url"];
    
    return movie;
}

@end

代码解释:这里主要封装了对本地数据库的 st_movie 表的数据进行增删查改的相关接口。

2.3、完成 Controller 层代码逻辑

修改 STExploreViewController.m 代码如下:

#import "STExploreViewController.h"
#import "STCommonUtil.h"
#import <SVProgressHUD/SVProgressHUD.h>
#import <Masonry/Masonry.h>
#import <SDWebImage/UIImageView+WebCache.h>
#import "STMovieWebService.h"
#import "STDBManager.h"
#import "STMovieLocalService.h"

static NSString * const STExploreCellIdentifier = @"STExploreCellIdentifier";

@interface STExploreViewController () <UITableViewDataSource, UITableViewDelegate>

@property (strong, nonatomic) UITableView *myTableView;
@property (strong, nonatomic) NSArray *movieList;

@end

@implementation STExploreViewController

#pragma mark - Property
- (UITableView *)myTableView {
    if (!_myTableView) {
        _myTableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
        _myTableView.delegate = self;
        _myTableView.dataSource = self;
    }
    
    return _myTableView;
}

#pragma mark - Lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // Setup.
    [[STDBManager sharedInstance] setupDB];
    [self setupUI];
    
    // Load data.
    [self requestData];
}

#pragma mark - Setup
- (void)setupUI {
    // Use full screen layout.
    self.edgesForExtendedLayout = UIRectEdgeAll;
    self.automaticallyAdjustsScrollViewInsets = YES;
    self.extendedLayoutIncludesOpaqueBars = YES;
    
    // myTableView.
    [self.view addSubview:self.myTableView];
    [self.myTableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
}

#pragma mark - Utility
- (void)requestData {
    // Load data from local db.
    self.movieList = [STMovieLocalService getAllMovies];
    if (self.movieList.count > 0) {
        [self.myTableView reloadData];
    }
    
    // Load new data from server and update local db data.
    NSDictionary *parameters = @{@"pageLimit" : @30, @"pageNum" : @1};
    [STMovieWebService requestMovieDataWithParameters:parameters start:^{
        [SVProgressHUD show];
    } success:^(NSDictionary *result) {
        NSArray *newMovieList = [result objectForKey:@"movieList"];

        // Update local db data.
        [STMovieLocalService removeAllMovies];
        for (STMovie *movie in newMovieList) {
            [STMovieLocalService addOrUpdateMovie:movie];
        }
        
        // Update displayed data.
        self.movieList = newMovieList;
        [self.myTableView reloadData];
        [SVProgressHUD dismiss];
    } failure:^(NSError *error) {
        [SVProgressHUD dismiss];
    }];
}

#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 50;
}

#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSString *) tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    return @"Movies";
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.movieList.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row >= self.movieList.count) {
        return nil;
    }
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:STExploreCellIdentifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:STExploreCellIdentifier];
    }
    
    STMovie *movie = [self.movieList objectAtIndex:indexPath.row];
    
    cell.textLabel.text = [NSString stringWithFormat:@"%@ - %@", movie.name, movie.year];
    cell.detailTextLabel.text = movie.synopsis;
    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:movie.thumbnailImageURLString] placeholderImage:[STCommonUtil imageWithColor:[UIColor grayColor] size:CGSizeMake(27, 40)] completed:nil];
    cell.layer.shouldRasterize = YES;
    cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
    
    return cell;
}

@end

代码解释:主要修改了 - (void)requestData 方法中的逻辑来完成我们前面说的需求:每次从网络请求数据成功后,将本次请求到的数据存储到本地数据库,并清理掉上一次存储的数据。在每次加载页面时,首先从本地数据库加载数据进行展示,再从网络请求数据对页面进行刷新。

运行项目你应该能看到下面的界面:

image

Demo

你可以接着前面的教程继续下面的步骤来获取这一节对应的 Demo 代码:

如果你还没有下载 iOSStartDemo,请先执行下列命令下载:

$ git clone https://github.com/samirchen/iOSStartDemo.git
$ cd iOSStartDemo/iOSStartDemo

如果已经下载过了,则直接进入正确的目录并执行下列命令:

$ git fetch origin s5
$ git checkout s5
$ pod install
$ open iOSStartDemo.xcworkspace

Blog

Opinion

Project