@javascriptcommon/react-native-carplay 2.4.7 → 2.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ios/RNCarPlay.m CHANGED
@@ -2,10 +2,12 @@
2
2
  #import "RNCarPlayViewController.h"
3
3
  #import <React/RCTConvert.h>
4
4
  #import <React/RCTRootView.h>
5
+ #import <React/RCTImageSource.h>
5
6
 
6
7
  @implementation RNCarPlay
7
8
  {
8
9
  bool hasListeners;
10
+ NSCache<NSString *, UIImage *> *_imageCache;
9
11
  }
10
12
 
11
13
  @synthesize interfaceController;
@@ -59,6 +61,8 @@ RCT_EXPORT_MODULE();
59
61
  static dispatch_once_t onceToken;
60
62
  dispatch_once(&onceToken, ^{
61
63
  sharedInstance = [super allocWithZone:zone];
64
+ sharedInstance->_imageCache = [[NSCache alloc] init];
65
+ sharedInstance->_imageCache.countLimit = 100; // Cache up to 100 images
62
66
  });
63
67
  return sharedInstance;
64
68
  }
@@ -167,69 +171,191 @@ RCT_EXPORT_MODULE();
167
171
  return resizedImage;
168
172
  }
169
173
 
174
+ // Helper method to load image asynchronously using React Native's image loader
175
+ - (void)loadImageAsync:(id)imageSource completion:(void (^)(UIImage *image))completion {
176
+ if (imageSource == nil || [imageSource isKindOfClass:[NSNull class]]) {
177
+ if (completion) completion(nil);
178
+ return;
179
+ }
180
+
181
+ // Check if it's a dictionary with a 'uri' key (React Native image source)
182
+ if ([imageSource isKindOfClass:[NSDictionary class]]) {
183
+ NSDictionary *imageDict = (NSDictionary *)imageSource;
184
+ NSString *uri = imageDict[@"uri"];
185
+
186
+ if (uri) {
187
+ // Check cache first
188
+ UIImage *cachedImage = [_imageCache objectForKey:uri];
189
+ if (cachedImage) {
190
+ if (completion) completion(cachedImage);
191
+ return;
192
+ }
193
+
194
+ // Use React Native's image loader for proper async loading with caching
195
+ RCTImageLoader *imageLoader = [self.bridge moduleForClass:[RCTImageLoader class]];
196
+ RCTImageSource *source = [[RCTImageSource alloc] initWithURLRequest:[RCTConvert NSURLRequest:imageSource] size:CGSizeZero scale:1];
197
+
198
+ [imageLoader loadImageWithURLRequest:source.request callback:^(NSError *error, UIImage *image) {
199
+ if (image) {
200
+ [self->_imageCache setObject:image forKey:uri];
201
+ }
202
+ if (completion) completion(image);
203
+ }];
204
+ return;
205
+ }
206
+ }
207
+
208
+ // Not a URL, use synchronous loading (local assets)
209
+ UIImage *image = [RCTConvert UIImage:imageSource];
210
+ if (completion) completion(image);
211
+ }
212
+
213
+ // Helper to load multiple images asynchronously
214
+ - (void)loadImagesAsync:(NSArray *)imageSources completion:(void (^)(NSArray<UIImage *> *images))completion {
215
+ if (imageSources == nil || imageSources.count == 0) {
216
+ if (completion) completion(@[]);
217
+ return;
218
+ }
219
+
220
+ NSMutableArray<UIImage *> *results = [NSMutableArray arrayWithCapacity:imageSources.count];
221
+ for (NSUInteger i = 0; i < imageSources.count; i++) {
222
+ [results addObject:[NSNull null]]; // Placeholder
223
+ }
224
+
225
+ __block NSUInteger completedCount = 0;
226
+ NSUInteger totalCount = imageSources.count;
227
+
228
+ for (NSUInteger i = 0; i < imageSources.count; i++) {
229
+ id source = imageSources[i];
230
+ [self loadImageAsync:source completion:^(UIImage *image) {
231
+ if (image) {
232
+ results[i] = image;
233
+ }
234
+ completedCount++;
235
+ if (completedCount == totalCount) {
236
+ // Remove NSNull placeholders
237
+ NSMutableArray<UIImage *> *finalResults = [NSMutableArray array];
238
+ for (id obj in results) {
239
+ if ([obj isKindOfClass:[UIImage class]]) {
240
+ [finalResults addObject:obj];
241
+ }
242
+ }
243
+ if (completion) completion(finalResults);
244
+ }
245
+ }];
246
+ }
247
+ }
248
+
249
+ // Helper to load images for NowPlayingButtons - maintains order, returns array with images or NSNull for failed loads
250
+ - (void)loadImagesForNowPlayingButtons:(NSArray *)imageSources completion:(void (^)(NSArray *images))completion {
251
+ if (imageSources == nil || imageSources.count == 0) {
252
+ if (completion) completion(@[]);
253
+ return;
254
+ }
255
+
256
+ NSMutableArray *results = [NSMutableArray arrayWithCapacity:imageSources.count];
257
+ for (NSUInteger i = 0; i < imageSources.count; i++) {
258
+ [results addObject:[NSNull null]]; // Placeholder
259
+ }
260
+
261
+ __block NSUInteger completedCount = 0;
262
+ NSUInteger totalCount = imageSources.count;
263
+
264
+ for (NSUInteger i = 0; i < imageSources.count; i++) {
265
+ id source = imageSources[i];
266
+ if ([source isKindOfClass:[NSNull class]]) {
267
+ completedCount++;
268
+ if (completedCount == totalCount && completion) {
269
+ completion([results copy]);
270
+ }
271
+ continue;
272
+ }
273
+
274
+ [self loadImageAsync:source completion:^(UIImage *image) {
275
+ if (image) {
276
+ results[i] = image;
277
+ }
278
+ // NSNull remains for failed loads
279
+ completedCount++;
280
+ if (completedCount == totalCount && completion) {
281
+ completion([results copy]);
282
+ }
283
+ }];
284
+ }
285
+ }
286
+
170
287
  - (void)updateItemImageWithURL:(CPListItem *)item imgUrl:(NSString *)imgUrlString placeholderImage:(UIImage *)placeholderImage {
288
+ // Check cache first
289
+ UIImage *cachedImage = [_imageCache objectForKey:imgUrlString];
290
+ if (cachedImage) {
291
+ [item setImage:cachedImage];
292
+ return;
293
+ }
294
+
171
295
  if (placeholderImage != nil) {
172
- dispatch_async(dispatch_get_main_queue(), ^{
173
- [item setImage:placeholderImage];
174
- });
296
+ [item setImage:placeholderImage];
175
297
  }
176
298
 
177
- NSURL *imgUrl = [NSURL URLWithString:imgUrlString];
299
+ // Use React Native's image loader
300
+ RCTImageLoader *imageLoader = [self.bridge moduleForClass:[RCTImageLoader class]];
301
+ NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:imgUrlString]];
178
302
 
