《Objective-C 编程》30.Property List

Property List 是一种既可以被计算机读取又可以被人阅读的文件格式。Property List 文件可以由以下对象组成:

  • NSArray
  • NSDictionary
  • NSString
  • NSDate
  • NSData
  • NSNumber (封装基础数据类型,整数、浮点数或布尔值)

读取或写入 Property List 文件

// 创建一个包含多个 NSDictionary 对象的 NSArray 对象
NSMutableArray *stocks = [[NSMutableArray alloc] init];
NSMutableDictionary *stock;

stock = [NSMutableDictionary dictionary];
[stock setObject:@"AAPL" forKey:@"symbol"];
[stock setObject:[NSNumber numberWithInt:200] forKey:@"shares"];
[stocks addObject:stock];

stock = [NSMutableDictionary dictionary];
[stock setObject:@"GOOG" forKey:@"symbol"];
[stock setObject:[NSNumber numberWithInt:160] forKey:@"shares"];
[stocks addObject:stock];

// 写入 property list 文件
[stocks writeToFile:@"/tmp/stocks.plist" atomically:YES];

// 读取之前生成的 property list 文件
NSArray *stockList = [NSArray arrayWithContentsOfFile:@"/tmp/stocks.plist"];
for (NSDictionary *d in stockList) {
    NSLog(@"I have %@ shares of %@",
          [d objectForKey:@"shares"],[d objectForKey:@"symbol"]);
}

使用 Xcode 内置的专用编辑器查看 Property List 文件数据:

在项目中使用 Property List 文件

其实我们在项目中看到的 info.plist 配置文件就是一个 Property List 文件。其次,还可以使用 Property List 文件格式化一些数据源。

例如:要在首页设置 4 个 Button,每个 Button 都有标题、图片、索引标志。直接用代码的方式将它们分散设置在代码中当然是可以的。但是如果使用 Property List 文件,我们可以把这些数据抽象提取出来格式化保存,一来代码中不同的属性被抽象提取,提高了复用性。二来,如果以后 Button 按钮标题需要修改,直接去 Property List 文件中修改就可以了。

  1. 创建 Property List文件:
  1. 读取 Property List 文件:
- (NSArray *)rootArray {
    if (!_rootArray) {
        NSString *path = [[NSBundle mainBundle] pathForResource:@"navigationButton" ofType:@"plist"];
        _rootArray = [NSArray arrayWithContentsOfFile:path];
    }
    return _rootArray;
}
  1. 读/写 Property List 文件
// 读/写 property list 文件:
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionary];
mutableDict[@"key"] = @"value";
[mutableDict writeToFile:@"/some/path/file" atomically:YES];
    
NSMutableDictionary *anotherDict = [[NSMutableDictionary alloc] initWithContentsOfFile:@"/some/path/file"];

读取 Property List 文件方法的演进

我需要将项目根目录下的 mainTableViewTitleModel.plist 文件读取到 NSArray 数组中,下面是关于如何迭代演进该方法的代码示例。

Version 1,数据模型转换

上面的示例中,我们只是简单的读取并加载了一个 NSArray 数组,数组中的每一个项都是 NSDictionary 实例,如果需要在数组加载完成后,把数组中的 JSON 数据转换成模型(Model),应该怎么做?

开源的数据模型转换框架有很多,YYModel、MJExtension、Mantle... 这里以 Mantle 框架为例,通过 Lazy Loading 的方式加载数据源:

// 从 mainTableViewTitleModel.plist 文件中读取数据源加载到 NSArray 类型的数组中
- (NSArray *)dataSourceArray {
    if (!_dataSourceArray) {
        // mainTableViewTitleModel.plist 文件路径
        NSString *path = [[NSBundle mainBundle] pathForResource:@"mainTableViewTitleModel" ofType:@"plist"];
        
        // 读取 mainTableViewTitleModel.plist 文件,并存放进 jsonArray 数组
        NSArray *jsonArray = [NSArray arrayWithContentsOfFile:path];
        
        // 将 jsonArray 数组中的 JSON 数据转换成 HQLTableViewDataSourceModel 模型
        _dataSourceArray = [MTLJSONAdapter modelsOfClass:HQLTableViewDataSourceModel.class
                                           fromJSONArray:jsonArray
                                                   error:NULL];
    }
    return _dataSourceArray;
}

