Commit 1511a7e0 authored by Wei Han's avatar Wei Han

code update

image caching
parent cf0ca12f
{ {
"images" : [ "images" : [
{ {
"filename" : "overlay.png", "filename" : "img_overlay_projdashboard3x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
......
{
"images" : [
{
"filename" : "image_placeholder.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
2FC779BD2F0E33410002A1D4 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FC779BC2F0E33410002A1D4 /* MobileCoreServices.framework */; }; 2FC779BD2F0E33410002A1D4 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FC779BC2F0E33410002A1D4 /* MobileCoreServices.framework */; };
2FD1557B2ECAC5CC00233981 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FD1557A2ECAC5CC00233981 /* WebKit.framework */; }; 2FD1557B2ECAC5CC00233981 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FD1557A2ECAC5CC00233981 /* WebKit.framework */; };
2FD9A1AE2EF0FA7100A7B53E /* Network.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FD9A0B72EF0E85B00A7B53E /* Network.framework */; }; 2FD9A1AE2EF0FA7100A7B53E /* Network.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FD9A0B72EF0E85B00A7B53E /* Network.framework */; };
2FDE729C2F15C48100C471A0 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2FDE729B2F15C48100C471A0 /* Images.xcassets */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
...@@ -60,6 +61,7 @@ ...@@ -60,6 +61,7 @@
2FD027B02EEFD25100CF5C33 /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.1.sdk/System/Library/Frameworks/Network.framework; sourceTree = DEVELOPER_DIR; }; 2FD027B02EEFD25100CF5C33 /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.1.sdk/System/Library/Frameworks/Network.framework; sourceTree = DEVELOPER_DIR; };
2FD1557A2ECAC5CC00233981 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.1.sdk/System/iOSSupport/System/Library/PrivateFrameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; 2FD1557A2ECAC5CC00233981 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.1.sdk/System/iOSSupport/System/Library/PrivateFrameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
2FD9A0B72EF0E85B00A7B53E /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = System/Library/Frameworks/Network.framework; sourceTree = SDKROOT; }; 2FD9A0B72EF0E85B00A7B53E /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = System/Library/Frameworks/Network.framework; sourceTree = SDKROOT; };
2FDE729B2F15C48100C471A0 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
...@@ -134,6 +136,7 @@ ...@@ -134,6 +136,7 @@
2F96475B2E86307D002CC7CB = { 2F96475B2E86307D002CC7CB = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2FDE729B2F15C48100C471A0 /* Images.xcassets */,
2F9647672E86307D002CC7CB /* QmsPluginFramework */, 2F9647672E86307D002CC7CB /* QmsPluginFramework */,
2FC779AA2F0E1C310002A1D4 /* testApp */, 2FC779AA2F0E1C310002A1D4 /* testApp */,
2FAEF0982E8B7BC80086EDA3 /* Frameworks */, 2FAEF0982E8B7BC80086EDA3 /* Frameworks */,
...@@ -271,6 +274,7 @@ ...@@ -271,6 +274,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
2FDE729C2F15C48100C471A0 /* Images.xcassets in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
......
// AccessTransactionViewController.mm // AccessTransactionViewController.mm
#import "ImageCacheHelper.h"
#import "AccessTransactionViewController.h" #import "AccessTransactionViewController.h"
#import <WebKit/WebKit.h> #import <WebKit/WebKit.h>
...@@ -358,24 +358,17 @@ ...@@ -358,24 +358,17 @@
submittedSignature.clipsToBounds = YES; submittedSignature.clipsToBounds = YES;
submittedSignature.layer.borderColor = UIColor.blackColor.CGColor; submittedSignature.layer.borderColor = UIColor.blackColor.CGColor;
submittedSignature.layer.borderWidth = 1.0; submittedSignature.layer.borderWidth = 1.0;
submittedSignature.layer.cornerRadius = 4.0; // optional rounded corners submittedSignature.layer.cornerRadius = 4.0;
submittedSignature.clipsToBounds = YES;
submittedSignature.userInteractionEnabled = YES; submittedSignature.userInteractionEnabled = YES;
UITapGestureRecognizer *tapSubmitted = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapSignature:)]; UITapGestureRecognizer *tapSubmitted = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapSignature:)];
[submittedSignature addGestureRecognizer:tapSubmitted]; [submittedSignature addGestureRecognizer:tapSubmitted];
[self.contentView addSubview:submittedSignature];
NSURL *submittedURL = [NSURL URLWithString:self.transactionData[@"signature_submit"]]; NSURL *submittedURL = [NSURL URLWithString:self.transactionData[@"signature_submit"]];
if (submittedURL) { [ImageCacheHelper loadImageWithURL:submittedURL completion:^(UIImage *image) {
// Simple async load submittedSignature.image = image;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ }];
NSData *data = [NSData dataWithContentsOfURL:submittedURL];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
submittedSignature.image = img;
});
}
});
}
[self.contentView addSubview:submittedSignature]; [self.contentView addSubview:submittedSignature];
UILabel *submittedName = [self regular16:[NSString stringWithFormat:@"Name: %@", self.transactionData[@"name_submit"] ?: @"--"]]; UILabel *submittedName = [self regular16:[NSString stringWithFormat:@"Name: %@", self.transactionData[@"name_submit"] ?: @"--"]];
...@@ -395,24 +388,16 @@ ...@@ -395,24 +388,16 @@
receivedSignature.layer.borderColor = UIColor.blackColor.CGColor; receivedSignature.layer.borderColor = UIColor.blackColor.CGColor;
receivedSignature.layer.borderWidth = 1.0; receivedSignature.layer.borderWidth = 1.0;
receivedSignature.layer.cornerRadius = 4.0; receivedSignature.layer.cornerRadius = 4.0;
receivedSignature.clipsToBounds = YES;
receivedSignature.userInteractionEnabled = YES; receivedSignature.userInteractionEnabled = YES;
UITapGestureRecognizer *tapReceived = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapSignature:)]; UITapGestureRecognizer *tapReceived = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapSignature:)];
[receivedSignature addGestureRecognizer:tapReceived]; [receivedSignature addGestureRecognizer:tapReceived];
NSURL *receivedURL = [NSURL URLWithString:self.transactionData[@"signature_receive"]];
if (receivedURL) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:receivedURL];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
receivedSignature.image = img;
});
}
});
}
[self.contentView addSubview:receivedSignature]; [self.contentView addSubview:receivedSignature];
NSURL *receivedURL = [NSURL URLWithString:self.transactionData[@"signature_receive"]];
[ImageCacheHelper loadImageWithURL:receivedURL completion:^(UIImage *image) {
receivedSignature.image = image;
}];
UILabel *receivedName = [self regular16:[NSString stringWithFormat:@"Name: %@", self.transactionData[@"name_receive"] ?: @"--"]]; UILabel *receivedName = [self regular16:[NSString stringWithFormat:@"Name: %@", self.transactionData[@"name_receive"] ?: @"--"]];
[self.contentView addSubview:receivedName]; [self.contentView addSubview:receivedName];
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
#import <WebKit/WebKit.h> #import <WebKit/WebKit.h>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import "APIClient.h" #import "APIClient.h"
#import "ImageCacheHelper.h"
@interface AppointmentConfirmationViewController () <WKNavigationDelegate> @interface AppointmentConfirmationViewController () <WKNavigationDelegate>
...@@ -636,18 +637,9 @@ ...@@ -636,18 +637,9 @@
NSURL *url = [NSURL URLWithString:urlString]; NSURL *url = [NSURL URLWithString:urlString];
if (!url) return; if (!url) return;
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (data && !error) {
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = image; imageView.image = image;
});
}
}
}]; }];
[task resume];
} }
-(void)confirmPressed { -(void)confirmPressed {
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#import "APIClient.h" #import "APIClient.h"
#import <WebKit/WebKit.h> #import <WebKit/WebKit.h>
#import <EventKit/EventKit.h> #import <EventKit/EventKit.h>
#import "ImageCacheHelper.h"
@interface AppointmentDetailsViewController () <UIScrollViewDelegate> @interface AppointmentDetailsViewController () <UIScrollViewDelegate>
...@@ -177,20 +178,9 @@ ...@@ -177,20 +178,9 @@
NSURL *url = [NSURL URLWithString:urlString]; NSURL *url = [NSURL URLWithString:urlString];
if (!url) return; if (!url) return;
NSURLSessionDataTask *task = [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
[[NSURLSession sharedSession] dataTaskWithURL:url imageView.image = image;
completionHandler:^(NSData *data, NSURLResponse *res, NSError *err) {
if (err || !data) return;
UIImage *img = [UIImage imageWithData:data];
if (!img) return;
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = img;
});
}]; }];
[task resume];
} }
- (void)handleFloorPlanTap:(UITapGestureRecognizer *)sender { - (void)handleFloorPlanTap:(UITapGestureRecognizer *)sender {
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
#import "OfflineSyncManager.h" #import "OfflineSyncManager.h"
#import "LocalStorage.h" #import "LocalStorage.h"
#import "APIConfig.h" #import "APIConfig.h"
#import "ImageCacheHelper.h"
@interface DashboardViewController () @interface DashboardViewController ()
...@@ -69,6 +70,7 @@ ...@@ -69,6 +70,7 @@
self.view.backgroundColor = [UIColor colorWithWhite:0.96 alpha:1.0]; self.view.backgroundColor = [UIColor colorWithWhite:0.96 alpha:1.0];
_sectionViews = [NSMutableDictionary dictionary]; _sectionViews = [NSMutableDictionary dictionary];
[self setupHeader]; [self setupHeader];
[self setupScrollContent]; [self setupScrollContent];
[self setupFooter]; [self setupFooter];
...@@ -212,10 +214,12 @@ ...@@ -212,10 +214,12 @@
// ---------- Overlay ---------- // ---------- Overlay ----------
self.overlayImageView = [[UIImageView alloc] init]; self.overlayImageView = [[UIImageView alloc] init];
self.overlayImageView.translatesAutoresizingMaskIntoConstraints = NO; self.overlayImageView.translatesAutoresizingMaskIntoConstraints = NO;
self.overlayImageView.contentMode = UIViewContentModeScaleAspectFill; self.overlayImageView.contentMode = UIViewContentModeScaleToFill;
self.overlayImageView.alpha = 0.4; self.overlayImageView.userInteractionEnabled = NO;
self.overlayImageView.alpha = 0.5;
[imageContainer addSubview:self.overlayImageView]; [imageContainer addSubview:self.overlayImageView];
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
[self.overlayImageView.topAnchor constraintEqualToAnchor:imageContainer.topAnchor], [self.overlayImageView.topAnchor constraintEqualToAnchor:imageContainer.topAnchor],
[self.overlayImageView.leadingAnchor constraintEqualToAnchor:imageContainer.leadingAnchor], [self.overlayImageView.leadingAnchor constraintEqualToAnchor:imageContainer.leadingAnchor],
...@@ -305,21 +309,27 @@ ...@@ -305,21 +309,27 @@
[self.infoButton.heightAnchor constraintEqualToConstant:24] [self.infoButton.heightAnchor constraintEqualToConstant:24]
]]; ]];
// ---------- Load Remote Image ---------- // ---------- Load Remote Image & Overlay ----------
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
UIImage *overlayImage =
[UIImage imageNamed:@"img_overlay"
inBundle:bundle
compatibleWithTraitCollection:nil];
self.overlayImageView.image = overlayImage;
[imageContainer bringSubviewToFront:self.overlayImageView];
NSLog(@"[Overlay] bundle = %@", bundle);
NSLog(@"[Overlay] image = %@", overlayImage);
NSString *remoteHeaderURL = self.projectLogo; NSString *remoteHeaderURL = self.projectLogo;
if (remoteHeaderURL.length > 0) { if (remoteHeaderURL.length > 0) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSURL *url = [NSURL URLWithString:remoteHeaderURL];
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:remoteHeaderURL]]; [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
UIImage *img = data ? [UIImage imageWithData:data] : [UIImage imageNamed:@"header_bg"]; self.backgroundImageView.image = image ?: [UIImage imageNamed:@"header_bg"];
[imageContainer bringSubviewToFront:self.overlayImageView];
dispatch_async(dispatch_get_main_queue(), ^{ }];
self.backgroundImageView.image = img;
self.overlayImageView.image = [UIImage imageNamed:@"overlay"];
});
});
} else { } else {
self.backgroundImageView.image = [UIImage imageNamed:@"header_bg"]; self.backgroundImageView.image = [UIImage imageNamed:@"header_bg"];
self.overlayImageView.image = [UIImage imageNamed:@"overlay"]; self.overlayImageView.image = [UIImage imageNamed:@"img_overlay"];
} }
} }
...@@ -670,7 +680,6 @@ ...@@ -670,7 +680,6 @@
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
self.projectNameLabel.text = self.projectName; self.projectNameLabel.text = self.projectName;
self.unitNameLabel.text = self.projectCode; self.unitNameLabel.text = self.projectCode;
self.overlayImageView.image = [UIImage imageNamed:@"overlay"];
[self refreshDashboardSectionsWithData:mainData]; [self refreshDashboardSectionsWithData:mainData];
}); });
...@@ -1195,20 +1204,12 @@ ...@@ -1195,20 +1204,12 @@
issueImage = lastArray[0][@"image"]; issueImage = lastArray[0][@"image"];
} }
if (issueImage && issueImage.length > 0) { if (issueImage.length > 0) {
NSURL *url = [NSURL URLWithString:issueImage]; NSURL *url = [NSURL URLWithString:issueImage];
if (url) { if (url) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
NSData *data = [NSData dataWithContentsOfURL:url]; if (image) thumb.image = image;
if (data) { }];
UIImage *img = [UIImage imageWithData:data];
if (img) {
dispatch_async(dispatch_get_main_queue(), ^{
thumb.image = img;
});
}
}
});
} }
} }
...@@ -1401,11 +1402,6 @@ ...@@ -1401,11 +1402,6 @@
} }
} }
- (UIImage *)downloadImageFrom:(NSString *)urlString {
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlString]];
return data ? [UIImage imageWithData:data] : [UIImage imageNamed:@"header_bg"];
}
#pragma mark - Card Factory with Tap Animation #pragma mark - Card Factory with Tap Animation
- (UIView *)createCardWithText:(NSString *)text frame:(CGRect)frame { - (UIView *)createCardWithText:(NSString *)text frame:(CGRect)frame {
UIView *card = [[UIView alloc] initWithFrame:frame]; UIView *card = [[UIView alloc] initWithFrame:frame];
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
#import <objc/runtime.h> #import <objc/runtime.h>
#import "GeneralInfoViewController.h" #import "GeneralInfoViewController.h"
#import "APIClient.h" #import "APIClient.h"
#import "ImageCacheHelper.h"
@interface GeneralInfoViewController () <UIDocumentInteractionControllerDelegate, UIScrollViewDelegate> @interface GeneralInfoViewController () <UIDocumentInteractionControllerDelegate, UIScrollViewDelegate>
...@@ -764,15 +765,11 @@ typedef struct { ...@@ -764,15 +765,11 @@ typedef struct {
NSURL *url = [NSURL URLWithString:layoutImageURL]; NSURL *url = [NSURL URLWithString:layoutImageURL];
if (url) { if (url) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
NSData *data = [NSData dataWithContentsOfURL:url]; if (image) {
if (data) { imgView.image = image;
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
imgView.image = img;
});
} }
}); }];
} }
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showFullImage:)]; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showFullImage:)];
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
#import "APIClient.h" #import "APIClient.h"
#import "APIConfig.h" #import "APIConfig.h"
#import "UnitViewController.h" #import "UnitViewController.h"
#import "ImageCacheHelper.h"
@interface ProjectCardCell : UICollectionViewCell @interface ProjectCardCell : UICollectionViewCell
@property (nonatomic, strong) UIImageView *imageView; @property (nonatomic, strong) UIImageView *imageView;
...@@ -160,7 +161,11 @@ ...@@ -160,7 +161,11 @@
forIndexPath:indexPath]; forIndexPath:indexPath];
// Default placeholder // Default placeholder
cell.imageView.image = [UIImage systemImageNamed:@"photo"]; NSBundle *bundle = [NSBundle bundleForClass:[self class]];
UIImage *placeholderImage = [UIImage imageNamed:@"img_placeholder"
inBundle:bundle
compatibleWithTraitCollection:nil];
cell.imageView.image = placeholderImage;
NSDictionary *project = self.projects[indexPath.item]; NSDictionary *project = self.projects[indexPath.item];
...@@ -168,35 +173,20 @@ ...@@ -168,35 +173,20 @@
id projectName = project[@"project_name"]; id projectName = project[@"project_name"];
cell.nameLabel.text = [projectName isKindOfClass:[NSString class]] ? projectName : @""; cell.nameLabel.text = [projectName isKindOfClass:[NSString class]] ? projectName : @"";
// Get logo URL safely // Determine logo URL
NSString *logo = project[@"logo"]; NSString *logoURLString = project[@"logo"];
NSString *thumb = project[@"logo_thumbnail"]; if (!logoURLString.length) logoURLString = project[@"logo_thumbnail"];
NSString *logoURLString = (logo.length > 0) ? logo : // Use ImageCacheHelper for async loading & caching
(thumb.length > 0) ? thumb : nil; [ImageCacheHelper loadImageWithURL:[NSURL URLWithString:logoURLString ?: @""] completion:^(UIImage *image) {
if (image) {
// Ensure the cell is still visible
NSURL *url = [NSURL URLWithString:logoURLString ?: @""];
// Async load image safely
if (url) {
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
UIImage *image = data ? [UIImage imageWithData:data] : nil;
if (!image) return; // leave placeholder
dispatch_async(dispatch_get_main_queue(), ^{
// Only update if the cell is still visible at this indexPath
ProjectCardCell *updateCell = (ProjectCardCell *)[collectionView cellForItemAtIndexPath:indexPath]; ProjectCardCell *updateCell = (ProjectCardCell *)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell) { if (updateCell) {
updateCell.imageView.image = image; updateCell.imageView.image = image;
} }
});
}];
[task resume];
} }
}];
return cell; return cell;
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import "OfflineSyncManager.h" #import "OfflineSyncManager.h"
#import "NetworkManager.h" #import "NetworkManager.h"
#import "ImageCacheHelper.h"
@interface AddIssueDetailsViewController () <UIImagePickerControllerDelegate, UINavigationControllerDelegate> @interface AddIssueDetailsViewController () <UIImagePickerControllerDelegate, UINavigationControllerDelegate>
@property (nonatomic, strong) UIView *headerView; @property (nonatomic, strong) UIView *headerView;
...@@ -159,9 +160,7 @@ ...@@ -159,9 +160,7 @@
NSURL *url = [NSURL URLWithString:urlStr]; NSURL *url = [NSURL URLWithString:urlStr];
if (!url) continue; if (!url) continue;
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
if (error || !data) return;
UIImage *img = [UIImage imageWithData:data];
if (!img) return; if (!img) return;
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
...@@ -179,7 +178,6 @@ ...@@ -179,7 +178,6 @@
} }
}); });
}]; }];
[task resume];
} }
} }
NSLog(@"✅ Prefilled Edit Mode UI successfully"); NSLog(@"✅ Prefilled Edit Mode UI successfully");
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#import "APIClient.h" #import "APIClient.h"
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
#import "IssueTutorialViewController.h" #import "IssueTutorialViewController.h"
#import "ImageCacheHelper.h"
@interface PlanViewController () @interface PlanViewController ()
...@@ -218,35 +219,41 @@ ...@@ -218,35 +219,41 @@
return; return;
} }
NSLog(@"⬇️ Downloading plan image from %@", self.planImageURL); NSLog(@"⬇️ Loading plan image from %@", self.planImageURL);
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error || !data) {
NSLog(@"❌ Failed to download plan image: %@", error.localizedDescription);
return;
}
UIImage *image = [UIImage imageWithData:data]; [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (!image) { if (!image) {
NSLog(@"⚠️ Could not decode image data"); NSLog(@"❌ Failed to load plan image from cache/network");
return; return;
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
self.planImageView.image = image; self.planImageView.image = image;
self.planImageView.userInteractionEnabled = YES; self.planImageView.userInteractionEnabled = YES;
// Add tap recognizer if not already added
BOOL hasTap = NO;
for (UIGestureRecognizer *gr in self.planImageView.gestureRecognizers) {
if ([gr isKindOfClass:[UITapGestureRecognizer class]]) {
hasTap = YES;
break;
}
}
if (!hasTap) {
UITapGestureRecognizer *tapRecognizer = UITapGestureRecognizer *tapRecognizer =
[[UITapGestureRecognizer alloc] initWithTarget:self [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleTapOnPlan:)]; action:@selector(handleTapOnPlan:)];
[self.planImageView addGestureRecognizer:tapRecognizer]; [self.planImageView addGestureRecognizer:tapRecognizer];
}
[self.scrollView setZoomScale:1.0 animated:NO]; [self.scrollView setZoomScale:1.0 animated:NO];
[self.view setNeedsLayout]; [self.view setNeedsLayout];
[self.view layoutIfNeeded]; [self.view layoutIfNeeded];
[self renderAllLocations:self.locations]; [self renderAllLocations:self.locations];
NSLog(@"✅ Plan image displayed successfully"); NSLog(@"✅ Plan image displayed successfully");
}); });
}]; }];
[task resume];
} }
- (void)handleTapOnPlan:(UITapGestureRecognizer *)recognizer { - (void)handleTapOnPlan:(UITapGestureRecognizer *)recognizer {
...@@ -426,28 +433,6 @@ ...@@ -426,28 +433,6 @@
]]; ]];
} }
- (void)loadPlanImageFromURL:(NSString *)urlString {
NSURL *url = [NSURL URLWithString:urlString];
if (!url) return;
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"❌ Failed to load image: %@", error);
return;
}
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
self.planImageView.image = image;
NSLog(@"🖼 Plan image loaded successfully");
});
}
}];
[task resume];
}
- (void)showAlertWithTitle:(NSString *)title message:(NSString *)message { - (void)showAlertWithTitle:(NSString *)title message:(NSString *)message {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#import "CalendarView.h" #import "CalendarView.h"
#import "NSDate+CalendarHelpers.h" #import "NSDate+CalendarHelpers.h"
#import "AddInfoViewController.h" #import "AddInfoViewController.h"
#import "ImageCacheHelper.h"
@interface HistoryViewController () <UITableViewDelegate, UITableViewDataSource> @interface HistoryViewController () <UITableViewDelegate, UITableViewDataSource>
...@@ -379,6 +380,7 @@ ...@@ -379,6 +380,7 @@
CGFloat imageSize = 56; CGFloat imageSize = 56;
CGFloat spacing = 8; CGFloat spacing = 8;
// Inside your thumbnail loop
for (NSDictionary *imgDict in images) { for (NSDictionary *imgDict in images) {
NSString *thumbURL = imgDict[@"thumb_image"]; NSString *thumbURL = imgDict[@"thumb_image"];
if (thumbURL.length == 0) continue; if (thumbURL.length == 0) continue;
...@@ -389,22 +391,17 @@ ...@@ -389,22 +391,17 @@
thumbView.contentMode = UIViewContentModeScaleAspectFill; thumbView.contentMode = UIViewContentModeScaleAspectFill;
thumbView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1]; thumbView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1];
// Load async [thumbScroll addSubview:thumbView];
NSURL *url = [NSURL URLWithString:thumbURL]; NSURL *url = [NSURL URLWithString:thumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
NSData *data = [NSData dataWithContentsOfURL:url]; if (image) {
if (data) { thumbView.image = image;
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
thumbView.image = img;
});
} }
}); }];
[thumbScroll addSubview:thumbView];
x += imageSize + spacing; x += imageSize + spacing;
} }
thumbScroll.contentSize = CGSizeMake(x, imageSize); thumbScroll.contentSize = CGSizeMake(x, imageSize);
return cell; return cell;
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
#import "HistoryViewController.h" #import "HistoryViewController.h"
#import "PlanViewController.h" #import "PlanViewController.h"
#import "WithdrawSingleIssueViewController.h" #import "WithdrawSingleIssueViewController.h"
#import "ImageCacheHelper.h"
@interface IssueDetailViewController () <UIScrollViewDelegate> @interface IssueDetailViewController () <UIScrollViewDelegate>
...@@ -342,29 +343,17 @@ ...@@ -342,29 +343,17 @@
[imageScroll addSubview:imgView]; [imageScroll addSubview:imgView];
NSURL *url = [NSURL URLWithString:urlStr]; NSURL *url = [NSURL URLWithString:urlStr];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
NSData *data = [NSData dataWithContentsOfURL:url]; if (image) imgView.image = image;
if (data) { }];
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
imgView.image = img;
});
}
});
} }
NSString *planThumbURL = self.issueData[@"plan_image_thumb"]; NSString *planThumbURL = self.issueData[@"plan_image_thumb"];
if (planThumbURL.length > 0) { if (planThumbURL.length > 0) {
NSURL *url = [NSURL URLWithString:planThumbURL]; NSURL *url = [NSURL URLWithString:planThumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
NSData *data = [NSData dataWithContentsOfURL:url]; if (image) self.floorPlanThumbnail.image = image;
if (data) { }];
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
self.floorPlanThumbnail.image = img;
});
}
});
} }
//thumbnail marker //thumbnail marker
...@@ -471,14 +460,9 @@ ...@@ -471,14 +460,9 @@
// Load full image asynchronously // Load full image asynchronously
NSURL *url = [NSURL URLWithString:planFullURL]; NSURL *url = [NSURL URLWithString:planFullURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
NSData *data = [NSData dataWithContentsOfURL:url];
if (!data) return;
UIImage *img = [UIImage imageWithData:data];
if (!img) return; if (!img) return;
dispatch_async(dispatch_get_main_queue(), ^{
fullImageView.image = img; fullImageView.image = img;
NSString *xStr = self.issueData[@"pos_x"]; NSString *xStr = self.issueData[@"pos_x"];
...@@ -504,8 +488,7 @@ ...@@ -504,8 +488,7 @@
marker.layer.borderWidth = 1.5; marker.layer.borderWidth = 1.5;
[fullImageView addSubview:marker]; [fullImageView addSubview:marker];
} }
}); }];
});
} }
- (void)closeFullFloorPlan { - (void)closeFullFloorPlan {
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#import "IssueDetailViewController.h" #import "IssueDetailViewController.h"
#import "WithdrawSingleIssueViewController.h" #import "WithdrawSingleIssueViewController.h"
#import "WithdrawIssuesViewController.h" #import "WithdrawIssuesViewController.h"
#import "ImageCacheHelper.h"
#pragma mark - Internal IssueCell #pragma mark - Internal IssueCell
@interface IssueCell : UITableViewCell @interface IssueCell : UITableViewCell
...@@ -605,30 +606,23 @@ ...@@ -605,30 +606,23 @@
// --- Thumbnail Handling --- // --- Thumbnail Handling ---
if (thumbURL.length > 0) { if (thumbURL.length > 0) {
NSURL *url = [NSURL URLWithString:thumbURL]; NSURL *url = [NSURL URLWithString:thumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
NSData *data = [NSData dataWithContentsOfURL:url]; // Make sure cell is still visible
dispatch_async(dispatch_get_main_queue(), ^{
IssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath]; IssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return; if (!updateCell) return;
// Clear any "No image" labels // Clear previous subviews / placeholders
for (UIView *subview in updateCell.thumbView.subviews) { for (UIView *subview in updateCell.thumbView.subviews) {
[subview removeFromSuperview]; [subview removeFromSuperview];
} }
if (data) {
UIImage *img = [UIImage imageWithData:data];
if (img) { if (img) {
updateCell.thumbView.image = img; updateCell.thumbView.image = img;
updateCell.thumbView.backgroundColor = UIColor.clearColor; updateCell.thumbView.backgroundColor = UIColor.clearColor;
} else { } else {
[self setNoImagePlaceholder:updateCell.thumbView]; [self setNoImagePlaceholder:updateCell.thumbView];
} }
} else { }];
[self setNoImagePlaceholder:updateCell.thumbView];
}
});
});
} else { } else {
[self setNoImagePlaceholder:cell.thumbView]; [self setNoImagePlaceholder:cell.thumbView];
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
#import "APIClient.h" #import "APIClient.h"
#import "WithdrawSingleIssueViewController.h" #import "WithdrawSingleIssueViewController.h"
#import "FilterViewController.h" #import "FilterViewController.h"
#import "ImageCacheHelper.h"
#pragma mark - custom cell structure #pragma mark - custom cell structure
@interface WithdrawIssueCell : UITableViewCell @interface WithdrawIssueCell : UITableViewCell
...@@ -631,9 +632,7 @@ ...@@ -631,9 +632,7 @@
// Thumbnail rendering (copied from IssueViewController) // Thumbnail rendering (copied from IssueViewController)
if (thumbURL.length > 0) { if (thumbURL.length > 0) {
NSURL *url = [NSURL URLWithString:thumbURL]; NSURL *url = [NSURL URLWithString:thumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
NSData *data = [NSData dataWithContentsOfURL:url];
dispatch_async(dispatch_get_main_queue(), ^{
WithdrawIssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath]; WithdrawIssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return; if (!updateCell) return;
...@@ -642,19 +641,13 @@ ...@@ -642,19 +641,13 @@
[subview removeFromSuperview]; [subview removeFromSuperview];
} }
if (data) {
UIImage *img = [UIImage imageWithData:data];
if (img) { if (img) {
updateCell.thumbView.image = img; updateCell.thumbView.image = img;
updateCell.thumbView.backgroundColor = UIColor.clearColor; updateCell.thumbView.backgroundColor = UIColor.clearColor;
} else { } else {
[self setNoImagePlaceholder:updateCell.thumbView]; [self setNoImagePlaceholder:updateCell.thumbView];
} }
} else { }];
[self setNoImagePlaceholder:updateCell.thumbView];
}
});
});
} else { } else {
[self setNoImagePlaceholder:cell.thumbView]; [self setNoImagePlaceholder:cell.thumbView];
} }
......
...@@ -620,7 +620,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -620,7 +620,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
[APIConfig projectId], [APIConfig drawingPlanId]); [APIConfig projectId], [APIConfig drawingPlanId]);
NSString *message = [NSString stringWithFormat: NSString *message = [NSString stringWithFormat:
@"Currently downloading unit \"%@\".\nPlease do not close the app.", sortName]; @"Currently downloading for unit \"%@\".\nPlease do not close the app.", sortName];
[[OfflineSyncManager shared] startDownloadUnitImagesWithProgress:^(NSInteger completed, NSInteger total) { [[OfflineSyncManager shared] startDownloadUnitImagesWithProgress:^(NSInteger completed, NSInteger total) {
__strong typeof(weakSelf) strongSelf2 = weakSelf; __strong typeof(weakSelf) strongSelf2 = weakSelf;
......
...@@ -1738,10 +1738,6 @@ ...@@ -1738,10 +1738,6 @@
+ (void)requestArrayImage:(NSArray *)imageArray + (void)requestArrayImage:(NSArray *)imageArray
completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion { completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"ArrayImage" completion:completion]) {
return;
}
// ✅ URL // ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/offline/syncImageArray"]; NSURL *url = [APIConfig urlWithPath:@"/owner/offline/syncImageArray"];
...@@ -1795,10 +1791,6 @@ ...@@ -1795,10 +1791,6 @@
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (completion) { if (completion) {
NSString *cacheKey = [NSString stringWithFormat:@"ArrayImage_%@", [APIConfig projectId]];
NSDictionary *safeJson = [APIConfig dictionaryByReplacingNullsWithBlanks:jsonResponse];
[LocalStorage saveDictionary:safeJson forKey:cacheKey];
NSLog(@"💾 Saved to cache: %@", cacheKey);
completion(YES, jsonResponse, jsonParseError); completion(YES, jsonResponse, jsonParseError);
} }
}); });
......
// ImageCacheHelper.h
#import <UIKit/UIKit.h>
@interface ImageCacheHelper : NSObject
/// Load image from cache, download if missing, or fallback to placeholder
+ (void)loadImageWithURL:(NSURL *)url
completion:(void (^)(UIImage *image))completion;
/// Shared placeholder image
+ (UIImage *)placeholderImage;
/// Get the expected cache file path for a URL
+ (NSString *)cachedFilePathForURL:(NSURL *)url;
@end
// ImageCacheHelper.mm
#import <CommonCrypto/CommonDigest.h>
#import "ImageCacheHelper.h"
#import "NetworkManager.h"
#import "APIClient.h"
@implementation ImageCacheHelper
#pragma mark - Public Methods
+ (void)loadImageWithURL:(NSURL *)url
completion:(void (^)(UIImage *image))completion {
if (!url) {
if (completion) completion([self placeholderImage]);
return;
}
NSString *filePath = [self cachedFilePathForURL:url];
NSFileManager *fm = [NSFileManager defaultManager];
// 1️⃣ Check cache
if ([fm fileExistsAtPath:filePath]) {
UIImage *cachedImage = [UIImage imageWithContentsOfFile:filePath];
if (cachedImage) {
if (completion) completion(cachedImage);
return;
}
}
// 2️⃣ Not cached → check network
if (![self canDownloadImages]) {
// No internet → return placeholder
if (completion) completion([self placeholderImage]);
return;
}
// 3️⃣ Download
[APIClient downloadAndCacheImage:url completion:^(BOOL success, NSString *filePath) {
UIImage *img = nil;
if (success && filePath) {
img = [UIImage imageWithContentsOfFile:filePath];
}
if (!img) img = [self placeholderImage];
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(img);
});
}];
}
+ (UIImage *)placeholderImage {
static UIImage *image = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSBundle *bundle = [NSBundle bundleForClass:self];
image = [UIImage imageNamed:@"img_placeholder"
inBundle:bundle
compatibleWithTraitCollection:nil];
});
return image;
}
#pragma mark - Cache File Helper
+ (NSString *)cachedFilePathForURL:(NSURL *)url {
if (!url) return nil;
NSString *cacheDir = [self offlineImageCacheDirectory]; // You already have this
NSFileManager *fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:cacheDir]) {
NSError *dirError = nil;
[fm createDirectoryAtPath:cacheDir
withIntermediateDirectories:YES
attributes:nil
error:&dirError];
if (dirError) {
NSLog(@"❌ Failed to create cache directory: %@", dirError);
}
}
NSString *cleanName = url.lastPathComponent.stringByDeletingPathExtension;
NSString *ext = url.pathExtension;
NSString *fileName = [NSString stringWithFormat:@"%@_%@.%@",
cleanName,
[self md5Hash:url.absoluteString],
ext];
return [cacheDir stringByAppendingPathComponent:fileName];
}
#pragma mark - MD5 Hash Helper
+ (NSString *)md5Hash:(NSString *)input {
if (!input) return @"";
const char *cStr = [input UTF8String];
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
[output appendFormat:@"%02x", digest[i]];
return output;
}
#pragma mark - Offline Cache Directory
+ (NSString *)offlineImageCacheDirectory {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cacheDir = [[paths firstObject] stringByAppendingPathComponent:@"OfflineImages"];
return cacheDir;
}
+ (BOOL)canDownloadImages {
return [NetworkManager.sharedManager isConnected];
}
@end
...@@ -3,15 +3,16 @@ ...@@ -3,15 +3,16 @@
#import "LocalStorage.h" #import "LocalStorage.h"
#import "NetworkManager.h" #import "NetworkManager.h"
#import "APIClient.h" #import "APIClient.h"
#import "APIConfig.h"
static NSString * const kOfflineQueueKey = @"OFFLINE_SYNC_QUEUE"; static NSString * const kOfflineQueueKey = @"OFFLINE_SYNC_QUEUE";
static NSString * const kSelectedOfflineUnitsKey = @"SELECTED_OFFLINE_UNITS";
static NSString * const kSyncHistoryKey = @"SYNC_HISTORY_QUEUE"; static NSString * const kSyncHistoryKey = @"SYNC_HISTORY_QUEUE";
static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
static NSString * const kSyncSelectedUnitsKey = @"SYNC_SELECTEDUNITS";
@implementation OfflineSyncManager @implementation OfflineSyncManager
#pragma mark - Singleton #pragma mark - Singleton
...@@ -542,7 +543,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -542,7 +543,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
return; return;
} }
NSArray *selectedUnits = [LocalStorage loadArrayForKey:kSelectedOfflineUnitsKey] ?: @[]; NSArray *selectedUnits = [self buildOfflineUnitPayloadForCurrentConfig] ?: @[];
// ------------------------------ // ------------------------------
// Build sequential tasks // Build sequential tasks
...@@ -986,10 +987,33 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE"; ...@@ -986,10 +987,33 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
if (img[@"link"]) [urls addObject:[NSURL URLWithString:img[@"link"]]]; if (img[@"link"]) [urls addObject:[NSURL URLWithString:img[@"link"]]];
} }
for (NSURL *url in urls) {
NSLog(@"URL for image downloads: %@", url.absoluteString);
}
completion(urls.array, nil); completion(urls.array, nil);
}]; }];
} }
- (NSArray *)buildOfflineUnitPayloadForCurrentConfig {
NSString *projectId = [APIConfig projectId];
NSString *planId = [APIConfig drawingPlanId];
if (!projectId || !planId) {
NSLog(@"⚠️ Missing projectId or planId for image sync");
return @[];
}
NSArray *result = @[
@{
@"project_id": projectId,
@"plan_id": @[planId] // API expects array
}
];
NSLog(@"🔄 Image sync payload (single unit): %@", result);
return result;
}
#pragma mark - Safe Sequential API Helper #pragma mark - Safe Sequential API Helper
- (void)safeSequentialTask:(NSString *)taskKey - (void)safeSequentialTask:(NSString *)taskKey
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment