本篇随笔除了介绍 iOS 数据持久化知识之外,还贯穿了以下内容:

(1)自定义 TableView,结合 block 从 ViewController 中分离出 View,轻 ViewController 的实现,提高 TableView 的复用性

(2)Model「实体层」+View「视图层」+ViewController「视图控制器层」+Service「服务层」+Database「数据库层」,MVC 多层架构的实现

(3)TableView 单元格滑动手势控制显示实用按钮,实用按钮为「修改」和「删除」

(4)模糊背景效果

(5)密码保护功能

现在,让我们脚踏实地一步一个脚印地 get 技能吧!

在 iOS 开发中,数据持久化是很重要的一块内容,有必要多加了解并合理地使用他。什么是数据持久化呢?

数据持久化就是将数据保存到移动设备本地存储内,使得 App 或移动设备重启后可以继续访问之前保存的数据。

实现数据持久化的方法有如下几种:

(1)plist 文件「属性列表」

(2)preference「偏好设置」

(3)NSKeyedArchiver「归档」

(4)Keychain Services「钥匙串服务」

(5)SQLite

(6)CoreData

(7)FMDB

工程结构:

效果如下:

1. 沙盒

在了解各种实现数据持久化的方法前,我们先了解什么是 App 的沙盒机制。App 在默认情况下只能访问自己的目录,这个目录就被称为「沙盒」,意思就是隔离的空间范围只有 App 有访问权。Apple 之所以这样设计,主要是为了安全性和管理方面考虑。

1.1 目录结构

1.2 目录特性

沙盒目录下的不同目录有各自的特性,了解他们的特性,我们才能更合理地选择存储数据的目录。

(1)Bundle Path「App 包」:虽然他不属于沙盒目录,但也是有必要了解下的。存放的是编译后的源文件,包括资源文件和可执行文件。

使用场景比如:以不缓存的形式读取项目图片资源文件。

(2)Home「App 沙盒根目录」

(3)Home > Documents「文档目录」:最常用的目录,iTunes 同步该 App 时会同步此目录中的内容,适合存储重要数据。

使用场景比如:存储 SQLite 生成的数据库文件。

(4)Home > Library「库目录」

(5)Home > Library > Caches「缓存目录」:iTunes 同步该 App 时不会同步此目录中的内容,适合存储体积大、不需要备份的非重要数据。

使用场景比如:读取网络图片时,使用 AFNetworking 的 UIImageView+AFNetworking 或者 SDWebImage 的 UIImageView+WebCache 缓存图片文件。

(6)Home > Library > Preferences「偏好设置目录」:iTunes 同步该 App 时会同步此目录中的内容,通常保存 App 的偏好设置信息。

使用场景比如:记录已登录的 App 用户的信息,非敏感信息,比如已登录状态。

(7)Home > tmp「临时目录」:iTunes 同步该 App 时不会同步此目录中的内容,系统可能在 App 没运行的某个时机就删除该目录的文件,适合存储临时性的数据,用完就删除。

使用场景比如:读取网络图片时,一般会先把图片作为临时文件下载存储到此目录,然后再拷贝临时文件到 Caches「缓存目录」下,拷贝完成后再删除此临时文件。这样做的目的是保证数据的原子性,我们常用的保存数据到文件的方法:writeToFile: ,常常设置 atomically 参数值为 YES 也是这样的目的。

1.3 那么如何获取这些目录呢?

 1 /// Bundle Path
 2 NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
 3 // 「/private/var/mobile/Containers/Bundle/Application/5DED9788-D62A-4AF7-9DF8-688119007D90/KMDataAccess.app」
 4 NSLog(@"%@", bundlePath);
 5
 6 /// Home
 7 NSString *homeDir = NSHomeDirectory();
 8 // 「/var/mobile/Containers/Data/Application/B6C462E3-D532-43B4-883A-E98350261999」
 9 NSLog(@"%@", homeDir);
10
11 /// Home > Documents
12 NSString *documentsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
13 // 「/var/mobile/Containers/Data/Application/B6C462E3-D532-43B4-883A-E98350261999/Documents」
14 NSLog(@"%@", documentsDir);
15
16 /// Home > Library
17 NSString *libraryDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
18 // 「/var/mobile/Containers/Data/Application/B6C462E3-D532-43B4-883A-E98350261999/Library」
19 NSLog(@"%@", libraryDir);
20
21 /// Home > Library > Caches
22 NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
23 // 「/var/mobile/Containers/Data/Application/B6C462E3-D532-43B4-883A-E98350261999/Library/Caches」
24 NSLog(@"%@", cachesDir);
25
26 /// Home > tmp
27 NSString *tmpDir = NSTemporaryDirectory();
28 // 「/var/mobile/Containers/Data/Application/B6C462E3-D532-43B4-883A-E98350261999/tmp/」
29 NSLog(@"%@", tmpDir);

PS:Preferences 没有相对应的取目录方法,因为该目录主要存储用户「偏好设置」信息,可以直接通过键值对进行读写访问,因此也不需要获取目录。

2. 数据持久化的方法

2.1 plist 文件「属性列表」

序列化操作:可以直接进行文件存取的类型有「NSArray」「NSDictionary」「NSString」「NSData」。

1、plist 文件「属性列表」内容是以 XML 格式存储的,在写入文件操作中,「NSArray」和「NSDictionary」可以正常识别,而对于「NSString」有中文的情况由于进行编码操作所以无法正常识别,最终「NSString」和「NSData」一样只能作为普通文件存储。无论普通文件还是 plist 文件,他们都是可以进行存取操作的。

2、plist 文件中,「NSArray」和「NSDictionary」的元素可以是 NSArray、NSDictionary、NSString、NSDate、NSNumber,所以一般常见的都是以「NSArray」和「NSDictionary」来作为文件存取的类型。

3、由于他存取都是整个文件覆盖操作,所以他只适合小数据量的存取。

如何使用:

 1 #pragma mark - 序列化操作:可以直接进行文件存取的类型有「NSArray」「NSDictionary」「NSString」「NSData」
 2 - (void)NSArrayWriteTo:(NSString *)filePath {
 3     NSArray *arrCustom = @[
 4                            @"KenmuHuang 的博客:\nhttp://www.cnblogs.com/huangjianwu/",
 5                            [DateHelper localeDate],
 6                            @6
 7                            ];
 8     [arrCustom writeToFile:filePath atomically:YES];
 9 }
10
11 - (NSArray *)NSArrayReadFrom:(NSString *)filePath {
12     return [NSArray arrayWithContentsOfFile:filePath];
13 }
14
15 - (void)NSDictionaryWriteTo:(NSString *)filePath {
16     NSDictionary *dicCustom = @{
17                                 @"Name" : @"KenmuHuang",
18                                 @"Technology" : @"iOS",
19                                 @"ModifiedTime" : [DateHelper localeDate],
20                                 @"iPhone" : @6 // @6 语法糖等价于 [NSNumber numberWithInteger:6]
21                                 };
22     [dicCustom writeToFile:filePath atomically:YES];
23 }
24
25 - (NSDictionary *)NSDictionaryReadFrom:(NSString *)filePath {
26     return [NSDictionary dictionaryWithContentsOfFile:filePath];
27 }
28
29 - (BOOL)NSStringWriteTo:(NSString *)filePath {
30     NSString *strCustom = @"KenmuHuang 的博客:\nhttp://www.cnblogs.com/huangjianwu/";
31     NSError *error;
32     // 当字符串内容有中文时,通过编码操作写入,无法作为正常的 plist 文件打开,只能作为普通文件存储
33     [strCustom writeToFile:filePath
34                 atomically:YES
35                   encoding:NSUTF8StringEncoding
36                      error:&error];
37     return error != nil;
38 }
39
40 - (NSString *)NSStringReadFrom:(NSString *)filePath {
41     NSError *error;
42     NSString *strCustom = [NSString stringWithContentsOfFile:filePath
43                                                     encoding:NSUTF8StringEncoding
44                                                        error:&error];
45     return error ? @"" : strCustom;
46 }
47
48 - (void)NSDataWriteTo:(NSString *)filePath {
49     NSURL *URL = [NSURL URLWithString:kBlogImageStr];
50     NSData *dataCustom = [[NSData alloc] initWithContentsOfURL:URL];
51     // 数据类型写入,无法作为正常的 plist 文件打开,只能作为普通文件存储
52     [dataCustom writeToFile:filePath atomically:YES];
53     _imgVDetailInfo.hidden = YES;
54 }
55
56 - (NSData *)NSDataReadFrom:(NSString *)filePath {
57     return [NSData dataWithContentsOfFile:filePath];
58 }

2.2 preference「偏好设置」

preference「偏好设置」可以直接进行存取的类型有「NSArray」「NSDictionary」「NSString」「BOOL」「NSInteger」「NSURL」「float」「double」。

1、偏好设置是专门用来保存 App 的配置信息的,一般不要在偏好设置中保存其他数据。使用场景比如:App 引导页的开启控制,以存储的键值对 appVersion「App 更新后的当前版本」为准,开启条件为 appVersion 不存在或者不为当前版本,这样版本更新后,第一次打开 App 也能正常开启引导页。

2、修改完数据,如果没有调用 synchronize 来立刻同步数据到文件内,系统会根据 I/O 情况不定时刻地执行保存。

3、偏好设置实际上也是一个 plist 文件,他保存在沙盒的 Preferences 目录中「Home > Library > Preferences」,文件以 App 包名「Bundle Identifier」来命名,例如本例子就是:com.kenmu.KMDataAccess.plist。

如何使用:

 1 - (IBAction)btnWriteToPressed:(id)sender {
 2     NSURL *URL = [NSURL URLWithString:kBlogImageStr];
 3
 4     NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
 5     [userDefaults setObject:@"KenmuHuang" forKey:kNameOfPreference];
 6     [userDefaults setBool:YES forKey:kIsMaleOfPreference];
 7     [userDefaults setInteger:6 forKey:kiPhoneOfPreference];
 8     [userDefaults setURL:URL forKey:kAvatarURLOfPreference];
 9     [userDefaults setFloat:6.5 forKey:kFloatValOfPreference];
10     [userDefaults setDouble:7.5 forKey:kDoubleValOfPreference];
11
12     NSArray *arrCustom = @[
13                            @"KenmuHuang 的博客:\nhttp://www.cnblogs.com/huangjianwu/",
14                            [DateHelper localeDate],
15                            @6
16                            ];
17     [userDefaults setObject:arrCustom forKey:kMyArrayOfPreference];
18
19     NSDictionary *dicCustom = @{
20                                 @"Name" : @"KenmuHuang",
21                                 @"Technology" : @"iOS",
22                                 @"ModifiedTime" : [DateHelper localeDate],
23                                 @"iPhone" : @6 // @6 语法糖等价于 [NSNumber numberWithInteger:6]
24                                 };
25     [userDefaults setObject:dicCustom forKey:kMyDictionaryOfPreference];
26     //立刻同步
27     [userDefaults synchronize];
28     _txtVDetailInfo.text = @"写入成功";
29 }
30
31 - (IBAction)btnReadFromPressed:(id)sender {
32     NSMutableString *mStrCustom = [NSMutableString new];
33
34     NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
35     [mStrCustom appendFormat:@"%@: %@\n", kNameOfPreference, [userDefaults objectForKey:kNameOfPreference]];
36     [mStrCustom appendFormat:@"%@: %@\n", kIsMaleOfPreference, [userDefaults boolForKey:kIsMaleOfPreference] ? @"YES" : @"NO"];
37     [mStrCustom appendFormat:@"%@: %ld\n", kiPhoneOfPreference, (long)[userDefaults integerForKey:kiPhoneOfPreference]];
38     [mStrCustom appendFormat:@"%@: %@\n", kAvatarURLOfPreference, [userDefaults URLForKey:kAvatarURLOfPreference]];
39     [mStrCustom appendFormat:@"%@: %f\n", kFloatValOfPreference, [userDefaults floatForKey:kFloatValOfPreference]];
40     [mStrCustom appendFormat:@"%@: %f\n", kDoubleValOfPreference, [userDefaults doubleForKey:kDoubleValOfPreference]];
41
42     [mStrCustom appendFormat:@"%@: (\n", kMyArrayOfPreference];
43     for (NSObject *obj in [userDefaults objectForKey:kMyArrayOfPreference]) {
44         [mStrCustom appendFormat:@"%@", obj];
45         if (![obj isKindOfClass:[NSNumber class]]) {
46             [mStrCustom appendString:@",\n"];
47         } else {
48             [mStrCustom appendString:@"\n)\n"];
49         }
50     }
51     [mStrCustom appendFormat:@"%@: %@\n", kMyDictionaryOfPreference, [userDefaults objectForKey:kMyDictionaryOfPreference]];
52
53     _txtVDetailInfo.text = mStrCustom;
54 }

2.3 NSKeyedArchiver「归档」

NSKeyedArchiver「归档」属于序列化操作,遵循并实现 NSCoding 协议的对象都可以通过他实现序列化。绝大多数支持存储数据的 Foundation 和 Cocoa Touch 类都遵循了 NSCoding 协议,因此,对于大多数类来说,归档相对而言还是比较容易实现的。

1、遵循并实现 NSCoding 协议中的归档「encodeWithCoder:」和解档「initWithCoder:」方法。

2、如果需要归档的类是某个自定义类的子类时,就需要在归档和解档之前先实现父类的归档和解档方法。

3、存取的文件扩展名可以任意。

4、跟 plist 文件存取类似,由于他存取都是整个文件覆盖操作,所以他只适合小数据量的存取。

如何使用:

 1 #import <Foundation/Foundation.h>
 2
 3 @interface GlobalInfoModel : NSObject <NSCoding>
 4 @property (strong, nonatomic) NSNumber *ID; ///< ID编号
 5 @property (copy, nonatomic) NSString *avatarImageStr; ///< 图标图片地址
 6 @property (copy, nonatomic) NSString *name; ///< 标题名称
 7 @property (copy, nonatomic) NSString *text; ///< 内容
 8 @property (copy, nonatomic) NSString *link; ///< 链接地址
 9 @property (strong, nonatomic) NSDate *createdAt; ///< 创建时间
10 @property (assign, nonatomic, getter=isHaveLink) BOOL haveLink; ///< 是否存在链接地址
11
12 ...
13 @end
 1 #import "GlobalInfoModel.h"
 2
 3 @implementation GlobalInfoModel
 4 ...
 5 #pragma mark - NSCoding
 6 - (void)encodeWithCoder:(NSCoder *)aCoder {
 7     // 归档
 8     [aCoder encodeObject:_ID forKey:kID];
 9     [aCoder encodeObject:_avatarImageStr forKey:kAvatarImageStr];
10     [aCoder encodeObject:_name forKey:kName];
11     [aCoder encodeObject:_text forKey:kText];
12     [aCoder encodeObject:_link forKey:kLink];
13     [aCoder encodeObject:_createdAt forKey:kCreatedAt];
14     [aCoder encodeBool:_haveLink forKey:kHaveLink];
15 }
16
17 - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
18     self = [super init];
19     if (self) {
20         // 解档
21         _ID = [aDecoder decodeObjectForKey:kID];
22         _avatarImageStr = [aDecoder decodeObjectForKey:kAvatarImageStr];
23         _name = [aDecoder decodeObjectForKey:kName];
24         _text = [aDecoder decodeObjectForKey:kText];
25         _link = [aDecoder decodeObjectForKey:kLink];
26         _createdAt = [aDecoder decodeObjectForKey:kCreatedAt];
27         _haveLink = [aDecoder decodeBoolForKey:kHaveLink];
28     }
29     return self;
30 }
31
32 @end
 1 - (IBAction)btnWriteToPressed:(id)sender {
 2     NSDictionary *dicGlobalInfoModel = @{
 3                                          kID : @1,
 4                                          kAvatarImageStr : kBlogImageStr,
 5                                          kName : @"KenmuHuang",
 6                                          kText : @"Say nothing...",
 7                                          kLink : @"http://www.cnblogs.com/huangjianwu/",
 8                                          kCreatedAt : [DateHelper localeDate]
 9                                          };
10     GlobalInfoModel *globalInfoModel = [[GlobalInfoModel alloc] initWithDictionary:dicGlobalInfoModel];
11
12     NSString *documentsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
13     NSString *filePath = [documentsDir stringByAppendingPathComponent:kNSKeyedArchiverName];
14     // 归档
15     [NSKeyedArchiver archiveRootObject:globalInfoModel toFile:filePath];
16     _txtVDetailInfo.text = @"写入成功";
17 }
18
19 - (IBAction)btnReadFromPressed:(id)sender {
20     NSString *documentsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
21     NSString *filePath = [documentsDir stringByAppendingPathComponent:kNSKeyedArchiverName];
22     // 解档
23     GlobalInfoModel *globalInfoModel = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
24
25     NSMutableString *mStrCustom = [NSMutableString new];
26     if (globalInfoModel) {
27         [mStrCustom appendFormat:@"%@: %@\n", kID, globalInfoModel.ID];
28         [mStrCustom appendFormat:@"%@: %@\n", kAvatarImageStr, globalInfoModel.avatarImageStr];
29         [mStrCustom appendFormat:@"%@: %@\n", kName, globalInfoModel.name];
30         [mStrCustom appendFormat:@"%@: %@\n", kText, globalInfoModel.text];
31         [mStrCustom appendFormat:@"%@: %@\n", kLink, globalInfoModel.link];
32         [mStrCustom appendFormat:@"%@: %@\n", kCreatedAt, globalInfoModel.createdAt];
33         [mStrCustom appendFormat:@"%@: %@\n", kHaveLink, globalInfoModel.haveLink ? @"YES" : @"NO"];
34     }
35     _txtVDetailInfo.text = mStrCustom;
36 }

另外,可以使用如下第三方库,在数据模型转换中很有帮助:

「Mantle」:https://github.com/Mantle/Mantle

「JSONModel」: https://github.com/icanzilb/JSONModel

「MJExtension」:https://github.com/CoderMJLee/MJExtension,NSObject+MJCoding 对于归档操作的实现原理是:把类对象属性都统一转换为对象类型,使用宏声明好归档「encodeWithCoder:」和解档「initWithCoder:」方法的执行内容,具体实现就是遍历类对象属性进行 encodeObject: 和 decodeObjectForKey: 操作。项目里引用他后,归档操作就很容易了。

在序列化方面,国外的朋友很会玩,他在 2.66GHz Mac Pro 上对10W条简单对象记录进行序列化和反序列化,性能的比较结果如下:

https://github.com/randomsequence/NSSerialisationTests

1 NSJSONSerialization         2.359547秒
2 NSPropertyListSerialization 3.560538秒
3 NSArchiver                  3.681572秒
4 NSKeyedArchiver             9.563317秒

顺便提下,NSJSONSerialization 的序列化方法如下:

1 // NSData 数据转为 NSDictionary
2 NSError *error;
3 NSDictionary *dicJSON = [NSJSONSerialization JSONObjectWithData:myData options:kNilOptions error:&error];
4
5 // NSDictionary 数据转为 NSData
6 NSError *error;
7 NSData *dataJSON = [NSJSONSerialization dataWithJSONObject:myDictionary options:NSJSONWritingPrettyPrinted error:&error];

2.4 Keychain Services「钥匙串服务」

Keychain Services「钥匙串服务」为一个或多个用户提供安全存储容器,存储内容可以是密码、密钥、证书、备注信息。

使用 Keychain Services 的好处有如下几点:

(1)安全性;存储的为加密后的信息。

(2)持久性;将敏感信息保存在 Keychain 中后,这些信息不会随着 App 的卸载而丢失,他在用户重装 App 后依然有效。然而,在 iPhone 中,Keychain Services「钥匙串服务」权限依赖 provisioning profile「配置文件」来标示对应的 App。所以我们在更新 App 版本时,应该注意使用一致的配置文件;否则会出现钥匙串服务数据丢失的情况。

(3)支持共享;可以实现多个 App 之间共享 Keychain 数据。

使用场景比如:App 用户登录界面提供「记住密码」功能,用户勾选「记住密码」复选框并且登录成功后,存储用户登录信息到 Keychain 里,包括特殊加密后的密码。当用户再次进入用户登录界面时,自动读取已存储到 Keychain 的用户登录信息,此时用户只需要点击「登录」按钮就可以直接登录了。

(官方文档提到的)使用场景比如:

(1)多个用户:邮箱或者计划任务服务器,他需要验证多个用户信息。

(2)多个服务器:银行业务或保险应用程序,他可能需要在多个安全的数据库之间交换数据信息。

(3)需要输入密码的用户:Web 浏览器,他能通过钥匙串服务为多个安全的 Web 站点存储用户密码。

iOS 系统中也使用 Keychain Services 来保存 Wi-Fi 网络密码、VPN 凭证等。Keychain 本身是一个 SQLite 数据库,位于移动设备的「/private/var/Keychains/keychain-2.db」文件中,保存的是加密过的数据。

这里提下,对 SQLite 数据库进行加密的第三方库:

「sqlcipher」:https://github.com/sqlcipher/sqlcipher,他为数据库文件提供了256 位的 AES 加密方式,提高了数据库安全性。

越狱手机用「iFile」工具可以直接看到 Keychain 数据库数据:

也可以使用「Keychain-Dumper」工具导出 Keychain 数据库数据:

https://github.com/ptoomey3/Keychain-Dumper

Keychain 有两个访问区

(1)私有区:不会存储沙盒目录下,但同沙盒目录一样只允许本身 App 访问。

(2)公共区:通过 Keychain Access Group「钥匙串访问组」配置,实现多个 App 之间共享 Keychain 数据,由于他编译跟证书签名有关,所以一般也只能是同家公司的产品 App 之间共享 Keychain 数据。

如下图片为公共区的通过 Keychain Access Group「钥匙串访问组」配置,关于公共区的详细内容这里不多介绍,接下来我们来关注私有区的用法,因为他更常用。

Keychain Services「钥匙串服务」原始代码是非 ARC 的,在 ARC 的环境下操作,需要通过 bridge 「桥接」方式把 Core Foundation 对象转换为 Objective-C 对象。

Keychain Services 的存储内容类型,常用的是 kSecClassGenericPassword,像 Apple 官网提供的 KeyChainItemWrapper 工程就是使用 kSecClassGenericPassword。

同样,第三方库「SSKeychain」也是使用 kSecClassGenericPassword:

https://github.com/soffes/sskeychain

1 extern const CFStringRef kSecClass;
2
3 extern const CFStringRef kSecClassGenericPassword; // 通用密码;「genp」表
4 extern const CFStringRef kSecClassInternetPassword; // 互联网密码;「inet」表
5 extern const CFStringRef kSecClassCertificate; //证书;「cert」表
6 extern const CFStringRef kSecClassKey; //密钥;「keys」表
7 extern const CFStringRef kSecClassIdentity; //身份

SSKeychain.h 公开的方法:

 1 + (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account;
 2 + (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
 3 + (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account;
 4 + (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
 5 + (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account;
 6 + (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
 7 + (NSArray *)allAccounts;
 8 + (NSArray *)allAccounts:(NSError *__autoreleasing *)error;
 9 + (NSArray *)accountsForService:(NSString *)serviceName;
10 + (NSArray *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error;

如何使用:

 1 #import "KeychainViewController.h"
 2 #import "SSKeychain.h"
 3
 4 static NSString *const kService = @"com.kenmu.KMDataAccess"; // 服务名任意;保持存取一致就好
 5 static NSString *const kAccount1 = @"KenmuHuang";
 6 static NSString *const kAccount2 = @"Kenmu";
 7
 8 ...
 9 - (IBAction)btnWriteToPressed:(id)sender {
10     NSError * (^setPasswordBlock)(NSString *) = ^(NSString *account){
11         // 创建 UUID 字符串作为密码进行测试;最终还是会被加密存储起来的
12         CFUUIDRef UUIDPassword = CFUUIDCreate(NULL);
13         CFStringRef UUIDPasswordStrRef = CFUUIDCreateString(NULL, UUIDPassword);
14         NSString *UUIDPasswordStr = [NSString stringWithFormat:@"%@", UUIDPasswordStrRef];
15         NSLog(@"UUIDPasswordStr: %@", UUIDPasswordStr);
16         // 释放资源
17         CFRelease(UUIDPasswordStrRef);
18         CFRelease(UUIDPassword);
19
20         NSError *error;
21         [SSKeychain setPassword:UUIDPasswordStr
22                      forService:kService
23                         account:account
24                           error:&error];
25
26         return error;
27     };
28
29     // 存储2个用户密码信息,相当于向「genp」表插入2条记录
30     NSError *errorOfAccount1 = setPasswordBlock(kAccount1);
31     NSError *errorOfAccount2 = setPasswordBlock(kAccount2);
32
33     NSMutableString *mStrCustom = [NSMutableString new];
34     [mStrCustom appendFormat:@"%@ 写入密码%@\n\n", kAccount1, errorOfAccount1 ? @"失败" : @"成功"];
35     [mStrCustom appendFormat:@"%@ 写入密码%@\n", kAccount2, errorOfAccount2 ? @"失败" : @"成功"];
36     _txtVDetailInfo.text = mStrCustom;
37 }
38
39 - (IBAction)btnReadFromPressed:(id)sender {
40     NSString * (^getPasswordBlock)(NSString *) = ^(NSString *account){
41         NSError *error;
42         return [SSKeychain passwordForService:kService
43                                       account:account
44                                         error:&error];
45     };
46
47     NSString *passwordOfAccount1 = getPasswordBlock(kAccount1);
48     NSString *passwordOfAccount2 = getPasswordBlock(kAccount2);
49
50     NSMutableString *mStrCustom = [NSMutableString new];
51     [mStrCustom appendFormat:@"%@ 读取密码%@: %@\n\n", kAccount1, passwordOfAccount1 ? @"成功" : @"失败", passwordOfAccount1];
52     [mStrCustom appendFormat:@"%@ 读取密码%@: %@\n", kAccount2, passwordOfAccount2 ? @"成功" : @"失败", passwordOfAccount2];
53     _txtVDetailInfo.text = mStrCustom;
54 }

2.5 SQLite

SQLite 顾名思义就是一种数据库,相比之前提到的 plist 文件「属性列表」、preference「偏好设置」、NSKeyedArchiver「归档」存取方式来说,数据库更适合存储大量数据,因为他的增删改查都可以逐条记录进行操作,不需要一次性加载整个数据库文件。同时他还可以处理更加复杂的关系型数据。

当然数据库存取方式的优点不止以上两点,这里就不一一列举了。(深入研究过数据库的朋友都知道,数据库技术也是一门学问)

他有以下几点特点:

(1)基于 C 语言开发的轻型数据库,支持跨平台

(2)在 iOS 中需要使用 C 语言语法进行数据库操作、访问(无法使用 ObjC 直接访问,因为 libsqlite3 框架基于 C 语言编写)

(3)SQLite 中采用的是动态数据类型,即使创建时定义为一种类型,在实际操作时也可以存储为其他类型,但还是推荐建库时使用合适的类型(特别是 App 需要考虑跨平台的情况时)

(4)建立连接后通常不需要关闭连接(尽管可以手动关闭)

这里我们使用 MVC 多层架构,Model「实体层」+View「视图层」+ViewController「视图控制器层」+Service「服务层」+Database「数据库层」,如下图所示:

如何使用:

GlobalInfoModel.h

 1 #import <Foundation/Foundation.h>
 2
 3 @interface GlobalInfoModel : NSObject <NSCoding>
 4 @property (strong, nonatomic) NSNumber *ID; ///< ID编号
 5 @property (copy, nonatomic) NSString *avatarImageStr; ///< 图标图片地址
 6 @property (copy, nonatomic) NSString *name; ///< 标题名称
 7 @property (copy, nonatomic) NSString *text; ///< 内容
 8 @property (copy, nonatomic) NSString *link; ///< 链接地址
 9 @property (strong, nonatomic) NSDate *createdAt; ///< 创建时间
10 @property (assign, nonatomic, getter=isHaveLink) BOOL haveLink; ///< 是否存在链接地址
11
12 - (GlobalInfoModel *)initWithDictionary:(NSDictionary *)dic;
13 - (GlobalInfoModel *)initWithAvatarImageStr:(NSString *)avatarImageStr name:(NSString *)name text:(NSString *)text link:(NSString *)link createdAt:(NSDate *)createdAt ID:(NSNumber *)ID;
14 @end

GlobalInfoModel.m

 1 #import "GlobalInfoModel.h"
 2 #import "DateHelper.h"
 3
 4 @implementation GlobalInfoModel
 5
 6 - (GlobalInfoModel *)initWithDictionary:(NSDictionary *)dic {
 7     if (self = [super init]) {
 8         // 这种方式在有 NSDate 数据类型的属性时,赋值操作的属性值都为字符串类型(不推荐在这种情况下使用),可以根据 NSLog(@"%@", [date class]); 看到是否是正确格式的 NSDate 数据类型
 9         //[self setValuesForKeysWithDictionary:dic];
10
11         // http://stackoverflow.com/questions/14233153/nsdateformatter-stringfromdate-datefromstring-both-returning-nil
12         id createdAt = dic[kCreatedAt];
13         if (![createdAt isKindOfClass:[NSDate class]]) {
14             createdAt = [DateHelper dateFromString:createdAt
15                                         withFormat:@"yyyy-MM-dd HH:mm:ss z"];
16         }
17
18         // 推荐使用自己构造的方式
19         return [self initWithAvatarImageStr:dic[kAvatarImageStr]
20                                        name:dic[kName]
21                                        text:dic[kText]
22                                        link:dic[kLink]
23                                   createdAt:createdAt
24                                          ID:dic[kID]];
25     }
26     return self;
27 }
28
29 - (GlobalInfoModel *)initWithAvatarImageStr:(NSString *)avatarImageStr name:(NSString *)name text:(NSString *)text link:(NSString *)link createdAt:(NSDate *)createdAt ID:(NSNumber *)ID {
30     if (self = [super init]) {
31         _ID = ID;
32         _avatarImageStr = avatarImageStr;
33         _name = name;
34         _text = text;
35         _link = link;
36         _createdAt = createdAt;
37         _haveLink = _link.length > 0;
38     }
39     return self;
40 }
41
42 #pragma mark - NSCoding
43 - (void)encodeWithCoder:(NSCoder *)aCoder {
44     // 归档
45     [aCoder encodeObject:_ID forKey:kID];
46     [aCoder encodeObject:_avatarImageStr forKey:kAvatarImageStr];
47     [aCoder encodeObject:_name forKey:kName];
48     [aCoder encodeObject:_text forKey:kText];
49     [aCoder encodeObject:_link forKey:kLink];
50     [aCoder encodeObject:_createdAt forKey:kCreatedAt];
51     [aCoder encodeBool:_haveLink forKey:kHaveLink];
52 }
53
54 - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
55     self = [super init];
56     if (self) {
57         // 解档
58         _ID = [aDecoder decodeObjectForKey:kID];
59         _avatarImageStr = [aDecoder decodeObjectForKey:kAvatarImageStr];
60         _name = [aDecoder decodeObjectForKey:kName];
61         _text = [aDecoder decodeObjectForKey:kText];
62         _link = [aDecoder decodeObjectForKey:kLink];
63         _createdAt = [aDecoder decodeObjectForKey:kCreatedAt];
64         _haveLink = [aDecoder decodeBoolForKey:kHaveLink];
65     }
66     return self;
67 }
68
69 @end

SQLiteManager.h

 1 #import <Foundation/Foundation.h>
 2 #import <sqlite3.h>
 3
 4 @interface SQLiteManager : NSObject
 5 @property (assign, nonatomic) sqlite3 *database;
 6
 7 + (SQLiteManager *)sharedManager;
 8 - (void)openDB:(NSString *)databaseName;
 9 - (BOOL)executeNonQuery:(NSString *)sql;
10 - (NSArray *)executeQuery:(NSString *)sql;
11
12 @end

SQLiteManager.m

 1 #import "SQLiteManager.h"
 2
 3 @implementation SQLiteManager
 4
 5 - (instancetype)init {
 6     if (self = [super init]) {
 7         [self openDB:kSQLiteDBName];
 8     }
 9     return self;
10 }
11
12 + (SQLiteManager *)sharedManager {
13     static SQLiteManager *manager;
14
15     static dispatch_once_t onceToken;
16     dispatch_once(&onceToken, ^{
17         manager = [SQLiteManager new];
18     });
19     return manager;
20 }
21
22 - (void)openDB:(NSString *)databaseName {
23     // 获取数据库保存路径,通常保存沙盒 Documents 目录下
24     NSString *directory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
25     NSString *filePath = [directory stringByAppendingPathComponent:databaseName];
26     // 打开数据库;如果数据库存在就直接打开,否则进行数据库创建并打开(filePath 是 ObjC 语言的字符串,需要转化为 C 语言字符串)
27     BOOL isSuccessToOpen = sqlite3_open(filePath.UTF8String, &_database) == SQLITE_OK;
28     NSLog(@"数据库打开%@", isSuccessToOpen ? @"成功" : @"失败");
29 }
30
31 - (BOOL)executeNonQuery:(NSString *)sql {
32     BOOL isSuccess = YES;
33     char *error;
34     // 单步执行sql语句;用于增删改
35     if (sqlite3_exec(_database, sql.UTF8String, NULL, NULL, &error)) {
36         isSuccess = NO;
37         NSLog(@"执行sql语句过程中出现错误,错误信息:%s", error);
38     }
39     return isSuccess;
40 }
41
42 - (NSArray *)executeQuery:(NSString *)sql {
43     NSMutableArray *mArrResult = [NSMutableArray array];
44
45     sqlite3_stmt *stmt;
46     // 检查语法正确性
47     if (sqlite3_prepare_v2(_database, sql.UTF8String, -1, &stmt, NULL) == SQLITE_OK) {
48         // 以游标的形式,逐行读取数据
49         while (sqlite3_step(stmt) == SQLITE_ROW) {
50             NSMutableDictionary *mDicResult = [NSMutableDictionary dictionary];
51             for (int i=0, columnCount=sqlite3_column_count(stmt); i<columnCount; i++) {
52                 // 获取列名
53                 const char *columnName = sqlite3_column_name(stmt, i);
54                 // 获取列值
55                 const char *columnText = (const char *)sqlite3_column_text(stmt, i);
56                 mDicResult[[NSString stringWithUTF8String:columnName]] = [NSString stringWithUTF8String:columnText];
57             }
58             [mArrResult addObject:mDicResult];
59         }
60     }
61
62     // 释放句柄
63     sqlite3_finalize(stmt);
64     return mArrResult;
65 }
66
67 @end

SQLiteDBCreator.h

1 #import <Foundation/Foundation.h>
2
3 @interface SQLiteDBCreator : NSObject
4 + (void)createDB;
5
6 @end

SQLiteDBCreator.m

 1 #import "SQLiteDBCreator.h"
 2 #import "SQLiteManager.h"
 3
 4 @implementation SQLiteDBCreator
 5
 6 #pragma mark - Private Method
 7 + (void)createTable {
 8     NSString *sql = [NSString stringWithFormat:@"CREATE TABLE GlobalInfo(ID integer PRIMARY KEY AUTOINCREMENT, %@ text, %@ text, \"%@\" text, %@ text, %@ date)", kAvatarImageStr, kName, kText, kLink, kCreatedAt];
 9     [[SQLiteManager sharedManager] executeNonQuery:sql];
10 }
11
12 #pragma mark - Public Method
13 + (void)createDB {
14     // 使用偏好设置保存「是否已经初始化数据库表」的键值;避免重复创建
15     NSString *const isInitializedTableStr = @"IsInitializedTableForSQLite";
16     NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
17     if (![userDefaults boolForKey:isInitializedTableStr]) {
18         [self createTable];
19
20         [userDefaults setBool:YES forKey:isInitializedTableStr];
21     }
22 }
23
24 @end

SQLiteGlobalInfoService.h

 1 #import <Foundation/Foundation.h>
 2 #import "GlobalInfoModel.h"
 3
 4 @interface SQLiteGlobalInfoService : NSObject
 5 + (SQLiteGlobalInfoService *)sharedService;
 6 - (BOOL)insertGlobalInfo:(GlobalInfoModel *)globalInfo;
 7 - (BOOL)deleteGlobalInfoByID:(NSNumber *)ID;
 8 - (BOOL)updateGlobalInfo:(GlobalInfoModel *)globalInfo;
 9 - (GlobalInfoModel *)getGlobalInfoByID:(NSNumber *)ID;
10 - (NSMutableArray *)getGlobalInfoGroup;
11
12 @end

SQLiteGlobalInfoService.m

 1 #import "SQLiteGlobalInfoService.h"
 2 #import "SQLiteManager.h"
 3 #import "SQLiteDBCreator.h"
 4
 5 @implementation SQLiteGlobalInfoService
 6
 7 + (SQLiteGlobalInfoService *)sharedService {
 8     static SQLiteGlobalInfoService *service;
 9
10     static dispatch_once_t onceToken;
11     dispatch_once(&onceToken, ^{
12         service = [SQLiteGlobalInfoService new];
13
14         [SQLiteDBCreator createDB];
15     });
16     return service;
17 }
18
19 - (BOOL)insertGlobalInfo:(GlobalInfoModel *)globalInfo {
20     NSString *sql = [NSString stringWithFormat:@"INSERT INTO GlobalInfo(%@, %@, %@, %@, %@) VALUES('%@','%@','%@','%@','%@')",
21                      kAvatarImageStr, kName, kText, kLink, kCreatedAt,
22                      globalInfo.avatarImageStr, globalInfo.name, globalInfo.text, globalInfo.link, globalInfo.createdAt];
23     return [[SQLiteManager sharedManager] executeNonQuery:sql];
24 }
25
26 - (BOOL)deleteGlobalInfoByID:(NSNumber *)ID {
27     NSString *sql =
28     [NSString stringWithFormat:@"DELETE FROM GlobalInfo WHERE %@='%ld'", kID,
29      (long)[ID integerValue]];
30     return [[SQLiteManager sharedManager] executeNonQuery:sql];
31 }
32
33 - (BOOL)updateGlobalInfo:(GlobalInfoModel *)globalInfo {
34     NSString *sql = [NSString stringWithFormat:@"UPDATE GlobalInfo SET %@='%@', %@='%@', %@='%@', %@='%@', %@='%@' WHERE %@='%ld'",
35                      kAvatarImageStr, globalInfo.avatarImageStr,
36                      kName, globalInfo.name,
37                      kText, globalInfo.text,
38                      kLink, globalInfo.link,
39                      kCreatedAt, globalInfo.createdAt,
40                      kID, (long)[globalInfo.ID integerValue]];
41     return [[SQLiteManager sharedManager] executeNonQuery:sql];
42 }
43
44 - (GlobalInfoModel *)getGlobalInfoByID:(NSNumber *)ID {
45     GlobalInfoModel *globalInfo;
46     NSString *sql =
47     [NSString stringWithFormat:
48      @"SELECT %@, %@, %@, %@, %@ FROM GlobalInfo WHERE %@='%ld'",
49      kAvatarImageStr, kName, kText, kLink, kCreatedAt, kID,
50      (long)[ID integerValue]];
51     NSArray *arrResult = [[SQLiteManager sharedManager] executeQuery:sql];
52     if (arrResult && arrResult.count > 0) {
53         globalInfo = [[GlobalInfoModel alloc] initWithDictionary:arrResult[0]];
54     }
55     return globalInfo;
56 }
57
58 - (NSMutableArray *)getGlobalInfoGroup {
59     NSMutableArray *mArrResult = [[NSMutableArray alloc] initWithCapacity:0];
60
61     NSString *sql = [NSString
62                      stringWithFormat:@"SELECT %@, %@, %@, %@, %@, %@ FROM GlobalInfo", kID,
63                      kAvatarImageStr, kName, kText, kLink, kCreatedAt];
64     NSArray *arrResult = [[SQLiteManager sharedManager] executeQuery:sql];
65     if (arrResult && arrResult.count > 0) {
66         for (NSDictionary *dicResult in arrResult) {
67             [mArrResult
68              addObject:[[GlobalInfoModel alloc] initWithDictionary:dicResult]];
69         }
70     }
71     return mArrResult;
72 }
73
74 @end

2.6 CoreData

在各类语言开发中,当牵扯到数据库操作时,通常都会引入 ORM 的概念,ORM 全称为 Object Relational Mapping「对象关系映射」。在面向对象编程语言中,他是一种实现不同类型系统数据转换的技术。

比如在 .NET 中是使用 Entity Framework、Linq、NHibernate,在 Java 中是使用 Hibernate,而在 iOS 中官方推荐的就是 CoreData 了。无论哪种开发平台和技术,ORM 的作用都是一样的,那就是将数据库表(准确的说是实体)转换为程序中的对象,其本质还是对数据库进行操作。

当 CoreData 中数据仓储类型配置为 SQLite ,其本质就是操作 SQLite 数据库。

在前面的 SQLite 介绍中,我们需要创建多层关系方便我们数据存取操作:GlobalInfoModel「实体层」、SQLiteManager「数据库数据访问管理者层」、SQLiteDBCreator「数据库初始化工作创建者层」、SQLiteGlobalInfoService「数据库数据转换为实体操作服务层」。

然而在 CoreData 中,他简化了 SQLite 多层关系创建的过程,他对数据库和其表的创建、数据库数据转换为实体操作进行封装,最终提供类似 SQLiteGlobalInfoService 的服务层用于数据存取,使用更加方便快捷。

在 CoreData 中,有四大核心类:

「Managed Object Model」:被管理对象模型;对应 Xcode 中创建的 .xcdatamodeld 对象模型文件

「Persistent Store Coordinator」:持久化存储协调器;被管理对象模型和实体类之间的转换协调器,可以为不同的被管理对象模型创建各自的持久化存储协调器,一般情况下会合并多个被管理对象模型,然后创建一个持久化存储协调器统一来管理

「Persistent Store」:持久化存储;可以理解为最终存储数据的数据仓储,CoreData 支持的数据仓储类型为四种,分别为 NSSQLiteStoreType「SQLite 数据库」、NSXMLStoreType「XML 文件」、NSBinaryStoreType「二进制文件」、NSInMemoryStoreType「内存中」,一般使用 NSSQLiteStoreType

「Managed Object Context」:被管理对象上下文;负责实体对象与数据库的数据交互

接下来让我们开始实践吧,首先创建被管理对象模型和对应映射实体,关键步骤如下图所示:

在创建完被管理对象模型和对应映射实体后,我们来创建被管理对象上下文,步骤如下:

(1)打开「被管理对象模型」文件,参数为 nil 则打开包中所有模型文件并合并成一个

(2)创建「持久化存储协调器」

(3)为「持久化存储协调器」添加 SQLite 类型的「持久化存储」

(4)创建「被管理对象上下文」,并设置他的「持久化存储协调器」

如何使用:

CoreDataManager.h

 1 #import <Foundation/Foundation.h>
 2 #import <CoreData/CoreData.h>
 3
 4 @interface CoreDataManager : NSObject
 5 @property (strong, nonatomic) NSManagedObjectContext *context;
 6
 7 + (CoreDataManager *)sharedManager;
 8 -(NSManagedObjectContext *)createDBContext:(NSString *)databaseName;
 9
10 @end

CoreDataManager.m

 1 #import "CoreDataManager.h"
 2
 3 @implementation CoreDataManager
 4
 5 - (instancetype)init {
 6     if (self = [super init]) {
 7         _context = [self createDBContext:kCoreDataDBName];
 8     }
 9     return self;
10 }
11
12 + (CoreDataManager *)sharedManager {
13     static CoreDataManager *manager;
14
15     static dispatch_once_t onceToken;
16     dispatch_once(&onceToken, ^{
17         manager = [CoreDataManager new];
18     });
19     return manager;
20 }
21
22 -(NSManagedObjectContext *)createDBContext:(NSString *)databaseName {
23     // 获取数据库保存路径,通常保存沙盒 Documents 目录下
24     NSString *directory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
25     NSString *filePath = [directory stringByAppendingPathComponent:databaseName];
26     NSURL *fileURL = [NSURL fileURLWithPath:filePath];
27
28     // 打开「被管理对象模型」文件,参数为 nil 则打开包中所有模型文件并合并成一个
29     NSManagedObjectModel *model=[NSManagedObjectModel mergedModelFromBundles:nil];
30     // 创建「持久化存储协调器」
31     NSPersistentStoreCoordinator *coordinator=[[NSPersistentStoreCoordinator alloc]initWithManagedObjectModel:model];
32     // 为「持久化存储协调器」添加 SQLite 类型的「持久化存储」
33     NSError *error;
34     [coordinator addPersistentStoreWithType:NSSQLiteStoreType
35                               configuration:nil
36                                         URL:fileURL
37                                     options:nil
38                                       error:&error];
39     // 创建「被管理对象上下文」,并设置他的「持久化存储协调器」
40     NSManagedObjectContext *context;
41     if (!error) {
42         NSLog(@"数据库打开成功");
43
44         context = [NSManagedObjectContext new];
45         context.persistentStoreCoordinator = coordinator;
46     } else {
47         NSLog(@"数据库打开失败,错误信息:%@", error.localizedDescription);
48     }
49     return context;
50
51     /*
52      // Persistent store types supported by Core Data:
53      COREDATA_EXTERN NSString * const NSSQLiteStoreType NS_AVAILABLE(10_4, 3_0);
54      COREDATA_EXTERN NSString * const NSXMLStoreType NS_AVAILABLE(10_4, NA);
55      COREDATA_EXTERN NSString * const NSBinaryStoreType NS_AVAILABLE(10_4, 3_0);
56      COREDATA_EXTERN NSString * const NSInMemoryStoreType NS_AVAILABLE(10_4, 3_0);
57      */
58 }
59
60 @end

CoreDataGlobalInfoService.h

 1 #import <Foundation/Foundation.h>
 2 #import <CoreData/CoreData.h>
 3 #import "GlobalInfoModel.h" // 为了 KMAddRecordViewController 和 KMTableView 统一格式管理,这里引入 GlobalInfoModel(特殊情况特殊处理),一般情况不需要这样做,因为实际上在 CoreData 中已经生成了 GlobalInfo 实体映射来方便我们操作数据库了
 4
 5 @interface CoreDataGlobalInfoService : NSObject
 6 @property (strong, nonatomic) NSManagedObjectContext *context;
 7
 8 + (CoreDataGlobalInfoService *)sharedService;
 9 - (BOOL)insertGlobalInfo:(GlobalInfoModel *)globalInfo;
10 - (BOOL)deleteGlobalInfoByID:(NSNumber *)ID;
11 - (BOOL)updateGlobalInfo:(GlobalInfoModel *)globalInfo;
12 - (GlobalInfoModel *)getGlobalInfoByID:(NSNumber *)ID;
13 - (NSMutableArray *)getGlobalInfoGroup;
14
15 @end

CoreDataGlobalInfoService.m

  1 #import "CoreDataGlobalInfoService.h"
  2 #import "CoreDataManager.h"
  3 #import "GlobalInfo.h"
  4
  5 static NSString *const kGlobalInfo = @"GlobalInfo";
  6
  7 @implementation CoreDataGlobalInfoService
  8
  9 + (CoreDataGlobalInfoService *)sharedService {
 10     static CoreDataGlobalInfoService *service;
 11
 12     static dispatch_once_t onceToken;
 13     dispatch_once(&onceToken, ^{
 14         service = [CoreDataGlobalInfoService new];
 15         service.context = [CoreDataManager sharedManager].context;
 16     });
 17     return service;
 18 }
 19
 20 - (BOOL)insertGlobalInfo:(GlobalInfoModel *)globalInfo {
 21     BOOL isSuccess = NO;
 22
 23     // 可以使用 insertNewObjectForEntityForName: 方法创建多个实体,最终只需执行一次 save: 方法就会全提交到数据库了
 24     GlobalInfo *globalInfoEntity =
 25     [NSEntityDescription insertNewObjectForEntityForName:kGlobalInfo
 26                                   inManagedObjectContext:_context];
 27     globalInfoEntity.customID = globalInfo.ID;
 28     globalInfoEntity.avatarImageStr = globalInfo.avatarImageStr;
 29     globalInfoEntity.name = globalInfo.name;
 30     globalInfoEntity.text = globalInfo.text;
 31     globalInfoEntity.link = globalInfo.link;
 32     globalInfoEntity.createdAt = globalInfo.createdAt;
 33     NSError *error;
 34     isSuccess = [_context save:&error];
 35     if (!isSuccess) {
 36         NSLog(@"插入记录过程出现错误,错误信息:%@", error.localizedDescription);
 37     }
 38     return isSuccess;
 39 }
 40
 41 - (BOOL)deleteGlobalInfoByID:(NSNumber *)ID {
 42     BOOL isSuccess = NO;
 43
 44     GlobalInfo *globalInfoEntity = [self getGlobalInfoEntityByID:ID];
 45     if (globalInfoEntity) {
 46         NSError *error;
 47         [_context deleteObject:globalInfoEntity];
 48         isSuccess = [_context save:&error];
 49         if (!isSuccess) {
 50             NSLog(@"删除记录过程出现错误,错误信息:%@", error.localizedDescription);
 51         }
 52     }
 53     return isSuccess;
 54 }
 55
 56 - (BOOL)updateGlobalInfo:(GlobalInfoModel *)globalInfo {
 57     BOOL isSuccess = NO;
 58
 59     GlobalInfo *globalInfoEntity = [self getGlobalInfoEntityByID:globalInfo.ID];
 60     if (globalInfoEntity) {
 61         NSError *error;
 62         globalInfoEntity.avatarImageStr = globalInfo.avatarImageStr;
 63         globalInfoEntity.name = globalInfo.name;
 64         globalInfoEntity.text = globalInfo.text;
 65         globalInfoEntity.link = globalInfo.link;
 66         globalInfoEntity.createdAt = globalInfo.createdAt;
 67         isSuccess = [_context save:&error];
 68         if (!isSuccess) {
 69             NSLog(@"修改记录过程出现错误,错误信息:%@", error.localizedDescription);
 70         }
 71     }
 72     return isSuccess;
 73 }
 74
 75 - (GlobalInfoModel *)getGlobalInfoByID:(NSNumber *)ID {
 76     GlobalInfoModel *globalInfo;
 77
 78     GlobalInfo *globalInfoEntity = [self getGlobalInfoEntityByID:ID];
 79     if (globalInfoEntity) {
 80         globalInfo = [[GlobalInfoModel alloc]
 81                       initWithAvatarImageStr:globalInfoEntity.avatarImageStr
 82                       name:globalInfoEntity.name
 83                       text:globalInfoEntity.text
 84                       link:globalInfoEntity.link
 85                       createdAt:globalInfoEntity.createdAt
 86                       ID:globalInfoEntity.customID];
 87     }
 88     return globalInfo;
 89 }
 90
 91 - (NSMutableArray *)getGlobalInfoGroup {
 92     NSMutableArray *mArrResult = [[NSMutableArray alloc] initWithCapacity:0];
 93
 94     NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:kGlobalInfo];
 95     //request.fetchLimit = 2; // 读取返回记录数限制
 96     //request.fetchOffset = 2; // 读取偏移量;默认值为0,表示不偏移;比如设置为2,表示前两条记录就不被读取了
 97     NSError *error;
 98     NSArray *arrResult = [_context executeFetchRequest:request
 99                                                  error:&error];
100     if (!error) {
101         for (GlobalInfo *globalInfoEntity in arrResult) {
102             GlobalInfoModel *globalInfo = [[GlobalInfoModel alloc]
103                                            initWithAvatarImageStr:globalInfoEntity.avatarImageStr
104                                            name:globalInfoEntity.name
105                                            text:globalInfoEntity.text
106                                            link:globalInfoEntity.link
107                                            createdAt:globalInfoEntity.createdAt
108                                            ID:globalInfoEntity.customID];
109             [mArrResult addObject:globalInfo];
110         }
111     } else {
112         NSLog(@"查询记录过程出现错误,错误信息:%@", error.localizedDescription);
113     }
114     return mArrResult;
115 }
116
117 #pragma mark - Private Method
118 - (GlobalInfo *)getGlobalInfoEntityByID:(NSNumber *)ID {
119     GlobalInfo *globalInfoEntity;
120
121     NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:kGlobalInfo];
122     // 使用谓词查询是基于 Keypath 查询的,如果键是一个变量,格式化字符串时需要使用 %K 而不是 %@
123     request.predicate = [NSPredicate predicateWithFormat:@"%K=%@", @"customID", ID];
124     NSError *error;
125     NSArray *arrResult = [_context executeFetchRequest:request
126                                                  error:&error];
127     if (!error) {
128         globalInfoEntity = [arrResult firstObject];
129     } else {
130         NSLog(@"查询记录过程出现错误,错误信息:%@", error.localizedDescription);
131     }
132     return globalInfoEntity;
133 }
134
135 @end

在 CoreData 中,为了更清晰地看到数据库层面的交互语句,我们可以开启 CoreData 的 SQL 调试功能:

在 「Product」-「Scheme」-「Edit Scheme...」-「Run」菜单下的「Arguments」中的「Arguments Passed On Launch」下按先后顺序新增两个项,内容分别为「-com.apple.CoreData.SQLDebug」和「1」

在 CoreData 生成的数据库相关文件中,以下三种文件对应的含义:

http://www.sqlite.org/fileformat2.html#walindexformat

databaseName.db:数据库文件

databaseName.db-shm「Shared Memory」:数据库预写式日志索引文件

databaseName.db-wal「Write-Ahead Log」:数据库预写式日志文件

2.7 FMDB

在使用过 SQLite 和 CoreData 之后,我们会发现 SQLite 由于是基于 C 语言开发的轻型数据库,他不像直接使用 ObjC 语言访问对象那么方便,而且他在数据库并发操作安全性方面也需要我们使用多线程技术去实现,比较繁琐。

然而使用 CoreData 这样的 ORM 框架虽然相比操作方便,但跟大多数语言的 ORM 框架一样,他在大量数据处理时性能方面并不够好(普通场景下用着还算爽)。

那么有没比较好的解决方案?这里我们会考虑使用第三方库「FMDB」

https://github.com/ccgus/fmdb

前面介绍 SQLite 时,会看到我们在 SQLiteManager 类中对 SQLite 方面的操作进行了简单封装。其实 FMDB 也是对 SQLite 方面的操作进行了封装,但他考虑得更全面更易用,有以下几个特点:

(1)封装为面向 ObjC 语言的类和方法,调用方便

(2)提供一系列用于列数据转换的方法

(3)结合多线程技术实现安全的数据库并发操作

(4)事务操作更方便

在 FMDB 中,有三大核心类:

「FMDatabase」:代表单一的 SQLite 数据库,他有对应方法用于执行 SQL 语句,他是线程不安全的。

「FMResultSet」:代表「FMDatabase」执行 SQL 语句返回的结果集。

「FMDatabaseQueue」:代表数据库队列,用于在多线程中执行查询和更新等操作,他是线程安全的。

「FMDatabase」生成的 SQLite 数据库文件的存放路径,databaseWithPath 方法参数值有如下三种:

(1)文件系统路径;指沙盒目录下的路径,如果数据库文件在此路径下不存在,他会被自动创建

(2)空字符串「@""」;在临时目录创建一个空数据库文件;当数据库连接关闭后,他会被自动删除

(3)NULL;在内存空间创建一个空数据库文件;当数据库连接关闭后,他会被自动销毁回收

FMDatabase *db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];

如何使用:

FMDBManager.h

 1 #import <Foundation/Foundation.h>
 2
 3 @interface FMDBManager : NSObject
 4
 5 + (FMDBManager *)sharedManager;
 6 - (void)openDB:(NSString *)databaseName;
 7 - (BOOL)executeNonQuery:(NSString *)sql withArgumentsInArray:(NSArray *)argumentsInArray;
 8 - (NSArray *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)argumentsInArray withDateArgumentsInArray:(NSArray *)dateArgumentsInArray;
 9
10 @end

FMDBManager.m

 1 #import "FMDBManager.h"
 2 #import "FMDatabase.h"
 3
 4 @implementation FMDBManager {
 5     FMDatabase *_database;
 6 }
 7
 8 - (instancetype)init {
 9     if (self = [super init]) {
10         [self openDB:kFMDBDBName];
11     }
12     return self;
13 }
14
15 + (FMDBManager *)sharedManager {
16     static FMDBManager *manager;
17
18     static dispatch_once_t onceToken;
19     dispatch_once(&onceToken, ^{
20         manager = [FMDBManager new];
21     });
22     return manager;
23 }
24
25 - (void)openDB:(NSString *)databaseName {
26     // 获取数据库保存路径,通常保存沙盒 Documents 目录下
27     NSString *directory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
28     NSString *filePath = [directory stringByAppendingPathComponent:databaseName];
29     // 打开数据库;如果数据库存在就直接打开,否则进行数据库创建并打开
30     _database = [FMDatabase databaseWithPath:filePath];
31     NSLog(@"数据库打开%@", [_database open] ? @"成功" : @"失败");
32 }
33
34 - (BOOL)executeNonQuery:(NSString *)sql withArgumentsInArray:(NSArray *)argumentsInArray {
35     BOOL isSuccess = [_database executeUpdate:sql withArgumentsInArray:argumentsInArray];
36
37     if (!isSuccess) {
38         NSLog(@"执行sql语句过程中出现错误");
39     }
40     return isSuccess;
41 }
42
43 - (NSArray *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)argumentsInArray withDateArgumentsInArray:(NSArray *)dateArgumentsInArray {
44     NSMutableArray *mArrResult = [NSMutableArray array];
45     FMResultSet *resultSet = [_database executeQuery:sql withArgumentsInArray:argumentsInArray];
46     while (resultSet.next) {
47         NSMutableDictionary *mDicResult = [NSMutableDictionary dictionary];
48         for (int i=0, columnCount=resultSet.columnCount; i<columnCount; i++) {
49             NSString *columnName = [resultSet columnNameForIndex:i];
50             // 对时间类型数据进行合适的转换
51             if ([dateArgumentsInArray indexOfObject:columnName] != NSNotFound) {
52                 mDicResult[columnName] = [resultSet dateForColumnIndex:i];
53             } else {
54                mDicResult[columnName] = [resultSet stringForColumnIndex:i];
55             }
56         }
57         [mArrResult addObject:mDicResult];
58     }
59     return mArrResult;
60
61     /*
62      一系列用于列数据转换的方法:
63
64      intForColumn:
65      longForColumn:
66      longLongIntForColumn:
67      boolForColumn:
68      doubleForColumn:
69      stringForColumn:
70      dateForColumn:
71      dataForColumn:
72      dataNoCopyForColumn:
73      UTF8StringForColumnName:
74      objectForColumnName:
75      */
76 }
77
78 @end

FMDBDBCreator.h

1 #import <Foundation/Foundation.h>
2
3 @interface FMDBDBCreator : NSObject
4 + (void)createDB;
5
6 @end

FMDBDBCreator.m

 1 #import "FMDBDBCreator.h"
 2 #import "FMDBManager.h"
 3
 4 @implementation FMDBDBCreator
 5
 6 #pragma mark - Private Method
 7 + (void)createTable {
 8     NSString *sql = [NSString stringWithFormat:@"CREATE TABLE GlobalInfo(ID integer PRIMARY KEY AUTOINCREMENT, %@ text, %@ text, \"%@\" text, %@ text, %@ date)", kAvatarImageStr, kName, kText, kLink, kCreatedAt];
 9     [[FMDBManager sharedManager] executeNonQuery:sql withArgumentsInArray:nil];
10 }
11
12 #pragma mark - Public Method
13 + (void)createDB {
14     // 使用偏好设置保存「是否已经初始化数据库表」的键值;避免重复创建
15     NSString *const isInitializedTableStr = @"IsInitializedTableForFMDB";
16     NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
17     if (![userDefaults boolForKey:isInitializedTableStr]) {
18         [self createTable];
19
20         [userDefaults setBool:YES forKey:isInitializedTableStr];
21     }
22 }
23
24 @end

FMDBGlobalInfoService.h

 1 #import <Foundation/Foundation.h>
 2 #import "GlobalInfoModel.h"
 3
 4 @interface FMDBGlobalInfoService : NSObject
 5 + (FMDBGlobalInfoService *)sharedService;
 6 - (BOOL)insertGlobalInfo:(GlobalInfoModel *)globalInfo;
 7 - (BOOL)deleteGlobalInfoByID:(NSNumber *)ID;
 8 - (BOOL)updateGlobalInfo:(GlobalInfoModel *)globalInfo;
 9 - (GlobalInfoModel *)getGlobalInfoByID:(NSNumber *)ID;
10 - (NSMutableArray *)getGlobalInfoGroup;
11
12 @end

FMDBGlobalInfoService.m

 1 #import "FMDBGlobalInfoService.h"
 2 #import "FMDBManager.h"
 3 #import "FMDBDBCreator.h"
 4
 5 @implementation FMDBGlobalInfoService
 6
 7 + (FMDBGlobalInfoService *)sharedService {
 8     static FMDBGlobalInfoService *service;
 9
10     static dispatch_once_t onceToken;
11     dispatch_once(&onceToken, ^{
12         service = [FMDBGlobalInfoService new];
13
14         [FMDBDBCreator createDB];
15     });
16     return service;
17 }
18
19 - (BOOL)insertGlobalInfo:(GlobalInfoModel *)globalInfo {
20     NSArray *arrArgument = @[
21                              globalInfo.avatarImageStr,
22                              globalInfo.name,
23                              globalInfo.text,
24                              globalInfo.link,
25                              globalInfo.createdAt
26                              ];
27     NSString *sql = [NSString
28                      stringWithFormat:
29                      @"INSERT INTO GlobalInfo(%@, %@, %@, %@, %@) VALUES(?, ?, ?, ?, ?)",
30                      kAvatarImageStr, kName, kText, kLink, kCreatedAt];
31     return [[FMDBManager sharedManager] executeNonQuery:sql
32                                    withArgumentsInArray:arrArgument];
33 }
34
35 - (BOOL)deleteGlobalInfoByID:(NSNumber *)ID {
36     NSString *sql =
37     [NSString stringWithFormat:@"DELETE FROM GlobalInfo WHERE %@=?", kID];
38     return [[FMDBManager sharedManager] executeNonQuery:sql
39                                    withArgumentsInArray:@[ ID ]];
40 }
41
42 - (BOOL)updateGlobalInfo:(GlobalInfoModel *)globalInfo {
43     NSArray *arrArgument = @[
44                              globalInfo.avatarImageStr,
45                              globalInfo.name,
46                              globalInfo.text,
47                              globalInfo.link,
48                              globalInfo.createdAt,
49                              globalInfo.ID
50                              ];
51     NSString *sql = [NSString
52                      stringWithFormat:
53                      @"UPDATE GlobalInfo SET %@=?, %@=?, %@=?, %@=?, %@=? WHERE %@=?",
54                      kAvatarImageStr, kName, kText, kLink, kCreatedAt, kID];
55     return [[FMDBManager sharedManager] executeNonQuery:sql
56                                    withArgumentsInArray:arrArgument];
57 }
58
59 - (GlobalInfoModel *)getGlobalInfoByID:(NSNumber *)ID {
60     GlobalInfoModel *globalInfo;
61     NSString *sql = [NSString
62                      stringWithFormat:@"SELECT %@, %@, %@, %@, %@ FROM GlobalInfo WHERE %@=?",
63                      kAvatarImageStr, kName, kText, kLink, kCreatedAt, kID];
64     NSArray *arrResult = [[FMDBManager sharedManager] executeQuery:sql
65                                               withArgumentsInArray:@[ ID ]
66                                           withDateArgumentsInArray:@[ kCreatedAt ]];
67     if (arrResult && arrResult.count > 0) {
68         globalInfo = [[GlobalInfoModel alloc] initWithDictionary:arrResult[0]];
69     }
70     return globalInfo;
71 }
72
73 - (NSMutableArray *)getGlobalInfoGroup {
74     NSMutableArray *mArrResult = [[NSMutableArray alloc] initWithCapacity:0];
75
76     NSString *sql = [NSString
77                      stringWithFormat:@"SELECT %@, %@, %@, %@, %@, %@ FROM GlobalInfo", kID,
78                      kAvatarImageStr, kName, kText, kLink, kCreatedAt];
79     NSArray *arrResult =
80     [[FMDBManager sharedManager] executeQuery:sql
81                          withArgumentsInArray:nil
82                      withDateArgumentsInArray:@[ kCreatedAt ]];
83     if (arrResult && arrResult.count > 0) {
84         for (NSDictionary *dicResult in arrResult) {
85             [mArrResult
86              addObject:[[GlobalInfoModel alloc] initWithDictionary:dicResult]];
87         }
88     }
89     return mArrResult;
90 }
91
92 @end

3. 扩展知识

3.1 模糊背景效果的实现

使用 FXBlurView 库对于 UIImage 的分类扩展方法 blurredImageWithRadius:

https://github.com/nicklockwood/FXBlurView

 1 // 模糊背景效果
 2 // 早上、黄昏、午夜、黎明;这里随机选择某张图片作为背景
 3 // 当然其他做法如:根据当前时间归属一天的某个时间段,选择对应相关的图片作为背景也是可以的
 4 NSArray *arrBlurImageName = @[ @"blur_morning",
 5                                @"blur_nightfall",
 6                                @"blur_midnight",
 7                                @"blur_midnight_afternoon" ];
 8 NSInteger randomVal = arc4random() % [arrBlurImageName count];
 9 UIImage *img = [UIImage imageNamed:arrBlurImageName[randomVal]];
10 // 这里使用 FXBlurView 库对于 UIImage 的分类扩展方法 blurredImageWithRadius:,
11 // blurredImageWithRadius: 模糊效果半径范围
12 // iterations: 重复渲染迭代次数;最低次数值需要为1,值越高表示质量越高
13 // tintColor: 原图混合颜色效果;可选操作,注意颜色的透明度会自动被忽略
14 img = [img blurredImageWithRadius:5.0
15                        iterations:1
16                         tintColor:nil];
17 UIImageView *imgV = [[UIImageView alloc] initWithFrame:kFrameOfMainScreen];
18 imgV.image = img;
19 [self addSubview:imgV];

3.2 密码保护功能的实现

首先,创建密码保护自定义窗口 PasswordInputWindow,继承自 UIWindow,当他需要显示时,设置为主要窗口并且可见;在多个 UIWindow 显示的情况下,也可以通过 windowLevel 属性控制窗口层级显示优先级

然后,在 AppDelegate 生命周期中的以下两个地方控制他的显示:

 1 // App 启动之后执行,只有在第一次启动后才执行,以后不再执行
 2 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
 3     ...
 4
 5     [[PasswordInputWindow sharedInstance] show];
 6     return YES;
 7 }
 8
 9 // App 将要进入前台时执行
10 - (void)applicationWillEnterForeground:(UIApplication *)application {
11     ...
12
13     PasswordInputWindow *passwordInputWindow = [PasswordInputWindow sharedInstance];
14     if (![passwordInputWindow isHaveLogined]) {
15         [passwordInputWindow show];
16     }
17 }

最后,通过时间点对比的方式判断用户是否长期没操作,如果达到预定时间间隔没操作,当再次进入前台时就启动密码保护功能,在 AppDelegate 生命周期中的以下两个地方控制他的时效:(有的方式是使用 Timer 计时器控制他的时效,对于这种不需要实时执行操作的需求来说,就没有必要了,浪费资源)

 1 @interface AppDelegate () {
 2     NSDate *_dateOfEnterBackground;
 3 }
 4 @end
 5
 6 @implementation AppDelegate
 7
 8 // App 已经进入后台时执行
 9 - (void)applicationDidEnterBackground:(UIApplication *)application {
10     _dateOfEnterBackground = [DateHelper localeDate];
11 }
12
13 // App 将要进入前台时执行
14 - (void)applicationWillEnterForeground:(UIApplication *)application {
15     NSDate *localeDate = [DateHelper localeDate];
16     // 这里设置为5秒用于测试;实际上合理场景应该是60 * 5 = 5分钟或者更长时间
17     _dateOfEnterBackground = [_dateOfEnterBackground dateByAddingTimeInterval:1 * 5];
18
19     PasswordInputWindow *passwordInputWindow = [PasswordInputWindow sharedInstance];
20     // 规定的一段时间没操作,就自动注销登录
21     if ([localeDate compare:_dateOfEnterBackground] == NSOrderedDescending) {
22         [passwordInputWindow loginOut];
23     }
24
25     if (![passwordInputWindow isHaveLogined]) {
26         [passwordInputWindow show];
27     }
28 }
29
30 @end

4. 其他关键代码

KMDatePicker 模块,请查看这篇随笔:自定义支持多种格式可控范围的时间选择器控件

PrefixHeader.pch

 1 // ***********************仅仅让支持 Objective-C 语言的文件调用***********************
 2 #ifdef __OBJC__
 3
 4 #define kTitleOfPList @"plist 文件「属性列表」"
 5 #define kTitleOfPreference @"preference「偏好设置」"
 6 #define kTitleOfNSKeyedArchiver @"NSKeyedArchiver「归档」"
 7 #define kTitleOfKeychain @"Keychain「钥匙串」"
 8 #define kTitleOfSQLite @"SQLite"
 9 #define kTitleOfCoreData @"CoreData"
10 #define kTitleOfFMDB @"FMDB"
11
12 #define kPListName @"GlobalInfo.plist"
13 #define kNSKeyedArchiverName @"GlobalInfo.data"
14 #define kSQLiteDBName @"SQLiteDB.db"
15 #define kCoreDataDBName @"CoreDataDB.db"
16 #define kFMDBDBName @"FMDBDB.db"
17
18 #define kID @"ID"
19 #define kAvatarImageStr @"avatarImageStr"
20 #define kName @"name"
21 #define kText @"text"
22 #define kLink @"link"
23 #define kCreatedAt @"createdAt"
24 #define kHaveLink @"haveLink"
25
26 #define kBlogImageStr @"http://pic.cnblogs.com/avatar/66516/20150521204639.png"
27
28 #define kApplication [UIApplication sharedApplication]
29
30 // *********************** iOS 通用宏定义内容 begin
31 // iOS 版本
32 #define kOSVersion [[[UIDevice currentDevice] systemVersion] floatValue]
33
34 // App 显示名称
35 #define kAppDisplayName [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]
36
37 // 当前语言
38 #define kLocaleLanguage [[NSLocale preferredLanguages] objectAtIndex:0]
39
40 // 是否是 iPhone
41 #define kIsPhone UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone
42
43 // 是否是 iPad
44 #define kIsPad UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad
45
46 // 判断机型;根据屏幕分辨率区别「像素」=屏幕大小「点素」*屏幕模式「iPhone 4开始比例就为2x」
47 #define funcIsMatchingSize(width,height) [UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(width, height), [[UIScreen mainScreen] currentMode].size) : NO
48 #define kIsPhone4 funcIsMatchingSize(640.0,960.0)
49 #define kIsPhone5 funcIsMatchingSize(640.0,1136.0)
50 #define kIsPhone6 funcIsMatchingSize(750.0,1334.0)
51 #define kIsPhone6Plus funcIsMatchingSize(1242.0,2208.0)
52
53 // 高度:状态栏、导航栏、状态栏和导航栏、选项卡、表格单元格、英国键盘、中国键盘
54 #define kHeightOfStatus 20.0
55 #define kHeightOfNavigation 44.0
56 #define kHeightOfStatusAndNavigation 64.0
57 #define kHeightOfTabBar 49.0
58 #define kHeightOfCell 44.0
59 #define kHeightOfEnglishKeyboard 216.0
60 #define kHeightOfChineseKeyboard 252.0
61
62 // 屏幕大小
63 #define kFrameOfMainScreen [[UIScreen mainScreen] bounds]
64 #define kWidthOfMainScreen kFrameOfMainScreen.size.width
65 #define kHeightOfMainScreen kFrameOfMainScreen.size.height
66 #define kAbsoluteHeightOfMainScreen kHeightOfMainScreen - kHeightOfStatusAndNavigation
67
68 // 去除状态栏后的屏幕大小
69 #define kFrameOfApplicationFrame [[UIScreen mainScreen] applicationFrame]
70 #define kWidthOfApplicationFrame kFrameOfApplicationFrame.size.width
71 #define kHeightOfApplicationFrame kFrameOfApplicationFrame.size.height
72
73 // View 的坐标(x, y)和宽高(width, height)
74 #define funcX(v) (v).frame.origin.x
75 #define funcY(v) (v).frame.origin.y
76 #define funcWidth(v) (v).frame.origin.width
77 #define funcHeight(v) (v).frame.origin.height
78
79 // View 的坐标(x, y):视图起始点、视图中间点、视图终止点(视图起始点和视图宽高)
80 #define funcMinX(v) CGRectGetMinX((v).frame)
81 #define funcMinY(v) CGRectGetMinY((v).frame)
82 #define funcMidX(v) CGRectGetMidX((v).frame)
83 #define funcMidY(v) CGRectGetMidY((v).frame)
84 #define funcMaxX(v) CGRectGetMaxX((v).frame)
85 #define funcMaxY(v) CGRectGetMaxY((v).frame)
86
87 // 文件路径
88 #define funcFilePath(fileName,type) [[NSBundle mainBundle] pathForResource:[NSString stringWithUTF8String:(fileName)] ofType:(type)]
89
90 // 读取图片
91 #define funcImage(fileName,type) [UIImage imageWithContentsOfFile:funcFilePath(fileName,type)]
92 // *********************** iOS 通用宏定义内容 end
93
94 #endif

PasswordInputWindow.h

 1 #import <UIKit/UIKit.h>
 2
 3 /**
 4  *  密码保护自定义窗口
 5  */
 6 @interface PasswordInputWindow : UIWindow
 7
 8 + (PasswordInputWindow *)sharedInstance;
 9 - (void)show;
10 - (BOOL)isHaveLogined;
11 - (void)loginIn;
12 - (void)loginOut;
13
14 @end

PasswordInputWindow.m

  1 #import "PasswordInputWindow.h"
  2 #import "FXBlurView.h"
  3
  4 static NSString *const isLoginedStr = @"IsLogined";
  5
  6 @implementation PasswordInputWindow {
  7     UITextField *_txtFPassword;
  8 }
  9
 10 - (instancetype)initWithFrame:(CGRect)frame {
 11     self = [super initWithFrame:frame];
 12     if (self) {
 13         // 模糊背景效果
 14         // 早上、黄昏、午夜、黎明;这里随机选择某张图片作为背景
 15         // 当然其他做法如:根据当前时间归属一天的某个时间段,选择对应相关的图片作为背景也是可以的
 16         NSArray *arrBlurImageName = @[ @"blur_morning",
 17                                        @"blur_nightfall",
 18                                        @"blur_midnight",
 19                                        @"blur_midnight_afternoon" ];
 20         NSInteger randomVal = arc4random() % [arrBlurImageName count];
 21         UIImage *img = [UIImage imageNamed:arrBlurImageName[randomVal]];
 22         // 这里使用 FXBlurView 库对于 UIImage 的分类扩展方法 blurredImageWithRadius:,
 23         // blurredImageWithRadius: 模糊效果半径范围
 24         // iterations: 重复渲染迭代次数;最低次数值需要为1,值越高表示质量越高
 25         // tintColor: 原图混合颜色效果;可选操作,注意颜色的透明度会自动被忽略
 26         img = [img blurredImageWithRadius:5.0
 27                                iterations:1
 28                                 tintColor:nil];
 29         UIImageView *imgV = [[UIImageView alloc] initWithFrame:kFrameOfMainScreen];
 30         imgV.image = img;
 31         [self addSubview:imgV];
 32
 33         // 密码输入文本框
 34         CGPoint pointCenter = CGPointMake(CGRectGetMidX(kFrameOfMainScreen), CGRectGetMidY(kFrameOfMainScreen));
 35
 36         _txtFPassword = [[UITextField alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 40.0)];
 37         _txtFPassword.center = pointCenter;
 38         _txtFPassword.borderStyle = UITextBorderStyleRoundedRect;
 39         _txtFPassword.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
 40         _txtFPassword.placeholder = @"请输入密码";
 41         _txtFPassword.secureTextEntry = YES;
 42         _txtFPassword.clearButtonMode = UITextFieldViewModeWhileEditing;
 43         [self addSubview:_txtFPassword];
 44
 45         // 确定按钮
 46         UIButton *btnOK = [[UIButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 40.0)];
 47         pointCenter.y += 50.0;
 48         btnOK.center = pointCenter;
 49         [btnOK setTitle:@"确定" forState:UIControlStateNormal];
 50         [btnOK setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
 51         btnOK.backgroundColor = [UIColor colorWithRed:0.400 green:0.800 blue:1.000 alpha:1.000];
 52         [btnOK addTarget:self
 53                   action:@selector(btnOKPressed:)
 54         forControlEvents:UIControlEventTouchUpInside];
 55         btnOK.layer.borderColor = [UIColor colorWithRed:0.354 green:0.707 blue:0.883 alpha:1.000].CGColor;
 56         btnOK.layer.borderWidth = 1.0;
 57         btnOK.layer.masksToBounds = YES;
 58         btnOK.layer.cornerRadius = 5.0;
 59         [self addSubview:btnOK];
 60
 61         // 点击手势控制隐藏键盘
 62         UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideKeyboard:)];
 63         [self addGestureRecognizer:tapGestureRecognizer];
 64     }
 65     return self;
 66 }
 67
 68 - (void)btnOKPressed:(UIButton *)btnOK {
 69     if ([_txtFPassword.text isEqualToString:@"123"]) {
 70         _txtFPassword.text = @"";
 71         [_txtFPassword resignFirstResponder];
 72         [self resignKeyWindow];
 73         self.hidden = YES;
 74
 75         [self loginIn];
 76     } else {
 77         UIAlertView *alertV = [[UIAlertView alloc] initWithTitle:@"提示信息"
 78                                                          message:@"密码错误,正确密码是123"
 79                                                         delegate:nil
 80                                                cancelButtonTitle:nil
 81                                                otherButtonTitles:@"确定", nil];
 82         [alertV show];
 83     }
 84 }
 85
 86 - (void)hideKeyboard:(id)sender {
 87     // 多个控件的情况下,可以用 [self endEditing:YES];
 88     [_txtFPassword resignFirstResponder];
 89 }
 90
 91 + (PasswordInputWindow *)sharedInstance {
 92     static PasswordInputWindow *sharedInstance;
 93
 94     static dispatch_once_t onceToken;
 95     dispatch_once(&onceToken, ^{
 96         sharedInstance = [[PasswordInputWindow alloc] initWithFrame:kFrameOfMainScreen];
 97     });
 98
 99     return sharedInstance;
100 }
101
102 - (void)show {
103     /*
104      // 窗口层级;层级值越大,越上层,就是覆盖在上面;可以设置的层级值不止以下三种,因为 UIWindowLevel 其实是 CGFloat 类型
105      self.windowLevel = UIWindowLevelAlert;
106
107      typedef CGFloat UIWindowLevel;
108      UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal; // 默认配置;值为0.0
109      UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert; // 弹出框;值为2000.0
110      UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar __TVOS_PROHIBITED; // 状态栏;值为1000.0
111      */
112
113     [self makeKeyWindow];
114     self.hidden = NO;
115 }
116
117 #pragma mark - NSUserDefaults
118 - (BOOL)isHaveLogined {
119     //使用偏好设置判断「是否已经登录」
120     return [[NSUserDefaults standardUserDefaults] boolForKey:isLoginedStr];
121 }
122
123 - (void)loginIn {
124     [[NSUserDefaults standardUserDefaults] setBool:YES forKey:isLoginedStr];
125 }
126
127 - (void)loginOut {
128     [[NSUserDefaults standardUserDefaults] setBool:NO forKey:isLoginedStr];
129 }
130
131 @end

AppDelegate.h

1 #import <UIKit/UIKit.h>
2
3 @interface AppDelegate : UIResponder <UIApplicationDelegate>
4
5 @property (strong, nonatomic) UIWindow *window;
6 @property (strong, nonatomic) UINavigationController *navigationController;
7 @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
8
9 @end

AppDelegate.m

 1 #import "AppDelegate.h"
 2 #import "ViewController.h"
 3 #import "PasswordInputWindow.h"
 4 #import "DateHelper.h"
 5
 6 @interface AppDelegate () {
 7     NSDate *_dateOfEnterBackground;
 8 }
 9
10 - (void)beginBackgroundUpdateTask;
11 - (void)longtimeToHandleSomething;
12 - (void)endBackgroundUpdateTask;
13
14 @end
15
16 @implementation AppDelegate
17
18
19 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
20     _window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
21     ViewController *viewController = [[ViewController alloc]
22                                       initWithSampleNameArray:@[ kTitleOfPList,
23                                                                  kTitleOfPreference,
24                                                                  kTitleOfNSKeyedArchiver,
25                                                                  kTitleOfKeychain,
26                                                                  kTitleOfSQLite,
27                                                                  kTitleOfCoreData,
28                                                                  kTitleOfFMDB ]];
29     _navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
30     _window.rootViewController = _navigationController;
31     [_window makeKeyAndVisible];
32
33     [[PasswordInputWindow sharedInstance] show];
34     return YES;
35 }
36
37 - (void)applicationWillResignActive:(UIApplication *)application {
38 }
39
40 - (void)applicationDidEnterBackground:(UIApplication *)application {
41     [self beginBackgroundUpdateTask];
42     [self longtimeToHandleSomething];
43     [self endBackgroundUpdateTask];
44 }
45
46 - (void)applicationWillEnterForeground:(UIApplication *)application {
47     NSDate *localeDate = [DateHelper localeDate];
48     // 这里设置为5秒用于测试;实际上合理场景应该是60 * 5 = 5分钟或者更长时间
49     _dateOfEnterBackground = [_dateOfEnterBackground dateByAddingTimeInterval:1 * 5];
50
51     PasswordInputWindow *passwordInputWindow = [PasswordInputWindow sharedInstance];
52     // 规定的一段时间没操作,就自动注销登录
53     if ([localeDate compare:_dateOfEnterBackground] == NSOrderedDescending) {
54         [passwordInputWindow loginOut];
55     }
56
57     if (![passwordInputWindow isHaveLogined]) {
58         [passwordInputWindow show];
59     }
60 }
61
62 - (void)applicationDidBecomeActive:(UIApplication *)application {
63 }
64
65 - (void)applicationWillTerminate:(UIApplication *)application {
66 }
67
68 #pragma mark - backgroundTask
69 - (void)beginBackgroundUpdateTask {
70     _backgroundTaskIdentifier = [kApplication beginBackgroundTaskWithExpirationHandler:^{
71         [self endBackgroundUpdateTask];
72     }];
73 }
74
75 - (void)longtimeToHandleSomething {
76     _dateOfEnterBackground = [DateHelper localeDate];
77
78     NSLog(@"默认情况下,当 App 被按 Home 键退出进入后台挂起前,应用仅有最多5秒时间做一些保存或清理资源的工作。");
79     NSLog(@"beginBackgroundTaskWithExpirationHandler 方法让 App 最多有10分钟时间在后台长久运行。这个时间可以用来清理本地缓存、发送统计数据等工作。");
80
81 //    for (NSInteger i=0; i<1000; i++) {
82 //        sleep(1);
83 //        NSLog(@"后台长久运行做一些事情%ld", (long)i);
84 //    }
85 }
86
87 - (void)endBackgroundUpdateTask {
88     [kApplication endBackgroundTask:_backgroundTaskIdentifier];
89     _backgroundTaskIdentifier = UIBackgroundTaskInvalid;
90 }
91
92 @end