其中,HQLTableViewDataSourceModel 类是一个模型类,它是 MTLModel 的子类对象,并且遵守 <MTLJSONSerializing> 协议。
现在,数组中的每一项都是一个 HQLTableViewDataSourceModel 对象的实例。

Version 2,方法兼容性适配

Version 1 的示例中,arrayWithContentsOfFile: 方法在 Apple 文档中已经被标记为失效(deprecated)了:

/* These methods are deprecated, and will be marked with API_DEPRECATED in a subsequent release. Use the variants that use errors instead. */
+ (nullable NSArray<ObjectType> *)arrayWithContentsOfFile:(NSString *)path API_DEPRECATED_WITH_REPLACEMENT("arrayWithContentsOfURL:error:", macos(10.0, API_TO_BE_DEPRECATED), ios(2.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED));

因此,我需要通过宏 @available(iOS 11.0, *) 根据 iOS 的不同版本实现兼容性适配。具体来说:

  • 在 iOS 11 以前的版本中,我会使用 NSArrayarrayWithContentsOfURL: 方法读取 plist 文件数据。
  • 在 iOS 11 及其之后的版本中,我会使用 NSArrayarrayWithContentsOfURL:error: 方法读取 plist 文件数据。

另外,我还想查看该方法是否正常工作并正确加载我所需要的数据源,所以,我尝试在控制台打印输出内容:

// 从 mainTableViewTitleModel.plist 文件中读取数据源加载到 NSArray 类型的数组中
- (NSArray *)dataSourceArray {
    if (!_dataSourceArray) {

        // 1.构造 mainTableViewTitleModel.plist 文件 URL 路径
        NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
        NSURL *url = [bundleURL URLByAppendingPathComponent:@"mainTableViewTitleModel.plist"];
        
        // 2.读取 mainTableViewTitleModel.plist 文件,并存放进 jsonArray 数组
        NSArray *jsonArray;
        if (@available(iOS 11.0, *)) {
            NSError *readFileError = nil;
            jsonArray = [NSArray arrayWithContentsOfURL:url error:&readFileError];
            if (!jsonArray || jsonArray.count == 0) {
                DDLogError(@"%@, NSPropertyList File read error:\n%@", @(__PRETTY_FUNCTION__), readFileError.localizedDescription);
                return nil;
            }
        } else {
            jsonArray = [NSArray arrayWithContentsOfURL:url];
            if (!jsonArray || jsonArray.count == 0) {
                DDLogError(@"%@, NSPropertyList File read error.", @(__PRETTY_FUNCTION__));
                return nil;
            }
        }
        
         // 3.将 jsonArray 数组中的 JSON 数据转换成 HQLTableViewDataSourceModel 模型
        NSError *decodeError = nil;
        _dataSourceArray = [MTLJSONAdapter modelsOfClass:HQLTableViewDataSourceModel.class
                                           fromJSONArray:jsonArray
                                                   error:&decodeError];
        if (!_dataSourceArray) {
            DDLogError(@"%@, jsonArray decode error:\n%@", @(__PRETTY_FUNCTION__), decodeError.localizedDescription);
            return nil;
        }
    }
    return _dataSourceArray;
}

以上代码中,每次执行完一个操作,我都进行了空值的判断(美其名曰:编写防御性代码),如果数据为空,则打印输出当前方法名、NSError 错误内容以便定位发生错误的代码行,并调试解决问题。

注:DDLogError() 是 CocoaLumberjack 框架中打印控制台输出,记录日志的方法,你可以简单理解为类似 NSLog() 的宏。

Version 3,使用断言处理异常

每次通过控制台打印日志的方式处理错误总不是十分友好,因此,我又尝试进一步改进,使用 NSAssert() 断言机制来处理异常:

// 从 mainTableViewTitleModel.plist 文件中读取数据源加载到 NSArray 类型的数组中
- (NSArray<HQLTableViewDataSourceModel *> *)dataSourceArray {
    if (!_dataSourceArray) {
        // 1.构造 mainTableViewTitleModel.plist 文件 URL 路径
        NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
        NSURL *url = [bundleURL URLByAppendingPathComponent:@"mainTableViewTitleModel.plist"];
        
        // 2.读取 mainTableViewTitleModel.plist 文件,并存放进 jsonArray 数组
        NSArray *jsonArray;
        if (@available(iOS 11.0, *)) {
            NSError *readFileError = nil;
            jsonArray = [NSArray arrayWithContentsOfURL:url error:&readFileError];
            NSAssert1(jsonArray, @"NSPropertyList File read error:\n%@", readFileError);
        } else {
            jsonArray = [NSArray arrayWithContentsOfURL:url];
            NSAssert(jsonArray, @"NSPropertyList File read error.");
        }
        
        // 3.将 jsonArray 数组中的 JSON 数据转换成 HQLTableViewDataSourceModel 模型
        NSError *decodeError = nil;
        _dataSourceArray = [MTLJSONAdapter modelsOfClass:HQLTableViewDataSourceModel.class
                                           fromJSONArray:jsonArray
                                                   error:&decodeError];
        NSAssert1(_dataSourceArray, @"JSONArray decode error:\n%@", decodeError);
    }
    return _dataSourceArray;
}

通过 NSAssert() 断言处理异常的好处是:

  1. NSAssert() 断言仅仅在开发环境下工作,而我的 Property List 文件本身就是静态文件,因此,我只需要在开发时确保加载的静态文件能正常工作即可。
  2. NSAssert() 断言方法可以自动在控制台打印当前方法名和各种调试信息,不用再手动敲类似于 NSLog(@"%s", __PRETTY_FUNCTION__); 的代码打印当前方法名了。
  3. 少写了一些不必要的判断语句和嵌套代码,代码整体更整洁。

Version 4,封装为 Store 类

Version 3 版本中,加载 Property List 文件的方法已经可以非常好的工作了,但当我的项目中有很多 Property List 文件需要加载时,难免不了需要编写大量重复的代码,或者称之为有冗余的样板代码。

而且从职责分离的角度来考虑,视图控制器也没有必要知道如何去加载文件并解析 JSON 数据。

所以我创建了一个 HQLPropertyListStore 对象来做这些事,通过 HQLPropertyListStore 对象来加载 Property List 数据。而且,通过将其封装到 HQLPropertyListStore 类中就可以实现职责分离,方便复用。

// ---------------------------------------------------------
#import <Foundation/Foundation.h>

@interface HQLPropertyListStore : NSObject

@property (nonatomic, readonly, copy) NSArray *dataSourceArray;

// 指定初始化方法
- (instancetype)initWithPlistFileName:(NSString *)fileName modelsOfClass:(Class)model;

@end

// ---------------------------------------------------------
#import "HQLPropertyListStore.h"

@interface HQLPropertyListStore ()
@property (nonatomic, readwrite, copy) NSArray *dataSourceArray;
@end

@implementation HQLPropertyListStore

- (instancetype)initWithPlistFileName:(NSString *)fileName modelsOfClass:(Class)model {
    self = [super init];
    if (self) {
        [self loadPlistDataWithFileName:fileName modelsOfClass:model];
    }
    return self;
}

