[Crazy wheel-iOS] JSON to Model series,-iosjson
[Crazy wheel creation-iOS] One of the JSON-to-Model series
Please specify the source for this reprinted article --Polobymulberry-blog
1. Preface
I have been reading other people's source code before. Although I have improved myself a lot, I did not write it myself, and it is easy to forget it. During this time, I am preparing to build some wheels on my own. The main purpose is to improve my own strength. I Can't Google/ICUser as soon as I encounter any problems. h # import <Foundation/Foundation. h> extern NSString * const kUserId; extern NSString * const kUserBlogId; extern NSString * const kUserDisplayName; extern NSString * const kUserAvatarURL; @ interface ICUser: NSObject @ property (nonatomic, copy) NSString * userId; @ property (nonatomic, assign) NSInteger blogId; @ property (nonatomic, copy) NSString * displayName; @ property (nonatomic, strong) NSURL * avatarURL;+ (Instancetype) initWithAttributes :( NSDictionary *
)attributes;@end// ICUser.m#import "ICUser.h"NSString *const kUserId = @"UserId";NSString *const kUserBlogId = @"BlogId";NSString *const kUserDisplayName = @"DisplayName";NSString *const kUserAvatarURL = @"Avatar";@implementation ICUser
+ (Instancetype) initWithAttributes :( NSDictionary *
)attributes{ ICUser *user = [[ICUser alloc] init];
user.userId = attributes[kUserId]; user.blogId = [attributes[kUserBlogId] integerValue]; user.displayName = attributes[kUserDisplayName]; user.avatarURL = [NSURL URLWithString:attributes[kUserAvatarURL]];return user;}@end
If the situations we need to handle meet the following two requirements:
In this case, the above method is acceptable. However, once the Model layer expands sharply, it will be miserable:
Considering the inconvenience caused by manual conversion of JSON to Model, I decided to write a JSON to Model library by myself. Although there are already many third-party libraries in this field on the internet, I still want to build my own wheels,The objective is to learn more about iOS.
2. Design Ideas
1. What is input and output first?
Input: NSDictionary type data
Here we should first make a simple description. Generally, we use network requests to parse JSON. When the server returns JSON data, we need to convert the local Model (It is not discussed whether NSDictionary can be directly used or converted to Model.). In addition, this article only assumes that the JSON data obtained by our network request has been processed on the client as NSDictionary data (more common ).
Output: Data of the Model type
Data of the Model type.
Example:
A simple example is as follows:
# Pragma mark-PJXUser @ interface PJXUser: NSObject @ property (nonatomic, copy) NSString * username; // username @ property (nonatomic, copy) NSString * password; // password @ property (nonatomic, copy) NSString * avatarImageURL; // URL of the Avatar @ end-(void) runSimpleSample {NSDictionary * userDict =@{ @ "username ": @ "shuaige", @ "password": @ "123456", @ "avatarImageURL": @ "http://www.example.com/shuaige.png"}; PJXUser * user = [[PJXUser alloc]InitWithAttributes: UserDict]; NSLog (@ "username: % @ \ n", user. username); NSLog (@ "password: % @ \ n", user. password); NSLog (@ "avatarImageURL: % @ \ n", user. avatarImageURL );}
The input in this example is the NSDictionary data of userDict, and the output is the object user of the PJXUser class. I wonder if you have noticed that the key in attributes must be the same as the property name in Model, such as the username and password attributes of PJXUser in the above example (of course, you can use a ing table to solve this problem, but we don't want that much at the moment ).
2. How to implement the core algorithm (input-to-output )?
The core algorithm is actually calling initWithAttributes: this function. How should we design this function?
Since we need all Model classes to call this initWithAttributes: To complete JSON-to-Model conversion. The first thing that comes to mind is to add this function to the NSObject category and require all Model classes to inherit from NSObject.
So I first created a new NSObject + Extension category. And added the-(instancetype) initWithAttributes :( NSDictionary *) attributes method. Next I will briefly describe the implementation of this function.
Actually myYYModel (portal) in the YYKit.The core part is to call the setter method of each attribute in the Model and use the value of each element in the imported attributes as the parameter of setter..
Okay, so far the core part has been finished. You may have a lot of questions, such as how to obtain the setter method of the attribute and how to call the setter method after obtaining it. After all, the operation is performed in the parent class NSObject, there is no specific subclass information. Here I will briefly mention that since we cannot solve the above problems during the compilation period, we need to use the OC runtime mechanism. Of course, the following describes how to implement it.
3. Specific implementation
Based on the above core ideas, I think there are still some problems in implementation:
How do I obtain the setter method for each attribute? If the setter method of each attribute is obtained (note that it is of the SEL type), how can this method be called for each attribute?
The operation is performed in NSObject, so obj. username = attributes [@ "username"] is not expected. Therefore, you need to use objc_msgSend In the runtime. The usage example is as follows:
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)self, NSSelectorFromString(@"setUsername:"), @"shuaige");
We can see that we only need to replace @ "setUsername" and @ "shuaige" with our own variables. How to replace it? At this time, we need to create some data structures to process and save relevant property information. Of course, these data structures are also the result of continuous correction in the implementation process. As for how to fix the problem in the middle, I will not elaborate on it. I will directly discuss the result.
The construction of data structures is also in line with our thinking habits. Since we need to process a Class, it is inevitable that we need to create a new Class to store Class information (PJXClassInfo), And each Class is composed of property, ivar, and method. Therefore, for different classes, we need to define three classes to store property, ivar, and method. But here we only need the property information, so only the property-related class (PJXPropertyInfo).
I first created a PJXClassInfo class. This Class currently only stores the propertyInfos attribute of the NSMutableDictionary type, which is used to store the property information of this Class. In propertyInfos, each element is actually a PJXPropertyInfo object. Each PJXPropertyInfo stores the property name, setter method, and so on. Of course, new properties will be added for PJXPropertyInfo later as needed.
The relationships between these two classes are as follows:
PJXPropertyInfo code
/*** @ Brief stores the information of each property in the Model * @ param property is an objc_property_t type variable * @ param name indicates the name of the property * @ param setter is a SEL Type Variable, setter Method */@ interface PJXPropertyInfo: NSObject @ property (nonatomic, assign) objc_property_t property; @ property (nonatomic, strong) NSString * name; @ property (nonatomic, assign) SEL setter; @ end @ implementation PJXPropertyInfo-(instancetype) initWithPropertyInfo :( objc_property_t) property {self = [self init]; if (self) {// In case of temporary need _ property = property; // use property_getName to obtain the property name const char * name = property_getName (property); if (name) {_ name = [NSString stringwithuf8string: name];} // currently, the custom setter method is not considered. Only the setter method generated by the system by default is considered. // that is, the setter method of the username attribute is setUsername: NSString * setter = [NSString stringWithFormat: @ "% @", [_ name substringToIndex: 1]. uppercaseString, [_ name substringFromIndex: 1]; _ setter = NSSelectorFromString ([NSString stringWithFormat: @ "set % @:", setter]);} return self;} @ end
PJXClassInfo code
/*** @ Brief stores the Class information of the Model, but currently only stores the property information of the Class * @ param propertyInfos is a variable of the NSMutableDictionary type, and the key stores the name of the property, value stores the corresponding PJXPropertyInfo object */@ interface PJXClassInfo: NSObject @ property (nonatomic, strong) NSMutableDictionary * propertyInfos; @ end @ implementation PJXClassInfo-(instancetype) usage) cls {self = [self init]; // use class_copyPropertyList to obtain all the properties of the Class (objc_property_t type) unsigned int propertyCount = 0; objc_property_t * properties = class_copyPropertyList (cls, & propertyCount); _ propertyInfos = [NSMutableDictionary dictionary]; // traverses the properties array // constructs the PJXPropertyInfo object based on the corresponding objc_property_t information and assigns the if (properties) value to propertyInfos) {for (unsigned int I = 0; I <propertyCount; I ++) {PJXPropertyInfo * propertyInfo = [[PJXPropertyInfo alloc] initWithPropertyInfo: properties [I]; _ propertyInfos [propertyInfo. name] = propertyInfo;} // note that free (properties) is released;} return self;} @ end
Now let's go back to the previous question: how to get the setter and apply it? We can see that with these two data structures, we have solved the problem of getting the setter of each property (using the propertyInfos attribute of PJXClassInfo ). The rest is simple. Call the setter method to assign values. Here, we refer to the method in YYModel and use a Core Foundation function.CFDictionaryApplyFunction.
Void CFDictionaryApplyFunction (CFDictionaryRef theDict, CFDictionaryApplierFunction applier, void * context );
This function applies the applier function to every key-value element of theDict.
So let's take a look at how to design this applier function.
Note that the applier callback function in this C language cannot be designed as a member function, because the member function hides a self parameter. Here we design the callback function as static and name it PropertyWithDictionaryFunction.
// Note that the input dictionary is the JSON data provided by the user. // For example, the input key ==@ "username ", value = @ "shuaige" static void PropertyWithDictionaryFunction (const void * key, const void * value, void * context) {// convert the key and value to NSString * keyStr = (_ bridge NSString *) (key) under the Cocoa framework; id setValue = (_ bridge id) (value); // modelSelf is actually self, but here I use the static function, so there is no default parameter self // at this time we need to use the context parameter to obtain this self // so I designed a PJXModelContext to store self information // In addition, each property information is not saved in the parameter of this function, and it must be passed by the context parameter. // Therefore, PJXModelContext also needs to store PJXClassInfo Object Information PJXModelContext * modelContext = context; id modelSelf = (_ bridge id) (modelContext-> modelSelf); PJXClassInfo * classInfo = (_ bridge PJXClassInfo *) (modelContext-> modelClassInfo); PJXPropertyInfo * info = classInfo. propertyInfos [keyStr]; (void (*) (id, SEL, id) (void *) objc_msgSend) (modelSelf, info. setter, setValue );}
The last step is to build PJXModelContext in our initWithAttributes: function and apply it to the above function.
Typedef struct {void * modelSelf; void * modelClassInfo;} PJXModelContext;-(instancetype) initWithAttributes :( NSDictionary *) attributes {self = [self init]; if (self) {// initialize the PJXClassInfo object and assign the value PJXModelContext modelContext = {0} To modelContext. modelSelf = (_ bridge void *) (self); PJXClassInfo * classInfo = [[PJXClassInfo alloc] initWithClassInfo: [self class]; modelContext. modelClassInfo = (_ bridge void *) classInfo; // apply this function to get the attributes, PropertyWithDictionaryFunction (CFDictionaryRef) Model data after the JSON-> Model, & modelContext);} return self ;}4. Test Results
In part 2. design ideas, we gave a case. Run the following command to check the NSLog result:
Successful!
5. Problems
Currently, the function is only 100 rows, but many problems are still not taken into account.
For example:
However, I can't eat a fat man at a Glance. I will fix these bugs one by one later, so stay tuned. The GitHub address of the Code is attached.