EnumKMDataAccessFunction.h

1 typedef NS_ENUM(NSUInteger, KMDataAccessFunction) {
2     KMDataAccessFunctionPList,
3     KMDataAccessFunctionPreference,
4     KMDataAccessFunctionNSKeyedArchiver,
5     KMDataAccessFunctionKeychain,
6     KMDataAccessFunctionSQLite,
7     KMDataAccessFunctionCoreData,
8     KMDataAccessFunctionFMDB
9 };

KMTableViewCell.h

 1 #import <UIKit/UIKit.h>
 2 #import "SWTableViewCell.h"
 3
 4 @interface KMTableViewCell : SWTableViewCell
 5 @property (strong, nonatomic) IBOutlet UIImageView *imgVAvatarImage;
 6 @property (strong, nonatomic) IBOutlet UILabel *lblName;
 7 @property (strong, nonatomic) IBOutlet UILabel *lblCreatedAt;
 8 @property (strong, nonatomic) IBOutlet UIImageView *imgVLink;
 9
10 @property (strong, nonatomic) UILabel *lblText;
11 @property (copy, nonatomic) NSString *avatarImageStr;
12 @property (copy, nonatomic) NSString *name;
13 @property (copy, nonatomic) NSString *text;
14 @property (strong, nonatomic) NSDate *createdAt;
15 @property (assign, nonatomic, getter=isHaveLink) BOOL haveLink;
16
17 @end

KMTableViewCell.m

 1 #import "KMTableViewCell.h"
 2 #import "UIImageView+WebCache.h"
 3 #import "DateHelper.h"
 4
 5 static UIImage *placeholderImage;
 6 static CGFloat widthOfLabel;
 7
 8 @implementation KMTableViewCell
 9
10 - (void)awakeFromNib {
11     // Initialization code
12     static dispatch_once_t onceToken;
13     dispatch_once(&onceToken, ^{
14         placeholderImage = [UIImage imageNamed:@"JSON"];
15         widthOfLabel = [[UIScreen mainScreen] bounds].size.width - 100.0;
16     });
17
18     _imgVAvatarImage.layer.masksToBounds = YES;
19     _imgVAvatarImage.layer.cornerRadius = 10.0;
20
21     // 由于 xib 中对标签自适应宽度找不到合适的方式来控制,所以这里用代码编写;这里屏幕复用的 Cell 有几个,就会执行几次 awakeFromNib 方法
22     _lblText = [[UILabel alloc] initWithFrame:CGRectMake(90.0, 23.0, widthOfLabel, 42.0)];
23     _lblText.numberOfLines = 2;
24     _lblText.font = [UIFont systemFontOfSize:12.0];
25     [self addSubview:_lblText];
26     [self sendSubviewToBack:_lblText]; // 把视图置于底层;避免遮住左右手势滑动出现的「实用按钮」
27 }
28
29 - (void)setSelected:(BOOL)selected animated:(BOOL)animated {
30     [super setSelected:selected animated:animated];
31
32     // Configure the view for the selected state
33 }
34
35 - (void)setAvatarImageStr:(NSString *)avatarImageStr {
36     if (![_avatarImageStr isEqualToString:avatarImageStr]) {
37         _avatarImageStr = [avatarImageStr copy];
38         NSURL *avatarImageURL = [NSURL URLWithString:_avatarImageStr];
39         // 图片缓存;使用 SDWebImage 框架:UIImageView+WebCache
40         [_imgVAvatarImage sd_setImageWithURL:avatarImageURL
41                             placeholderImage:placeholderImage];
42     }
43 }
44
45 - (void)setName:(NSString *)name {
46     _name = [name copy];
47     _lblName.text = _name;
48 }
49
50 - (void)setText:(NSString *)text {
51     _text = [text copy];
52     _lblText.text = _text;
53 }
54
55 - (void)setCreatedAt:(NSDate *)createdAt {
56     _createdAt = [createdAt copy];
57     _lblCreatedAt.text = [DateHelper dateToString:_createdAt withFormat:nil];
58 }
59
60 - (void)setHaveLink:(BOOL)haveLink {
61     _haveLink = haveLink;
62     _imgVLink.hidden = !_haveLink;
63 }
64
65 @end

