diff --git a/platform/darwin/src/MLNStyle.h b/platform/darwin/src/MLNStyle.h
index ced327ca3..54757147a 100644
--- a/platform/darwin/src/MLNStyle.h
+++ b/platform/darwin/src/MLNStyle.h
@@ -70,6 +70,20 @@ MLN_EXPORT
*/
@property (readonly, copy, nullable) NSString *name;
+/**
+ * The style JSON representation of the map.
+ *
+ * Setting this property results in an asynchronous style change. If you wish to know when the style
+ * change is complete, observe the ``MLNMapViewDelegate/mapView:didFinishLoadingStyle:`` method
+ * on ``MLNMapViewDelegate``.
+ *
+ * The JSON must conform to the
+ * MapLibre Style Specification.
+ *
+ * @throws NSInvalidArgumentException if styleJSON is nil or invalid JSON
+ */
+@property (nonatomic, copy) NSString *styleJSON;
+
// MARK: Managing Sources
/**
diff --git a/platform/darwin/src/MLNStyle.mm b/platform/darwin/src/MLNStyle.mm
index 48cbe1727..3f4d211ff 100644
--- a/platform/darwin/src/MLNStyle.mm
+++ b/platform/darwin/src/MLNStyle.mm
@@ -139,6 +139,16 @@ const MLNExceptionName MLNRedundantSourceIdentifierException = @"MLNRedundantSou
return name.empty() ? nil : @(name.c_str());
}
+- (NSString *)styleJSON {
+ std::string styleJSON = self.rawStyle->getJSON();
+ // Return empty string if no style is set
+ return styleJSON.empty() ? @"" : @(styleJSON.c_str());
+}
+
+- (void)setStyleJSON:(NSString *)styleJSON {
+ self.rawStyle->loadJSON([styleJSON UTF8String]);
+}
+
// MARK: Sources
- (NSSet<__kindof MLNSource *> *)sources {
diff --git a/platform/darwin/test/MLNMapViewTests.m b/platform/darwin/test/MLNMapViewTests.m
index 06fa816fc..55d9f037d 100644
--- a/platform/darwin/test/MLNMapViewTests.m
+++ b/platform/darwin/test/MLNMapViewTests.m
@@ -9,11 +9,13 @@
#endif
static MLNMapView *mapView;
-
-@interface MLNMapViewTests : XCTestCase
+@interface MLNMapViewTests : XCTestCase
@end
-@implementation MLNMapViewTests
+@implementation MLNMapViewTests {
+ XCTestExpectation *_styleLoadingExpectation;
+ XCTestExpectation *_styleLoadErrorExpectation;
+}
- (void)setUp {
[super setUp];
@@ -21,14 +23,35 @@ static MLNMapView *mapView;
[MLNSettings setApiKey:@"pk.feedcafedeadbeefbadebede"];
NSURL *styleURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"one-liner" withExtension:@"json"];
mapView = [[MLNMapView alloc] initWithFrame:CGRectMake(0, 0, 64, 64) styleURL:styleURL];
+ mapView.delegate = self;
+ if (!mapView.style) {
+ _styleLoadingExpectation = [self expectationWithDescription:@"Map view should finish loading style."];
+ [self waitForExpectationsWithTimeout:10 handler:nil];
+ }
}
- (void)tearDown {
+ _styleLoadingExpectation = nil;
mapView = nil;
[MLNSettings setApiKey:nil];
[super tearDown];
}
+- (void)mapView:(MLNMapView *)mapView didFinishLoadingStyle:(MLNStyle *)style {
+ XCTAssertNotNil(mapView.style);
+ XCTAssertEqual(mapView.style, style);
+
+ [_styleLoadingExpectation fulfill];
+}
+
+- (void)mapViewDidFailLoadingMap:(MLNMapView *)mapView withError:(NSError *)error {
+ XCTAssertNotNil(error);
+ XCTAssertEqual(error.domain, MLNErrorDomain);
+ if (error.code == MLNErrorCodeLoadStyleFailed || error.code == MLNErrorCodeParseStyleFailed) {
+ [_styleLoadErrorExpectation fulfill];
+ }
+}
+
- (void)testCoordinateBoundsConversion {
[mapView setCenterCoordinate:CLLocationCoordinate2DMake(33, 179)];
@@ -160,5 +183,105 @@ static MLNMapView *mapView;
XCTAssertEqual(mapView.tileCacheEnabled, YES);
}
+- (void)testStyleJSONWhenMapViewInitWithStyleURL {
+ // Test getting style JSON
+ NSString *styleJSON = mapView.styleJSON;
+ XCTAssertNotNil(styleJSON, @"Style JSON should not be nil");
+ NSString * expectedJSON = @"{\"version\":8,\"sources\":{},\"layers\":[]}";
+ XCTAssertEqualObjects([self normalizeJSON:expectedJSON],
+ [self normalizeJSON:styleJSON],
+ @"Style JSON should be expected");
+
+ // Verify the JSON is valid
+ NSError *error = nil;
+ id jsonObject = [NSJSONSerialization JSONObjectWithData:[styleJSON dataUsingEncoding:NSUTF8StringEncoding]
+ options:0
+ error:&error];
+ XCTAssertNil(error, @"Style JSON should be valid JSON");
+ XCTAssertNotNil(jsonObject, @"Style JSON should parse to a valid object");
+ XCTAssertTrue([jsonObject isKindOfClass:[NSDictionary class]], @"Style JSON should represent a dictionary");
+}
+
+- (void)testMapViewInitWithStyleJSON {
+ // Test setting style JSON
+ NSString *styleJSON = @"{\"version\": 8, \"sources\": { \"mapbox\": {\"type\": \"vector\", \"tiles\": [ \"local://tiles/{z}-{x}-{y}.mvt\" ] }}, \"layers\": [], \"metadata\": { \"test\": 1, \"type\": \"template\"}}";
+ MLNMapView* mapViewWithJSON = [[MLNMapView alloc] initWithFrame:CGRectMake(0, 0, 64, 64) styleJSON:styleJSON];
+
+ // Verify the style was updated
+ NSString *loadedStyleJSON = mapViewWithJSON.styleJSON;
+ XCTAssertEqualObjects([self normalizeJSON:loadedStyleJSON],
+ [self normalizeJSON:styleJSON],
+ @"Style JSON should match what was set");
+
+ XCTAssertNotNil(mapViewWithJSON.style);
+ XCTAssertNotNil(mapViewWithJSON.style.sources);
+}
+
+- (void)testUpdateStyleJSON {
+ _styleLoadingExpectation = nil;
+ _styleLoadErrorExpectation = [self expectationWithDescription:@"Style should load error"];
+
+ // Test setting style JSON
+ NSString *newStyleJSON = @"{\"version\": 8, \"sources\": { \"mapbox\": {\"type\": \"vector\", \"tiles\": [ \"local://tiles/{z}-{x}-{y}.mvt\" ] }}, \"layers\": [], \"metadata\": { \"test\": 1, \"type\": \"template\"}}";
+ mapView.styleJSON = newStyleJSON;
+
+ // Verify the style was updated
+ NSString *updatedStyleJSON = mapView.styleJSON;
+ XCTAssertEqualObjects([self normalizeJSON:updatedStyleJSON],
+ [self normalizeJSON:newStyleJSON],
+ @"Style JSON should match what was set");
+
+ XCTAssertNotNil(mapView.style);
+ XCTAssertNotNil(mapView.style.sources);
+ // source "org.maplibre.annotations" is added by default
+ XCTAssertEqual(mapView.style.sources.count, 2UL);
+
+ // Test invalid JSON syntax
+ NSString *invalidJSON = @"{invalid json";
+ mapView.styleJSON = invalidJSON;
+ [self waitForExpectations:@[_styleLoadErrorExpectation] timeout:10];
+}
+
+- (void)testStyleJSONAfterAddLayer {
+ // Test getting style JSON
+ NSString *styleJSON = mapView.styleJSON;
+ XCTAssertNotNil(styleJSON, @"Style JSON should not be nil");
+
+ // Add a raster tile source
+ MLNVectorTileSource *vectorTileSource = [[MLNVectorTileSource alloc] initWithIdentifier:@"some-identifier" tileURLTemplates:@[] options:nil];
+ [mapView.style addSource:vectorTileSource];
+
+ // Add a layer using it
+ MLNFillStyleLayer *fillLayer = [[MLNFillStyleLayer alloc] initWithIdentifier:@"fillLayer" source:vectorTileSource];
+ [mapView.style addLayer:fillLayer];
+
+ // Style JSON should not be updated
+ NSString *updatedStyleJSON = mapView.styleJSON;
+
+ XCTAssertEqualObjects([self normalizeJSON:updatedStyleJSON],
+ [self normalizeJSON:styleJSON],
+ @"Style JSON should be updated");
+}
+
+// Helper method to normalize JSON strings for comparison
+- (NSString *)normalizeJSON:(NSString *)jsonString {
+ NSError *error = nil;
+ id jsonObject = [NSJSONSerialization JSONObjectWithData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]
+ options:0
+ error:&error];
+ if (error) {
+ return jsonString;
+ }
+
+ NSData *normalizedData = [NSJSONSerialization dataWithJSONObject:jsonObject
+ options:0
+ error:&error];
+ if (error) {
+ return jsonString;
+ }
+
+ NSString *normalizedString = [[NSString alloc] initWithData:normalizedData encoding:NSUTF8StringEncoding];
+ return normalizedString ?: jsonString;
+}
@end
diff --git a/platform/darwin/test/MLNStyleTests.mm b/platform/darwin/test/MLNStyleTests.mm
index 5c4d69fbb..9f36afeef 100644
--- a/platform/darwin/test/MLNStyleTests.mm
+++ b/platform/darwin/test/MLNStyleTests.mm
@@ -422,4 +422,19 @@
XCTAssertFalse(self.style.performsPlacementTransitions, @"Enabling placement transitions should be NO.");
}
+- (void)testStyleJSON {
+ // Test getting style JSON
+ NSString *styleJSON = self.style.styleJSON;
+ XCTAssertNotNil(styleJSON, @"Style JSON should not be nil");
+
+ // Verify the JSON is valid
+ NSError *error = nil;
+ id jsonObject = [NSJSONSerialization JSONObjectWithData:[styleJSON dataUsingEncoding:NSUTF8StringEncoding]
+ options:0
+ error:&error];
+ XCTAssertNil(error, @"Style JSON should be valid JSON");
+ XCTAssertNotNil(jsonObject, @"Style JSON should parse to a valid object");
+ XCTAssertTrue([jsonObject isKindOfClass:[NSDictionary class]], @"Style JSON should represent a dictionary");
+}
+
@end
diff --git a/platform/ios/src/MLNMapView.h b/platform/ios/src/MLNMapView.h
index beafb9fb0..89f787841 100644
--- a/platform/ios/src/MLNMapView.h
+++ b/platform/ios/src/MLNMapView.h
@@ -207,6 +207,18 @@ MLN_EXPORT
*/
- (instancetype)initWithFrame:(CGRect)frame styleURL:(nullable NSURL *)styleURL;
+/**
+ * Initializes and returns a newly allocated map view with the specified frame
+ * and style JSON.
+ *
+ * @param frame The frame for the view, measured in points.
+ * @param styleJSON JSON string of the map style to display. The JSON must conform to the
+ * MapLibre Style Specification.
+ * Specify `nil` for the default style.
+ * @return An initialized map view.
+ */
+- (instancetype)initWithFrame:(CGRect)frame styleJSON:(NSString *)styleJSON;
+
// MARK: Accessing the Delegate
/**
@@ -253,6 +265,20 @@ MLN_EXPORT
*/
@property (nonatomic, null_resettable) NSURL *styleURL;
+/**
+ * The style JSON representation of the map.
+ *
+ * Setting this property results in an asynchronous style change. If you wish to know when the style
+ * change is complete, observe the ``MLNMapViewDelegate/mapView:didFinishLoadingStyle:`` method
+ * on ``MLNMapViewDelegate``.
+ *
+ * The JSON must conform to the
+ * MapLibre Style Specification.
+ *
+ * @throws NSInvalidArgumentException if styleJSON is nil or invalid JSON
+ */
+@property (nonatomic, copy) NSString *styleJSON;
+
/**
Reloads the style.
diff --git a/platform/ios/src/MLNMapView.mm b/platform/ios/src/MLNMapView.mm
index e5329c809..da42ab8b8 100644
--- a/platform/ios/src/MLNMapView.mm
+++ b/platform/ios/src/MLNMapView.mm
@@ -424,6 +424,7 @@ public:
@property (nonatomic, copy) MLNMapCamera *residualCamera;
@property (nonatomic) MLNMapDebugMaskOptions residualDebugMask;
@property (nonatomic, copy) NSURL *residualStyleURL;
+@property (nonatomic, copy, nullable) NSString *initialStyleJSON;
/// Tilt gesture recognizer helper
@property (nonatomic, assign) CGPoint dragGestureMiddlePoint;
@@ -522,6 +523,20 @@ public:
return self;
}
+- (instancetype)initWithFrame:(CGRect)frame styleJSON:(NSString *)styleJSON
+{
+ if (self = [super initWithFrame:frame])
+ {
+ MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class]));
+ MLNLogDebug(@"Initializing frame: %@ styleJSON: %@", NSStringFromCGRect(frame), styleJSON);
+ [self commonInit];
+ self.styleJSON = styleJSON;
+ _initialStyleJSON = [styleJSON copy];
+ MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class]));
+ }
+ return self;
+}
+
- (instancetype)initWithCoder:(nonnull NSCoder *)decoder
{
if (self = [super initWithCoder:decoder])
@@ -552,6 +567,11 @@ public:
return self.residualStyleURL;
}
+ if (self.mbglMap.getStyle().getJSON().length() > 0 &&
+ self.mbglMap.getStyle().getURL().empty()) {
+ return [NSURL URLWithString:@"local://style.json"];
+ }
+
NSString *styleURLString = @(self.mbglMap.getStyle().getURL().c_str()).mgl_stringOrNilIfEmpty;
MLNAssert(styleURLString, @"Invalid style URL string %@", styleURLString);
return styleURLString ? [NSURL URLWithString:styleURLString] : nil;
@@ -569,6 +589,16 @@ public:
self.mbglMap.getStyle().loadURL([[styleURL absoluteString] UTF8String]);
}
+- (NSString *)styleJSON {
+ return self.style.styleJSON;
+}
+
+- (void)setStyleJSON:(NSString *)styleJSON {
+ // Reset style and load new JSON
+ self.style = nil;
+ self.mbglMap.getStyle().loadJSON([styleJSON UTF8String]);
+}
+
- (IBAction)reloadStyle:(__unused id)sender {
MLNLogInfo(@"Reloading style.");
NSURL *styleURL = self.styleURL;
@@ -890,7 +920,9 @@ public:
self.terminated = YES;
self.residualCamera = self.camera;
self.residualDebugMask = self.debugMask;
- self.residualStyleURL = self.styleURL;
+ if (!_initialStyleJSON) {
+ self.residualStyleURL = self.styleURL;
+ }
// Tear down C++ objects, insuring worker threads correctly terminate.
// Because of how _mbglMap is constructed, we need to destroy it first.