179
- NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:imgUrl completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
180
- if (data) {
181
- UIImage *image = [UIImage imageWithData:data];
182
- dispatch_async(dispatch_get_main_queue(), ^{
183
- [item setImage:image];
184
- });
185
- } else {
186
- NSLog(@"Failed to load image from URL: %@", imgUrl);
303
+ [imageLoader loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) {
304
+ if (image) {
305
+ [self->_imageCache setObject:image forKey:imgUrlString];
306
+ [item setImage:image];
307
+ } else if (error) {
308
+ NSLog(@"Failed to load image from URL: %@ - %@", imgUrlString, error);
187
309
  }
188
310
  }];
189
- [task resume];
190
311
  }
191
312
 
192
313
  - (void)updateListRowItemImageWithURL:(CPListImageRowItem *)item imgUrl:(NSString *)imgUrlString index:(int)index placeholderImage:(UIImage *)placeholderImage {
314
+ // Check cache first
315
+ UIImage *cachedImage = [_imageCache objectForKey:imgUrlString];
316
+ if (cachedImage) {
317
+ NSMutableArray* newImages = [item.gridImages mutableCopy];
318
+ @try {
319
+ newImages[index] = cachedImage;
320
+ [item updateImages:newImages];
321
+ }
322
+ @catch (NSException *exception) {
323
+ NSLog(@"Failed to update images array of CPListImageRowItem");
324
+ }
325
+ return;
326
+ }
327
+
193
328
  if (placeholderImage != nil) {
194
- dispatch_async(dispatch_get_main_queue(), ^{
329
+ NSMutableArray* newImages = [item.gridImages mutableCopy];
330
+ @try {
331
+ newImages[index] = placeholderImage;
332
+ [item updateImages:newImages];
333
+ }
334
+ @catch (NSException *exception) {
335
+ NSLog(@"Failed to update images array of CPListImageRowItem");
336
+ }
337
+ }
338
+
339
+ // Use React Native's image loader
340
+ RCTImageLoader *imageLoader = [self.bridge moduleForClass:[RCTImageLoader class]];
341
+ NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:imgUrlString]];
342
+
343
+ [imageLoader loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) {
344
+ if (image) {
345
+ [self->_imageCache setObject:image forKey:imgUrlString];
346
+
195
347
  NSMutableArray* newImages = [item.gridImages mutableCopy];
196
-
197
348
  @try {
198
- newImages[index] = placeholderImage;
349
+ newImages[index] = image;
350
+ [item updateImages:newImages];
199
351
  }
200
352
  @catch (NSException *exception) {
201
- // Best effort updating the array
202
353
  NSLog(@"Failed to update images array of CPListImageRowItem");
203
354
  }
204
-
205
- [item updateImages:newImages];
206
- });
207
- }
208
-
209
- NSURL *imgUrl = [NSURL URLWithString:imgUrlString];
210
-
211
- NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:imgUrl completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
212
- if (data) {
213
- UIImage *image = [UIImage imageWithData:data];
214
-
215
- dispatch_async(dispatch_get_main_queue(), ^{
216
- NSMutableArray* newImages = [item.gridImages mutableCopy];
217
-
218
- @try {
219
- newImages[index] = image;
220
- }
221
- @catch (NSException *exception) {
222
- // Best effort updating the array
223
- NSLog(@"Failed to update images array of CPListImageRowItem");
224
- }
225
-
226
- [item updateImages:newImages];
227
- });
228
- } else {
229
- NSLog(@"Failed to load image for CPListImageRowItem from URL: %@", imgUrl);
355
+ } else if (error) {
356
+ NSLog(@"Failed to load image for CPListImageRowItem from URL: %@ - %@", imgUrlString, error);
230
357
  }
231
358
  }];
232
- [task resume];
233
359
  }
234
360
 
