diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m
index d508c50163e..e6ccdad9d5c 100644
--- a/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m
+++ b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m
@@ -67,6 +67,7 @@ RCT_TEST(IntegrationTestHarnessTest)
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)
diff --git a/Examples/UIExplorer/js/ImageExample.js b/Examples/UIExplorer/js/ImageExample.js
index 67c379cfe16..d0951ed302c 100644
--- a/Examples/UIExplorer/js/ImageExample.js
+++ b/Examples/UIExplorer/js/ImageExample.js
@@ -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 (
+
+
+
+
+ );
+ },
+ platform: 'ios',
+ },
{
title: 'Border Color',
render: function() {
diff --git a/IntegrationTests/ImageCachePolicyTest.js b/IntegrationTests/ImageCachePolicyTest.js
new file mode 100644
index 00000000000..f597dc26bd9
--- /dev/null
+++ b/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 = 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 (
+
+ Hello
+ this.testComplete('only-if-cached', false)}
+ onError={() => this.testComplete('only-if-cached', true)}
+ style={styles.base}
+ />
+ this.testComplete('default', true)}
+ onError={() => this.testComplete('default', false)}
+ style={styles.base}
+ />
+ this.testComplete('reload', true)}
+ onError={() => this.testComplete('reload', false)}
+ style={styles.base}
+ />
+ this.testComplete('force-cache', true)}
+ onError={() => this.testComplete('force-cache', false)}
+ style={styles.base}
+ />
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ base: {
+ width: 100,
+ height: 100,
+ },
+});
+
+ImageCachePolicyTest.displayName = 'ImageCachePolicyTest';
+
+module.exports = ImageCachePolicyTest;
diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js
index 1a8a27caad4..efc5e697929 100644
--- a/IntegrationTests/IntegrationTestsApp.js
+++ b/IntegrationTests/IntegrationTestsApp.js
@@ -29,6 +29,7 @@ var TESTS = [
require('./LayoutEventsTest'),
require('./AppEventsTest'),
require('./SimpleSnapshotTest'),
+ require('./ImageCachePolicyTest'),
require('./ImageSnapshotTest'),
require('./PromiseTest'),
require('./WebSocketTest'),
diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js
index 6da32488cb2..70b804f37ca 100644
--- a/Libraries/Image/Image.ios.js
+++ b/Libraries/Image/Image.ios.js
@@ -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,
/**
diff --git a/Libraries/Image/ImageSourcePropType.js b/Libraries/Image/ImageSourcePropType.js
index 2b32c169c87..4c7b3a7c90f 100644
--- a/Libraries/Image/ImageSourcePropType.js
+++ b/Libraries/Image/ImageSourcePropType.js
@@ -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 `` component dimensions.
diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h
index 117d990a6eb..a71cc7857e3 100644
--- a/React/Base/RCTConvert.h
+++ b/React/Base/RCTConvert.h
@@ -47,6 +47,7 @@
+ (NSData *)NSData:(id)json;
+ (NSIndexSet *)NSIndexSet:(id)json;
++ (NSURLRequestCachePolicy)NSURLRequestCachePolicy:(id)json;
+ (NSURL *)NSURL:(id)json;
+ (NSURLRequest *)NSURLRequest:(id)json;
diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m
index c834d16644e..084caec3494 100644
--- a/React/Base/RCTConvert.m
+++ b/React/Base/RCTConvert.m
@@ -118,6 +118,14 @@ RCT_CUSTOM_CONVERTER(NSData *, NSData, [json dataUsingEncoding:NSUTF8StringEncod
}
}
+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]]) {
@@ -140,8 +148,9 @@ RCT_CUSTOM_CONVERTER(NSData *, NSData, [json dataUsingEncoding:NSUTF8StringEncod
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];
}
@@ -164,6 +173,7 @@ RCT_CUSTOM_CONVERTER(NSData *, NSData, [json dataUsingEncoding:NSUTF8StringEncod
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
request.HTTPBody = body;
request.HTTPMethod = method;
+ request.cachePolicy = cachePolicy;
request.allHTTPHeaderFields = headers;
return [request copy];
}
diff --git a/docs/Images.md b/docs/Images.md
index 7bc89c3b2c1..0d43ca276e1 100644
--- a/docs/Images.md
+++ b/docs/Images.md
@@ -89,6 +89,26 @@ Many of the images you will display in your app will not be available at compile
```
+### 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
+
+````
+
## Local Filesystem Images
See [CameraRoll](docs/cameraroll.html) for an example of