Skip to content

Commit

Permalink
Cache policy control for image source
Browse files Browse the repository at this point in the history
Summary:
In the context of an app an image exists in three resolutions on the server: `thumb` (30px) `feed` (300px) `full` (900px). When looking at an individual item a user can come either from the feed, via a permalink or from other parts of the app. This allows a situation where the `feed` image might or might not already be loaded somewhere in the app. In the detail view I want to render `thumb` with a blur (to quickly display something), then the `feed` image if it exists to have something decent to display until `full` loads. However it is quite a waste to load the `feed` image if it isn't already in cache, and will slow down the time until `full` is loaded. It is possible to track the navigation from feed->detail and that the `feed` image has actually completed loading by the feed component however as component hierarchies grow this turns into quite a lot of prop passing and bad separation of concerns.

NSURLRequests accepts a [Cache Policy](https://developer.apple.com/reference/fo
Closes #10844

Differential Revision: D4425959

Pulled By: lacker

fbshipit-source-id: 679835439c761a2fc894f56eb6d744c036cf0b49
  • Loading branch information
salanki authored and facebook-github-bot committed Jan 18, 2017
1 parent a6844bd commit 52d8851
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 2 deletions.
Expand Up @@ -67,6 +67,7 @@ - (void)testTheTester_ExpectError
RCT_TEST(TimersTest)
RCT_TEST(AsyncStorageTest)
RCT_TEST(AppEventsTest)
RCT_TEST(ImageCachePolicyTest)
//RCT_TEST(ImageSnapshotTest) // Disabled: #8985988
//RCT_TEST(LayoutEventsTest) // Disabled due to flakiness: #8686784
RCT_TEST(SimpleSnapshotTest)
Expand Down
29 changes: 29 additions & 0 deletions Examples/UIExplorer/js/ImageExample.js
Expand Up @@ -304,6 +304,35 @@ exports.examples = [
},
platform: 'ios',
},
{
title: 'Cache Policy',
description: 'First image has never been loaded before and is instructed not to load unless in cache.' +
'Placeholder image from above will stay. Second image is the same but forced to load regardless of' +
' local cache state.',
render: function () {
return (
<View style={styles.horizontal}>
<Image
defaultSource={require('./bunny.png')}
source={{
uri: smallImage.uri + '?cacheBust=notinCache' + Date.now(),
cache: 'only-if-cached'
}}
style={styles.base}
/>
<Image
defaultSource={require('./bunny.png')}
source={{
uri: smallImage.uri + '?cacheBust=notinCache' + Date.now(),
cache: 'reload'
}}
style={styles.base}
/>
</View>
);
},
platform: 'ios',
},
{
title: 'Border Color',
render: function() {
Expand Down
115 changes: 115 additions & 0 deletions IntegrationTests/ImageCachePolicyTest.js
@@ -0,0 +1,115 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';

var React = require('react');
var ReactNative = require('react-native');
var {
Image,
View,
Text,
StyleSheet,
} = ReactNative;
var { TestModule } = ReactNative.NativeModules;

/*
* The reload and force-cache tests don't actually verify that the complete functionality.
*
* reload: Should have the server set a long cache header, then swap the image on next load
* with the test comparing the old image to the new image and making sure they are different.
*
* force-cache: Should do the above but set a no-cache header. The test should compare the first
* image with the new one and make sure they are the same.
*/

const TESTS = ['only-if-cached', 'default', 'reload', 'force-cache'];

type Props = {}
type State = {
'only-if-cached'?: boolean,
'default'?: boolean,
'reload'?: boolean,
'force-cache'?: boolean,
}

class ImageCachePolicyTest extends React.Component {
state = {}

shouldComponentUpdate(nextProps: Props, nextState: State) {
const results: Array<?boolean> = TESTS.map(x => nextState[x]);

if (!results.includes(undefined)) {
const result: boolean = results.reduce((x,y) => x === y === true, true)
TestModule.markTestPassed(result);
}

return false;
}

testComplete(name: string, pass: boolean) {
this.setState({[name]: pass});
}

render() {
return (
<View style={{flex: 1}}>
<Text>Hello</Text>
<Image
source={{
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
cache: 'only-if-cached'
}}
onLoad={() => this.testComplete('only-if-cached', false)}
onError={() => this.testComplete('only-if-cached', true)}
style={styles.base}
/>
<Image
source={{
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
cache: 'default'
}}
onLoad={() => this.testComplete('default', true)}
onError={() => this.testComplete('default', false)}
style={styles.base}
/>
<Image
source={{
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
cache: 'reload'
}}
onLoad={() => this.testComplete('reload', true)}
onError={() => this.testComplete('reload', false)}
style={styles.base}
/>
<Image
source={{
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
cache: 'force-cache'
}}
onLoad={() => this.testComplete('force-cache', true)}
onError={() => this.testComplete('force-cache', false)}
style={styles.base}
/>
</View>
);
}
}

const styles = StyleSheet.create({
base: {
width: 100,
height: 100,
},
});

ImageCachePolicyTest.displayName = 'ImageCachePolicyTest';

module.exports = ImageCachePolicyTest;
1 change: 1 addition & 0 deletions IntegrationTests/IntegrationTestsApp.js
Expand Up @@ -29,6 +29,7 @@ var TESTS = [
require('./LayoutEventsTest'),
require('./AppEventsTest'),
require('./SimpleSnapshotTest'),
require('./ImageCachePolicyTest'),
require('./ImageSnapshotTest'),
require('./PromiseTest'),
require('./WebSocketTest'),
Expand Down
3 changes: 2 additions & 1 deletion Libraries/Image/Image.ios.js
Expand Up @@ -140,7 +140,8 @@ const Image = React.createClass({
* This prop can also contain several remote URLs, specified together with
* their width and height and potentially with scale/other URI arguments.
* The native side will then choose the best `uri` to display based on the
* measured size of the image container.
* measured size of the image container. A `cache` property can be added to
* control how networked request interacts with the local cache.
*/
source: ImageSourcePropType,
/**
Expand Down
26 changes: 26 additions & 0 deletions Libraries/Image/ImageSourcePropType.js
Expand Up @@ -42,6 +42,32 @@ const ImageURISourcePropType = PropTypes.shape({
* additional encoding (e.g. URL-escaping or base64) applied.
*/
body: PropTypes.string,
/**
* `cache` determines how the requests handles potentially cached
* responses.
*
* - `default`: Use the native platforms default strategy. `useProtocolCachePolicy` on iOS.
*
* - `reload`: The data for the URL will be loaded from the originating source.
* No existing cache data should be used to satisfy a URL load request.
*
* - `force-cache`: The existing cached data will be used to satisfy the request,
* regardless of its age or expiration date. If there is no existing data in the cache
* corresponding the request, the data is loaded from the originating source.
*
* - `only-if-cached`: The existing cache data will be used to satisfy a request, regardless of
* its age or expiration date. If there is no existing data in the cache corresponding
* to a URL load request, no attempt is made to load the data from the originating source,
* and the load is considered to have failed.
*
* @platform ios
*/
cache: PropTypes.oneOf([
'default',
'reload',
'force-cache',
'only-if-cached',
]),
/**
* `width` and `height` can be specified if known at build time, in which case
* these will be used to set the default `<Image/>` component dimensions.
Expand Down
1 change: 1 addition & 0 deletions React/Base/RCTConvert.h
Expand Up @@ -47,6 +47,7 @@
+ (NSData *)NSData:(id)json;
+ (NSIndexSet *)NSIndexSet:(id)json;

+ (NSURLRequestCachePolicy)NSURLRequestCachePolicy:(id)json;
+ (NSURL *)NSURL:(id)json;
+ (NSURLRequest *)NSURLRequest:(id)json;

Expand Down
12 changes: 11 additions & 1 deletion React/Base/RCTConvert.m
Expand Up @@ -118,6 +118,14 @@ + (NSURL *)NSURL:(id)json
}
}

RCT_ENUM_CONVERTER(NSURLRequestCachePolicy, (@{
@"default": @(NSURLRequestUseProtocolCachePolicy),
@"reload": @(NSURLRequestReloadIgnoringLocalCacheData),
@"force-cache": @(NSURLRequestReturnCacheDataElseLoad),
@"only-if-cached": @(NSURLRequestReturnCacheDataDontLoad),
}), NSURLRequestUseProtocolCachePolicy, integerValue)


+ (NSURLRequest *)NSURLRequest:(id)json
{
if ([json isKindOfClass:[NSString class]]) {
Expand All @@ -140,8 +148,9 @@ + (NSURLRequest *)NSURLRequest:(id)json

NSData *body = [self NSData:json[@"body"]];
NSString *method = [self NSString:json[@"method"]].uppercaseString ?: @"GET";
NSURLRequestCachePolicy cachePolicy = [self NSURLRequestCachePolicy:json[@"cache"]];
NSDictionary *headers = [self NSDictionary:json[@"headers"]];
if ([method isEqualToString:@"GET"] && headers == nil && body == nil) {
if ([method isEqualToString:@"GET"] && headers == nil && body == nil && cachePolicy == NSURLRequestUseProtocolCachePolicy) {
return [NSURLRequest requestWithURL:URL];
}

Expand All @@ -164,6 +173,7 @@ + (NSURLRequest *)NSURLRequest:(id)json
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
request.HTTPBody = body;
request.HTTPMethod = method;
request.cachePolicy = cachePolicy;
request.allHTTPHeaderFields = headers;
return [request copy];
}
Expand Down
20 changes: 20 additions & 0 deletions docs/Images.md
Expand Up @@ -89,6 +89,26 @@ Many of the images you will display in your app will not be available at compile
<Image source={{uri: 'https://facebook.github.io/react/img/logo_og.png'}} />
```

### Cache Control (iOS Only)

In some cases you might only want to display an image if it is already in the local cache, i.e. a low resolution placeholder until a higher resolution is available. In other cases you do not care if the image is outdated and are willing to display an outdated image to save bandwidth. The `cache` source property gives you control over how the network layer interacts with the cache.

* `default`: Use the native platforms default strategy.
* `reload`: The data for the URL will be loaded from the originating source.
No existing cache data should be used to satisfy a URL load request.
* `force-cache`: The existing cached data will be used to satisfy the request,
regardless of its age or expiration date. If there is no existing data in the cache
corresponding the request, the data is loaded from the originating source.
* `only-if-cached`: The existing cache data will be used to satisfy a request, regardless of
its age or expiration date. If there is no existing data in the cache corresponding
to a URL load request, no attempt is made to load the data from the originating source,
and the load is considered to have failed.

```javascript
<Image source={{uri: 'https://facebook.github.io/react/img/logo_og.png', cache: 'only-if-cached'}}
style={{width: 400, height: 400}} />
````

## Local Filesystem Images

See [CameraRoll](docs/cameraroll.html) for an example of
Expand Down

1 comment on commit 52d8851

@devKrishnan
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache policy attached in the description is broken.

Please sign in to comment.