KMTableViewCell.xib

 

KMTableView.h

 1 #import <UIKit/UIKit.h>
 2 #import "KMTableViewCell.h"
 3 #import "GlobalInfoModel.h"
 4
 5 typedef void (^TableViewCellConfigureBlock)(KMTableViewCell *cell, GlobalInfoModel *globalInfo);
 6 typedef void (^DidSelectRowBlock)(NSInteger row, GlobalInfoModel *globalInfo);
 7 typedef void (^DidModifyRowBlock)(NSNumber *ID);
 8 typedef void (^DidDelectRowBlock)(NSNumber *ID);
 9
10 @interface KMTableView : UITableView <UITableViewDataSource, UITableViewDelegate, SWTableViewCellDelegate>
11 @property (strong, nonatomic) NSMutableArray *mArrGlobalInfo; // 这里不能用 copy,必须用 strong,因为 copy 的话会复制出不可变数组,删除操作会出错;我们这里是需要可变数组的
12 @property (copy, nonatomic) TableViewCellConfigureBlock cellConfigureBlock;
13 @property (copy, nonatomic) DidSelectRowBlock didSelectRowBlock;
14 @property (copy, nonatomic) DidModifyRowBlock didModifyRowBlock;
15 @property (copy, nonatomic) DidDelectRowBlock didDelectRowBlock;
16
17 - (instancetype)initWithGlobalInfoArray:(NSMutableArray *)mArrGlobalInfo frame:(CGRect)frame cellConfigureBlock:(TableViewCellConfigureBlock) cellConfigureBlock didSelectRowBlock:(DidSelectRowBlock)didSelectRowBlock didModifyRowBlock:(DidModifyRowBlock)didModifyRowBlock didDelectRowBlock:(DidDelectRowBlock)didDelectRowBlock;
18
19 @end