235
361
  RCT_EXPORT_METHOD(checkForConnection) {
@@ -325,44 +451,80 @@ RCT_EXPORT_METHOD(createTemplate:(NSString *)templateId config:(NSDictionary*)co
325
451
  [nowPlayingTemplate setAlbumArtistButtonEnabled:[RCTConvert BOOL:config[@"albumArtistButtonEnabled"]]];
326
452
  [nowPlayingTemplate setUpNextTitle:[RCTConvert NSString:config[@"upNextButtonTitle"]]];
327
453
  [nowPlayingTemplate setUpNextButtonEnabled:[RCTConvert BOOL:config[@"upNextButtonEnabled"]]];
328
- NSMutableArray<CPNowPlayingButton *> *buttons = [NSMutableArray new];
454
+
329
455
  NSArray<NSDictionary*> *_buttons = [RCTConvert NSDictionaryArray:config[@"buttons"]];
330
-
331
- NSDictionary *buttonTypeMapping = @{
332
- @"shuffle": CPNowPlayingShuffleButton.class,
333
- @"add-to-library": CPNowPlayingAddToLibraryButton.class,
334
- @"more": CPNowPlayingMoreButton.class,
335
- @"playback": CPNowPlayingPlaybackRateButton.class,
336
- @"repeat": CPNowPlayingRepeatButton.class,
337
- @"image": CPNowPlayingImageButton.class
338
- };
339
-
340
- for (NSDictionary *_button in _buttons) {
456
+
457
+ // Collect all image sources for image-type buttons
458
+ NSMutableArray *imageSources = [NSMutableArray new];
459
+ for (NSUInteger i = 0; i < _buttons.count; i++) {
460
+ NSDictionary *_button = _buttons[i];
341
461
  NSString *buttonType = [RCTConvert NSString:_button[@"type"]];
342
- NSDictionary *body = @{@"templateId":templateId, @"id": _button[@"id"] };
343
- Class buttonClass = buttonTypeMapping[buttonType];
344
- if (buttonClass) {
345
- CPNowPlayingButton *button;
346
-
347
- if ([buttonType isEqualToString:@"image"]) {
348
- UIImage *_image = [RCTConvert UIImage:[_button objectForKey:@"image"]];
349
- button = [[CPNowPlayingImageButton alloc] initWithImage:_image handler:^(__kindof CPNowPlayingImageButton * _Nonnull) {
350
- if (self->hasListeners) {
351
- [self sendEventWithName:@"buttonPressed" body:body];
352
- }
353
- }];
354
- } else {
355
- button = [[buttonClass alloc] initWithHandler:^(__kindof CPNowPlayingButton * _Nonnull) {
356
- if (self->hasListeners) {
357
- [self sendEventWithName:@"buttonPressed" body:body];
462
+ if ([buttonType isEqualToString:@"image"]) {
463
+ id imageSource = [_button objectForKey:@"image"];
464
+ [imageSources addObject:imageSource ?: [NSNull null]];
465
+ }
466
+ }
467
+
468
+ // Pre-load all images, then create buttons
469
+ [self loadImagesForNowPlayingButtons:imageSources completion:^(NSArray *loadedImages) {
470
+ NSMutableArray<CPNowPlayingButton *> *buttons = [NSMutableArray new];
471
+
472
+ NSDictionary *buttonTypeMapping = @{
473
+ @"shuffle": CPNowPlayingShuffleButton.class,
474
+ @"add-to-library": CPNowPlayingAddToLibraryButton.class,
475
+ @"more": CPNowPlayingMoreButton.class,
476
+ @"playback": CPNowPlayingPlaybackRateButton.class,
477
+ @"repeat": CPNowPlayingRepeatButton.class,
478
+ @"image": CPNowPlayingImageButton.class
479
+ };
480
+
481
+ NSUInteger imageIndex = 0;
482
+ for (NSUInteger i = 0; i < _buttons.count; i++) {
483
+ NSDictionary *_button = _buttons[i];
484
+ NSString *buttonType = [RCTConvert NSString:_button[@"type"]];
485
+ NSDictionary *body = @{@"templateId":templateId, @"id": _button[@"id"] ?: @"" };
486
+ Class buttonClass = buttonTypeMapping[buttonType];
487
+
488
+ if (buttonClass) {
489
+ CPNowPlayingButton *button;
490
+
491
+ if ([buttonType isEqualToString:@"image"]) {
492
+ id imageObj = (imageIndex < loadedImages.count) ? loadedImages[imageIndex] : nil;
493
+ UIImage *image = ([imageObj isKindOfClass:[UIImage class]]) ? (UIImage *)imageObj : nil;
494
+ imageIndex++;
495
+
496
+ if (image) {
497
+ button = [[CPNowPlayingImageButton alloc] initWithImage:image handler:^(__kindof CPNowPlayingImageButton * _Nonnull) {
498
+ if (self->hasListeners) {
499
+ [self sendEventWithName:@"buttonPressed" body:body];
500
+ }
501
+ }];
502
+ } else {
503
+ // Fallback: create a minimal placeholder if image failed to load
504
+ UIImage *placeholder = [UIImage systemImageNamed:@"circle"];
505
+ button = [[CPNowPlayingImageButton alloc] initWithImage:placeholder handler:^(__kindof CPNowPlayingImageButton * _Nonnull) {
506
+ if (self->hasListeners) {
507
+ [self sendEventWithName:@"buttonPressed" body:body];
508
+ }
509
+ }];
358
510
  }
359
- }];
511
+ } else {
512
+ button = [[buttonClass alloc] initWithHandler:^(__kindof CPNowPlayingButton * _Nonnull) {
513
+ if (self->hasListeners) {
514
+ [self sendEventWithName:@"buttonPressed" body:body];
515
+ }
516
+ }];
517
+ }
518
+
519
+ [buttons addObject:button];
360
520
  }
361
-
362
- [buttons addObject:button];
363
521
  }
364
- }
365
- [nowPlayingTemplate updateNowPlayingButtons:buttons];
522
+
523
+ dispatch_async(dispatch_get_main_queue(), ^{
524
+ [nowPlayingTemplate updateNowPlayingButtons:buttons];
525
+ });
526
+ }];
527
+
366
528
  carPlayTemplate = nowPlayingTemplate;
367
529
  } else if ([type isEqualToString:@"tabbar"]) {
368
530
  CPTabBarTemplate *tabBarTemplate = [[CPTabBarTemplate alloc] initWithTemplates:[self parseTemplatesFrom:config]];
@@ -370,7 +532,8 @@ RCT_EXPORT_METHOD(createTemplate:(NSString *)templateId config:(NSDictionary*)co
370
532
  carPlayTemplate = tabBarTemplate;
371
533
  } else if ([type isEqualToString:@"contact"]) {
372
534
  NSString *nm = [RCTConvert NSString:config[@"name"]];
373
- UIImage *img = [RCTConvert UIImage:config[@"image"]];
535
+ id imageSource = config[@"image"];
536
+ UIImage *img = [self isImageSourceURL:imageSource] ? nil : [RCTConvert UIImage:imageSource];
374
537
  CPContact *contact = [[CPContact alloc] initWithName:nm image:img];
375
538
  [contact setSubtitle:config[@"subtitle"]];
376
539
  [contact setActions:[self parseButtons:config[@"actions"] templateId:templateId]];
@@ -452,7 +615,13 @@ RCT_EXPORT_METHOD(createTemplate:(NSString *)templateId config:(NSDictionary*)co
452
615
  carPlayTemplate.tabImage = [UIImage systemImageNamed:[RCTConvert NSString:config[@"tabSystemImageName"]]];
453
616
  }
454
617
  if (config[@"tabImage"]) {
455
- carPlayTemplate.tabImage = [RCTConvert UIImage:config[@"tabImage"]];
618
+ id tabImageSource = config[@"tabImage"];
619
+ if ([self isImageSourceURL:tabImageSource]) {
620
+ // Tab images with URLs need to be loaded async, but tabImage setter doesn't update later
621
+ // For now, skip URL-based tab images
622
+ } else {
623
+ carPlayTemplate.tabImage = [RCTConvert UIImage:tabImageSource];
624
+ }
456
625
  }
457
626
  if (config[@"tabTitle"]) {
458
627
  carPlayTemplate.tabTitle = [RCTConvert NSString:config[@"tabTitle"]];
@@ -569,11 +738,12 @@ RCT_EXPORT_METHOD(setRootTemplate:(NSString *)templateId animated:(BOOL)animated
569
738
 
570
739
  if (template) {
571
740
  [store.interfaceController setRootTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
572
- NSLog(@"error %@", err);
573
- // noop
741
+ if (err) {
742
+ NSLog(@"Set root template error: %@", err);
743
+ }
574
744
  }];
575
745
  } else {
576
- NSLog(@"Failed to find template %@", template);
746
+ NSLog(@"Failed to find template %@", templateId);
577
747
  }
578
748
  }
579
749
 
@@ -581,12 +751,19 @@ RCT_EXPORT_METHOD(pushTemplate:(NSString *)templateId animated:(BOOL)animated) {
581
751
  RNCPStore *store = [RNCPStore sharedManager];
582
752
  CPTemplate *template = [store findTemplateById:templateId];
583
753
  if (template) {
754
+ // Check if template is already on the navigation stack
755
+ NSArray *currentTemplates = store.interfaceController.templates;
756
+ if ([currentTemplates containsObject:template]) {
757
+ NSLog(@"Template %@ is already on the navigation stack, skipping push", templateId);
758
+ return;
759
+ }
584
760
  [store.interfaceController pushTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
585
- NSLog(@"error %@", err);
586
- // noop
761
+ if (err) {
762
+ NSLog(@"Push template error: %@", err);
763
+ }
587
764
  }];
588
765
  } else {
589
- NSLog(@"Failed to find template %@", template);
766
+ NSLog(@"Failed to find template %@", templateId);
590
767
  }
591
768
  }
592
769
 
@@ -595,27 +772,30 @@ RCT_EXPORT_METHOD(popToTemplate:(NSString *)templateId animated:(BOOL)animated)
595
772
  CPTemplate *template = [store findTemplateById:templateId];
596
773
  if (template) {
597
774
  [store.interfaceController popToTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
598
- NSLog(@"error %@", err);
599
- // noop
775
+ if (err) {
776
+ NSLog(@"Pop to template error: %@", err);
777
+ }
600
778
  }];
601
779
  } else {
602
- NSLog(@"Failed to find template %@", template);
780
+ NSLog(@"Failed to find template %@", templateId);
603
781
  }
604
782
  }
605
783
 
606
784
  RCT_EXPORT_METHOD(popToRootTemplate:(BOOL)animated) {
607
785
  RNCPStore *store = [RNCPStore sharedManager];
608
786
  [store.interfaceController popToRootTemplateAnimated:animated completion:^(BOOL done, NSError * _Nullable err) {
609
- NSLog(@"error %@", err);
610
- // noop
787
+ if (err) {
788
+ NSLog(@"Pop to root template error: %@", err);
789
+ }
611
790
  }];
612
791
  }
613
792
 
614
793
  RCT_EXPORT_METHOD(popTemplate:(BOOL)animated) {
615
794
  RNCPStore *store = [RNCPStore sharedManager];
616
795
  [store.interfaceController popTemplateAnimated:animated completion:^(BOOL done, NSError * _Nullable err) {
617
- NSLog(@"error %@", err);
618
- // noop
796
+ if (err) {
797
+ NSLog(@"Pop template error: %@", err);
798
+ }
619
799
  }];
620
800
  }
621
801
 
@@ -623,12 +803,18 @@ RCT_EXPORT_METHOD(presentTemplate:(NSString *)templateId animated:(BOOL)animated
623
803
  RNCPStore *store = [RNCPStore sharedManager];
624
804
  CPTemplate *template = [store findTemplateById:templateId];
625
805
  if (template) {
806
+ // Check if template is already presented
807
+ if (store.interfaceController.presentedTemplate == template) {
808
+ NSLog(@"Template %@ is already presented, skipping", templateId);
809
+ return;
810
+ }
626
811
  [store.interfaceController presentTemplate:template animated:animated completion:^(BOOL done, NSError * _Nullable err) {
627
- NSLog(@"error %@", err);
628
- // noop
812
+ if (err) {
813
+ NSLog(@"Present template error: %@", err);
814
+ }
629
815
  }];
630
816
  } else {
631
- NSLog(@"Failed to find template %@", template);
817
+ NSLog(@"Failed to find template %@", templateId);
632
818
  }
633
819
  }
634
820
 
@@ -637,6 +823,152 @@ RCT_EXPORT_METHOD(dismissTemplate:(BOOL)animated) {
637
823
  [store.interfaceController dismissTemplateAnimated:animated];
638
824
  }
639
825
 
826
+ RCT_EXPORT_METHOD(updateTemplate:(NSString *)templateId config:(NSDictionary*)config) {
827
+ RNCPStore *store = [RNCPStore sharedManager];
828
+ CPTemplate *template = [store findTemplateById:templateId];
829
+ NSString *type = [RCTConvert NSString:config[@"type"]];
830
+
831
+ if (!template) {
832
+ NSLog(@"Failed to find template %@", templateId);
833
+ return;
834
+ }
835
+
836
+ // Route to specific update methods based on template type
837
+ if ([template isKindOfClass:[CPListTemplate class]]) {
838
+ CPListTemplate *listTemplate = (CPListTemplate *)template;
839
+ if (config[@"leadingNavigationBarButtons"]) {
840
+ NSArray *leadingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"leadingNavigationBarButtons"]] templateId:templateId];
841
+ [listTemplate setLeadingNavigationBarButtons:leadingNavigationBarButtons];
842
+ }
843
+ if (config[@"trailingNavigationBarButtons"]) {
844
+ NSArray *trailingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"trailingNavigationBarButtons"]] templateId:templateId];
845
+ [listTemplate setTrailingNavigationBarButtons:trailingNavigationBarButtons];
846
+ }
847
+ if (config[@"emptyViewTitleVariants"]) {
848
+ listTemplate.emptyViewTitleVariants = [RCTConvert NSArray:config[@"emptyViewTitleVariants"]];
849
+ }
850
+ if (config[@"emptyViewSubtitleVariants"]) {
851
+ listTemplate.emptyViewSubtitleVariants = [RCTConvert NSArray:config[@"emptyViewSubtitleVariants"]];
852
+ }
853
+ if (config[@"sections"]) {
854
+ [listTemplate updateSections:[self parseSections:[RCTConvert NSArray:config[@"sections"]] templateId:templateId]];
855
+ }
856
+ } else if ([template isKindOfClass:[CPGridTemplate class]]) {
857
+ CPGridTemplate *gridTemplate = (CPGridTemplate *)template;
858
+ if (config[@"leadingNavigationBarButtons"]) {
859
+ NSArray *leadingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"leadingNavigationBarButtons"]] templateId:templateId];
860
+ [gridTemplate setLeadingNavigationBarButtons:leadingNavigationBarButtons];
861
+ }
862
+ if (config[@"trailingNavigationBarButtons"]) {
863
+ NSArray *trailingNavigationBarButtons = [self parseBarButtons:[RCTConvert NSArray:config[@"trailingNavigationBarButtons"]] templateId:templateId];
864
+ [gridTemplate setTrailingNavigationBarButtons:trailingNavigationBarButtons];
865
+ }
866
+ if (config[@"buttons"]) {
867
+ NSArray *buttons = [self parseGridButtons:[RCTConvert NSArray:config[@"buttons"]] templateId:templateId];
868
+ [gridTemplate updateGridButtons:buttons];
869
+ }
870
+ } else if ([template isKindOfClass:[CPNowPlayingTemplate class]]) {
871
+ CPNowPlayingTemplate *nowPlayingTemplate = (CPNowPlayingTemplate *)template;
872
+ if (config[@"albumArtistButtonEnabled"]) {
873
+ [nowPlayingTemplate setAlbumArtistButtonEnabled:[RCTConvert BOOL:config[@"albumArtistButtonEnabled"]]];
874
+ }
875
+ if (config[@"upNextButtonTitle"]) {
876
+ [nowPlayingTemplate setUpNextTitle:[RCTConvert NSString:config[@"upNextButtonTitle"]]];
877
+ }
878
+ if (config[@"upNextButtonEnabled"]) {
879
+ [nowPlayingTemplate setUpNextButtonEnabled:[RCTConvert BOOL:config[@"upNextButtonEnabled"]]];
880
+ }
881
+ if (config[@"buttons"]) {
882
+ NSArray<NSDictionary*> *_buttons = [RCTConvert NSDictionaryArray:config[@"buttons"]];
883
+
884
+ // Collect all image sources for image-type buttons
885
+ NSMutableArray *imageSources = [NSMutableArray new];
886
+ NSMutableArray *imageIndices = [NSMutableArray new]; // Track which buttons need images
887
+
888
+ for (NSUInteger i = 0; i < _buttons.count; i++) {
889
+ NSDictionary *_button = _buttons[i];
890
+ NSString *buttonType = [RCTConvert NSString:_button[@"type"]];
891
+ if ([buttonType isEqualToString:@"image"]) {
892
+ id imageSource = [_button objectForKey:@"image"];
893
+ [imageSources addObject:imageSource ?: [NSNull null]];
894
+ [imageIndices addObject:@(i)];
895
+ }
896
+ }
897
+
898
+ // Pre-load all images, then create buttons
899
+ [self loadImagesForNowPlayingButtons:imageSources completion:^(NSArray *loadedImages) {
900
+ NSMutableArray<CPNowPlayingButton *> *buttons = [NSMutableArray new];
901
+
902
+ NSDictionary *buttonTypeMapping = @{
903
+ @"shuffle": CPNowPlayingShuffleButton.class,
904
+ @"add-to-library": CPNowPlayingAddToLibraryButton.class,
905
+ @"more": CPNowPlayingMoreButton.class,
906
+ @"playback": CPNowPlayingPlaybackRateButton.class,
907
+ @"repeat": CPNowPlayingRepeatButton.class,
908
+ @"image": CPNowPlayingImageButton.class
909
+ };
910
+
911
+ NSUInteger imageIndex = 0;
912
+ for (NSUInteger i = 0; i < _buttons.count; i++) {
913
+ NSDictionary *_button = _buttons[i];
914
+ NSString *buttonType = [RCTConvert NSString:_button[@"type"]];
915
+ NSDictionary *body = @{@"templateId":templateId, @"id": _button[@"id"] ?: @"" };
916
+ Class buttonClass = buttonTypeMapping[buttonType];
917
+
918
+ if (buttonClass) {
919
+ CPNowPlayingButton *button;
920
+
921
+ if ([buttonType isEqualToString:@"image"]) {
922
+ id imageObj = (imageIndex < loadedImages.count) ? loadedImages[imageIndex] : nil;
923
+ UIImage *image = ([imageObj isKindOfClass:[UIImage class]]) ? (UIImage *)imageObj : nil;
924
+ imageIndex++;
925
+
926
+ if (image) {
927
+ button = [[CPNowPlayingImageButton alloc] initWithImage:image handler:^(__kindof CPNowPlayingImageButton * _Nonnull btn) {
928
+ if (self->hasListeners) {
929
+ [self sendEventWithName:@"buttonPressed" body:body];
930
+ }
931
+ }];
932
+ } else {
933
+ // Fallback: create a minimal placeholder if image failed to load
934
+ UIImage *placeholder = [UIImage systemImageNamed:@"circle"];
935
+ button = [[CPNowPlayingImageButton alloc] initWithImage:placeholder handler:^(__kindof CPNowPlayingImageButton * _Nonnull btn) {
936
+ if (self->hasListeners) {
937
+ [self sendEventWithName:@"buttonPressed" body:body];
938
+ }
939
+ }];
940
+ }
941
+ } else {
942
+ button = [[buttonClass alloc] initWithHandler:^(__kindof CPNowPlayingButton * _Nonnull btn) {
943
+ if (self->hasListeners) {
944
+ [self sendEventWithName:@"buttonPressed" body:body];
945
+ }
946
+ }];
947
+ }
948
+
949
+ [buttons addObject:button];
950
+ }
951
+ }
952
+
953
+ dispatch_async(dispatch_get_main_queue(), ^{
954
+ [nowPlayingTemplate updateNowPlayingButtons:buttons];
955
+ });
956
+ }];
957
+ }
958
+ } else if ([template isKindOfClass:[CPMapTemplate class]]) {
959
+ CPMapTemplate *mapTemplate = (CPMapTemplate *)template;
960
+ [self applyConfigForMapTemplate:mapTemplate templateId:templateId config:config];
961
+ } else if ([template isKindOfClass:[CPInformationTemplate class]]) {
962
+ CPInformationTemplate *informationTemplate = (CPInformationTemplate *)template;
963
+ if (config[@"items"]) {
964
+ informationTemplate.items = [self parseInformationItems:[RCTConvert NSArray:config[@"items"]]];
965
+ }
966
+ if (config[@"actions"]) {
967
+ informationTemplate.actions = [self parseInformationActions:[RCTConvert NSArray:config[@"actions"]] templateId:templateId];
968
+ }
969
+ }
970
+ }
971
+
640
972
  RCT_EXPORT_METHOD(updateListTemplate:(NSString*)templateId config:(NSDictionary*)config) {
641
973
  RNCPStore *store = [RNCPStore sharedManager];
642
974
  CPTemplate *template = [store findTemplateById:templateId];
@@ -702,14 +1034,21 @@ RCT_EXPORT_METHOD(updateListTemplateItem:(NSString *)templateId config:(NSDictio
702
1034
  CPListItem *item = (CPListItem *)section.items[index];
703
1035
  if (config[@"imgUrl"]) {
704
1036
  NSString *imgUrlString = [RCTConvert NSString:config[@"imgUrl"]];
705
- UIImage *placeholderImage;
706
- if (config[@"placeholderImage"] != nil) {
707
- placeholderImage = [RCTConvert UIImage:config[@"placeholderImage"]];
1037
+ UIImage *placeholderImage = nil;
1038
+ id placeholderSource = config[@"placeholderImage"];
1039
+ if (placeholderSource != nil && ![self isImageSourceURL:placeholderSource]) {
1040
+ placeholderImage = [RCTConvert UIImage:placeholderSource];
708
1041
  }
709
1042
  [self updateItemImageWithURL:item imgUrl:imgUrlString placeholderImage:placeholderImage];
710
1043
  }
711
1044
  if (config[@"image"]) {
712
- [item setImage:[RCTConvert UIImage:config[@"image"]]];
1045
+ id imageSource = config[@"image"];
1046
+ if ([self isImageSourceURL:imageSource]) {
1047
+ NSString *imgUrlString = [self getURLStringFromImageSource:imageSource];
1048
+ [self updateItemImageWithURL:item imgUrl:imgUrlString placeholderImage:nil];
1049
+ } else {
1050
+ [item setImage:[RCTConvert UIImage:imageSource]];
1051
+ }
713
1052
  }
714
1053
  if (config[@"text"]) {
715
1054
  [item setText:[RCTConvert NSString:config[@"text"]]];
@@ -724,7 +1063,16 @@ RCT_EXPORT_METHOD(updateListTemplateItem:(NSString *)templateId config:(NSDictio
724
1063
  [item setPlaybackProgress:[RCTConvert CGFloat:config[@"playbackProgress"]]];
725
1064
  }
726
1065
  if (@available(iOS 14.0, *) && config[@"accessoryImage"]) {
727
- [item setAccessoryImage:[RCTConvert UIImage:config[@"accessoryImage"]]];
1066
+ id accessorySource = config[@"accessoryImage"];
1067
+ if ([self isImageSourceURL:accessorySource]) {
1068
+ [self loadImageAsync:accessorySource completion:^(UIImage *image) {
1069
+ if (image) {
1070
+ [item setAccessoryImage:image];
1071
+ }
1072
+ }];
1073
+ } else {
1074
+ [item setAccessoryImage:[RCTConvert UIImage:accessorySource]];
1075
+ }
728
1076
  }
729
1077
  } else {
730
1078
  NSLog(@"Failed to find template %@", template);
@@ -1086,8 +1434,18 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
1086
1434
  NSString *_title = [barButton objectForKey:@"title"];
1087
1435
  [_barButton setTitle:_title];
1088
1436
  } else if (_type == CPBarButtonTypeImage) {
1089
- UIImage *_image = [RCTConvert UIImage:[barButton objectForKey:@"image"]];
1090
- [_barButton setImage:_image];
1437
+ id imageSource = [barButton objectForKey:@"image"];
1438
+ if ([self isImageSourceURL:imageSource]) {
1439
+ // Load image asynchronously
1440
+ [self loadImageAsync:imageSource completion:^(UIImage *image) {
1441
+ if (image) {
1442
+ [_barButton setImage:image];
1443
+ }
1444
+ }];
1445
+ } else {
1446
+ UIImage *_image = [RCTConvert UIImage:imageSource];
1447
+ [_barButton setImage:_image];
1448
+ }
1091
1449
  }
1092
1450
  [result addObject:_barButton];
1093
1451
  }
@@ -1110,6 +1468,27 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
1110
1468
  return result;
1111
1469
  }
1112
1470
 
1471
+ // Helper to check if image source is a URL
1472
+ - (BOOL)isImageSourceURL:(id)imageSource {
1473
+ if ([imageSource isKindOfClass:[NSDictionary class]]) {
1474
+ NSDictionary *imageDict = (NSDictionary *)imageSource;
1475
+ NSString *uri = imageDict[@"uri"];
1476
+ if (uri && ([uri hasPrefix:@"http://"] || [uri hasPrefix:@"https://"])) {
1477
+ return YES;
1478
+ }
1479
+ }
1480
+ return NO;
1481
+ }
1482
+
1483
+ // Helper to get URL string from image source
1484
+ - (NSString *)getURLStringFromImageSource:(id)imageSource {
1485
+ if ([imageSource isKindOfClass:[NSDictionary class]]) {
1486
+ NSDictionary *imageDict = (NSDictionary *)imageSource;
1487
+ return imageDict[@"uri"];
1488
+ }
1489
+ return nil;
1490
+ }
1491
+
1113
1492
  - (NSArray<CPSelectableListItem>*)parseListItems:(NSArray*)items startIndex:(int)startIndex templateId:(NSString *)templateId {
1114
1493
  NSMutableArray *_items = [NSMutableArray array];
1115
1494
  int listIndex = startIndex;
@@ -1118,12 +1497,14 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
1118
1497
  NSString *_detailText = [item objectForKey:@"detailText"];
1119
1498
  NSString *_text = [item objectForKey:@"text"];
1120
1499
  NSObject *_imageObj = [item objectForKey:@"image"];
1121
-
1500
+
1122
1501
  NSArray *_imageItems = [item objectForKey:@"images"];
1123
1502
  NSArray *_imageUrls = [item objectForKey:@"imgUrls"];
1124
-
1503
+
1125
1504
  if (_imageItems == nil && _imageUrls == nil) {
1126
- UIImage *_image = [RCTConvert UIImage:_imageObj];
1505
+ // Check if image is a URL - if so, use async loading
1506
+ BOOL isURL = [self isImageSourceURL:_imageObj];
1507
+ UIImage *_image = isURL ? nil : [RCTConvert UIImage:_imageObj];
1127
1508
  CPListItem *_item;
1128
1509
  if (@available(iOS 14.0, *)) {
1129
1510
  CPListItemAccessoryType accessoryType = _showsDisclosureIndicator ? CPListItemAccessoryTypeDisclosureIndicator : CPListItemAccessoryTypeNone;
@@ -1136,11 +1517,16 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
1136
1517
  }
1137
1518
  if (item[@"imgUrl"]) {
1138
1519
  NSString *imgUrlString = [RCTConvert NSString:item[@"imgUrl"]];
1139
- UIImage *placeholderImage;
1140
- if ([item objectForKey:@"placeholderImage"] != nil) {
1141
- placeholderImage = [RCTConvert UIImage:[item objectForKey:@"placeholderImage"]];
1520
+ UIImage *placeholderImage = nil;
1521
+ id placeholderSource = [item objectForKey:@"placeholderImage"];
1522
+ if (placeholderSource != nil && ![self isImageSourceURL:placeholderSource]) {
1523
+ placeholderImage = [RCTConvert UIImage:placeholderSource];
1142
1524
  }
1143
1525
  [self updateItemImageWithURL:_item imgUrl:imgUrlString placeholderImage:placeholderImage];
1526
+ } else if (isURL) {
1527
+ // Load image asynchronously for URL-based images
1528
+ NSString *imgUrlString = [self getURLStringFromImageSource:_imageObj];
1529
+ [self updateItemImageWithURL:_item imgUrl:imgUrlString placeholderImage:nil];
1144
1530
  }
1145
1531
  [_item setUserInfo:@{ @"index": @(listIndex) }];
1146
1532
  [_items addObject:_item];
@@ -1152,8 +1538,13 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
1152
1538
  NSArray* slicedArray = [_imageItems subarrayWithRange:NSMakeRange(0, MIN(CPMaximumNumberOfGridImages, _imageItems.count))];
1153
1539
 
1154
1540
  for (NSObject *imageObj in slicedArray){
1155
- UIImage *_image = [RCTConvert UIImage:imageObj];
1156
- [_images addObject:_image];
1541
+ // Skip URL-based images for initial creation (use placeholder)
1542
+ if ([self isImageSourceURL:imageObj]) {
1543
+ [_images addObject:[[UIImage alloc] init]];
1544
+ } else {
1545
+ UIImage *_image = [RCTConvert UIImage:imageObj];
1546
+ [_images addObject:_image];
1547
+ }
1157
1548
  }
1158
1549
  }
1159
1550
  if (@available(iOS 14.0, *)) {
@@ -1181,7 +1572,8 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
1181
1572
  UIImage *placeholderImage = nil;
1182
1573
  if (_placeholderImages != nil && _index < _placeholderImages.count) {
1183
1574
  id placeholderImageObj = _placeholderImages[_index];
1184
- if (placeholderImageObj != nil && placeholderImageObj != [NSNull null]) {
1575
+ // Only load placeholder synchronously if it's not a URL
1576
+ if (placeholderImageObj != nil && placeholderImageObj != [NSNull null] && ![self isImageSourceURL:placeholderImageObj]) {
1185
1577
  placeholderImage = [RCTConvert UIImage:placeholderImageObj];
1186
1578
  }
1187
1579
  }
@@ -1239,10 +1631,22 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
1239
1631
  for (NSDictionary *button in buttons) {
1240
1632
  NSString *_id = [button objectForKey:@"id"];
1241
1633
  NSArray<NSString*> *_titleVariants = [button objectForKey:@"titleVariants"];
1242
- UIImage *_image = [RCTConvert UIImage:[button objectForKey:@"image"]];
1634
+ id imageSource = [button objectForKey:@"image"];
1635
+ UIImage *_image;
1636
+
1637
+ if ([self isImageSourceURL:imageSource]) {
1638
+ // CPGridButton doesn't support updating image after creation
1639
+ // For URL-based images, we create with placeholder and load async
1640
+ _image = [[UIImage alloc] init];
1641
+ // Note: In production, images should be bundled locally to avoid this issue
1642
+ } else {
1643
+ _image = [RCTConvert UIImage:imageSource];
1644
+ }
1645
+
1646
+ int currentIndex = index; // Capture for block
1243
1647
  CPGridButton *_button = [[CPGridButton alloc] initWithTitleVariants:_titleVariants image:_image handler:^(CPGridButton * _Nonnull barButton) {
1244
1648
  if (self->hasListeners) {
1245
- [self sendEventWithName:@"gridButtonPressed" body:@{@"id": _id, @"templateId":templateId, @"index": @(index) }];
1649
+ [self sendEventWithName:@"gridButtonPressed" body:@{@"id": _id, @"templateId":templateId, @"index": @(currentIndex) }];
1246
1650
  }
1247
1651
  }];
1248
1652
  BOOL _disabled = [button objectForKey:@"disabled"];
@@ -1281,8 +1685,11 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
1281
1685
  CPManeuver* maneuver = [[CPManeuver alloc] init];
1282
1686
 
1283
1687
  if ([json objectForKey:@"junctionImage"]) {
1284
- UIImage *junctionImage = [RCTConvert UIImage:json[@"junctionImage"]];
1285
- [maneuver setJunctionImage:[self imageWithTint:junctionImage andTintColor:[UIColor whiteColor]]];
1688
+ id junctionSource = json[@"junctionImage"];
1689
+ if (![self isImageSourceURL:junctionSource]) {
1690
+ UIImage *junctionImage = [RCTConvert UIImage:junctionSource];
1691
+ [maneuver setJunctionImage:[self imageWithTint:junctionImage andTintColor:[UIColor whiteColor]]];
1692
+ }
1286
1693
  }
1287
1694
 
1288
1695
  if ([json objectForKey:@"initialTravelEstimates"]) {
@@ -1291,7 +1698,11 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
1291
1698
  }
1292
1699
 
1293
1700
  if ([json objectForKey:@"symbolImage"]) {
1294
- UIImage *symbolImage = [RCTConvert UIImage:json[@"symbolImage"]];
1701
+ id symbolSource = json[@"symbolImage"];
1702
+ if ([self isImageSourceURL:symbolSource]) {
1703
+ return maneuver; // Skip URL-based symbol images for now
1704
+ }
1705
+ UIImage *symbolImage = [RCTConvert UIImage:symbolSource];
1295
1706
 
1296
1707
  if ([json objectForKey:@"symbolImageSize"]) {
1297
1708
  NSDictionary *size = [RCTConvert NSDictionary:json[@"symbolImageSize"]];
@@ -1348,9 +1759,11 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
1348
1759
  }
1349
1760
 
1350
1761
  - (CPNavigationAlert*)parseNavigationAlert:(NSDictionary*)json templateId:(NSString*)templateId {
1351
- CPImageSet *imageSet;
1352
- if ([json objectForKey:@"lightImage"] && [json objectForKey:@"darkImage"]) {
1353
- imageSet = [[CPImageSet alloc] initWithLightContentImage:[RCTConvert UIImage:json[@"lightImage"]] darkContentImage:[RCTConvert UIImage:json[@"darkImage"]]];
1762
+ CPImageSet *imageSet = nil;
1763
+ id lightSource = json[@"lightImage"];
1764
+ id darkSource = json[@"darkImage"];
1765
+ if (lightSource && darkSource && ![self isImageSourceURL:lightSource] && ![self isImageSourceURL:darkSource]) {
1766
+ imageSet = [[CPImageSet alloc] initWithLightContentImage:[RCTConvert UIImage:lightSource] darkContentImage:[RCTConvert UIImage:darkSource]];
1354
1767
  }
1355
1768
  CPAlertAction *secondaryAction = [json objectForKey:@"secondaryAction"] ? [self parseAlertAction:json[@"secondaryAction"] body:@{ @"templateId": templateId, @"secondary": @(YES) }] : nil;
1356
1769
 
@@ -1374,7 +1787,12 @@ RCT_EXPORT_METHOD(updateMapTemplateMapButtons:(NSString*) templateId mapButtons:
1374
1787
  }
1375
1788
 
1376
1789
  - (CPVoiceControlState*)parseVoiceControlState:(NSDictionary*)json {
1377
- return [[CPVoiceControlState alloc] initWithIdentifier:[RCTConvert NSString:json[@"identifier"]] titleVariants:[RCTConvert NSStringArray:json[@"titleVariants"]] image:[RCTConvert UIImage:json[@"image"]] repeats:[RCTConvert BOOL:json[@"repeats"]]];
1790
+ id imageSource = json[@"image"];
1791
+ UIImage *image = nil;
1792
+ if (imageSource && ![self isImageSourceURL:imageSource]) {
1793
+ image = [RCTConvert UIImage:imageSource];
1794
+ }
1795
+ return [[CPVoiceControlState alloc] initWithIdentifier:[RCTConvert NSString:json[@"identifier"]] titleVariants:[RCTConvert NSStringArray:json[@"titleVariants"]] image:image repeats:[RCTConvert BOOL:json[@"repeats"]]];
1378
1796
  }
1379
1797
 
1380
1798
  - (NSString*)panDirectionToString:(CPPanDirection)panDirection {