diff --git a/platform/darwin/src/MLNComputedShapeSource.mm b/platform/darwin/src/MLNComputedShapeSource.mm index 2b5c1990df0..a4b9e0318a5 100644 --- a/platform/darwin/src/MLNComputedShapeSource.mm +++ b/platform/darwin/src/MLNComputedShapeSource.mm @@ -144,8 +144,12 @@ - (void)main { featureCollection.push_back(geoJsonObject); } const auto geojson = mbgl::GeoJSON{featureCollection}; - if(![self isCancelled] && self.rawSource) { - self.rawSource->setTileData(mbgl::CanonicalTileID(self.z, self.x, self.y), geojson); + + // Note: potential race condition with `cancel` + if(![self isCancelled]) { + if (auto *rawSource = self.rawSource) { + rawSource->setTileData(mbgl::CanonicalTileID(self.z, self.x, self.y), geojson); + } } } } diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.mm similarity index 92% rename from platform/ios/app/MBXViewController.m rename to platform/ios/app/MBXViewController.mm index d724e248d33..fd8a80b8bfb 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.mm @@ -13,6 +13,8 @@ #import "MBXState.h" #import "MLNSettings.h" +#import "platform/ios/src/MLNMapView_Private.h" + #import "CustomStyleLayerExample.h" #if MLN_DRAWABLE_RENDERER @@ -238,10 +240,26 @@ @implementation MBXViewController BOOL _isTouringWorld; BOOL _contentInsetsEnabled; UIEdgeInsets _originalContentInsets; + + NSLayoutConstraint* _firstMapLayout; + NSArray* _secondMapLayout; + + NSDictionary* _pointFeatures; + NSLock* _loadLock; } // MARK: - Setup & Teardown +- (instancetype)init { + if (self = [super init]) { + _firstMapLayout = nil; + _secondMapLayout = nil; + _pointFeatures = nil; + _loadLock = [NSLock new]; + } + return self; +} + - (void)viewDidLoad { [super viewDidLoad]; @@ -830,22 +848,32 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath // MARK: - Debugging Actions -- (void)parseFeaturesAddingCount:(NSUInteger)featuresCount usingViews:(BOOL)useViews +- (NSDictionary*)loadPointFeatures { - [self.mapView removeAnnotations:self.mapView.annotations]; + [_loadLock lock]; + if (!_pointFeatures) { + NSData *featuresData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"points" ofType:@"geojson"]]; + if (featuresData) { + id features = [NSJSONSerialization JSONObjectWithData:featuresData + options:0 + error:nil]; + if ([features isKindOfClass:[NSDictionary class]]) + { + _pointFeatures = (NSDictionary*)features; + } + } + } + [_loadLock unlock]; + return _pointFeatures; +} +- (void)parseFeaturesAddingCount:(NSUInteger)featuresCount usingViews:(BOOL)useViews +{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ { - NSData *featuresData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"points" ofType:@"geojson"]]; - - id features = [NSJSONSerialization JSONObjectWithData:featuresData - options:0 - error:nil]; - - if ([features isKindOfClass:[NSDictionary class]]) + if (auto *features = [self loadPointFeatures]) { NSMutableArray *annotations = [NSMutableArray array]; - for (NSDictionary *feature in features[@"features"]) { CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake([feature[@"geometry"][@"coordinates"][1] doubleValue], @@ -864,6 +892,7 @@ - (void)parseFeaturesAddingCount:(NSUInteger)featuresCount usingViews:(BOOL)useV dispatch_async(dispatch_get_main_queue(), ^ { + [self.mapView removeAnnotations:self.mapView.annotations]; [self.mapView addAnnotations:annotations]; [self.mapView showAnnotations:annotations animated:YES]; }); @@ -1131,34 +1160,59 @@ - (void)styleShapeSource - (void)styleSymbolLayer { - MLNSymbolStyleLayer *stateLayer = (MLNSymbolStyleLayer *)[self.mapView.style layerWithIdentifier:@"state-label-lg"]; - stateLayer.textColor = [NSExpression expressionForConstantValue:[UIColor redColor]]; + if (auto *stateLayer = (MLNSymbolStyleLayer *)[self.mapView.style layerWithIdentifier:@"state-label-lg"]) + { + stateLayer.textColor = [NSExpression expressionForConstantValue:[UIColor redColor]]; + } } - (void)styleBuildingLayer { MLNTransition transition = { 5, 1 }; self.mapView.style.transition = transition; - MLNFillStyleLayer *buildingLayer = (MLNFillStyleLayer *)[self.mapView.style layerWithIdentifier:@"building"]; - buildingLayer.fillColor = [NSExpression expressionForConstantValue:[UIColor purpleColor]]; + if (auto *buildingLayer = (MLNFillStyleLayer *)[self.mapView.style layerWithIdentifier:@"building"]) + { + buildingLayer.fillColor = [NSExpression expressionForConstantValue:[UIColor purpleColor]]; + } } - (void)styleFerryLayer { - MLNLineStyleLayer *ferryLineLayer = (MLNLineStyleLayer *)[self.mapView.style layerWithIdentifier:@"ferry"]; - ferryLineLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor redColor]]; + if (auto *ferryLineLayer = (MLNLineStyleLayer *)[self.mapView.style layerWithIdentifier:@"ferry"]) + { + ferryLineLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor redColor]]; + } } - (void)removeParkLayer { - MLNFillStyleLayer *parkLayer = (MLNFillStyleLayer *)[self.mapView.style layerWithIdentifier:@"park"]; - [self.mapView.style removeLayer:parkLayer]; + if (auto *parkLayer = (MLNFillStyleLayer *)[self.mapView.style layerWithIdentifier:@"park"]) + { + [self.mapView.style removeLayer:parkLayer]; + } +} + +- (BOOL)loadStyleFromBundle:(NSString*)path +{ + NSString *resourcePath = [[NSBundle mainBundle] pathForResource:path ofType:@"json"]; + if (NSURL* url = resourcePath ? [NSURL fileURLWithPath:resourcePath] : nil) { + [self.mapView setStyleURL:url]; + return TRUE; + } else { + NSString *msg = [NSString stringWithFormat:@"Missing Style %@", path]; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Style File" message:msg preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleCancel handler:nil]]; + [self presentViewController:alertController animated:YES completion:nil]; + return FALSE; + } } - (void)styleFilteredFill { // set style and focus on Texas - [self.mapView setStyleURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"fill_filter_style" ofType:@"json"]]]; + if (![self loadStyleFromBundle:@"fill_filter_style"]) { + return; + } [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(31, -100) zoomLevel:3 animated:NO]; // after slight delay, fill in Texas (atypical use; we want to clearly see the change for test purposes) @@ -1178,7 +1232,9 @@ - (void)styleFilteredFill - (void)styleFilteredLines { // set style and focus on lower 48 - [self.mapView setStyleURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"line_filter_style" ofType:@"json"]]]; + if (![self loadStyleFromBundle:@"line_filter_style"]) { + return; + } [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(40, -97) zoomLevel:5 animated:NO]; // after slight delay, change styling for all Washington-named counties (atypical use; we want to clearly see the change for test purposes) @@ -1199,7 +1255,9 @@ - (void)styleFilteredLines - (void)styleNumericFilteredFills { // set style and focus on lower 48 - [self.mapView setStyleURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"numeric_filter_style" ofType:@"json"]]]; + if (![self loadStyleFromBundle:@"numeric_filter_style"]) { + return; + } [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(40, -97) zoomLevel:5 animated:NO]; // after slight delay, change styling for regions 200-299 (atypical use; we want to clearly see the change for test purposes) @@ -1458,6 +1516,12 @@ - (void)styleRasterTileSource [self.mapView.style addLayer:rasterLayer]; } +- (NSURL*)radarImageURL:(int)index +{ + return [NSURL URLWithString: + [NSString stringWithFormat:@"https://maplibre.org/maplibre-gl-js/docs/assets/radar%d.gif", index]]; +} + - (void)styleImageSource { MLNCoordinateQuad coordinateQuad = { @@ -1466,7 +1530,9 @@ - (void)styleImageSource { 37.936, -71.516 }, { 46.437, -71.516 } }; - MLNImageSource *imageSource = [[MLNImageSource alloc] initWithIdentifier:@"style-image-source-id" coordinateQuad:coordinateQuad URL:[NSURL URLWithString:@"https://maplibre.org/maplibre-gl-js-docs/assets/radar0.gif"]]; + MLNImageSource *imageSource = [[MLNImageSource alloc] initWithIdentifier:@"style-image-source-id" + coordinateQuad:coordinateQuad + URL:[self radarImageURL:0]]; [self.mapView.style addSource:imageSource]; @@ -1478,17 +1544,31 @@ - (void)styleImageSource selector:@selector(updateAnimatedImageSource:) userInfo:imageSource repeats:YES]; + + const CGFloat maximumPadding = 50; + const CGSize frameSize = self.mapView.frame.size; + const CGFloat yPadding = (frameSize.height / 5 <= maximumPadding) ? (frameSize.height / 5) : maximumPadding; + const CGFloat xPadding = (frameSize.width / 5 <= maximumPadding) ? (frameSize.width / 5) : maximumPadding; + [self.mapView setVisibleCoordinateBounds:MLNCoordinateBoundsMake(coordinateQuad.bottomLeft, coordinateQuad.topRight) + edgePadding:UIEdgeInsetsMake(yPadding, xPadding, yPadding, xPadding) + animated:YES + completionHandler:nil]; } -- (void)updateAnimatedImageSource:(NSTimer *)timer { - static int radarSuffix = 0; +- (void)updateAnimatedImageSource:(NSTimer *)timer +{ MLNImageSource *imageSource = (MLNImageSource *)timer.userInfo; - NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://maplibre.org/maplibre-gl-js-docs/assets/radar%d.gif", radarSuffix++]]; - [imageSource setValue:url forKey:@"URL"]; - if (radarSuffix > 3) { - radarSuffix = 0; + if (![self.mapView.style sourceWithIdentifier:imageSource.identifier]) { + // the source has been removed, probably by reloading the style, if we try to update + // it now, we will crash with 'This source got invalidated after the style change' + [timer invalidate]; + return; } + + static int radarSuffix = 0; + [imageSource setValue:[self radarImageURL:radarSuffix] forKey:@"URL"]; + radarSuffix = (radarSuffix + 1) % 5; } -(void)toggleStyleLabelsLanguage @@ -1518,7 +1598,6 @@ - (void)styleLineGradient NSDictionary *sourceOptions = @{ MLNShapeSourceOptionLineDistanceMetrics: @YES }; MLNShapeSource *routeSource = [[MLNShapeSource alloc] initWithIdentifier:@"style-route-source" shape:routeLine options:sourceOptions]; - [self.mapView.style addSource:routeSource]; MLNLineStyleLayer *baseRouteLayer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"style-base-route-layer" source:routeSource]; baseRouteLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor orangeColor]]; @@ -1526,7 +1605,6 @@ - (void)styleLineGradient baseRouteLayer.lineOpacity = [NSExpression expressionForConstantValue:@0.95]; baseRouteLayer.lineCap = [NSExpression expressionForConstantValue:@"round"]; baseRouteLayer.lineJoin = [NSExpression expressionForConstantValue:@"round"]; - [self.mapView.style addLayer:baseRouteLayer]; MLNLineStyleLayer *routeLayer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"style-route-layer" source:routeSource]; routeLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor whiteColor]]; @@ -1547,6 +1625,12 @@ - (void)styleLineGradient // (mgl_interpolate:withCurveType:parameters:stops:($lineProgress, 'linear', nil, %@)) NSExpression *lineGradientExpression = [NSExpression expressionWithFormat:@"mgl_interpolate:withCurveType:parameters:stops:($lineProgress, 'linear', nil, %@)", stops]; routeLayer.lineGradient = lineGradientExpression; + + [self removeLayer:baseRouteLayer.identifier]; + [self removeLayer:routeLayer.identifier]; + [self removeSource:routeSource.identifier]; + [self.mapView.style addSource:routeSource]; + [self.mapView.style addLayer:baseRouteLayer]; [self.mapView.style addLayer:routeLayer]; } @@ -1562,6 +1646,22 @@ - (void)addCustomDrawableLayer } #endif +- (void)removeSource:(NSString*)ident +{ + if (MLNSource *source = [self.mapView.style sourceWithIdentifier:ident]) + { + [self.mapView.style removeSource:source]; + } +} + +- (void)removeLayer:(NSString*)ident +{ + if (MLNStyleLayer* layer = [self.mapView.style layerWithIdentifier:ident]) + { + [self.mapView.style removeLayer:layer]; + } +} + - (void)styleRouteLine { CLLocationCoordinate2D coords[] = { @@ -1581,7 +1681,6 @@ - (void)styleRouteLine MLNPolylineFeature *routeLine = [MLNPolylineFeature polylineWithCoordinates:coords count:count]; MLNShapeSource *routeSource = [[MLNShapeSource alloc] initWithIdentifier:@"style-route-source" shape:routeLine options:nil]; - [self.mapView.style addSource:routeSource]; MLNLineStyleLayer *baseRouteLayer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"style-base-route-layer" source:routeSource]; baseRouteLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor orangeColor]]; @@ -1589,7 +1688,6 @@ - (void)styleRouteLine baseRouteLayer.lineOpacity = [NSExpression expressionForConstantValue:@0.5]; baseRouteLayer.lineCap = [NSExpression expressionForConstantValue:@"round"]; baseRouteLayer.lineJoin = [NSExpression expressionForConstantValue:@"round"]; - [self.mapView.style addLayer:baseRouteLayer]; MLNLineStyleLayer *routeLayer = [[MLNLineStyleLayer alloc] initWithIdentifier:@"style-route-layer" source:routeSource]; routeLayer.lineColor = [NSExpression expressionForConstantValue:[UIColor whiteColor]]; @@ -1597,16 +1695,24 @@ - (void)styleRouteLine routeLayer.lineOpacity = [NSExpression expressionForConstantValue:@0.8]; routeLayer.lineCap = [NSExpression expressionForConstantValue:@"round"]; routeLayer.lineJoin = [NSExpression expressionForConstantValue:@"round"]; + + [self removeLayer:baseRouteLayer.identifier]; + [self removeLayer:routeLayer.identifier]; + [self removeSource:routeSource.identifier]; + [self.mapView.style addSource:routeSource]; + [self.mapView.style addLayer:baseRouteLayer]; [self.mapView.style addLayer:routeLayer]; } - (void)styleAddCustomTriangleLayer { - CustomStyleLayerExample *layer = [[CustomStyleLayerExample alloc] initWithIdentifier:@"mbx-custom"]; - [self.mapView.style addLayer:layer]; + if (CustomStyleLayerExample *layer = [[CustomStyleLayerExample alloc] initWithIdentifier:@"mbx-custom"]) { + [self.mapView.style addLayer:layer]; + } } -- (void)stylePolygonWithDDS { +- (void)stylePolygonWithDDS +{ CLLocationCoordinate2D leftCoords[] = { {37.73081027834234, -122.49412536621094}, {37.7566013348511, -122.49412536621094}, @@ -1920,72 +2026,80 @@ - (void)toggleSecondMapView { secondMapView.showsScale = YES; secondMapView.translatesAutoresizingMaskIntoConstraints = NO; secondMapView.tag = 2; + + // Remove the main map bottom constraint for (NSLayoutConstraint *constraint in self.view.constraints) { if ((constraint.firstItem == self.mapView && constraint.firstAttribute == NSLayoutAttributeBottom) || (constraint.secondItem == self.mapView && constraint.secondAttribute == NSLayoutAttributeBottom)) { + _firstMapLayout = constraint; [self.view removeConstraint:constraint]; break; } } + + // Place the second map in the bottom half of the view + _secondMapLayout = @[ + [NSLayoutConstraint constraintWithItem:self.mapView + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterY + multiplier:1 + constant:0], + [NSLayoutConstraint constraintWithItem:secondMapView + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterX + multiplier:1 + constant:0], + [NSLayoutConstraint constraintWithItem:secondMapView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeWidth + multiplier:1 + constant:0], + [NSLayoutConstraint constraintWithItem:secondMapView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterY + multiplier:1 + constant:0], + [secondMapView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor]]; + [self.view addSubview:secondMapView]; - [self.view addConstraints:@[ - [NSLayoutConstraint constraintWithItem:self.mapView - attribute:NSLayoutAttributeBottom - relatedBy:NSLayoutRelationEqual - toItem:self.view - attribute:NSLayoutAttributeCenterY - multiplier:1 - constant:0], - [NSLayoutConstraint constraintWithItem:secondMapView - attribute:NSLayoutAttributeCenterX - relatedBy:NSLayoutRelationEqual - toItem:self.view - attribute:NSLayoutAttributeCenterX - multiplier:1 - constant:0], - [NSLayoutConstraint constraintWithItem:secondMapView - attribute:NSLayoutAttributeWidth - relatedBy:NSLayoutRelationEqual - toItem:self.view - attribute:NSLayoutAttributeWidth - multiplier:1 - constant:0], - [NSLayoutConstraint constraintWithItem:secondMapView - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:self.view - attribute:NSLayoutAttributeCenterY - multiplier:1 - constant:0], - [NSLayoutConstraint constraintWithItem:secondMapView - attribute:NSLayoutAttributeBottom - relatedBy:NSLayoutRelationEqual - toItem:self.view.safeAreaLayoutGuide.bottomAnchor - attribute:NSLayoutAttributeBottom - multiplier:1 - constant:0], - ]]; + [self.view addConstraints:_secondMapLayout]; + + secondMapView.styleURL = _mapView.styleURL; + + __weak decltype(_mapView) weakMapView = _mapView; + __weak decltype(secondMapView) weakSecondMap = secondMapView; + auto moveComplete = ^{ + weakSecondMap.camera = [_mapView cameraByTiltingToPitch:weakMapView.camera.pitch]; + }; + + // Navigate the new map to the same view as the main one + [secondMapView setCenterCoordinate:_mapView.centerCoordinate + zoomLevel:_mapView.zoomLevel + direction:_mapView.direction + animated:YES + completionHandler:moveComplete]; + + secondMapView.accessibilityIdentifier = @"Second Map"; } else { - NSMutableArray *constraintsToRemove = [NSMutableArray array]; MLNMapView *secondMapView = (MLNMapView *)[self.view viewWithTag:2]; - for (NSLayoutConstraint *constraint in self.view.constraints) - { - if (constraint.firstItem == secondMapView || constraint.secondItem == secondMapView) - { - [constraintsToRemove addObject:constraint]; - } - } - [self.view removeConstraints:constraintsToRemove]; + + // Reset the layout to the original state + [self.view removeConstraints:_secondMapLayout]; + [self.view addConstraint:_firstMapLayout]; + _firstMapLayout = nil; + _secondMapLayout = nil; + [secondMapView removeFromSuperview]; - [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.mapView - attribute:NSLayoutAttributeBottom - relatedBy:NSLayoutRelationEqual - toItem:self.view.safeAreaLayoutGuide.bottomAnchor - attribute:NSLayoutAttributeTop - multiplier:1 - constant:0]]; } } @@ -2484,8 +2598,18 @@ - (void)alertAccuracyChanges { [self presentViewController:alert animated:YES completion:nil]; } +- (BOOL)isUITesting { + NSString* value = NSProcessInfo.processInfo.environment[@"UITesting"]; + return value && [value isEqual: @"YES"]; +} + - (void)saveCurrentMapState:(__unused NSNotification *)notification { + // saved changes to the settings can break UI tests, so always start from the defaults when testing + if ([self isUITesting]) { + return; + } + // The following properties can change after the view loads so we need to save their // state before exiting the view controller. self.currentState.camera = self.mapView.camera; @@ -2502,6 +2626,11 @@ - (void)saveCurrentMapState:(__unused NSNotification *)notification { } - (void)restoreMapState:(__unused NSNotification *)notification { + + if ([self isUITesting]) { + return; + } + MBXState *currentState = [MBXStateManager sharedManager].currentState; self.mapView.camera = currentState.camera; diff --git a/platform/ios/bazel/files.bzl b/platform/ios/bazel/files.bzl index 3e88047b478..b7bb3c65c0b 100644 --- a/platform/ios/bazel/files.bzl +++ b/platform/ios/bazel/files.bzl @@ -92,7 +92,7 @@ MLN_PUBLIC_IOS_APP_SOPURCE = [ "app/MBXState.m", "app/MBXStateManager.m", "app/MBXUserLocationAnnotationView.m", - "app/MBXViewController.m", + "app/MBXViewController.mm", "app/main.m", "app/MBXAnnotationView.h", "app/MBXAppDelegate.h", diff --git a/platform/ios/iosapp-UITests/BUILD.bazel b/platform/ios/iosapp-UITests/BUILD.bazel index 23cdc4130a5..eee82f4a6f0 100644 --- a/platform/ios/iosapp-UITests/BUILD.bazel +++ b/platform/ios/iosapp-UITests/BUILD.bazel @@ -15,6 +15,7 @@ swift_library( ios_ui_test( name = "uitest", + size = "large", minimum_os_version = "12.0", provisioning_profile = "xcode_profile", test_host = "//platform/ios:App", diff --git a/platform/ios/iosapp-UITests/iosapp_UITests.swift b/platform/ios/iosapp-UITests/iosapp_UITests.swift index 50a590fbd08..46d27287201 100644 --- a/platform/ios/iosapp-UITests/iosapp_UITests.swift +++ b/platform/ios/iosapp-UITests/iosapp_UITests.swift @@ -14,6 +14,7 @@ class iosapp_UITests: XCTestCase { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. // UI tests must launch the application that they test. + app.launchEnvironment.updateValue("YES", forKey: "UITesting") app.launch() // In UI tests it is usually best to stop immediately when a failure occurs. @@ -38,7 +39,6 @@ class iosapp_UITests: XCTestCase { /// Turn on Debug tile boundaries, tile info and FPS ornaments /// Demonstrates how the Tile Boundaries look when rendered func testDebugBoundaryTiles() { - app.windows.children(matching: .other).element.children(matching: .other).element.children(matching: .other).element.doubleTap() let mapSettingsButton = app.navigationBars["MapLibre Basic"].buttons["Map settings"] @@ -65,7 +65,174 @@ class iosapp_UITests: XCTestCase { sleep(1) add(screenshot(name: "Null Island, Zoom=0")) } - + + var mapSettingsButton: XCUIElement { get { app.navigationBars["MapLibre Basic"].buttons["Map settings"] } } + + /// Open and close the secondary map view a few times to ensure that the dynamic layout adjustment doesn't crash + func testSecondMap() { + let showTimeout = 1.0 + let hideTimeout = 10.0 + let iterations = 3 + + let secondMapQuery = app.otherElements["Second Map"] + XCTAssert(!secondMapQuery.exists) + + for _ in 0.. XCUIElement? { + let item = app.staticTexts[ident] + return item.exists ? item : nil + } + + private func tapBackButton(_ label: String, timeout: TimeInterval) -> Bool { + let bar = app.navigationBars[label] + if (bar.waitForExistence(timeout: timeout)) { + let button = bar.buttons["Back"] + if (button.waitForExistence(timeout: 1)) { + button.tap() + return true + } + } + return false + } + func testRecord() { /// Use recording to get started writing UI tests. /// Use `Editor` > `Start Recording UI Test` while your cursor is in this `func` diff --git a/platform/ios/src/MLNMapView_Private.h b/platform/ios/src/MLNMapView_Private.h index de090836777..17e93358b2c 100644 --- a/platform/ios/src/MLNMapView_Private.h +++ b/platform/ios/src/MLNMapView_Private.h @@ -75,6 +75,8 @@ FOUNDATION_EXTERN MLN_EXPORT MLNExceptionName const _Nonnull MLNUnderlyingMapUna @property (nonatomic, readonly) BOOL enablePresentsWithTransaction; @property (nonatomic, assign) BOOL needsDisplayRefresh; +- (MLNMapCamera *_Nullable)cameraByTiltingToPitch:(CGFloat)pitch; + - (BOOL) _opaque; @end