[ios, macos] Streamlined MGLMapSnapshotter

Upgraded MGLMapSnapshotter to be compatible with the latest mbgl.

Converted as many of MGLMapSnapshotter’s instance variables and properties as possible into local variables close to the point of use. The completion handler of -startWithCompletionHandler: and its variants is no longer called as part of -cancel, when the snapshotter is deallocated, or when the application is terminating. -loading once again works and is used to avoid concurrent requests, now that the completion handler is no longer memoized. Snapshotter options are copied and converted into a snapshotter on demand as late as possible.

Made the decoration step synchronous while continuing to perform it on a worker thread. Only access self on user-facing queues. Ensure MGLMapSnapshotter.loading is reset after snapshotting.

Pushed some drawing and completion code up from inner completion blocks to outer completion blocks to avoid excessively passing state around. Converted a few class methods into standalone C functions to emphasize the avoidance of captured state.
This commit is contained in:
Minh Nguyễn
2020-03-13 00:20:48 -07:00
committed by Minh Nguyễn
parent 5be63a0d4a
commit 98e8af96d3
5 changed files with 157 additions and 228 deletions

View File

@@ -58,7 +58,7 @@ typedef void (^MGLMapSnapshotOverlayHandler)(MGLMapSnapshotOverlay * snapshotOve
The options to use when creating images with the `MGLMapSnapshotter`.
*/
MGL_EXPORT
@interface MGLMapSnapshotOptions : NSObject
@interface MGLMapSnapshotOptions : NSObject <NSCopying>
/**
Creates a set of options with the minimum required information.
@@ -252,7 +252,7 @@ MGL_EXPORT
/**
Starts the snapshot creation and executes the specified blocks with the result
on the specified queue. Use this option if you want to add custom drawing on top of the
resulting `MGLMapSnapShot`.
resulting `MGLMapSnapshot`.
@param overlayHandler The block to handle manipulation of the `MGLMapSnapshotter`'s `CGContext`.
@param completionHandler The block to handle the result in.
*/

View File

@@ -33,6 +33,9 @@
const CGPoint MGLLogoImagePosition = CGPointMake(8, 8);
const CGFloat MGLSnapshotterMinimumPixelSize = 64;
MGLImage *MGLAttributedSnapshot(mbgl::MapSnapshotter::Attributions attributions, MGLImage *mglImage, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn, MGLMapSnapshotOptions *options, MGLMapSnapshotOverlayHandler overlayHandler);
MGLMapSnapshot *MGLSnapshotWithDecoratedImage(MGLImage *mglImage, MGLMapSnapshotOptions *options, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn, MGLMapSnapshotOverlayHandler overlayHandler, NSError * _Nullable *outError);
NSArray<MGLAttributionInfo *> *MGLAttributionInfosFromAttributions(mbgl::MapSnapshotter::Attributions attributions);
@interface MGLMapSnapshotOverlay() <MGLMapSnapshotProtocol>
@property (nonatomic, assign) CGFloat scale;
@@ -131,6 +134,15 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
return self;
}
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
__typeof__(self) copy = [[[self class] alloc] initWithStyleURL:_styleURL camera:_camera size:_size];
copy.zoomLevel = _zoomLevel;
copy.coordinateBounds = _coordinateBounds;
copy.scale = _scale;
copy.showsLogo = _showsLogo;
return copy;
}
@end
@interface MGLMapSnapshot() <MGLMapSnapshotProtocol>
@@ -190,32 +202,19 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
@end
@interface MGLMapSnapshotter()
@property (nonatomic) BOOL cancelled;
@property (nonatomic) BOOL loading;
@property (nonatomic) BOOL terminated;
@property (nonatomic) dispatch_queue_t resultQueue;
@property (nonatomic, copy) MGLMapSnapshotCompletionHandler completion;
+ (void)completeWithErrorCode:(MGLErrorCode)errorCode description:(nonnull NSString*)description onQueue:(dispatch_queue_t)queue completion:(MGLMapSnapshotCompletionHandler)completion;
@end
@implementation MGLMapSnapshotter {
std::unique_ptr<mbgl::MapSnapshotter> _mbglMapSnapshotter;
std::unique_ptr<mbgl::Actor<mbgl::MapSnapshotter::Callback>> _snapshotCallback;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (_completion) {
MGLAssert(_snapshotCallback, @"Snapshot in progress - there should be a valid callback");
[MGLMapSnapshotter completeWithErrorCode:MGLErrorCodeSnapshotFailed
description:@"MGLMapSnapshotter deallocated prior to snapshot completion."
onQueue:_resultQueue
completion:_completion];
}
[self cancel];
}
- (instancetype)init {
NSAssert(NO, @"Please use -[MGLMapSnapshotter initWithOptions:]");
[super doesNotRecognizeSelector:_cmd];
@@ -227,7 +226,7 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
MGLLogDebug(@"Initializing withOptions: %@", options);
self = [super init];
if (self) {
[self setOptions:options];
self.options = options;
#if TARGET_OS_IPHONE
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil];
#else
@@ -242,13 +241,7 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
- (void)applicationWillTerminate:(NSNotification *)notification
{
if (self.completion) {
[self cancel];
}
_mbglMapSnapshotter.reset();
_snapshotCallback.reset();
[self cancel];
self.terminated = YES;
}
@@ -273,36 +266,25 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
format:@"startWithQueue:completionHandler: must be called from a thread with an active run loop."];
}
if (self.completion) {
if (self.loading) {
// Consider replacing this exception with an error passed to the completion block.
[NSException raise:NSInternalInconsistencyException
format:@"Already started this snapshotter."];
}
self.loading = YES;
if (self.terminated) {
[NSException raise:NSInternalInconsistencyException
format:@"Starting a snapshotter after application termination is not supported."];
}
MGLMapSnapshotOptions *options = [self.options copy];
[self configureWithOptions:options];
MGLLogDebug(@"Starting with options: %@", self.options);
self.completion = completion;
self.resultQueue = queue;
self.cancelled = NO;
// Capture weakSelf to avoid retain cycle if callback is never called (i.e., snapshot canceled).
__weak __typeof__(self) weakSelf = self;
// mbgl::Scheduler::GetCurrent() scheduler means "run callback on current (ie UI/main) thread"
// capture weakSelf to avoid retain cycle if callback is never called (ie snapshot cancelled)
_snapshotCallback = std::make_unique<mbgl::Actor<mbgl::MapSnapshotter::Callback>>(
*mbgl::Scheduler::GetCurrent(),
[=](std::exception_ptr mbglError, mbgl::PremultipliedImage image, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn) {
__typeof__(self) strongSelf = weakSelf;
// If self had died, _snapshotCallback would have been destroyed and this block would not be executed
MGLCAssert(strongSelf, @"Snapshot callback executed after being destroyed.");
if (!strongSelf.completion)
return;
_mbglMapSnapshotter->snapshot([=](std::exception_ptr mbglError, mbgl::PremultipliedImage image, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn) {
if (mbglError) {
NSString *description = @(mbgl::util::toString(mbglError).c_str());
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description};
@@ -312,34 +294,41 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
#endif
// Dispatch to result queue
dispatch_async(queue, ^{
strongSelf.completion(nil, error);
strongSelf.completion = nil;
weakSelf.loading = NO;
completion(nil, error);
});
} else {
#if TARGET_OS_IPHONE
MGLImage *mglImage = [[MGLImage alloc] initWithMGLPremultipliedImage:std::move(image) scale:strongSelf.options.scale];
MGLImage *mglImage = [[MGLImage alloc] initWithMGLPremultipliedImage:std::move(image) scale:options.scale];
#else
MGLImage *mglImage = [[MGLImage alloc] initWithMGLPremultipliedImage:std::move(image)];
mglImage.size = NSMakeSize(mglImage.size.width / strongSelf.options.scale,
mglImage.size.height / strongSelf.options.scale);
mglImage.size = NSMakeSize(mglImage.size.width / options.scale,
mglImage.size.height / options.scale);
#endif
[strongSelf drawAttributedSnapshot:attributions snapshotImage:mglImage pointForFn:pointForFn latLngForFn:latLngForFn overlayHandler:overlayHandler];
// Process image watermark in a work queue
dispatch_queue_t workQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(workQueue, ^{
// Call a function that cannot accidentally capture self.
NSError *error;
MGLMapSnapshot *snapshot = MGLSnapshotWithDecoratedImage(mglImage, options, attributions, pointForFn, latLngForFn, overlayHandler, &error);
// Dispatch result to result queue
dispatch_async(queue, ^{
__typeof__(self) strongSelf = weakSelf;
strongSelf.loading = NO;
if (completion) {
completion(snapshot, error);
}
});
});
}
strongSelf->_snapshotCallback = NULL;
});
// Launches snapshot on background Thread owned by mbglMapSnapshotter
// _snapshotCallback->self() is an ActorRef: if the callback is destroyed, further messages
// to the callback are just no-ops
_mbglMapSnapshotter->snapshot(_snapshotCallback->self());
});
}
+ (MGLImage*)drawAttributedSnapshotWorker:(mbgl::MapSnapshotter::Attributions)attributions snapshotImage:(MGLImage *)mglImage pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn scale:(CGFloat)scale size:(CGSize)size showsLogo:(BOOL)showsLogo overlayHandler:(MGLMapSnapshotOverlayHandler)overlayHandler {
MGLImage *MGLAttributedSnapshot(mbgl::MapSnapshotter::Attributions attributions, MGLImage *mglImage, MGLMapSnapshotOptions *options, void (^overlayHandler)()) {
NSArray<MGLAttributionInfo *>* attributionInfo = [MGLMapSnapshotter generateAttributionInfos:attributions];
NSArray<MGLAttributionInfo *> *attributionInfo = MGLAttributionInfosFromAttributions(attributions);
#if TARGET_OS_IPHONE
MGLAttributionInfoStyle attributionInfoStyle = MGLAttributionInfoStyleLong;
@@ -375,31 +364,20 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
attributionBackgroundSize.width * mglImage.scale,
attributionBackgroundSize.height * mglImage.scale);
UIGraphicsBeginImageContextWithOptions(mglImage.size, NO, scale);
UIGraphicsBeginImageContextWithOptions(mglImage.size, NO, options.scale);
[mglImage drawInRect:CGRectMake(0, 0, mglImage.size.width, mglImage.size.height)];
overlayHandler();
CGContextRef currentContext = UIGraphicsGetCurrentContext();
if (currentContext && overlayHandler) {
MGLMapSnapshotOverlay *snapshotOverlay = [[MGLMapSnapshotOverlay alloc] initWithContext:currentContext
scale:scale
pointForFn:pointForFn
latLngForFn:latLngForFn];
CGContextSaveGState(snapshotOverlay.context);
overlayHandler(snapshotOverlay);
CGContextRestoreGState(snapshotOverlay.context);
currentContext = UIGraphicsGetCurrentContext();
}
if (!currentContext && overlayHandler) {
if (!currentContext) {
// If the current context has been corrupted by the user,
// return nil so we can generate an error later.
return nil;
}
if (showsLogo) {
if (options.showsLogo) {
[logoImage drawInRect:logoImageRect];
}
@@ -423,9 +401,7 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
return compositedImage;
#else
NSSize targetSize = NSMakeSize(size.width, size.height);
NSRect targetFrame = NSMakeRect(0, 0, targetSize.width, targetSize.height);
NSRect targetFrame = { .origin = NSZeroPoint, .size = options.size };
MGLAttributionInfoStyle attributionInfoStyle = MGLAttributionInfoStyleLong;
for (NSUInteger styleValue = MGLAttributionInfoStyleLong; styleValue >= MGLAttributionInfoStyleShort; styleValue--) {
@@ -461,25 +437,14 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
NSImageRep *sourceImageRep = [sourceImage bestRepresentationForRect:targetFrame
context:nil
hints:nil];
compositedImage = [[NSImage alloc] initWithSize:targetSize];
compositedImage = [[NSImage alloc] initWithSize:targetFrame.size];
[compositedImage lockFocus];
[sourceImageRep drawInRect: targetFrame];
NSGraphicsContext *currentContext = [NSGraphicsContext currentContext];
if (currentContext && overlayHandler) {
MGLMapSnapshotOverlay *snapshotOverlay = [[MGLMapSnapshotOverlay alloc] initWithContext:currentContext.CGContext
scale:scale
pointForFn:pointForFn
latLngForFn:latLngForFn];
[currentContext saveGraphicsState];
overlayHandler(snapshotOverlay);
[currentContext restoreGraphicsState];
currentContext = [NSGraphicsContext currentContext];
}
if (!currentContext && overlayHandler) {
if (!currentContext) {
// If the current context has been corrupted by the user,
// return nil so we can generate an error later.
return nil;
@@ -505,59 +470,55 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
#endif
}
- (void)drawAttributedSnapshot:(mbgl::MapSnapshotter::Attributions)attributions snapshotImage:(MGLImage *)mglImage pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn overlayHandler:(MGLMapSnapshotOverlayHandler)overlayHandler {
// Process image watermark in a work queue
dispatch_queue_t workQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t resultQueue = self.resultQueue;
// Capture scale and size by value to avoid accessing self from another thread
CGFloat scale = self.options.scale;
CGSize size = self.options.size;
BOOL showsLogo = self.options.showsLogo;
// pointForFn is a copyable std::function that captures state by value: see MapSnapshotter::Impl::snapshot
__weak __typeof__(self) weakself = self;
dispatch_async(workQueue, ^{
// Call a class method to ensure we're not accidentally capturing self
MGLImage *compositedImage = [MGLMapSnapshotter drawAttributedSnapshotWorker:attributions
snapshotImage:mglImage
pointForFn:pointForFn
latLngForFn:latLngForFn
scale:scale
size:size
showsLogo:showsLogo
overlayHandler:overlayHandler];
// Dispatch result to origin queue
dispatch_async(resultQueue, ^{
__typeof__(self) strongself = weakself;
if (strongself.completion) {
if (!compositedImage) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to generate composited snapshot."};
NSError *error = [NSError errorWithDomain:MGLErrorDomain
code:MGLErrorCodeSnapshotFailed
userInfo:userInfo];
strongself.completion(nil, error);
strongself.completion = nil;
} else {
MGLMapSnapshot* snapshot = [[MGLMapSnapshot alloc] initWithImage:compositedImage
scale:scale
pointForFn:pointForFn
latLngForFn:latLngForFn];
strongself.completion(snapshot, nil);
strongself.completion = nil;
}
}
});
MGLMapSnapshot *MGLSnapshotWithDecoratedImage(MGLImage *mglImage, MGLMapSnapshotOptions *options, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn, MGLMapSnapshotOverlayHandler overlayHandler, NSError * _Nullable *outError) {
MGLImage *compositedImage = MGLAttributedSnapshot(attributions, mglImage, options, ^{
if (!overlayHandler) {
return;
}
#if TARGET_OS_IPHONE
CGContextRef context = UIGraphicsGetCurrentContext();
if (!context) {
return;
}
MGLMapSnapshotOverlay *snapshotOverlay = [[MGLMapSnapshotOverlay alloc] initWithContext:context
scale:options.scale
pointForFn:pointForFn
latLngForFn:latLngForFn];
CGContextSaveGState(context);
overlayHandler(snapshotOverlay);
CGContextRestoreGState(context);
#else
NSGraphicsContext *context = [NSGraphicsContext currentContext];
if (!context) {
return;
}
MGLMapSnapshotOverlay *snapshotOverlay = [[MGLMapSnapshotOverlay alloc] initWithContext:context.CGContext
scale:options.scale
pointForFn:pointForFn
latLngForFn:latLngForFn];
[context saveGraphicsState];
overlayHandler(snapshotOverlay);
[context restoreGraphicsState];
#endif
});
if (compositedImage) {
return [[MGLMapSnapshot alloc] initWithImage:compositedImage
scale:options.scale
pointForFn:pointForFn
latLngForFn:latLngForFn];
} else {
if (outError) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to generate composited snapshot."};
*outError = [NSError errorWithDomain:MGLErrorDomain
code:MGLErrorCodeSnapshotFailed
userInfo:userInfo];
}
return nil;
}
}
+ (NSArray<MGLAttributionInfo *>*) generateAttributionInfos:(mbgl::MapSnapshotter::Attributions)attributions {
NSArray<MGLAttributionInfo *> *MGLAttributionInfosFromAttributions(mbgl::MapSnapshotter::Attributions attributions) {
NSMutableArray *infos = [NSMutableArray array];
#if TARGET_OS_IPHONE
@@ -567,8 +528,8 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
CGFloat fontSize = [NSFont systemFontSizeForControlSize:NSMiniControlSize];
NSColor *attributeFontColor = [NSColor blackColor];
#endif
for (auto attribution = attributions.begin(); attribution != attributions.end(); ++attribution) {
NSString *attributionHTMLString = @(attribution->c_str());
for (auto attribution : attributions) {
NSString *attributionHTMLString = @(attribution.c_str());
NSArray *tileSetInfos = [MGLAttributionInfo attributionInfosFromHTMLString:attributionHTMLString
fontSize:fontSize
linkColor:attributeFontColor];
@@ -688,57 +649,16 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
- (void)cancel
{
MGLLogInfo(@"Cancelling snapshotter.");
self.cancelled = YES;
if (_snapshotCallback) {
[MGLMapSnapshotter completeWithErrorCode:MGLErrorCodeSnapshotFailed
description:[NSString stringWithFormat:@"MGLMapSnapshotter cancelled from %s", __PRETTY_FUNCTION__]
onQueue:self.resultQueue
completion:self.completion];
self.completion = nil;
if (_mbglMapSnapshotter) {
_mbglMapSnapshotter->cancel();
}
_snapshotCallback.reset();
_mbglMapSnapshotter.reset();
}
+ (void)completeWithErrorCode:(MGLErrorCode)errorCode description:(nonnull NSString*)description onQueue:(dispatch_queue_t)queue completion:(MGLMapSnapshotCompletionHandler)completion {
// The snapshot hasn't completed, so we should alert the caller
if (completion && queue) {
dispatch_async(queue, ^{
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description};
NSError *error = [NSError errorWithDomain:MGLErrorDomain
code:errorCode
userInfo:userInfo];
#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR
[[MMEEventsManager sharedManager] reportError:error];
#endif
completion(NULL, error);
});
}
}
- (void)setOptions:(MGLMapSnapshotOptions *)options
{
if (_terminated) {
[NSException raise:NSInternalInconsistencyException
format:@"Calling MGLMapSnapshotter.options after application termination is not supported."];
}
MGLLogDebug(@"Setting options: %@", options);
if (_completion) {
[self cancel];
}
_cancelled = NO;
_options = options;
- (void)configureWithOptions:(MGLMapSnapshotOptions *)options {
auto mbglFileSource = [[MGLOfflineStorage sharedOfflineStorage] mbglFileSource];
std::string styleURL = std::string([options.styleURL.absoluteString UTF8String]);
std::pair<bool, std::string> style = std::make_pair(false, styleURL);
// Size; taking into account the minimum texture size for OpenGL ES
// For non retina screens the ratio is 1:1 MGLSnapshotterMinimumPixelSize
mbgl::Size size = {
@@ -748,6 +668,19 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
float pixelRatio = MAX(options.scale, 1);
// App-global configuration
MGLRendererConfiguration *config = [MGLRendererConfiguration currentConfiguration];
mbgl::ResourceOptions resourceOptions;
resourceOptions.withCachePath(MGLOfflineStorage.sharedOfflineStorage.mbglCachePath)
.withAssetPath(NSBundle.mainBundle.resourceURL.path.UTF8String);
// Create the snapshotter
_mbglMapSnapshotter = std::make_unique<mbgl::MapSnapshotter>(
size, pixelRatio, resourceOptions, mbgl::MapSnapshotterObserver::nullObserver(), config.localFontFamilyName);
_mbglMapSnapshotter->setStyleURL(std::string(options.styleURL.absoluteString.UTF8String));
// Camera options
mbgl::CameraOptions cameraOptions;
if (CLLocationCoordinate2DIsValid(options.camera.centerCoordinate)) {
@@ -756,23 +689,12 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
cameraOptions.bearing = MAX(0, options.camera.heading);
cameraOptions.zoom = MAX(0, options.zoomLevel);
cameraOptions.pitch = MAX(0, options.camera.pitch);
_mbglMapSnapshotter->setCameraOptions(cameraOptions);
// Region
mbgl::optional<mbgl::LatLngBounds> coordinateBounds;
if (!MGLCoordinateBoundsIsEmpty(options.coordinateBounds)) {
coordinateBounds = MGLLatLngBoundsFromCoordinateBounds(options.coordinateBounds);
_mbglMapSnapshotter->setRegion(MGLLatLngBoundsFromCoordinateBounds(options.coordinateBounds));
}
// App-global configuration
MGLRendererConfiguration* config = [MGLRendererConfiguration currentConfiguration];
mbgl::ResourceOptions resourceOptions;
resourceOptions.withCachePath([[MGLOfflineStorage sharedOfflineStorage] mbglCachePath])
.withAssetPath([NSBundle mainBundle].resourceURL.path.UTF8String);
// Create the snapshotter
_mbglMapSnapshotter = std::make_unique<mbgl::MapSnapshotter>(
style, size, pixelRatio, cameraOptions, coordinateBounds, config.localFontFamilyName, resourceOptions);
}
@end

View File

@@ -19,6 +19,8 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT
* Added the `MGLMapView.minimumPitch` and `MGLMapView.maximumPitch` properties to further limit how much the user or your code can tilt the map. ([#208](https://github.com/mapbox/mapbox-gl-native-ios/pull/208))
* Fixed issues where an offline pack would stop downloading before completion. ([#16230](https://github.com/mapbox/mapbox-gl-native/pull/16230), [#16240](https://github.com/mapbox/mapbox-gl-native/pull/16240))
* When an offline pack encounters an HTTP 404 error, the `MGLOfflinePackUserInfoKeyError` user info key of the `MGLOfflinePackErrorNotification` now indicates the resource that could not be downloaded. ([#16240](https://github.com/mapbox/mapbox-gl-native/pull/16240))
* The `-[MGLMapSnapshotter cancel]` method no longer calls the completion handler passed into `-[MGLMapSnapshotter startWithCompletionHandler:]`. ([#209](https://github.com/mapbox/mapbox-gl-native-ios/pull/210))
* Fixed an issue where the `MGLMapSnapshotter.loading` property always returned `NO`, even while loading a snapshot. ([#209](https://github.com/mapbox/mapbox-gl-native-ios/pull/210))
* Fixed a memory leak when zooming with any options enabled in the `MGLMapView.debugMask` property. ([#15179](https://github.com/mapbox/mapbox-gl-native/issues/15179))
* Added the `-[MGLOfflineStorage preloadData:forURL:modificationDate:expirationDate:eTag:mustRevalidate:completionHandler:]` method for determining when the data is ready to retrieve from the cache. ([#188](https://github.com/mapbox/mapbox-gl-native-ios/pull/188))

View File

@@ -72,6 +72,27 @@ MGLMapSnapshotter* snapshotterWithBounds(MGLCoordinateBounds bounds, CGSize size
[self waitForExpectations:@[expectation] timeout:10.0];
}
- (void)testSnapshotterWithoutStrongReference🔒 {
XCTestExpectation *expectation = [self expectationWithDescription:@"Completion handler shouldn’t be called if there’s no strong reference to the snapshotter and the snapshotter goes away."];
expectation.inverted = YES;
CGSize size = self.mapView.bounds.size;
CLLocationCoordinate2D coordinates = CLLocationCoordinate2DMake(30.0, 30.0);
__weak MGLMapSnapshotter *weakSnapshotter;
@autoreleasepool {
MGLMapSnapshotter *snapshotter = snapshotterWithCoordinates(coordinates, size);
weakSnapshotter = snapshotter;
[snapshotter startWithCompletionHandler:^(MGLMapSnapshot * _Nullable snapshot, NSError * _Nullable error) {
// This completion block should not be called.
[expectation fulfill];
}];
MGLTestAssertNotNil(self, weakSnapshotter, @"Locally scoped snapshotter should not go away while in scope.");
}
MGLTestAssertNil(self, weakSnapshotter, @"Snapshotter should go away on its own if there’s no strong reference to it.");
[self waitForExpectations:@[expectation] timeout:5];
}
- (void)testDeallocatingSnapshotterDuringSnapshot🔒 {
// See also https://github.com/mapbox/mapbox-gl-native/issues/12336
@@ -93,20 +114,12 @@ MGLMapSnapshotter* snapshotterWithBounds(MGLCoordinateBounds bounds, CGSize size
dispatch_async(dispatch_get_main_queue(), ^{
MGLMapSnapshotter *snapshotter = snapshotterWithCoordinates(coord, size);
__weak MGLMapSnapshotter *weakSnapshotter = snapshotter;
[snapshotter startWithCompletionHandler:^(MGLMapSnapshot * _Nullable snapshot, NSError * _Nullable error) {
// We expect this completion block to be called with an error
// This completion block should not be called.
__typeof__(self) strongself = weakself;
MGLTestAssertNil(strongself, snapshot);
MGLTestAssert(strongself,
([error.domain isEqualToString:MGLErrorDomain] && error.code == MGLErrorCodeSnapshotFailed),
@"Should have errored");
MGLTestAssertNil(strongself, weakSnapshotter, @"Snapshotter should have been deallocated");
dispatch_group_leave(dg);
MGLTestFail(strongself, @"Completion handler should not be called after snapshotter is deallocated.");
}];
dispatch_group_leave(dg);
});
dispatch_group_notify(dg, dispatch_get_main_queue(), ^{
@@ -162,10 +175,6 @@ MGLMapSnapshotter* snapshotterWithBounds(MGLCoordinateBounds bounds, CGSize size
}
- (void)testCancellingSnapshot🔒 {
XCTestExpectation *expectation = [self expectationWithDescription:@"snapshots"];
expectation.assertForOverFulfill = YES;
expectation.expectedFulfillmentCount = 1;
CGSize size = self.mapView.bounds.size;
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(30.0, 30.0);
@@ -174,20 +183,11 @@ MGLMapSnapshotter* snapshotterWithBounds(MGLCoordinateBounds bounds, CGSize size
__weak __typeof__(self) weakself = self;
[snapshotter startWithCompletionHandler:^(MGLMapSnapshot * _Nullable snapshot, NSError * _Nullable error) {
// We expect this completion block to be called with an error
__typeof__(self) strongself = weakself;
MGLTestAssertNil(strongself, snapshot);
MGLTestAssert(strongself,
([error.domain isEqualToString:MGLErrorDomain] && error.code == MGLErrorCodeSnapshotFailed),
@"Should have been cancelled");
MGLTestAssert(strongself, snapshotter.cancelled, @"Should have been cancelled");
[expectation fulfill];
MGLTestFail(strongself, @"Completion handler should not be called when canceling.");
}];
[snapshotter cancel];
[self waitForExpectations:@[expectation] timeout:5.0];
}
- (void)testAllocatingSnapshotOnBackgroundQueue🔒 {

View File

@@ -43,6 +43,12 @@
* Fixed an issue where `-[MGLMapView visibleFeaturesInRect:]` and `-[MGLShapeSource featuresMatchingPredicate:]` could return incorrect coordinates at zoom levels 20 and higher. ([#15560](https://github.com/mapbox/mapbox-gl-native/pull/15560))
* Improved feature querying performance. ([#14930](https://github.com/mapbox/mapbox-gl-native/pull/14930))
### Snapshots
* Added an `-[MGLMapSnapshotter startWithOverlayHandler:completionHandler:]` method to provide the snapshot's current `CGContext` in order to perform custom drawing on `MGLMapSnapshot` objects. ([#15530](https://github.com/mapbox/mapbox-gl-native/pull/15530))
* The `-[MGLMapSnapshotter cancel]` method no longer calls the completion handler passed into `-[MGLMapSnapshotter startWithCompletionHandler:]`. ([#209](https://github.com/mapbox/mapbox-gl-native-ios/pull/210))
* Fixed an issue where the `MGLMapSnapshotter.loading` property always returned `NO`, even while loading a snapshot. ([#209](https://github.com/mapbox/mapbox-gl-native-ios/pull/210))
### Networking and storage
* Ideographic glyphs from Chinese, Japanese, and Korean are no longer downloaded by default as part of offline packs; they are instead rendered on-device, saving bandwidth and storage while improving performance. ([#14176](https://github.com/mapbox/mapbox-gl-native/pull/14176))
@@ -55,7 +61,6 @@
### Other changes
* Added an `-[MGLMapSnapshotter startWithOverlayHandler:completionHandler:]` method to provide the snapshot's current `CGContext` in order to perform custom drawing on `MGLMapSnapshot` objects. ([#15530](https://github.com/mapbox/mapbox-gl-native/pull/15530))
* Fixed a memory leak when zooming with any options enabled in the `MGLMapView.debugMask` property. ([#15179](https://github.com/mapbox/mapbox-gl-native/issues/15179))
* `MGLLoggingLevel` has been updated to better match core log levels. You can now use `MGLLoggingConfiguration.loggingLevel` to filter logs from core. ([#15120](https://github.com/mapbox/mapbox-gl-native/pull/15120))