Skip to content

Commit

Permalink
Support aspect ratio for components (#244)
Browse files Browse the repository at this point in the history
* Support aspect ratio for components Jasonette/documentation#62

* More ways of expressing ratio (ex: 2:1,1/3)

* Support ratio for layers

* Remove image prefetching to optimize loading

## Solution

DO NOT prefetch if:
1. height is fixed
2. width and ratio are specified

## What problem it solves

Previously ALL images were prefetched because that was the only way to estimate the height, which is how Jasonette constructs the layout in a robust manner.

This mostly works and does what it's designed to do, but with a side effect:

Since it prefetches EVERY image on the screen, say if you had 2000 images, the view would start prefetching ALL of these images immediately upon load, even those that are not immediately visible because they're outside the viewport. This means you have 2000 concurrent network requests open just for these images.

Normally a view doesn't contain that many images and it works just fine as is, but when we have a lot of images it can congest up the network, sometimes even resulting in a delayed load of the images that DO need to load immediately.

For example the images in the currently visible viewport need to be loaded as soon as possible, but with prefetching this becomes non-deterministic, especially when there are a lot of images. This is why sometimes it feels like an image is loading forever when you open the app.

## Effect

This should have an immediate effect if your view contains a lot of images. More specifically image loading should feel visibly faster (because there's no prefetch and it only loads the images that need to be loaded on-demand)

To get this to work, you need to either set:

1. style.height
2. style.width AND style.ratio
  • Loading branch information
gliechtenstein authored Jul 25, 2017
1 parent 5b5c577 commit ad3369a
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 34 deletions.
13 changes: 13 additions & 0 deletions app/Jasonette/JasonComponent.m
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ + (void) stylize: (NSDictionary *)json component: (UIView *)component{
}
}

// ratio
if(style[@"ratio"]){
NSLayoutConstraint *ratio_constraint = [NSLayoutConstraint constraintWithItem: component
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:component
attribute:NSLayoutAttributeHeight
multiplier:[JasonHelper parseRatio:style[@"ratio"]]
constant:0];
ratio_constraint.identifier = @"ratio";
[component addConstraint:ratio_constraint];
}

// width
if(style[@"width"]){
NSString *widthStr = style[@"width"];
Expand Down
1 change: 1 addition & 0 deletions app/Jasonette/JasonHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@
+ (NSArray *)childOf: (UIView *)view withClassName: (NSString *)className;
+ (id) read_local_json: (NSString *)url;
+ (NSString *)normalized_url: (NSString *)url forOptions: (id)options;
+ (CGFloat)parseRatio: (NSString *) ratio;
@end
17 changes: 17 additions & 0 deletions app/Jasonette/JasonHelper.m
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,23 @@ + (BOOL)isURL:(NSURL*)url equivalentTo:(NSString *)urlString{
urlComponents2.query = nil; // Strip out query parameters.
return [urlComponents.string isEqualToString:urlComponents2.string];
}
+ (CGFloat)parseRatio: (NSString *) ratio {
if([ratio containsString:@":"] || [ratio containsString:@"/"]) {
NSError *error = nil;
NSRegularExpression* regex = [NSRegularExpression regularExpressionWithPattern:@"^[ ]*([0-9]+)[ ]*[:/][ ]*([0-9]+)[ ]*$" options:0 error:&error];
NSRange searchedRange = NSMakeRange(0, [ratio length]);
NSTextCheckingResult *match = [regex firstMatchInString:ratio options:0 range: searchedRange];
if(match){
NSString *w = [ratio substringWithRange:[match rangeAtIndex:1]];
NSString *h = [ratio substringWithRange:[match rangeAtIndex:2]];
return [w floatValue]/[h floatValue];
} else {
return 1; // shouldn't happen
}
} else {
return [ratio floatValue];
}
}
+ (CGFloat)pixelsInDirection: (NSString *)direction fromExpression: (NSString *)expression {
NSError *error = nil;
CGFloat full_dimension;
Expand Down
76 changes: 44 additions & 32 deletions app/Jasonette/JasonImageComponent.m
Original file line number Diff line number Diff line change
Expand Up @@ -94,49 +94,61 @@ + (UIView *)build: (UIImageView *)component withJSON: (NSDictionary *)json withO
NSString *url = (NSString *)[JasonHelper cleanNull: json[@"url"] type:@"string"];
UIImageView *imageView = (UIImageView *)component;

if(style[@"width"] && !style[@"height"]){
// Width is set but height is not
CGFloat aspectRatioMult;
if(JasonComponentFactory.imageLoaded[url]){
@try{
CGSize size = [JasonComponentFactory.imageLoaded[url] CGSizeValue];
if(size.width > 0 && size.height > 0){
aspectRatioMult = (size.height / size.width);
if(style[@"width"]) {
if(style[@"ratio"]){
// don't do anything about the height, it will be handled in JasonComponent
} else {
if (!style[@"height"]){
// Width is set but height is not
CGFloat aspectRatioMult;
if(JasonComponentFactory.imageLoaded[url]){
@try{
CGSize size = [JasonComponentFactory.imageLoaded[url] CGSizeValue];
if(size.width > 0 && size.height > 0){
aspectRatioMult = (size.height / size.width);
} else {
aspectRatioMult = (imageView.image.size.height / imageView.image.size.width);
}
}
@catch (NSException *e){
aspectRatioMult = (imageView.image.size.height / imageView.image.size.width);
}
} else {
aspectRatioMult = (imageView.image.size.height / imageView.image.size.width);
}
NSString *widthStr = style[@"width"];
CGFloat width = [JasonHelper pixelsInDirection:@"horizontal" fromExpression:widthStr];
style[@"height"] = [NSString stringWithFormat:@"%d", (int)(width * aspectRatioMult)];
}
@catch (NSException *e){
aspectRatioMult = (imageView.image.size.height / imageView.image.size.width);
}
} else {
aspectRatioMult = (imageView.image.size.height / imageView.image.size.width);
}
NSString *widthStr = style[@"width"];
CGFloat width = [JasonHelper pixelsInDirection:@"horizontal" fromExpression:widthStr];
style[@"height"] = [NSString stringWithFormat:@"%d", (int)(width * aspectRatioMult)];
}
if(style[@"height"] && !style[@"width"]){
// Height is set but width is not
CGFloat aspectRatioMult;
if(JasonComponentFactory.imageLoaded[url]){
@try {
CGSize size = [JasonComponentFactory.imageLoaded[url] CGSizeValue];
if(size.width > 0 && size.height > 0){
aspectRatioMult = (size.width / size.height);
if(style[@"height"]){
if(style[@"ratio"]){
// don't do anything about the width, it will be handled in JasonComponent
} else {
if(!style[@"width"]) {
// Height is set but width is not
CGFloat aspectRatioMult;
if(JasonComponentFactory.imageLoaded[url]){
@try {
CGSize size = [JasonComponentFactory.imageLoaded[url] CGSizeValue];
if(size.width > 0 && size.height > 0){
aspectRatioMult = (size.width / size.height);
} else {
aspectRatioMult = (imageView.image.size.width / imageView.image.size.height);
}
}
@catch (NSException *e){
aspectRatioMult = (imageView.image.size.width / imageView.image.size.height);
}
} else {
aspectRatioMult = (imageView.image.size.width / imageView.image.size.height);
}
NSString *heightStr = style[@"height"];
CGFloat height = [JasonHelper pixelsInDirection:@"vertical" fromExpression:heightStr];
style[@"width"] = [NSString stringWithFormat:@"%d", (int)(height * aspectRatioMult)];
}
@catch (NSException *e){
aspectRatioMult = (imageView.image.size.width / imageView.image.size.height);
}
} else {
aspectRatioMult = (imageView.image.size.width / imageView.image.size.height);
}
NSString *heightStr = style[@"height"];
CGFloat height = [JasonHelper pixelsInDirection:@"vertical" fromExpression:heightStr];
style[@"width"] = [NSString stringWithFormat:@"%d", (int)(height * aspectRatioMult)];
}
mutable_json[@"style"] = style;
}
Expand Down
6 changes: 5 additions & 1 deletion app/Jasonette/JasonLayer.m
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@ + (void)setStyle: (NSDictionary *)style ForLayerChild: (UIView *)layerChild ofSi
CGFloat width = -1;
CGFloat height = -1;
CGFloat aspectRatioMult;
aspectRatioMult = (size.height / size.width);
if(style[@"ratio"]) {
aspectRatioMult = 1/[JasonHelper parseRatio:style[@"ratio"]];
} else {
aspectRatioMult = (size.height / size.width);
}

if(style[@"width"]){
width = [JasonHelper pixelsInDirection:@"horizontal" fromExpression:style[@"width"]];
Expand Down
13 changes: 12 additions & 1 deletion app/Jasonette/JasonViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,18 @@ - (void)loadAssets: (NSDictionary *)body{
// see if it's an image type
if(body[@"type"]){
if([body[@"type"] isEqualToString:@"image"] || [body[@"type"] isEqualToString:@"button"]){

if(body[@"style"]) {
// [Image load optimization] Don't load assets if
// 1. height exists or
// 2. width + ratio exist
if(body[@"style"][@"height"]) {
return;
} else if (body[@"style"][@"width"] && body[@"style"][@"ratio"]) {
return;
}
}

// it's an image. Let's download!
NSString *url = body[key];
if(![url containsString:@"{{"] && ![url containsString:@"}}"]){
Expand Down Expand Up @@ -1415,7 +1427,6 @@ - (void)reloadSections:(NSArray *)sections{
[headers addObject:section[@"header"]];
}
}

[self.tableView reloadData];
if(!top_aligned){
[self scrollToBottom];
Expand Down

0 comments on commit ad3369a

Please sign in to comment.