[ios] Cache Management API (#14978)

This commit is contained in:
Jordan Kiley
2019-07-16 14:58:32 -07:00
committed by GitHub
parent 0baa54a194
commit 9d8839c4b8
6 changed files with 288 additions and 37 deletions

View File

@@ -171,9 +171,9 @@ typedef NS_ENUM(NSUInteger, MGLResourceKind) {
/**
MGLOfflineStorage implements a singleton (shared object) that manages offline
packs. All of this class’s instance methods are asynchronous, reflecting the
fact that offline resources are stored in a database. The shared object
maintains a canonical collection of offline packs in its `packs` property.
packs and ambient caching. All of this class’s instance methods are asynchronous,
reflecting the fact that offline resources are stored in a database. The shared
object maintains a canonical collection of offline packs in its `packs` property.
#### Related examples
See the <a href="https://docs.mapbox.com/ios/maps/examples/offline-pack/">
@@ -303,6 +303,21 @@ MGL_EXPORT
*/
- (void)removePack:(MGLOfflinePack *)pack withCompletionHandler:(nullable MGLOfflinePackRemovalCompletionHandler)completion;
/**
Invalidates the specified offline pack. This method checks that the tiles
in the specified offline pack match those from the server. Local tiles that
do not match the latest version on the server are updated.
This is more efficient than deleting the offline pack and downloading it
again. If the data stored locally matches that on the server, new data will
not be downloaded.
@param pack The offline pack to be invalidated.
@param completion The completion handler to call once the pack has been
removed. This handler is executed asynchronously on the main queue.
*/
- (void)invalidatePack:(MGLOfflinePack *)pack withCompletionHandler:(void (^)(NSError * _Nullable))completion;
/**
Forcibly, asynchronously reloads the `packs` property. At some point after this
method is called, the pointer values of the `MGLOfflinePack` objects in the
@@ -341,6 +356,72 @@ MGL_EXPORT
*/
@property (nonatomic, readonly) unsigned long long countOfBytesCompleted;
#pragma mark - Managing Ambient Cache
/**
Sets the maximum ambient cache size in megabytes. The default maximum cache
size is 50 MB. To disable ambient caching, set the maximum ambient cache size
to `0`. Setting the maximum ambient cache size does not impact the maximum size
of offline packs.
While this method does not limit the space available to offline packs,
data in offline packs count towards this limit. If the maximum ambient
cache size is set to 30 MB and 20 MB of offline packs are downloaded,
there may be only 10 MB reserved for the ambient cache.
This method should be called before the map and map style have been loaded.
This method is potentially expensive, as the database will trim cached data
in order to prevent the ambient cache from being larger than the
specified amount.
@param cacheSize The maximum size in bytes for the ambient cache.
@param completion The completion handler to call once the maximum ambient cache size
has been set. This handler is executed synchronously on the main queue.
*/
- (void)setMaximumAmbientCacheSize:(NSUInteger)cacheSize withCompletionHandler:(void (^)(NSError *_Nullable error))completion;
/**
Invalidates the ambient cache. This method checks that the tiles in the
ambient cache match those from the server. If the local tiles do not match
those on the server, they are re-downloaded.
This is recommended over clearing the cache or resetting the database
because valid local tiles will not be downloaded again.
Resources shared with offline packs will not be affected by this method.
@param completion The completion handler to call once the ambient cache has
been revalidated. This handler is executed asynchronously on the main queue.
*/
- (void)invalidateAmbientCacheWithCompletionHandler:(void (^)(NSError *_Nullable error))completion;
/**
Clears the ambient cache by deleting resources. This method does not
affect resources shared with offline regions.
@param completion The completion handler to call once resources from
the ambient cache have been cleared. This handler is executed
asynchronously on the main queue.
*/
- (void)clearAmbientCacheWithCompletionHandler:(void (^)(NSError *_Nullable error))completion;
/**
Deletes the existing database, which includes both the ambient cache and offline packs,
then reinitializes it.
You typically do not need to call this method.
@param completion The completion handler to call once the pack has database has
been reset. This handler is executed asynchronously on the main queue.
*/
- (void)resetDatabaseWithCompletionHandler:(void (^)(NSError *_Nullable error))completion;
/*
Inserts the provided resource into the ambient cache.

View File

@@ -48,7 +48,6 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio
@property (nonatomic) std::shared_ptr<mbgl::DefaultFileSource> mbglFileSource;
@property (nonatomic) std::string mbglCachePath;
@property (nonatomic, getter=isPaused) BOOL paused;
@end
@implementation MGLOfflineStorage {
@@ -341,10 +340,11 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio
NSMutableArray *packs;
if (!result) {
NSString *description = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"ADD_FILE_CONTENTS_FAILED_DESC", @"Foundation", nil, @"Unable to add offline packs from the file at %@.", @"User-friendly error description"), filePath];
error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:@{
NSLocalizedDescriptionKey: description,
NSLocalizedFailureReasonErrorKey: @(mbgl::util::toString(result.error()).c_str())
}];
error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed
userInfo:@{
NSLocalizedDescriptionKey: description,
NSLocalizedFailureReasonErrorKey: @(mbgl::util::toString(result.error()).c_str())
}];
} else {
auto& regions = result.value();
packs = [NSMutableArray arrayWithCapacity:regions.size()];
@@ -401,7 +401,7 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio
NSError *error;
if (!mbglOfflineRegion) {
NSString *errorDescription = @(mbgl::util::toString(mbglOfflineRegion.error()).c_str());
error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:errorDescription ? @{
error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:errorDescription ? @{
NSLocalizedDescriptionKey: errorDescription,
} : nil];
}
@@ -431,11 +431,10 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio
completion(nil);
return;
}
_mbglFileSource->deleteOfflineRegion(std::move(*mbglOfflineRegion), [&, completion](std::exception_ptr exception) {
NSError *error;
if (exception) {
error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:@{
error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:@{
NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()),
}];
}
@@ -445,6 +444,29 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio
});
}
});
}
- (void)invalidatePack:(MGLOfflinePack *)pack withCompletionHandler:(void (^)(NSError * _Nullable))completion {
mbgl::OfflineRegion& region = *pack.mbglOfflineRegion;
NSError *error;
if (!pack.mbglOfflineRegion) {
completion(nil);
return;
}
_mbglFileSource->invalidateOfflineRegion(region, [&](std::exception_ptr exception) {
if (exception) {
error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:@{
NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()),
}];
}
});
if (completion) {
dispatch_async(dispatch_get_main_queue(), [&, completion, error](void) {
completion(error);
});
}
}
- (void)reloadPacks {
@@ -462,7 +484,7 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio
NSError *error;
NSMutableArray *packs;
if (!result) {
error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:@{
error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeUnknown userInfo:@{
NSLocalizedDescriptionKey: @(mbgl::util::toString(result.error()).c_str()),
}];
} else {
@@ -486,6 +508,72 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio
_mbglFileSource->setOfflineMapboxTileCountLimit(maximumCount);
}
#pragma mark - Ambient Cache management
- (void)setMaximumAmbientCacheSize:(NSUInteger)cacheSize withCompletionHandler:(void (^)(NSError * _Nullable))completion {
_mbglFileSource->setMaximumAmbientCacheSize(cacheSize, [&, completion](std::exception_ptr exception) {
NSError *error;
if (completion) {
if (exception) {
error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:@{
NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()),
}];
}
dispatch_sync(dispatch_get_main_queue(), ^ {
completion(error);
});
}
});
}
- (void)invalidateAmbientCacheWithCompletionHandler:(void (^)(NSError *_Nullable))completion {
_mbglFileSource->invalidateAmbientCache([&, completion](std::exception_ptr exception){
NSError *error;
if (completion) {
if (exception) {
// Convert std::exception_ptr to an NSError.
error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:@{
NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()),
}];
}
dispatch_async(dispatch_get_main_queue(), ^ {
completion(error);
});
}
});
}
- (void)clearAmbientCacheWithCompletionHandler:(void (^)(NSError *_Nullable error))completion {
_mbglFileSource->clearAmbientCache([&, completion](std::exception_ptr exception){
NSError *error;
if (completion) {
if (exception) {
error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:@{
NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()),
}];
}
dispatch_async(dispatch_get_main_queue(), [&, completion, error](void) {
completion(error);
});
}
});
}
- (void)resetDatabaseWithCompletionHandler:(void (^)(NSError *_Nullable error))completion {
_mbglFileSource->resetDatabase([&, completion](std::exception_ptr exception) {
NSError *error;
if (completion) {
if (exception) {
error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeUnknown userInfo:@{
NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()),
}];
}
dispatch_async(dispatch_get_main_queue(), ^{
completion(error);
});
}
});
}
#pragma mark -
- (unsigned long long)countOfBytesCompleted {

View File

@@ -53,7 +53,9 @@ typedef NS_ENUM(NSInteger, MGLErrorCode) {
/** Source is in use and cannot be removed */
MGLErrorCodeSourceIsInUseCannotRemove = 7,
/** Source is in use and cannot be removed */
MGLErrorCodeSourceIdentifierMismatch = 8
MGLErrorCodeSourceIdentifierMismatch = 8,
/** An error occurred while modifying the offline storage database */
MGLErrorCodeModifyingOfflineStorageFailed = 9
};
/** Options for enabling debugging features in an `MGLMapView` instance. */

View File

@@ -201,6 +201,68 @@
pack = nil;
}
- (void)testInvalidatePack {
XCTestExpectation *expectation = [self expectationWithDescription:@"Expect offline pack to be invalidated without an error."];
MGLCoordinateBounds bounds = {
{ .latitude = 48.8660, .longitude = 2.3306 },
{ .latitude = 48.8603, .longitude = 2.3213 },
};
NSURL *styleURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"one-liner" withExtension:@"json"];
MGLTilePyramidOfflineRegion *region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:styleURL bounds:bounds fromZoomLevel:10 toZoomLevel:11];
NSString *nameKey = @"Name";
NSString *name = @"Paris square";
NSData *context = [NSKeyedArchiver archivedDataWithRootObject:@{nameKey: name}];
[[MGLOfflineStorage sharedOfflineStorage] addPackForRegion:region withContext:context completionHandler:^(MGLOfflinePack * _Nullable pack, NSError * _Nullable error) {
XCTAssertNotNil(pack);
[[MGLOfflineStorage sharedOfflineStorage] invalidatePack:pack withCompletionHandler:^(NSError * _Nullable) {
XCTAssertNotNil(pack);
XCTAssertNil(error);
[expectation fulfill];
}];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}
- (void)testSetMaximumAmbientCache {
XCTestExpectation *expectation = [self expectationWithDescription:@"Expect maximum cache size to be raised without an error."];
[[MGLOfflineStorage sharedOfflineStorage] setMaximumAmbientCacheSize:0 withCompletionHandler:^(NSError * _Nullable error) {
XCTAssertNil(error);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}
- (void)testInvalidateAmbientCache {
XCTestExpectation *expectation = [self expectationWithDescription:@"Expect cache to be invalidated without an error."];
[[MGLOfflineStorage sharedOfflineStorage] invalidateAmbientCacheWithCompletionHandler:^(NSError * _Nullable error) {
XCTAssertNil(error);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}
- (void)testClearCache {
XCTestExpectation *expectation = [self expectationWithDescription:@"Expect cache to be cleared without an error."];
[[MGLOfflineStorage sharedOfflineStorage] clearAmbientCacheWithCompletionHandler:^(NSError * _Nullable error) {
XCTAssertNil(error);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}
- (void)testResetDatabase {
XCTestExpectation *expectation = [self expectationWithDescription:@"Expect database to be reset without an error."];
[[MGLOfflineStorage sharedOfflineStorage] resetDatabaseWithCompletionHandler:^(NSError * _Nullable error) {
XCTAssertNil(error);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}
- (void)testBackupExclusion {
NSURL *cacheDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory
inDomain:NSUserDomainMask

View File

@@ -128,6 +128,19 @@ static NSString * const MBXOfflinePacksTableViewActiveCellReuseIdentifier = @"Ac
[self presentViewController:alertController animated:YES completion:nil];
}
- (IBAction)invalidatePacks:(id)sender {
for (MGLOfflinePack *pack in [MGLOfflineStorage sharedOfflineStorage].packs) {
CFTimeInterval start = CACurrentMediaTime();
[[MGLOfflineStorage sharedOfflineStorage] invalidatePack:pack withCompletionHandler:^(NSError * _Nullable error) {
CFTimeInterval end = CACurrentMediaTime();
CFTimeInterval difference = end - start;
NSLog(@"invalidatePack Started: %f Ended: %f Total Time: %f", start, end, difference);
}];
}
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

View File

@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="PSe-Ot-7Ff">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14810.11" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="PSe-Ot-7Ff">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14766.13"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -15,14 +13,14 @@
<objects>
<viewController id="WaX-pd-UZQ" userLabel="Map View Controller" customClass="MBXViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Z9X-fc-PUC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="203" height="33"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kNe-zV-9ha" customClass="MGLMapView">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="203" height="33"/>
<subviews>
<button hidden="YES" opaque="NO" userInteractionEnabled="NO" alpha="0.69999999999999996" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="tailTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="58y-pX-YyB">
<rect key="frame" x="8" y="102" width="40" height="20"/>
<rect key="frame" x="8" y="82" width="40" height="20"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" button="YES" notEnabled="YES"/>
@@ -40,7 +38,7 @@
</userDefinedRuntimeAttributes>
</button>
<view hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="BHE-Wn-x69" customClass="MBXFrameTimeGraphView">
<rect key="frame" x="0.0" y="467" width="375" height="200"/>
<rect key="frame" x="0.0" y="-167" width="203" height="200"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" notEnabled="YES"/>
@@ -143,7 +141,7 @@
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="fGu-Ys-Eh1" id="sUf-bc-8xG">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="My Inactive Offline Pack" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="JtH-Ce-MI5">
@@ -167,7 +165,7 @@
<rect key="frame" x="0.0" y="72" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="mKB-tz-Zfl" id="nS3-aU-nBr">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="My Active Offline Pack" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="9ZK-gS-wJ4">
@@ -194,11 +192,18 @@
</connections>
</tableView>
<navigationItem key="navigationItem" title="Offline Packs" id="UcK-PK-eQA">
<barButtonItem key="rightBarButtonItem" systemItem="add" id="gCV-hl-Mzc">
<connections>
<action selector="addCurrentRegion:" destination="7q0-lI-zqb" id="G2O-3V-aEA"/>
</connections>
</barButtonItem>
<rightBarButtonItems>
<barButtonItem systemItem="add" id="gCV-hl-Mzc">
<connections>
<action selector="addCurrentRegion:" destination="7q0-lI-zqb" id="G2O-3V-aEA"/>
</connections>
</barButtonItem>
<barButtonItem style="plain" systemItem="refresh" id="2fx-iS-Veb">
<connections>
<action selector="invalidatePacks:" destination="7q0-lI-zqb" id="5lx-FY-aTt"/>
</connections>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<segue destination="x2D-ga-sM5" kind="unwind" identifier="ReturnToMap" unwindAction="unwindToMapViewController:" id="6MZ-Ed-tu2"/>
@@ -215,7 +220,7 @@
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="PSe-Ot-7Ff" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="ONr-CS-J5X">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
@@ -257,7 +262,7 @@
</constraints>
</scrollView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="l5l-w7-P80" userLabel="Control Panel View">
<rect key="frame" x="0.0" y="20" width="375" height="64"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="64"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Zoom" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DgB-BD-Ltx">
<rect key="frame" x="29.5" y="6" width="35" height="16"/>
@@ -381,22 +386,22 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="miZ-Fw-EWq" userLabel="Image View TL">
<rect key="frame" x="0.0" y="64" width="125" height="301.5"/>
<rect key="frame" x="0.0" y="44" width="125" height="311.5"/>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="XuN-T4-Z83" userLabel="Image View TM">
<rect key="frame" x="125" y="64" width="125" height="301.5"/>
<rect key="frame" x="125" y="44" width="125" height="311.5"/>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="ykR-Ku-i9l" userLabel="Image View TR">
<rect key="frame" x="250" y="64" width="125" height="301.5"/>
<rect key="frame" x="250" y="44" width="125" height="311.5"/>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="TL0-V8-T2F" userLabel="Image View BL">
<rect key="frame" x="0.0" y="365.5" width="125" height="301.5"/>
<rect key="frame" x="0.0" y="355.5" width="125" height="311.5"/>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="eMy-JU-rq4" userLabel="Image View BM">
<rect key="frame" x="125" y="365.5" width="125" height="301.5"/>
<rect key="frame" x="125" y="355.5" width="125" height="311.5"/>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="zT0-3J-0xw" userLabel="Image View BR">
<rect key="frame" x="250" y="365.5" width="125" height="301.5"/>
<rect key="frame" x="250" y="355.5" width="125" height="311.5"/>
</imageView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>