KMTableView.m

  1 #import "KMTableView.h"
  2
  3 static NSString *const cellIdentifier = @"cellIdentifier";
  4
  5 @implementation KMTableView {
  6     UILabel *_lblEmptyDataMsg;
  7 }
  8
  9 - (instancetype)initWithGlobalInfoArray:(NSMutableArray *)mArrGlobalInfo frame:(CGRect)frame cellConfigureBlock:(TableViewCellConfigureBlock) cellConfigureBlock didSelectRowBlock:(DidSelectRowBlock)didSelectRowBlock didModifyRowBlock:(DidModifyRowBlock)didModifyRowBlock didDelectRowBlock:(DidDelectRowBlock)didDelectRowBlock{
 10     if (self = [super init]) {
 11         _mArrGlobalInfo = mArrGlobalInfo;
 12         self.frame = frame;
 13         _cellConfigureBlock = [cellConfigureBlock copy];
 14         _didSelectRowBlock = [didSelectRowBlock copy];
 15         _didModifyRowBlock = [didModifyRowBlock copy];
 16         _didDelectRowBlock = [didDelectRowBlock copy];
 17
 18         [self tableViewLayout];
 19     }
 20     return self;
 21 }
 22
 23 - (void)tableViewLayout {
 24     // 设置边距,解决单元格分割线默认偏移像素过多的问题
 25     if ([self respondsToSelector:@selector(setSeparatorInset:)]) {
 26         [self setSeparatorInset:UIEdgeInsetsZero]; // 设置单元格(上左下右)内边距
 27     }
 28     if ([self respondsToSelector:@selector(setLayoutMargins:)]) {
 29         [self setLayoutMargins:UIEdgeInsetsZero]; // 设置单元格(上左下右)外边距
 30     }
 31
 32     // 注册可复用的单元格
 33     UINib *nib = [UINib nibWithNibName:@"KMTableViewCell" bundle:nil];
 34     [self registerNib:nib forCellReuseIdentifier:cellIdentifier];
 35
 36     self.dataSource = self;
 37     self.delegate = self;
 38
 39     // 空数据时,显示的提示内容
 40     _lblEmptyDataMsg = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 50.0)];
 41     CGPoint newPoint = self.center;
 42     newPoint.y -= 60.0;
 43     _lblEmptyDataMsg.center = newPoint;
 44     _lblEmptyDataMsg.text = @"点击「+」按钮添加全球新闻信息";
 45     _lblEmptyDataMsg.textColor = [UIColor grayColor];
 46     _lblEmptyDataMsg.textAlignment = NSTextAlignmentCenter;
 47     _lblEmptyDataMsg.font = [UIFont systemFontOfSize:16.0];
 48     [self addSubview:_lblEmptyDataMsg];
 49 }
 50
 51 - (NSArray *)rightButtons {
 52     NSMutableArray *rightUtilityButtons = [NSMutableArray new];
 53     [rightUtilityButtons sw_addUtilityButtonWithColor:
 54      [UIColor colorWithRed:0.78f green:0.78f blue:0.8f alpha:1.0]
 55                                                 title:@"修改"];
 56     [rightUtilityButtons sw_addUtilityButtonWithColor:
 57      [UIColor colorWithRed:1.0f green:0.231f blue:0.188 alpha:1.0f]
 58                                                 title:@"删除"];
 59
 60     return rightUtilityButtons;
 61 }
 62
 63 #pragma mark - TableView DataSource and Delegate
 64 - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
 65     return @"全球新闻信息列表";
 66 }
 67
 68 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
 69     return 1;
 70 }
 71
 72 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
 73     NSUInteger count = _mArrGlobalInfo.count;
 74     _lblEmptyDataMsg.hidden = count > 0;
 75
 76     return count;
 77 }
 78
 79 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 80     KMTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
 81     if (!cell) {
 82         cell = [[KMTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
 83                                       reuseIdentifier:cellIdentifier];
 84     }
 85     cell.rightUtilityButtons = [self rightButtons];
 86     cell.delegate = self;
 87
 88     GlobalInfoModel *globalInfo = _mArrGlobalInfo[indexPath.row];
 89     cell.tag = [globalInfo.ID integerValue]; // 存储 ID 用于「修改」和「删除」记录操作
 90     cell.avatarImageStr = globalInfo.avatarImageStr;
 91     cell.name = globalInfo.name;
 92     cell.text = globalInfo.text;
 93     cell.createdAt = globalInfo.createdAt;
 94     cell.haveLink = globalInfo.haveLink;
 95
 96     _cellConfigureBlock(cell, globalInfo);
 97     return cell;
 98 }
 99
