Why do I have this idea?
- I have been working on a project for a long time, and sometimes I have to go to the App Store to fix a small BUG or update a bit of content. The process is so long that I feel bored.
- Also, sometimes the server is not updated in time, or you want to control the app content.
- I considered introducing ReactNative, but it was too cumbersome for me.
- Writing Native in existing ways should be easy to control, easy to update, easy to write, consider using HTML,CSS,JS.
New development methods
In order to solve the above problems, it is a unique way to achieve a novel and possibly acceptable way to build iOS native apps. This way has the following characteristics:
1. No special server required!! 2. It is very convenient to update the APP and change the function of the APP at any time!! 3. Easy to extend new components, to achieve their own way of parsing or compatible with existing HTML standards!! 4. Use HTML,CSS,JS to write native features, Flex layouts.Copy the code
Before I talk about how to build such a novel approach to development, the last two images, the native functionality implemented this way
Start framing
To make such a framework, you must do the following:
- Parsing the HTML generates a DOM tree
- Download CSS, JS files according to the corresponding tags of HTML
- Parse the CSS and merge the stylesheets into the appropriate nodes
- Use OC or Swift to create views based on the DOM tree
- The layout system uses the front end Flex layout, yoga from Facebook can help us
- In order to interact, JS must be executed, which requires the communication ability between JS and Native
See the TokenHybrid source code for the implementation
Step 1 – Parse the HTML
Apple’s native NSXMLParser is recommended, but NSXMLParser has some drawbacks
- Can’t parse non-closed tags for example
<meta>
, it should be<meta>/<meta>
- When the text inside the label is scanned, if the text is too long, it may not be completed in one scan, and you need to record it yourself (not a pit).
To avoid the non-closed tag pit above, you need to find all the non-closed tags and complete them so that they become closed tags. I’m going to use the regular expression here and here’s the code that I use to find and complete all the autism and tags
-(void)parserHTML:(NSString *)html
{
dispatch_async(tokenXMLParserQueue(), ^{
NSString *closedHTML = [self handleSimeClosedTagWithTagNameArray:@[@"meta"The @"input"] html:html]; NSData *data = [closedHTML dataUsingEncoding:NSUTF8StringEncoding]; _parser = [[NSXMLParser alloc] initWithData:data]; _parser.delegate = self; [_parser parse]; }); } -(NSString *)handleSimeClosedTagWithTagNameArray:(NSArray *)tagNameArray html:(NSString *)html{ __block NSString *temp = html;for (NSString *tagName in tagNameArray) {
NSString *testString = @"<".token_append(tagName);
NSString *closedString = [NSString stringWithFormat:@"< % @ >",tagName];
if ([html containsString:testString]) {// Check whether to close NSString *pattern = [NSString stringWithFormat:@"< % @ (. *?) >",tagName]; NSRegularExpression *exp = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil]; NSArray<NSTextCheckingResult *> *results = [exp matchesInString:html options:0 range:NSMakeRange(0, html.length)]; [results enumerateObjectsUsingBlock:^(NSTextCheckingResult * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSString *matchString = [html substringWithRange:obj.range]; NSString *nextString = [html substringWithRange:NSMakeRange(obj.range.length+obj.range.location, tagName.length+3)];if(! [nextString isEqualToString:closedString]) { temp = temp.token_replace(matchString,matchString.token_append(closedString)); }}]; }}return temp;
}
Copy the code
HTML parsing at the same time, if there are
Here’s what you do:
- After the HTML has been parsed, you can merge the CSS into the Node that the CSS selector matches
- And how to match CSS selectors to Nodes
- Build the corresponding from the DOM tree
UIView
hierarchy - There may be thread synchronization issues involved
[nodes enumerateObjectsUsingBlock:^(TokenXMLNode * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *linkURL = obj.innerAttributes[@"href"];
if (linkURL == nil || linkURL.length == 0) return;
NSString *absoluteLinkURL = [NSString token_completeRelativeURLString:linkURL
withAbsoluteURLString:_document.sourceURL];
HybridLog(@"Start downloading CSS files");
TokenNetworking.networking()
.sendRequest(^NSURLRequest *(TokenNetworking *netWorking) {
return NSMutableURLRequest.token_requestWithURL(absoluteLinkURL)
.token_setPolicy(NSURLRequestReloadIgnoringLocalCacheData);
}).transform(^id(TokenNetworking *netWorking, id responsedObj) {
HybridLog(@"CSS file download completed");
NSString *cssText = [netWorking HTMLTextSerializeWithData:responsedObj];
NSDictionary *rules = [TokenCSSParser parserCSSWithString:cssText];
if (rules.allKeys.count) {
[_document addCSSRuels:rules];
}
self.styleAndLinkNodeCount -= 1;
return cssText;
}).finish(nil, ^(TokenNetworking *netWorkingObj, NSError *error) {
self.styleAndLinkNodeCount -= 1;
HybridLog(@"CSS file download error: %@",error);
[_document addFailedCSSURL:absoluteLinkURL];
});
}];
Copy the code
Step 2 – Parse the CSS
Step 2.1 – Resolve CSS to NSDictionary
If you can parse CSS, then you can implement some CSS functions like calc() yourself, isn’t that exciting? You have to do two things
- Evaluates a string mathematical expression
- Remove comments from CSS
Evaluate the mathematical expression NSString *mathExp = @"7 + 8 * 3"; NSExpression *expression = [NSExpression expressionWithFormat:mathExp]; id value = [expression expressionValueWithObject:nil context:nil]; Value is an NSNumber with a value of 31Copy the code
Here is the code to remove the comments and parse to NSDictionary
// I added the regular expression method for NSString under cssString.token_replaceWithregexp (commentRegExp,@)"")
-(TokenStringReplaceWithRegExpBlock)token_replaceWithRegExp{
return^NSString *(NSString *regExp,NSString *newString) { __block NSString *temp = [self copy]; NSRegularExpression *exp = [NSRegularExpression regularExpressionWithPattern:regExp options:0 error:nil]; NSArray<NSTextCheckingResult *> *result = [exp matchesInString:temp options:0 range:NSMakeRange(0, temp.length)]; [result enumerateObjectsUsingBlock:^(NSTextCheckingResult * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSString *stringWillBeReplaced = [self substringWithRange:obj.range]; temp = [temp stringByReplacingOccurrencesOfString:stringWillBeReplaced withString:newString]; }];returntemp; }; } // DTCoreText +(NSDictionary *)parserCSSWithString:(NSString *)cssString{if (cssString == nil) return@ {}; NSMutableDictionary *styleSheets = @{}.mutableCopy; NSString *commentRegExp = @"(?
; // Remove comments from CSS NSString * CSS = cssString.token_replaceWithregexp (commentRegExp,@"")
.token_replace(@"\n"The @"")
.token_replace(@"\r"The @"");
int braceMarker = 0;
NSString *selector;
NSString *rule;
for (int i = 0; i < css.length; i ++) {
unichar c = [css characterAtIndex:i];
if (c == '{') {
selector = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
braceMarker = i + 1;
}
if (c == '} ') {
rule = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
braceMarker = i + 1;
if (selector.length && rule.length) {
NSDictionary *dic = [self converAttrStringToDictionary:rule];
if ([selector hasPrefix:@""] || [selector hasSuffix:@""]) {
selector = [selector stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}
[styleSheets setObject:dic forKey:selector]; }}}return styleSheets;
}
Copy the code
Calling -parsercssWithString will parse the CSS file into an NSDictionary as follows
Body {--> {backgroundColor: RGB (120,120,120); @"backgroundColor": @"RGB (120120120)",
width:120px; @"width": @"120px"}}Copy the code
Step 2.2 – Matching CSS selectors Support ID selectors, class selectors, and simple combination selectors
To match the corresponding CSS selector to the corresponding Nodes in the DOM, you need to match from the right to the left of the selector string. This will speed up the matching. Why?
+(NSSet <TokenXMLNode *> *)matchNodesWithRootNode:(TokenXMLNode *)node selector:(NSString *)selector{// remove the Spaces at both endsif ([selector hasPrefix:@""]) { selector = [selector stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; } // Use space to separate NSMutableArray *selectors = nsmutablearray.token_arrayWitharray (selector. Token_separator (@)""));
if ([selectors containsObject:@""]) {
[selectors removeObject:@""];
}
NSMutableSet <TokenXMLNode *> *matchNodeSet = [NSMutableSet set]; / / to create a basic collection [TokenXMLNode enumerateTreeFromRootToChildWithNode: node block: ^ (TokenXMLNode * node, BOOL * stop) { [matchNodeSet addObject:node]; }]; // Matches the selector from right to leftfor (NSInteger i = selectors.count - 1 ; i>= 0; i--) {
NSString *selector = selectors[i];
NSMutableSet *matchNodeSetCopy = [NSMutableSet setWithSet:matchNodeSet]; [matchNodeSet enumerateObjectsUsingBlock: ^ (TokenXMLNode * node, BOOL * _Nonnull stop) {/ / id selectorif ([selector hasPrefix:@"#"]) {
if(! [node.innerAttributes[@"id"] isEqualToString:[selector substringWithRange:NSMakeRange(1, selector.length-1)]]) { [matchNodeSetCopy removeObject:node]; }}else if ([selector hasPrefix:@"."]) {
NSString *nodeClass = node.innerAttributes[@"class"];
NSString *selectorToBeMatched = [selector substringWithRange:NSMakeRange(1, selector.length-1)];
if ([nodeClass containsString:@""]) {/ / contains more than one type of NSArray * nodeClassArray = [nodeClass componentsSeparatedByString: @""];
if (![nodeClassArray containsObject:selectorToBeMatched]) {
[matchNodeSetCopy removeObject:node];
}
}
else{// Does not contain multiple classesif(! [nodeClass isEqualToString:[selector substringWithRange:NSMakeRange(1, selector.length-1)]]) { [matchNodeSetCopy removeObject:node]; }}}else {
if (i == selectors.count-1) {
if (![node.name isEqualToString:selector]) {
[matchNodeSetCopy removeObject:node];
}
}
else{ BOOL nodeMatchd = NO; // Start matching parent TokenXMLNode *currentNode = node;while(currentnode.parentnode) {// matches the parentNodeif ([currentNode.name isEqualToString:selector]) {
nodeMatchd = YES;
break;
}
currentNode = currentNode.parentNode;
}
if(! nodeMatchd) { [matchNodeSetCopy removeObject:node]; }}}}]; matchNodeSet = matchNodeSetCopy; }return matchNodeSet;
}
Copy the code
Step 3 – Build UIView hierarchy according to DOM tree
When NSXMLParser parses into these two methods, you can build a view hierarchy because the internal structure of the HTML tag corresponds exactly to the UIView hierarchy, which has a parent-child relationship, which is essentially a multi-fork tree, traversed using Stack hierarchies.
#pragma mark - XMLParserDelegate-(void)parserDidStart{// Create a new stack _viewStack = [[TokenHybridStack alloc] init]; } - (void) parser: (TokenXMLParser *) parser didStartNodeWithinBodyNode node (TokenPureNode *) {/ / create the corresponding Native components according to the corresponding node TokenPureComponent *view = [UIView token_produceViewWithNode:node];if(view == nil) { view = [[TokenPureComponent alloc] init]; } view.associatedNode = node; node.associatedView = view; [_viewStack push:view]; } - (void) parser: (TokenXMLParser *) parser didEndNodeWithinBodyNode: (TokenXMLNode *) node {/ / in the End adjust UIView hierarchy UIView *currentView = [_viewStack pop]; UIView *parentView = [_viewStack top]; [parentView addSubview:currentView]; }Copy the code
Step 4 – Set UIView’s corresponding properties
It’s actually pretty easy to set it up because in the previous section, all uiViews that are generated hold a Node, and you can set it up based on the data that’s parsed inside of Node. You can write a summary method, and I recommend that you write a Category for UIView and add a method that sets Node properties to UIView properties. There may be a lot of if-else in there, I’m limited, I hope someone can help simplify if-else
Here’s how I wrote it
// // UIView+Attributes. M // TokenHybrid // // Created by Xiong Chen on 2017/11/9. // Copyright © 2017 com.feelings reserved. // @implementation UIView (Attributes) ... -(void)token_updateAppearanceWithNormalDictionary:(NSDictionary *)dictionary{ NSDictionary *d = dictionary;if(d[@"borderRadius"]) { self.layer.cornerRadius = [d[@"borderRadius"] floatValue]; }if(d[@"zIndex"]) { self.layer.zPosition = [d[@"zIndex"] floatValue]; }if(d[@"borderWidth"]) { self.layer.borderWidth = [d[@"borderWidth"] floatValue]; }if(d[@"borderColor"]) { self.layer.borderColor = [UIColor ss_colorWithString:d[@"borderColor"]].CGColor; }if(d[@"backgroundColor"]) { self.backgroundColor = [UIColor ss_colorWithString:d[@"backgroundColor"]]. } NSString *hidden = d[@"hidden"];
if(hidden) {self.hidden = hidden.token_turnBoolStringToBoolValue(); }
}
@end
Copy the code
Interaction between Step 5-JS and OC/Swift
I talk about my practice model: TokenDomcument, TokenXMLNode, TokenTool tools: TokenViewBuilder, TokenJSContext
TokenViewBuilder
Used as aXMLParser
Delegate, and build a DOM tree, download JS,CSS, generate render treeTokenDomcument
A document that mimics the browser, which contains the entire DOM tree, and uses JSExport to guide JS to useTokenXMLNode
The parent class of the node, which also follows the JSExport protocol, is used by JS and controls the Native component through itTokenTool
It is used to provide various Native APIS for JS, such as: positioning, getting photos, popup prompt box, etcTokenJSContext
Provides an environment for JS to inject extra and execute JS- And how to interact with the basics, see very easy to understand JS and OC interaction
I have made a source code according to such an idea TokenHybrid source code hope you can give a little more advice!