- (void)loadPlistDataWithFileName:(NSString *)fileName modelsOfClass:(Class)model {
    // 1.构造 plist 文件 URL 路径
    NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
    NSURL *url = [bundleURL URLByAppendingPathComponent:fileName];
    
    // 2.读取 plist 文件,并存放进 jsonArray 数组
    NSArray *jsonArray;
    if (@available(iOS 11.0, *)) {
        NSError *readFileError = nil;
        jsonArray = [NSArray arrayWithContentsOfURL:url error:&readFileError];
        NSAssert1(jsonArray, @"NSPropertyList File read error:\n%@", readFileError);
    } else {
        jsonArray = [NSArray arrayWithContentsOfURL:url];
        NSAssert(jsonArray, @"NSPropertyList File read error.");
    }
    
    // 3.将 jsonArray 数组中的 JSON 数据转换成 model 模型
    NSError *decodeError = nil;
    self.dataSourceArray = [MTLJSONAdapter modelsOfClass:model.class
                                           fromJSONArray:jsonArray
                                                   error:&decodeError];
    NSAssert1(_dataSourceArray, @"JSONArray decode error:\n%@", decodeError);
}

@end

编写完成 HQLPropertyListStore 类之后,在任何需要加载 Property List 文件的视图控制器中,只需要编写类似如下的两行代码即可:

HQLPropertyListStore *store = [[HQLPropertyListStore alloc] initWithPlistFileName:@"mainTableViewTitleModel.plist" modelsOfClass:HQLTableViewDataSourceModel.class];
self.dataSourceArray = store.dataSourceArray;

Version 5,生命不息,优化不止

你要知道,不是所有的本地 JSON 文件都被保存为 .plist 文件,也有可能被保存为 .json 的原始文件了。最近,我在尝试编写城市选择器页面时,就遇到了这样的情况,如:https://github.com/houshixian/CityList/blob/master/CityList/CityList/JsonFile/CityArray.json

所以,我需要加载 .json 文件,并将其转换为 Model 数据。如果想要单纯的实现读取本地 JSON 文件并转换为 Model 的需求,我用两个方法就可以搞定了:

- (void)loadProvinceData {
    // 1.读取 ProvinceCity.json 文件,并存放进 jsonArray 数组
    NSArray *jsonArray = [self readLocalFileWithName:@"ProvinceCity.json"];
    
    // 2.将 jsonArray 数组中的 JSON 数据转换成 HQLProvince 模型
    NSError *decodeError = nil;
    _provinceArray = [MTLJSONAdapter modelsOfClass:HQLProvince.class
                                     fromJSONArray:jsonArray
                                             error:&decodeError];
    NSAssert1(_provinceArray, @"JSONArray decode error:\n%@", decodeError);
    
    // 初始化并设置默认值
    self.currentProvince = _provinceArray.firstObject;
    self.currentCity = _currentProvince.children.firstObject;
}

// 读取本地 JSON 文件,如:city.json
- (NSArray *)readLocalFileWithName:(NSString *)name {
    // 1.构造文件路径
    NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
    NSURL *url = [bundleURL URLByAppendingPathComponent:name];
    // 2.将文件数据化
    NSData *data = [[NSData alloc] initWithContentsOfURL:url];
    NSError *readFileError = nil;
    // 3.对数据进行 JSON 格式化并返回字典形式
    NSArray *jsonArray = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&readFileError];
    NSAssert1(jsonArray, @"JSON File read error:\n%@", readFileError);
    
    return jsonArray;
}

但是,为了实现代码复用,我希望将这段代码也添加到 Version 4 中的 HQLPropertyListStore 类中:

// ---------------------------------------------------------
#import <Foundation/Foundation.h>

@interface HQLPropertyListStore : NSObject

@property (nonatomic, readonly, copy) NSArray *dataSourceArray;

- (instancetype)initWithPlistFileName:(NSString *)fileName modelsOfClass:(Class)modelClass;

- (instancetype)initWithJSONFileName:(NSString *)fileName modelsOfClass:(Class)modelClass;

@end

// ---------------------------------------------------------
#import "HQLPropertyListStore.h"

@interface HQLPropertyListStore ()
@property (nonatomic, readwrite, copy) NSArray *dataSourceArray;
@end

@implementation HQLPropertyListStore

#pragma mark - Initialize

- (instancetype)initWithPlistFileName:(NSString *)fileName modelsOfClass:(Class)modelClass {
    self = [super init];
    if (self) {
        [self loadPlistFile:fileName modelsOfClass:modelClass];
    }
    return self;
}

- (instancetype)initWithJSONFileName:(NSString *)fileName modelsOfClass:(Class)modelClass {
    self = [super init];
    if (self) {
        [self loadJSONFile:fileName modelsOfClass:modelClass];
    }
    return self;
}

#pragma mark - Private

- (void)loadPlistFile:(NSString *)fileName modelsOfClass:(Class)modelClass {
    NSArray *jsonArray = [self readLocalPlistFile:fileName];
    [self convertJSONArray:jsonArray toModelsOfClass:modelClass];
}

- (void)loadJSONFile:(NSString *)fileName modelsOfClass:(Class)modelClass {
    NSArray *jsonArray = [self readLocalJSONFile:fileName];
    [self convertJSONArray:jsonArray toModelsOfClass:modelClass];
}

// 读取 plist 文件
- (NSArray *)readLocalPlistFile:(NSString *)fileName {
    // 1.构造 plist 文件 URL 路径
    NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
    NSURL *url = [bundleURL URLByAppendingPathComponent:fileName];
    
    // 2.读取 plist 文件,并存放进 jsonArray 数组
    NSArray *jsonArray;
    if (@available(iOS 11.0, *)) {
        NSError *readFileError = nil;
        jsonArray = [NSArray arrayWithContentsOfURL:url error:&readFileError];
        NSAssert1(jsonArray, @"NSPropertyList File read error:\n%@", readFileError);
    } else {
        jsonArray = [NSArray arrayWithContentsOfURL:url];
        NSAssert(jsonArray, @"NSPropertyList File read error.");
    }
    
    return jsonArray;
}

// 读取本地 JSON 文件,如:city.json
- (NSArray *)readLocalJSONFile:(NSString *)fileName {
    // 1.构造 json 文件路径
    NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
    NSURL *url = [bundleURL URLByAppendingPathComponent:fileName];
    
    // 2.将文件数据化
    NSData *data = [[NSData alloc] initWithContentsOfURL:url];
    
    // 3.对数据进行 JSON 格式化并返回字典形式
    NSError *readFileError = nil;
    NSArray *jsonArray = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&readFileError];
    NSAssert1(jsonArray, @"JSON File read error:\n%@", readFileError);
    
    return jsonArray;
}

// 将 JSON 数据转换为 Model
- (void)convertJSONArray:(NSArray *)jsonArray toModelsOfClass:(Class)modelClass {
    if ([modelClass isSubclassOfClass:MTLModel.class]) {
        NSError *decodeError = nil;
        self.dataSourceArray = [MTLJSONAdapter modelsOfClass:modelClass fromJSONArray:jsonArray error:&decodeError];
        NSAssert1(_dataSourceArray, @"JSONArray decode error:\n%@", decodeError);
    } else {
        NSAssert(NO, @"Unsupported Class Types.");
    }
}

@end

更新完 HQLPropertyListStore 类后,你会发现,即使我在实现文件中对这两个方法都进行了拆分和优化,但之前的读取 Plist 数据的方法声明并没有变化,所以不会对之前项目中的代码造成破坏,而且我为它增加了可以读取 JSON 文件的新特性。

🎉🎉🎉

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 226,913评论 6 527
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 97,710评论 3 412
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 174,561评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,278评论 1 306
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,080评论 6 405
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,604评论 1 320
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,698评论 3 434
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,847评论 0 285
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,361评论 1 329
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,330评论 3 353
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,487评论 1 365
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,044评论 5 355
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,738评论 3 342
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,134评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,378评论 1 281
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,053评论 3 385
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,471评论 2 370

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,771评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,562评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,884评论 6 342
  • 昨天,气象局发布了帝都高温预警信息。这次终于预报准确了。这几天,天气炎热的很想像小狗一样伸出舌头呼吸。 恰逢赶上今...
    木鱼78阅读 221评论 1 0
  • 这次来美国,每天美景美食发朋友圈,很怕拉仇恨。转念一想,不少人等着看连载,又恰逢清明小长假,大家都在刷。图个手痒,...
    Ab1de4world阅读 192评论 0 0