100 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
101     return 90.0;
102 }
103
104 - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
105     if ([cell respondsToSelector:@selector(setSeparatorInset:)]) {
106         [cell setSeparatorInset:UIEdgeInsetsZero];
107     }
108     if ([cell respondsToSelector:@selector(setLayoutMargins:)]) {
109         [cell setLayoutMargins:UIEdgeInsetsZero];
110     }
111 }
112
113 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
114     NSInteger row = indexPath.row;
115     _didSelectRowBlock(row, _mArrGlobalInfo[row]);
116 }
117
118 #pragma mark - SWTableViewCellDelegate
119 - (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerRightUtilityButtonWithIndex:(NSInteger)index {
120     NSNumber *ID = @(cell.tag);
121     switch (index) {
122         case 0: {
123             NSLog(@"点击了修改按钮");
124             _didModifyRowBlock(ID);
125             break;
126         }
127         case 1: {
128             NSLog(@"点击了删除按钮");
129             _didDelectRowBlock(ID);
130
131             NSIndexPath *cellIndexPath = [self indexPathForCell:cell];
132             [_mArrGlobalInfo removeObjectAtIndex:cellIndexPath.row];
133             [self deleteRowsAtIndexPaths:@[ cellIndexPath ]
134                         withRowAnimation:UITableViewRowAnimationAutomatic];
135             break;
136         }
137         default:
138             break;
139     }
140 }
141
142 - (BOOL)swipeableTableViewCellShouldHideUtilityButtonsOnSwipe:(SWTableViewCell *)cell {
143     return YES; // 设置是否隐藏其他行的「实用按钮」,即不同时出现多行的「实用按钮」;默认值为NO
144 }
145
146 @end

KMAddRecordViewController.h

 1 #import <UIKit/UIKit.h>
 2 #import "EnumKMDataAccessFunction.h"
 3 #import "KMDatePicker.h"
 4
 5 @interface KMAddRecordViewController : UIViewController <KMDatePickerDelegate, UIAlertViewDelegate>
 6 @property (assign, nonatomic) KMDataAccessFunction dataAccessFunction;
 7 @property (strong, nonatomic) NSNumber *ID;
 8
 9 @property (strong, nonatomic) IBOutlet UITextField *txtFAvatarImageStr;
10 @property (strong, nonatomic) IBOutlet UITextField *txtFName;
11 @property (strong, nonatomic) IBOutlet UITextView *txtVText;
12 @property (strong, nonatomic) IBOutlet UITextField *txtFLink;
13 @property (strong, nonatomic) IBOutlet UITextField *txtFCreatedAt;
14 @property (strong, nonatomic) IBOutlet UIButton *btnSave;
15 @property (strong, nonatomic) IBOutlet UIButton *btnCancel;
16
17 @end

KMAddRecordViewController.m

  1 #import "KMAddRecordViewController.h"
  2 #import "UIButton+BeautifulButton.h"
  3 #import "GlobalInfoModel.h"
  4 #import "SQLiteGlobalInfoService.h"
  5 #import "CoreDataGlobalInfoService.h"
  6 #import "FMDBGlobalInfoService.h"
  7 #import "DateHelper.h"
  8
  9 @interface KMAddRecordViewController ()
 10 - (void)layoutUI;
 11 - (void)showAlertView:(NSString *)message;
 12 @end
 13
 14 @implementation KMAddRecordViewController
 15
 16 - (void)viewDidLoad {
 17     [super viewDidLoad];
 18
 19     [self layoutUI];
 20 }
 21
 22 - (void)didReceiveMemoryWarning {
 23     [super didReceiveMemoryWarning];
 24     // Dispose of any resources that can be recreated.
 25 }
 26
 27 - (void)layoutUI {
 28     self.navigationItem.title = _ID ? @"修改记录" : @"添加记录";
 29
 30     CGRect rect = [[UIScreen mainScreen] bounds];
 31     rect = CGRectMake(0.0, 0.0, rect.size.width, 216.0);
 32     //年月日时分
 33     KMDatePicker *datePicker = [[KMDatePicker alloc]
 34                                 initWithFrame:rect
 35                                 delegate:self
 36                                 datePickerStyle:KMDatePickerStyleYearMonthDayHourMinute];
 37     _txtFCreatedAt.inputView = datePicker;
 38
 39     [_btnSave beautifulButton:[UIColor blackColor]];
 40     [_btnCancel beautifulButton:[UIColor brownColor]];
 41
 42     [self setValueFromGlobalInfo];
 43 }
 44
 45 - (void)setValueFromGlobalInfo {
 46     if (_ID) {
 47         GlobalInfoModel *globalInfo;
 48         switch (_dataAccessFunction) {
 49             case KMDataAccessFunctionSQLite: {
 50                 globalInfo = [[SQLiteGlobalInfoService sharedService] getGlobalInfoByID:_ID];
 51                 break;
 52             }
 53             case KMDataAccessFunctionCoreData: {
 54                 globalInfo = [[CoreDataGlobalInfoService sharedService] getGlobalInfoByID:_ID];
 55                 break;
 56             }
 57             case KMDataAccessFunctionFMDB: {
 58                 globalInfo = [[FMDBGlobalInfoService sharedService] getGlobalInfoByID:_ID];
 59                 break;
 60             }
 61             default: {
 62                 break;
 63             }
 64         }
 65
 66         if (globalInfo) {
 67             _txtFAvatarImageStr.text = globalInfo.avatarImageStr;
 68             _txtFName.text = globalInfo.name;
 69             _txtVText.text = globalInfo.text;
 70             _txtFLink.text = globalInfo.link;
 71             _txtFCreatedAt.text = [DateHelper dateToString:globalInfo.createdAt
 72                                                 withFormat:nil];
 73         }
 74     } else {
 75         NSDate *localeDate = [DateHelper localeDate];
 76         _txtFCreatedAt.text = [DateHelper dateToString:localeDate
 77                                             withFormat:nil];
 78         _txtFName.text =
 79             [NSString stringWithFormat:@"anthonydali22 (%@)",
 80                                        [DateHelper dateToString:localeDate
 81                                                      withFormat:@"yyyyMMddHHmmss"]];
 82     }
 83 }
 84
 85 - (void)showAlertView:(NSString *)message {
 86     // iOS (8.0 and later)
 87     /*
 88     UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"提示信息"
 89                                                                    message:message
 90                                                             preferredStyle:UIAlertControllerStyleAlert];
 91
 92     UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"确定"
 93                                                             style:UIAlertActionStyleDefault
 94                                                           handler:^(UIAlertAction * action) {
 95                                                               if ([message hasSuffix:@"成功"]) {
 96                                                                   [self.navigationController popViewControllerAnimated:YES];
 97                                                               }
 98                                                           }];
 99
100     [alert addAction:defaultAction];
101     [self presentViewController:alert animated:YES completion:nil];
102      */
103
104     // iOS (2.0 and later)
105     UIAlertView *alertV = [[UIAlertView alloc] initWithTitle:@"提示信息"
106                                                      message:message
107                                                     delegate:self
108                                            cancelButtonTitle:nil
109                                            otherButtonTitles:@"确定", nil];
110     [alertV show];
111 }
112
113 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
114 //    [_txtFAvatarImageStr resignFirstResponder];
115 //    [_txtFName resignFirstResponder];
116 //    [_txtVText resignFirstResponder];
117 //    [_txtFLink resignFirstResponder];
118 //    [_txtFCreatedAt resignFirstResponder];
119
120     [self.view endEditing:YES];
121 }
122
123 - (IBAction)save:(id)sender {
124     NSString *avatarImageStr = [_txtFAvatarImageStr text];
125     NSString *name = [_txtFName text];
126     NSString *text = [_txtVText text];
127     NSString *link = [_txtFLink text];
128     NSDate *createdAt = [DateHelper dateFromString:[_txtFCreatedAt text]
129                                         withFormat:nil];
130
131     GlobalInfoModel *globalInfo = [[GlobalInfoModel alloc] initWithAvatarImageStr:avatarImageStr
132                                                                              name:name
133                                                                              text:text
134                                                                              link:link
135                                                                         createdAt:createdAt
136                                                                                ID:_ID];
137     BOOL isSuccess;
138     NSString *message;
139     //NSLog(@"KMDataAccessFunction: %lu", (unsigned long)_dataAccessFunction);
140     switch (_dataAccessFunction) {
141         case KMDataAccessFunctionSQLite: {
142             if (_ID) {
143                 isSuccess = [[SQLiteGlobalInfoService sharedService] updateGlobalInfo:globalInfo];
144             } else {
145                 isSuccess = [[SQLiteGlobalInfoService sharedService] insertGlobalInfo:globalInfo];
146             }
147             break;
148         }
149         case KMDataAccessFunctionCoreData: {
150             if (_ID) {
151                 isSuccess = [[CoreDataGlobalInfoService sharedService] updateGlobalInfo:globalInfo];
152             } else {
153                 globalInfo.ID = [self getIdentityNumber];
154                 isSuccess = [[CoreDataGlobalInfoService sharedService] insertGlobalInfo:globalInfo];
155             }
156             break;
157         }
158         case KMDataAccessFunctionFMDB: {
159             if (_ID) {
160                 isSuccess = [[FMDBGlobalInfoService sharedService] updateGlobalInfo:globalInfo];
161             } else {
162                 isSuccess = [[FMDBGlobalInfoService sharedService] insertGlobalInfo:globalInfo];
163             }
164             break;
165         }
166         default: {
167             break;
168         }
169     }
170
171     message = [NSString stringWithFormat:@"%@%@", _ID ? @"修改" : @"添加",
172                isSuccess ? @"成功" : @"失败"];
173     [self showAlertView:message];
174 }
175
176 - (IBAction)cancel:(id)sender {
177     [self.navigationController popViewControllerAnimated:YES];
178 }
179
180 - (NSNumber *)getIdentityNumber {
181     NSNumber *identityNumber;
182
183     // 用于 CoreData 操作数据库新增记录时,自增长标示值
184     NSString *const kIdentityValOfCoreData = @"identityValOfCoreData";
185     NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
186     NSInteger identityVal = [userDefaults integerForKey:kIdentityValOfCoreData];
187     identityVal = identityVal == 0 ? 1 : ++identityVal; //当键值对不存在时,identityVal 的默认值为0
188     [userDefaults setInteger: identityVal forKey:kIdentityValOfCoreData];
189     identityNumber = @(identityVal);
190     return identityNumber;
191 }
192
193 #pragma mark - KMDatePickerDelegate
194 - (void)datePicker:(KMDatePicker *)datePicker didSelectDate:(KMDatePickerDateModel *)datePickerDate {
195     NSString *dateStr = [NSString stringWithFormat:@"%@-%@-%@ %@:%@",
196                          datePickerDate.year,
197                          datePickerDate.month,
198                          datePickerDate.day,
199                          datePickerDate.hour,
200                          datePickerDate.minute
201                          ];
202     _txtFCreatedAt.text = dateStr;
203 }
204
205 #pragma mark - UIAlertViewDelegate
206 - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
207     // 如果有提醒对话框有多个按钮,可以根据 buttonIndex 判断点击了哪个按钮
208     if ([alertView.message hasSuffix:@"成功"]) {
209         [self.navigationController popViewControllerAnimated:YES];
210     }
211 }
212
213 @end

KMAddRecordViewController.xib

iOS---数据本地化的更多相关文章

  1. iOS数据本地化

    本篇随笔除了介绍 iOS 数据持久化知识之外,还贯穿了以下内容: (1)自定义 TableView,结合 block 从 ViewController 中分离出 View,轻 ViewControll ...

  2. iOS数据持久化-OC

    沙盒详解 1.IOS沙盒机制 IOS应用程序只能在为该改程序创建的文件系统中读取文件,不可以去其它地方访问,此区域被成为沙盒,所以所有的非代码文件都要保存在此,例如图像,图标,声音,映像,属性列表,文 ...

  3. OC 数据持久化(数据本地化)- 本地存储

    // // ViewController.m // IOS_0113_本地存储 // // Created by ma c on 16/1/13. // Copyright (c) 2016年 博文科 ...

  4. iOS 数据持久化(扩展知识:模糊背景效果和密码保护功能)

    本篇随笔除了介绍 iOS 数据持久化知识之外,还贯穿了以下内容: (1)自定义 TableView,结合 block 从 ViewController 中分离出 View,轻 ViewControll ...

  5. spark新能优化之数据本地化

    数据本地化的背景: 数据本地化对于Spark Job性能有着巨大的影响.如果数据以及要计算它的代码是在一起的,那么性能当然会非常高.但是,如果数据和计算它的代码是分开的,那么其中之一必须到另外一方的机 ...

  6. iOS开发笔记-swift实现iOS数据持久化之归档NSKeyedArchiver

    IOS数据持久化的方式分为三种: 属性列表 (plist.NSUserDefaults) 归档 (NSKeyedArchiver) 数据库 (SQLite.Core Data.第三方类库等 归档(又名 ...

  7. iOS数据存储之对象归档

    iOS数据存储之对象归档 对象归档 对象归档是iOS中数据持久化的一种方式. 归档是指另一种形式的序列化,但它是任何对象都可以实现的更常规的类型.使用对模型对象进行归档的技术可以轻松将复杂的对象写入文 ...

  8. iOS数据存储之属性列表理解

    iOS数据存储之属性列表理解 数据存储简介 数据存储,即数据持久化,是指以何种方式保存应用程序的数据. 我的理解是,开发了一款应用之后,应用在内存中运行时会产生很多数据,这些数据在程序运行时和程序一起 ...

  9. 7.数据本地化CCString,CCArray,CCDictionary,tinyxml2,写入UserDefault.xml文件,操作xml,解析xml

     数据本地化 A CCUserDefault 系统会在默认路径cocos2d-x-2.2.3\projects\Hello\proj.win32\Debug.win32下生成一个名为UserDef ...

  10. Spark数据本地化-->如何达到性能调优的目的

    Spark数据本地化-->如何达到性能调优的目的 1.Spark数据的本地化:移动计算,而不是移动数据 2.Spark中的数据本地化级别: TaskSetManager 的 Locality L ...

随机推荐

  1. Coming

    Hi,there, I am coming here to keep a great habit-one day a blog to share what new I have learned tod ...

  2. iOS 面试总结 二

    1.用三种方法生成内容为数字 1,2 ,3 的可变数组.(使用Objective-C,尽量一行代码实现) //方法一 NSMutableArray *arr1 = [[NSMutableArray a ...

  3. ESLint 规则

    ESLint由 JavaScript 红宝书 作者 Nicholas C.Zakas 编写, 2013 年发布第一个版本. ESLint是一个以可扩展.每条规则独立的,被设计为完全可配置的lint工具 ...

  4. [IOS]使用了cocoapods 抱错Pods was rejected as an implicit dependency for ‘libPods.a’ because its architectures ......

    Pods was rejected as an implicit dependency for ‘libPods.a’ because its architectures ‘i386’ didn’t ...

  5. Linux 下安装中文 ctex 指南

    大家在用 $\LaTeX$ 进行中文排版时相信会遇到不少问题,而$\textbf{ctex}$套装的出现则有效的解决了这一问题,只要安装了$\textbf{ctex}$那么在文中不用进行引用设置就可以 ...

  6. 移动端图片上传base64编码

    $base64 = "/9j/4AAQSkZJRgABAQEAkACQAAD/4QCMRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAA ...

  7. ThinkPHP3.* 模型操作相关函数

    ThinkPHP3.* 版本,大家所不熟知的,且与数据库操作相关的函数做以简单罗列: 1.getLastSql 别名 _sql (鉴于getLastSql比较常用,故出现了别名函数_sql) 2.se ...

  8. Apache服务器在80端口配置多域名虚拟主机的方法

    我们在配置一台服务器的时候,如果只运行一个站点,往往过于浪费资源.Nginx和Apache都可以通过配置虚拟主机实现多站点.配置虚拟主机的方式主要有两种,一种是多个不同端口对应的多个虚拟主机站点,一种 ...

  9. listview嵌套gridview,并实现grid元素部分显示以及点击展开与折叠

    原文链接:http://blog.csdn.net/duguju/article/details/49538341 有时我们需要用GridView显示目录列表,有时甚至是二级的,即listview每一 ...

  10. TCP三次握手四次挥手

    看到一篇总结很好的TCP三次握手,学习一下,原文链接. 建立TCP需要三次握手才能建立,而断开连接则需要四次握手.整个过程如下图所示: 先来看看如何建立连接的. 首先Client端发送连接请求报文,S ...