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

code update

image caching
parent cf0ca12f
{
"images" : [
{
"filename" : "overlay.png",
"filename" : "img_overlay_projdashboard3x.png",
"idiom" : "universal",
"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 @@
2FC779BD2F0E33410002A1D4 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FC779BC2F0E33410002A1D4 /* MobileCoreServices.framework */; };
2FD1557B2ECAC5CC00233981 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FD1557A2ECAC5CC00233981 /* WebKit.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 */
/* Begin PBXContainerItemProxy section */
......@@ -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; };
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; };
2FDE729B2F15C48100C471A0 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
......@@ -134,6 +136,7 @@
2F96475B2E86307D002CC7CB = {
isa = PBXGroup;
children = (
2FDE729B2F15C48100C471A0 /* Images.xcassets */,
2F9647672E86307D002CC7CB /* QmsPluginFramework */,
2FC779AA2F0E1C310002A1D4 /* testApp */,
2FAEF0982E8B7BC80086EDA3 /* Frameworks */,
......@@ -271,6 +274,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2FDE729C2F15C48100C471A0 /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
......
// AccessTransactionViewController.mm
#import "ImageCacheHelper.h"
#import "AccessTransactionViewController.h"
#import <WebKit/WebKit.h>
......@@ -358,24 +358,17 @@
submittedSignature.clipsToBounds = YES;
submittedSignature.layer.borderColor = UIColor.blackColor.CGColor;
submittedSignature.layer.borderWidth = 1.0;
submittedSignature.layer.cornerRadius = 4.0; // optional rounded corners
submittedSignature.clipsToBounds = YES;
submittedSignature.layer.cornerRadius = 4.0;
submittedSignature.userInteractionEnabled = YES;
UITapGestureRecognizer *tapSubmitted = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapSignature:)];
[submittedSignature addGestureRecognizer:tapSubmitted];
[self.contentView addSubview:submittedSignature];
NSURL *submittedURL = [NSURL URLWithString:self.transactionData[@"signature_submit"]];
if (submittedURL) {
// Simple async load
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;
});
}
});
}
[ImageCacheHelper loadImageWithURL:submittedURL completion:^(UIImage *image) {
submittedSignature.image = image;
}];
[self.contentView addSubview:submittedSignature];
UILabel *submittedName = [self regular16:[NSString stringWithFormat:@"Name: %@", self.transactionData[@"name_submit"] ?: @"--"]];
......@@ -395,24 +388,16 @@
receivedSignature.layer.borderColor = UIColor.blackColor.CGColor;
receivedSignature.layer.borderWidth = 1.0;
receivedSignature.layer.cornerRadius = 4.0;
receivedSignature.clipsToBounds = YES;
receivedSignature.userInteractionEnabled = YES;
UITapGestureRecognizer *tapReceived = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapSignature:)];
[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];
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"] ?: @"--"]];
[self.contentView addSubview:receivedName];
......
......@@ -3,6 +3,7 @@
#import <WebKit/WebKit.h>
#import <UIKit/UIKit.h>
#import "APIClient.h"
#import "ImageCacheHelper.h"
@interface AppointmentConfirmationViewController () <WKNavigationDelegate>
......@@ -632,22 +633,13 @@
- (void)loadImage:(NSString *)urlString intoImageView:(UIImageView *)imageView {
if (!urlString) return;
NSURL *url = [NSURL URLWithString:urlString];
if (!url) return;
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
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;
});
}
}
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
imageView.image = image;
}];
[task resume];
}
-(void)confirmPressed {
......
......@@ -4,6 +4,7 @@
#import "APIClient.h"
#import <WebKit/WebKit.h>
#import <EventKit/EventKit.h>
#import "ImageCacheHelper.h"
@interface AppointmentDetailsViewController () <UIScrollViewDelegate>
......@@ -177,20 +178,9 @@
NSURL *url = [NSURL URLWithString:urlString];
if (!url) return;
NSURLSessionDataTask *task =
[[NSURLSession sharedSession] dataTaskWithURL:url
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;
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
imageView.image = image;
}];
[task resume];
}
- (void)handleFloorPlanTap:(UITapGestureRecognizer *)sender {
......
......@@ -14,6 +14,7 @@
#import "OfflineSyncManager.h"
#import "LocalStorage.h"
#import "APIConfig.h"
#import "ImageCacheHelper.h"
@interface DashboardViewController ()
......@@ -68,7 +69,8 @@
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithWhite:0.96 alpha:1.0];
_sectionViews = [NSMutableDictionary dictionary];
[self setupHeader];
[self setupScrollContent];
[self setupFooter];
......@@ -212,10 +214,12 @@
// ---------- Overlay ----------
self.overlayImageView = [[UIImageView alloc] init];
self.overlayImageView.translatesAutoresizingMaskIntoConstraints = NO;
self.overlayImageView.contentMode = UIViewContentModeScaleAspectFill;
self.overlayImageView.alpha = 0.4;
self.overlayImageView.contentMode = UIViewContentModeScaleToFill;
self.overlayImageView.userInteractionEnabled = NO;
self.overlayImageView.alpha = 0.5;
[imageContainer addSubview:self.overlayImageView];
[NSLayoutConstraint activateConstraints:@[
[self.overlayImageView.topAnchor constraintEqualToAnchor:imageContainer.topAnchor],
[self.overlayImageView.leadingAnchor constraintEqualToAnchor:imageContainer.leadingAnchor],
......@@ -305,21 +309,27 @@
[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;
if (remoteHeaderURL.length > 0) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:remoteHeaderURL]];
UIImage *img = data ? [UIImage imageWithData:data] : [UIImage imageNamed:@"header_bg"];
dispatch_async(dispatch_get_main_queue(), ^{
self.backgroundImageView.image = img;
self.overlayImageView.image = [UIImage imageNamed:@"overlay"];
});
});
NSURL *url = [NSURL URLWithString:remoteHeaderURL];
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
self.backgroundImageView.image = image ?: [UIImage imageNamed:@"header_bg"];
[imageContainer bringSubviewToFront:self.overlayImageView];
}];
} else {
self.backgroundImageView.image = [UIImage imageNamed:@"header_bg"];
self.overlayImageView.image = [UIImage imageNamed:@"overlay"];
self.overlayImageView.image = [UIImage imageNamed:@"img_overlay"];
}
}
......@@ -670,7 +680,6 @@
dispatch_async(dispatch_get_main_queue(), ^{
self.projectNameLabel.text = self.projectName;
self.unitNameLabel.text = self.projectCode;
self.overlayImageView.image = [UIImage imageNamed:@"overlay"];
[self refreshDashboardSectionsWithData:mainData];
});
......@@ -1195,20 +1204,12 @@
issueImage = lastArray[0][@"image"];
}
if (issueImage && issueImage.length > 0) {
if (issueImage.length > 0) {
NSURL *url = [NSURL URLWithString:issueImage];
if (url) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
UIImage *img = [UIImage imageWithData:data];
if (img) {
dispatch_async(dispatch_get_main_queue(), ^{
thumb.image = img;
});
}
}
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (image) thumb.image = image;
}];
}
}
......@@ -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
- (UIView *)createCardWithText:(NSString *)text frame:(CGRect)frame {
UIView *card = [[UIView alloc] initWithFrame:frame];
......
......@@ -3,6 +3,7 @@
#import <objc/runtime.h>
#import "GeneralInfoViewController.h"
#import "APIClient.h"
#import "ImageCacheHelper.h"
@interface GeneralInfoViewController () <UIDocumentInteractionControllerDelegate, UIScrollViewDelegate>
......@@ -764,15 +765,11 @@ typedef struct {
NSURL *url = [NSURL URLWithString:layoutImageURL];
if (url) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
imgView.image = img;
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (image) {
imgView.image = image;
}
});
}];
}
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showFullImage:)];
......
......@@ -3,6 +3,7 @@
#import "APIClient.h"
#import "APIConfig.h"
#import "UnitViewController.h"
#import "ImageCacheHelper.h"
@interface ProjectCardCell : UICollectionViewCell
@property (nonatomic, strong) UIImageView *imageView;
......@@ -160,7 +161,11 @@
forIndexPath:indexPath];
// 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];
......@@ -168,35 +173,20 @@
id projectName = project[@"project_name"];
cell.nameLabel.text = [projectName isKindOfClass:[NSString class]] ? projectName : @"";
// Get logo URL safely
NSString *logo = project[@"logo"];
NSString *thumb = project[@"logo_thumbnail"];
NSString *logoURLString = (logo.length > 0) ? logo :
(thumb.length > 0) ? thumb : nil;
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];
if (updateCell) {
updateCell.imageView.image = image;
}
});
}];
[task resume];
}
// Determine logo URL
NSString *logoURLString = project[@"logo"];
if (!logoURLString.length) logoURLString = project[@"logo_thumbnail"];
// Use ImageCacheHelper for async loading & caching
[ImageCacheHelper loadImageWithURL:[NSURL URLWithString:logoURLString ?: @""] completion:^(UIImage *image) {
if (image) {
// Ensure the cell is still visible
ProjectCardCell *updateCell = (ProjectCardCell *)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell) {
updateCell.imageView.image = image;
}
}
}];
return cell;
}
......
......@@ -4,6 +4,7 @@
#import <Foundation/Foundation.h>
#import "OfflineSyncManager.h"
#import "NetworkManager.h"
#import "ImageCacheHelper.h"
@interface AddIssueDetailsViewController () <UIImagePickerControllerDelegate, UINavigationControllerDelegate>
@property (nonatomic, strong) UIView *headerView;
......@@ -150,37 +151,34 @@
// 🖼 Prefill images under "first"
NSArray *firstImages = self.issueFullData[@"first"];
if ([firstImages isKindOfClass:[NSArray class]] && firstImages.count > 0) {
for (int i = 0; i < MIN(firstImages.count, 3); i++) {
NSDictionary *imgDict = firstImages[i];
NSString *urlStr = imgDict[@"thumb_image"] ?: imgDict[@"image"];
if (![urlStr isKindOfClass:[NSString class]]) continue;
for (int i = 0; i < MIN(firstImages.count, 3); i++) {
NSDictionary *imgDict = firstImages[i];
NSString *urlStr = imgDict[@"thumb_image"] ?: imgDict[@"image"];
if (![urlStr isKindOfClass:[NSString class]]) continue;
[self.existingImages addObject:urlStr];
NSURL *url = [NSURL URLWithString:urlStr];
if (!url) continue;
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
if (!img) return;
[self.existingImages addObject:urlStr];
NSURL *url = [NSURL URLWithString:urlStr];
if (!url) continue;
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error || !data) return;
UIImage *img = [UIImage imageWithData:data];
if (!img) return;
dispatch_async(dispatch_get_main_queue(), ^{
if (i < self.uploadedImages.count)
self.uploadedImages[i] = img;
else
[self.uploadedImages addObject:img];
UIButton *btn = [self.view viewWithTag:1000 + i];
if ([btn isKindOfClass:[UIButton class]]) {
[btn setImage:img forState:UIControlStateNormal];
btn.imageView.contentMode = UIViewContentModeScaleAspectFill;
btn.clipsToBounds = YES;
[self addRemoveButtonToUpload:btn atIndex:i];
}
});
}];
[task resume];
}
dispatch_async(dispatch_get_main_queue(), ^{
if (i < self.uploadedImages.count)
self.uploadedImages[i] = img;
else
[self.uploadedImages addObject:img];
UIButton *btn = [self.view viewWithTag:1000 + i];
if ([btn isKindOfClass:[UIButton class]]) {
[btn setImage:img forState:UIControlStateNormal];
btn.imageView.contentMode = UIViewContentModeScaleAspectFill;
btn.clipsToBounds = YES;
[self addRemoveButtonToUpload:btn atIndex:i];
}
});
}];
}
}
NSLog(@"✅ Prefilled Edit Mode UI successfully");
}
......
......@@ -4,6 +4,7 @@
#import "APIClient.h"
#import <AVFoundation/AVFoundation.h>
#import "IssueTutorialViewController.h"
#import "ImageCacheHelper.h"
@interface PlanViewController ()
......@@ -218,35 +219,41 @@
return;
}
NSLog(@"⬇️ Downloading 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;
}
NSLog(@"⬇️ Loading plan image from %@", self.planImageURL);
UIImage *image = [UIImage imageWithData:data];
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (!image) {
NSLog(@"⚠️ Could not decode image data");
NSLog(@"❌ Failed to load plan image from cache/network");
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
self.planImageView.image = image;
self.planImageView.userInteractionEnabled = YES;
UITapGestureRecognizer *tapRecognizer =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleTapOnPlan:)];
[self.planImageView addGestureRecognizer:tapRecognizer];
// 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 alloc] initWithTarget:self
action:@selector(handleTapOnPlan:)];
[self.planImageView addGestureRecognizer:tapRecognizer];
}
[self.scrollView setZoomScale:1.0 animated:NO];
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
[self renderAllLocations:self.locations];
NSLog(@"✅ Plan image displayed successfully");
});
}];
[task resume];
}
- (void)handleTapOnPlan:(UITapGestureRecognizer *)recognizer {
......@@ -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 {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
......
......@@ -5,6 +5,7 @@
#import "CalendarView.h"
#import "NSDate+CalendarHelpers.h"
#import "AddInfoViewController.h"
#import "ImageCacheHelper.h"
@interface HistoryViewController () <UITableViewDelegate, UITableViewDataSource>
......@@ -379,6 +380,7 @@
CGFloat imageSize = 56;
CGFloat spacing = 8;
// Inside your thumbnail loop
for (NSDictionary *imgDict in images) {
NSString *thumbURL = imgDict[@"thumb_image"];
if (thumbURL.length == 0) continue;
......@@ -389,22 +391,17 @@
thumbView.contentMode = UIViewContentModeScaleAspectFill;
thumbView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1];
// Load async
[thumbScroll addSubview:thumbView];
NSURL *url = [NSURL URLWithString:thumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
thumbView.image = img;
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (image) {
thumbView.image = image;
}
});
}];
[thumbScroll addSubview:thumbView];
x += imageSize + spacing;
}
thumbScroll.contentSize = CGSizeMake(x, imageSize);
return cell;
......
......@@ -2,6 +2,7 @@
#import "HistoryViewController.h"
#import "PlanViewController.h"
#import "WithdrawSingleIssueViewController.h"
#import "ImageCacheHelper.h"
@interface IssueDetailViewController () <UIScrollViewDelegate>
......@@ -342,31 +343,19 @@
[imageScroll addSubview:imgView];
NSURL *url = [NSURL URLWithString:urlStr];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
imgView.image = img;
});
}
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (image) imgView.image = image;
}];
}
NSString *planThumbURL = self.issueData[@"plan_image_thumb"];
if (planThumbURL.length > 0) {
NSURL *url = [NSURL URLWithString:planThumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
UIImage *img = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
self.floorPlanThumbnail.image = img;
});
}
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *image) {
if (image) self.floorPlanThumbnail.image = image;
}];
}
//thumbnail marker
NSString *x = self.issueData[@"pos_x"];
NSString *y = self.issueData[@"pos_y"];
......@@ -471,41 +460,35 @@
// Load full image asynchronously
NSURL *url = [NSURL URLWithString:planFullURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
if (!data) return;
UIImage *img = [UIImage imageWithData:data];
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
if (!img) return;
dispatch_async(dispatch_get_main_queue(), ^{
fullImageView.image = img;
NSString *xStr = self.issueData[@"pos_x"];
NSString *yStr = self.issueData[@"pos_y"];
if (xStr && yStr) {
CGFloat posX = [xStr floatValue];
CGFloat posY = [yStr floatValue];
// Calculate scale and offsets based on aspect-fit
CGSize imageSize = img.size;
CGSize viewSize = fullImageView.bounds.size;
CGFloat scale = MIN(viewSize.width / imageSize.width, viewSize.height / imageSize.height);
CGFloat offsetX = (viewSize.width - imageSize.width * scale) / 2.0;
CGFloat offsetY = (viewSize.height - imageSize.height * scale) / 2.0;
CGFloat dotX = posX * scale + offsetX;
CGFloat dotY = posY * scale + offsetY;
UIView *marker = [[UIView alloc] initWithFrame:CGRectMake(dotX - 5, dotY - 5, 10, 10)];
marker.backgroundColor = [UIColor systemRedColor];
marker.layer.cornerRadius = 5;
marker.layer.borderColor = [UIColor whiteColor].CGColor;
marker.layer.borderWidth = 1.5;
[fullImageView addSubview:marker];
}
});
});
fullImageView.image = img;
NSString *xStr = self.issueData[@"pos_x"];
NSString *yStr = self.issueData[@"pos_y"];
if (xStr && yStr) {
CGFloat posX = [xStr floatValue];
CGFloat posY = [yStr floatValue];
// Calculate scale and offsets based on aspect-fit
CGSize imageSize = img.size;
CGSize viewSize = fullImageView.bounds.size;
CGFloat scale = MIN(viewSize.width / imageSize.width, viewSize.height / imageSize.height);
CGFloat offsetX = (viewSize.width - imageSize.width * scale) / 2.0;
CGFloat offsetY = (viewSize.height - imageSize.height * scale) / 2.0;
CGFloat dotX = posX * scale + offsetX;
CGFloat dotY = posY * scale + offsetY;
UIView *marker = [[UIView alloc] initWithFrame:CGRectMake(dotX - 5, dotY - 5, 10, 10)];
marker.backgroundColor = [UIColor systemRedColor];
marker.layer.cornerRadius = 5;
marker.layer.borderColor = [UIColor whiteColor].CGColor;
marker.layer.borderWidth = 1.5;
[fullImageView addSubview:marker];
}
}];
}
- (void)closeFullFloorPlan {
......
......@@ -4,6 +4,7 @@
#import "IssueDetailViewController.h"
#import "WithdrawSingleIssueViewController.h"
#import "WithdrawIssuesViewController.h"
#import "ImageCacheHelper.h"
#pragma mark - Internal IssueCell
@interface IssueCell : UITableViewCell
......@@ -605,30 +606,23 @@
// --- Thumbnail Handling ---
if (thumbURL.length > 0) {
NSURL *url = [NSURL URLWithString:thumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
dispatch_async(dispatch_get_main_queue(), ^{
IssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return;
// Clear any "No image" labels
for (UIView *subview in updateCell.thumbView.subviews) {
[subview removeFromSuperview];
}
if (data) {
UIImage *img = [UIImage imageWithData:data];
if (img) {
updateCell.thumbView.image = img;
updateCell.thumbView.backgroundColor = UIColor.clearColor;
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
});
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
// Make sure cell is still visible
IssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return;
// Clear previous subviews / placeholders
for (UIView *subview in updateCell.thumbView.subviews) {
[subview removeFromSuperview];
}
if (img) {
updateCell.thumbView.image = img;
updateCell.thumbView.backgroundColor = UIColor.clearColor;
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
}];
} else {
[self setNoImagePlaceholder:cell.thumbView];
}
......
......@@ -3,6 +3,7 @@
#import "APIClient.h"
#import "WithdrawSingleIssueViewController.h"
#import "FilterViewController.h"
#import "ImageCacheHelper.h"
#pragma mark - custom cell structure
@interface WithdrawIssueCell : UITableViewCell
......@@ -631,34 +632,26 @@
// Thumbnail rendering (copied from IssueViewController)
if (thumbURL.length > 0) {
NSURL *url = [NSURL URLWithString:thumbURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
dispatch_async(dispatch_get_main_queue(), ^{
WithdrawIssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return;
// Clear any "No image" placeholder labels
for (UIView *subview in updateCell.thumbView.subviews) {
[subview removeFromSuperview];
}
if (data) {
UIImage *img = [UIImage imageWithData:data];
if (img) {
updateCell.thumbView.image = img;
updateCell.thumbView.backgroundColor = UIColor.clearColor;
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
});
});
[ImageCacheHelper loadImageWithURL:url completion:^(UIImage *img) {
WithdrawIssueCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (!updateCell) return;
// Clear any "No image" placeholder labels
for (UIView *subview in updateCell.thumbView.subviews) {
[subview removeFromSuperview];
}
if (img) {
updateCell.thumbView.image = img;
updateCell.thumbView.backgroundColor = UIColor.clearColor;
} else {
[self setNoImagePlaceholder:updateCell.thumbView];
}
}];
} else {
[self setNoImagePlaceholder:cell.thumbView];
}
// Text fields
cell.referenceLabel.text = issue[@"reference"] ?: @"";
cell.statusLabel.text = issue[@"status_external"] ?: @"";
......
......@@ -620,7 +620,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
[APIConfig projectId], [APIConfig drawingPlanId]);
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) {
__strong typeof(weakSelf) strongSelf2 = weakSelf;
......
......@@ -1738,10 +1738,6 @@
+ (void)requestArrayImage:(NSArray *)imageArray
completion:(void(^)(BOOL success, NSDictionary * _Nullable response, NSError * _Nullable error))completion {
if ([APIConfig handleOfflineForAPI:@"ArrayImage" completion:completion]) {
return;
}
// ✅ URL
NSURL *url = [APIConfig urlWithPath:@"/owner/offline/syncImageArray"];
......@@ -1795,10 +1791,6 @@
dispatch_async(dispatch_get_main_queue(), ^{
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);
}
});
......
// 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 @@
#import "LocalStorage.h"
#import "NetworkManager.h"
#import "APIClient.h"
#import "APIConfig.h"
static NSString * const kOfflineQueueKey = @"OFFLINE_SYNC_QUEUE";
static NSString * const kSelectedOfflineUnitsKey = @"SELECTED_OFFLINE_UNITS";
static NSString * const kSyncHistoryKey = @"SYNC_HISTORY_QUEUE";
static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
static NSString * const kSyncSelectedUnitsKey = @"SYNC_SELECTEDUNITS";
@implementation OfflineSyncManager
#pragma mark - Singleton
......@@ -542,7 +543,7 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
return;
}
NSArray *selectedUnits = [LocalStorage loadArrayForKey:kSelectedOfflineUnitsKey] ?: @[];
NSArray *selectedUnits = [self buildOfflineUnitPayloadForCurrentConfig] ?: @[];
// ------------------------------
// Build sequential tasks
......@@ -986,10 +987,33 @@ static NSString * const kLastOfflineSyncDateKey = @"LAST_OFFLINE_SYNC_DATE";
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);
}];
}
- (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
- (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