本文轉載請註明出處 —— polobymulberry-博客園html
上一篇《【瘋狂造輪子-iOS】JSON轉Model系列之一》實現了一個簡陋的JSON轉Model的庫,不過還存在不少問題。下面我會嘗試一個個去解決。git
有時候JSON並不必定是NSDictionary類型,多是一個字符串,也多是NSData類型的數據。不過不論是哪一種類型,通通先將其轉化爲NSData數據,而後使用+[NSJSONSerialization JSONObjectWithData:options:error:]來轉化。因此我在initWithAttributes:上面又封裝了一層。github
- (instancetype)initWithJSONData:(id)json { NSDictionary *dict = [self pjx_dictionaryWithJSON:json]; return [self initWithAttributes:dict]; } /** * @brief 將NSString和NSData格式的json數據轉化爲NSDictionary類型 */ - (NSDictionary *)pjx_dictionaryWithJSON:(id)json { if (!json) { return nil; } // 如果NSDictionary類型,直接返回 if ([json isKindOfClass:[NSDictionary class]]) { return json; } NSDictionary *dict = nil; NSData *jsonData = nil; if ([json isKindOfClass:[NSString class]]) { // 若是是NSString,就先轉化爲NSData jsonData = [(NSString*)json dataUsingEncoding:NSUTF8StringEncoding]; } else if ([json isKindOfClass:[NSData class]]) { jsonData = json; } if (jsonData && [jsonData isKindOfClass:[NSData class]]) { // 若是時NSData類型,使用NSJSONSerialization NSError *error = nil; dict = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; if (error) { NSLog(@"pjx_dictionaryWithJSON error:%@", error); return nil; } if (![dict isKindOfClass:[NSDictionary class]]) { return nil; } } return dict; }
爲此,我在ViewController添加了兩個sample。分別用來解析NSString類型的JSON數據和NSData類型的JSON數據。json
// NSString類型的JSON數據 - (void)runSimpleSample2 { NSString *userStr = @" \ { \ \"username\" : \"shuaige\", \ \"password\" : \"123456\", \ \"avatarImageURL\" : \"http://www.example.com/shuaige.png\" \ }"; PJXUser *user = [[PJXUser alloc] initWithJSONData:userStr]; NSLog(@"runSimpleSample2\n"); NSLog(@"----------------------------------------"); NSLog(@"username:%@\n",user.username); NSLog(@"password:%@\n",user.password); NSLog(@"avatarImageURL:%@\n",user.avatarImageURL); } // NSData類型的JSON數據 - (void)runSimpleSample3 { NSString *userInfoFilePath = [[NSBundle mainBundle] pathForResource:@"UserInfo" ofType:@"txt"]; NSData *data = [NSData dataWithContentsOfFile:userInfoFilePath]; PJXUser *user = [[PJXUser alloc] initWithJSONData:data]; NSLog(@"runSimpleSample3\n"); NSLog(@"----------------------------------------"); NSLog(@"username:%@\n",user.username); NSLog(@"password:%@\n",user.password); NSLog(@"avatarImageURL:%@\n",user.avatarImageURL); }
輸出結果也是正確的:app
我第一反應是使用一個映射表。也就是說用戶使用時須要自定義一套property和key的映射表。YYModel中使用了一個+ (NSDictionary *)modelCustomPropertyMapper函數,用戶能夠自定義該函數達到映射表的效果,而這個函數是放在一個protocol中的。我挺認同這種設計的,由於modelCustomPropertyMapper這種函數和Model是一種組合關係,無關緊要(optional),因此設計成協議更合適。可是做者在設計protocol又說了一句:函數
// There's no need to add '<YYModel>' to your class header. @protocol YYModel <NSObject>
什麼意思呢,就是說你自定義一個NSObject子類(如YYBook)時,若是想實現自定義的property映射關係,只須要實現modelCustomPropertyMapper函數便可,而不須要寫成@interface YYBook : NSObject <YYModel>。做者的意思是你遵不遵循YYModel這個protocol都沒事,反正你只要在YYBook實現了modelCustomPropertyMapper便可。具體解釋,你們請參考這個issue。post
這種設計我不是很贊同,我是有潔癖的人,要否則你就別定義YYModel這個protocol,說明文檔裏面着重說明一下就行。因此此處我仍是選擇判斷NSObject的子類是否遵循protocol,也就是說只有遵循了這個protocol,才能自定義property映射關係。測試
首先咱們看如何使用自定義propertyMapper。我先創建一個PJXUserPropertyMapper類,遵循了JSONProtocol協議,並實現了propertyMapper協議函數。ui
// 遵循JSONProtocol協議,這個JSONProtocol中定義的就是個人propertyMapper協議函數 @interface PJXUserPropertyMapper : NSObject <JSONProtocol> @property (nonatomic, copy) NSString* username; // 用戶名 @property (nonatomic, copy) NSString* password; // 密碼 @property (nonatomic, copy) NSString* avatarImageURL; // 頭像的URL地址 @end @implementation PJXUserPropertyMapper // 實現propertyMapper這個協議方法 + (NSDictionary *)propertyMapper { return @{@"Username" : @"username", @"Password" : @"password", @"AvatarImageURL" : @"avatarImageURL"}; } @end
隨後我定義了一個example。atom
#pragma mark - PropertyMapper Sample - (void)runPropertyMapperSample { NSDictionary *userDict = @{@"Username" : @"shuaige", @"Password" : @"123456", @"AvatarImageURL" : @"http://www.example.com/shuaige.png"}; PJXUserPropertyMapper *user = [[PJXUserPropertyMapper alloc] initWithJSONData:userDict]; NSLog(@"runPropertyMapperSample\n"); NSLog(@"----------------------------------------"); NSLog(@"username:%@\n",user.username); NSLog(@"password:%@\n",user.password); NSLog(@"avatarImageURL:%@\n",user.avatarImageURL); }
是否是感受調用上和以前的非property映射沒什麼區別?那是由於咱們須要在initWithJSONData中增長一些東西。
具體的作法是在PropertyWithDictionary函數增長了一個查表操做。
// 注意我傳入的dictionary就是用戶提供的JSON數據 // 好比此處傳入的key==@"username",value==@"shuaige" static void PropertyWithDictionaryFunction(const void *key, const void *value, void *context) { NSString *keyStr = (__bridge NSString *)(key); ...... // 若是使用了JSONProtocol,而且自定義了propertyMapper,那麼還須要將keyStr轉化下 if ([modelSelf conformsToProtocol:@protocol(JSONProtocol)] && [[modelSelf class] respondsToSelector:@selector(propertyMapper)]) { keyStr = [[[modelSelf class] propertyMapper] objectForKey:keyStr]; } ...... }
這樣就能夠啦.咱們看看效果:
開始的時候,挺擔憂我這種寫法會不會不兼容別的數據類型。不過我以爲應該沒什麼問題,畢竟我使用的setter方法本質上沒啥問題,個人類型全用id來代替了(事實上,個人想法大錯特錯):
((void (*)(id, SEL, id))(void *) objc_msgSend)(modelSelf, info.setter, setValue);
不過本着不怕一萬,就怕萬一的心態。我仍是作了一個example來試驗一下:
@interface PJXUserVariousType : NSObject @property (nonatomic, copy) NSString *blogTitle; // 博客標題 @property (nonatomic, strong) NSURL *blogURL; // 博客網址 @property (nonatomic, assign) NSInteger blogIndex; // 博客索引值 @property (nonatomic, strong) NSDate *postDate; // 博客發佈時間 @property (nonatomic, strong) NSArray *friends; // 個人好友名稱 @property (nonatomic, strong) NSSet *collections; // 個人收藏 @end @implementation PJXUserVariousType @end #pragma mark - VariousType Sample - (void)runVariousTypeSample { NSDictionary *userDict = @{@"blogTitle" : @"iOS developer", @"blogURL" : @"http://www.example.com/blog.html", @"blogIndex" : @666, @"postDate" : [NSDate date], @"friends" : @[@"meinv1", @"meinv2", @"meinv3"], @"collections" : @[@"shuaige1", @"shuaige2", @"shuaige3"]}; PJXUserVariousType *user = [[PJXUserVariousType alloc] initWithJSONData:userDict]; NSLog(@"runVariousTypeSample\n"); NSLog(@"----------------------------------------"); NSLog(@"blogTitle:%@\n",user.blogTitle); NSLog(@"blogURL:%@\n",user.blogURL); NSLog(@"blogIndex:%ld\n",user.blogIndex); NSLog(@"postDate:%@\n",user.postDate); NSLog(@"friends:%@\n",user.friends); NSLog(@"collections:%@\n",user.collections); }
你猜輸出啥?
其餘都正確,惟獨咱們的blogIndex出錯了。這裏確實是我欠考慮了,相似NSInteger,BOOL這些NSNumber類型(我暫時只考慮這些經常使用類型)須要單獨處理一下。這一部分看起來容易,可是爲了處理這種特殊狀況確實要下很大功夫。好比你得先判斷該屬性是否是double或int這種類型,只有判斷除了該屬性是double仍是int,你才能正確使用setter方法,而此處的調用方式也要單獨寫一個,由於和以前調用方式有一些些區別,須要判斷Number的類型是double,是int,仍是BOOl…….
對此我在PJXPropertyInfo中定義了兩個函數,一個叫isNumber,用來判斷該屬性是否是一個Number,另外一個叫setNumberValue:withModelSelf:,用來給是Number類型的屬性賦值。另外,我仿照YYModel(比YYModel簡化不少了)建了一個PJXEncodingType的enum類型,用來存儲Number的類型(int?double?BOOL?……),與之配套的還有一個PJXGetEncodingType函數,來獲取當前屬性的類型(是int?double?BOOL?),具體怎麼作還挺複雜的,後面會詳細說明。
代碼以下:
// Number類型 typedef NS_ENUM(NSUInteger, PJXEncodingType) { PJXEncodingTypeUnknown = 0, ///< unknown PJXEncodingTypeBool = 1, ///< bool PJXEncodingTypeInt8 = 2, ///< char / BOOL PJXEncodingTypeUInt8 = 3, ///< unsigned char PJXEncodingTypeInt16 = 4, ///< short PJXEncodingTypeUInt16 = 5, ///< unsigned short PJXEncodingTypeInt32 = 6, ///< int PJXEncodingTypeUInt32 = 7, ///< unsigned int PJXEncodingTypeInt64 = 8, ///< long long PJXEncodingTypeUInt64 = 9, ///< unsigned long long PJXEncodingTypeFloat = 10, ///< float PJXEncodingTypeDouble = 11, ///< double PJXEncodingTypeLongDouble = 12, ///< long double }; // 根據objc_property_attribute_t能夠獲取到property的類型PJXEncodingType // 參考YYModel PJXGetEncodingType(const char *encodingType) { char *type = (char *)encodingType; if (!type) return PJXEncodingTypeUnknown; size_t len = strlen(type); if (len == 0) return PJXEncodingTypeUnknown; switch (*type) { case 'B': return PJXEncodingTypeBool; case 'c': return PJXEncodingTypeInt8; case 'C': return PJXEncodingTypeUInt8; case 's': return PJXEncodingTypeInt16; case 'S': return PJXEncodingTypeUInt16; case 'i': return PJXEncodingTypeInt32; case 'I': return PJXEncodingTypeUInt32; case 'l': return PJXEncodingTypeInt32; case 'L': return PJXEncodingTypeUInt32; case 'q': return PJXEncodingTypeInt64; case 'Q': return PJXEncodingTypeUInt64; case 'f': return PJXEncodingTypeFloat; case 'd': return PJXEncodingTypeDouble; case 'D': return PJXEncodingTypeLongDouble; default: return PJXEncodingTypeUnknown; } } /** * @brief 存儲Model中每一個property的信息 * ...... * @param type 是一個PJXEncodingType類型變量,爲了存儲該屬性是哪一種Number(int?double?BOOL?) */ @interface PJXPropertyInfo : NSObject ...... @property (nonatomic, assign) PJXEncodingType type; @end @implementation PJXPropertyInfo - (instancetype)initWithPropertyInfo:(objc_property_t)property { self = [self init]; if (self) { ...... // 判斷屬性類型 unsigned int attrCount; // 關於objc_property_attribute_t,這裏有一篇文章介紹的很好 // http://www.henishuo.com/runtime-property-ivar/ objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount); for (unsigned int i = 0; i < attrCount; i++) { switch (attrs[i].name[0]) { case 'T': {// EncodingType if (attrs[i].value) { //NSLog(@"attrs[%d].value = %s", i, attrs[i].value); // 能夠根據value獲取到property類型 _type = PJXGetEncodingType(attrs[i].value); } break; } default: break; } } ...... } return self; } // 根據propertyInfo中存儲的type判斷其是否爲Number - (BOOL)isNumber { switch (self.type) { case PJXEncodingTypeBool: case PJXEncodingTypeInt8: case PJXEncodingTypeUInt8: case PJXEncodingTypeInt16: case PJXEncodingTypeUInt16: case PJXEncodingTypeInt32: case PJXEncodingTypeUInt32: case PJXEncodingTypeInt64: case PJXEncodingTypeUInt64: case PJXEncodingTypeFloat: case PJXEncodingTypeDouble: case PJXEncodingTypeLongDouble: return YES; default: return NO; break; } } // 使用objc_msgSend調用modelSelf中該屬性對應的setter方法 - (void)setNumberValue:(NSNumber *)number withModelSelf:(id)modelSelf { switch (self.type) { case PJXEncodingTypeBool: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.boolValue); break; case PJXEncodingTypeInt8: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.charValue); break; case PJXEncodingTypeUInt8: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedCharValue); break; case PJXEncodingTypeInt16: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.shortValue); break; case PJXEncodingTypeUInt16: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedShortValue); break; case PJXEncodingTypeInt32: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.intValue); break; case PJXEncodingTypeUInt32: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedIntValue); break; case PJXEncodingTypeInt64: ((void (*)(id, SEL, uint64_t))(void *) objc_msgSend)(modelSelf, self.setter, number.longLongValue); break; case PJXEncodingTypeUInt64: ((void (*)(id, SEL, uint64_t))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedLongLongValue); break; case PJXEncodingTypeFloat: ((void (*)(id, SEL, float))(void *) objc_msgSend)(modelSelf, self.setter, number.floatValue); break; case PJXEncodingTypeDouble: ((void (*)(id, SEL, double))(void *) objc_msgSend)(modelSelf, self.setter, number.doubleValue); break; case PJXEncodingTypeLongDouble: ((void (*)(id, SEL, long double))(void *) objc_msgSend)(modelSelf, self.setter, number.doubleValue); break; default: break; } } @end
有了上述的幾個方法,後面就好辦了,只需在PropertyWithDictionaryFunction函數中添加一個Number的判斷就行:
static void PropertyWithDictionaryFunction(const void *key, const void *value, void *context) { ...... // 若是該屬性是Number,那麼就用Number賦值方法給其賦值 if ([info isNumber]) { [info setNumberValue:setValue withModelSelf:modelSelf]; } else { ((void (*)(id, SEL, id))(void *) objc_msgSend)(modelSelf, info.setter, setValue); } }
這下終於成功了:
這個其實比較簡單,只須要對property的attribute(objc_property_attribute_t)進行判斷便可:
- (instancetype)initWithPropertyInfo:(objc_property_t)property { ...... BOOL isCustomSetter = NO; // 判斷屬性類型 unsigned int attrCount; // 關於objc_property_attribute_t,這裏有一篇文章介紹的很好 // http://www.henishuo.com/runtime-property-ivar/ objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount); for (unsigned int i = 0; i < attrCount; i++) { switch (attrs[i].name[0]) { case 'T': { // EncodingType if (attrs[i].value) { //NSLog(@"attrs[%d].value = %s", i, attrs[i].value); // 能夠根據value獲取到property類型 _type = PJXGetEncodingType(attrs[i].value); } break; } case 'S': { // 自定義setter方法 if (attrs[i].value) { isCustomSetter = YES; _setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]); } } break; default: break; } } if (!isCustomSetter) { // 若是沒有自定義setter方法,只考慮系統默認生成setter方法 // 也就是說屬性username的setter方法爲setUsername: NSString *setter = [NSString stringWithFormat:@"%@%@", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]; _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@:", setter]); } } return self; }
使用下面這個例子測試:
@interface PJXUserCustomSetter : NSObject @property (nonatomic, copy, setter=setCustomUserName:) NSString* username; // 用戶名 @property (nonatomic, copy, setter=setCustomBirthday:) NSDate* birthday; // 生日 @end @implementation PJXUserCustomSetter - (void)setCustomUserName:(NSString *)username { _username = [NSString stringWithFormat:@"My name is %@", username]; } - (void)setCustomBirthday:(NSDate *)birthday { NSTimeInterval timeInterval = 24*60*60; // 過一天 _birthday = [NSDate dateWithTimeInterval:timeInterval sinceDate:birthday]; } @end #pragma mark - Custom Setter Sample - (void)runCustomSetterSample { NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; NSDate *birthday = [dateFormatter dateFromString:@"2016-04-07 00:20:03"]; NSDictionary *userDict = @{@"username" : @"shuaige", @"birthday" : birthday}; PJXUserCustomSetter *user = [[PJXUserCustomSetter alloc] initWithJSONData:userDict]; NSLog(@"runCustomSetterSample\n"); NSLog(@"----------------------------------------"); NSLog(@"username:%@\n",user.username); NSLog(@"birthday:%@\n",user.birthday); }
獲得的結果爲:
我我的感受這個應該沒什麼問題,爲何這麼說呢?由於我嵌套的無非也是一個NSObject類型,那麼就調用其自身的setter方法就OK啊.不過仍是以防萬一,我構造了一下案例:
@interface PJXBlog : NSObject @property (nonatomic, copy) NSString *title; // 博客名稱 @property (nonatomic, strong) NSDate *postDate; // 博客發表日期 @property (nonatomic, copy) PJXUser *author; // 博客做者 @end @implementation PJXBlog @end #pragma mark - Nest Sample - (void)runNestSample { NSDictionary *blogDict = @{@"title" : @"how to convert JSON to Model?", @"postDate" : [NSDate date], @"author" : @{@"username" : @"shuaige", @"password" : @"123456", @"avatarImageURL":@"http://www.example.com/shuaige.png"}}; PJXBlog *blog = [[PJXBlog alloc] initWithJSONData:blogDict]; NSLog(@"runNestSample\n"); NSLog(@"----------------------------------------"); NSLog(@"title:%@\n",blog.title); NSLog(@"postDate:%@\n",blog.postDate); NSLog(@"author:%@\n",blog.author); }
輸出結果以下:
結果沒什麼問題.不過這樣說可能不是很負責任,可是目前我也想不到反例.暫時先當作成功了.
以個人能力,目前只能將JSON轉化Model實現到這個地步了.整體來講,實現的難度不是很大(由於我考慮的狀況仍是比較少的,另外還有些功能沒添加),不過涉及的知識點仍是挺多的,挺不錯的一個練手項目:).
附上GitHub地址。