diff --git a/Theos/Classes/JDELogsVC.m b/Theos/Classes/JDELogsVC.m index 8d697b5..cb4bcf2 100644 --- a/Theos/Classes/JDELogsVC.m +++ b/Theos/Classes/JDELogsVC.m @@ -1,6 +1,7 @@ +#import #import "JDELogsVC.h" #import "JDESettingsManager.h" - +#import "../Utils/Logging/JELog.h" @interface JDELogsVC() @property (strong, nonatomic) UITextView *logView; @@ -14,10 +15,10 @@ - (void)viewDidLoad{ self.view.backgroundColor = UIColor.systemBackgroundColor; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] - initWithImage:[UIImage systemImageNamed:@"ellipsis"] - style:UIBarButtonItemStylePlain - target:self - action:@selector(didTapMore:)]; + initWithImage:[UIImage systemImageNamed:@"ellipsis"] + style:UIBarButtonItemStylePlain + target:self + action:@selector(didTapMore:)]; [self configureLogView]; [self loadLogs]; @@ -39,13 +40,58 @@ - (void)configureLogView{ ]]; } - (void)loadLogs{ - if([[NSFileManager defaultManager] fileExistsAtPath:self.manager.logFile]){ - self.logView.text = [NSString stringWithContentsOfFile:self.manager.logFile encoding:NSUTF8StringEncoding error:nil]; + if([[NSFileManager defaultManager] fileExistsAtPath:logFilePath()]){ + NSError *err; + + NSDictionary *logsDict = [NSDictionary + dictionaryWithContentsOfURL:[NSURL fileURLWithPath:logFilePath()] + error:&err]; + + if (err.code == NSFileReadCorruptFileError){ + [self clearLogs]; + } + else if (err != nil){ + self.logView.text = err.debugDescription; + return; + } + NSMutableArray *logs = [NSMutableArray new]; + for(NSData *log in logsDict[@"logs"]){ + [logs addObject:[[NSAttributedString alloc] initWithData:log + options:@{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType} + documentAttributes:nil + error:nil]]; + } + NSMutableAttributedString *finalLog = [NSMutableAttributedString new]; + static NSString * const newLineRaw = @"\n"; + NSAttributedString * const newLineAttr = [[NSAttributedString alloc] initWithString:newLineRaw]; + for (NSAttributedString *log in logs){ + [finalLog appendAttributedString:log]; + [finalLog appendAttributedString:newLineAttr]; + } + self.logView.attributedText = finalLog; } else{ self.logView.text = @"No Log File Found"; } } + +- (void)clearLogs{ + NSError *err; + [[NSFileManager defaultManager] removeItemAtPath:logFilePath() error:&err]; + if(err){ + UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"An error happened while deleting log file" + message:err.description + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction* action = [UIAlertAction actionWithTitle:@"Dismiss" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) {}]; + [alert addAction:action]; + [self presentViewController:alert animated:YES completion:nil]; + } + else{ + [self loadLogs]; + } +} - (void)didTapMore:(id)sender{ UIAlertController* alert = [UIAlertController alertControllerWithTitle:nil message:nil @@ -64,24 +110,7 @@ - (void)didTapMore:(id)sender{ UIAlertAction* clearAction = [UIAlertAction actionWithTitle:@"Clear Logs" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - dispatch_async(dispatch_get_main_queue(), ^{ - if(self.manager.logFileExists){ - NSError *err; - [[NSFileManager defaultManager] removeItemAtPath:self.manager.logFile error:&err]; - if(err){ - UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"An error happened while deleting log file" - message:err.description - preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction* action = [UIAlertAction actionWithTitle:@"Dismiss" - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) {}]; - [alert addAction:action]; - [self presentViewController:alert animated:YES completion:nil]; - } - else{ - [self loadLogs]; - } - }}); + dispatch_async(dispatch_get_main_queue(), ^{ [self clearLogs]; }); } ]; diff --git a/Theos/Classes/JDEMapView.h b/Theos/Classes/JDEMapView.h index 851277f..656d373 100644 --- a/Theos/Classes/JDEMapView.h +++ b/Theos/Classes/JDEMapView.h @@ -4,7 +4,7 @@ // // Created by Natheer on 08/07/2022. // - +#import #import #import #import diff --git a/Theos/Classes/JDEMapView.m b/Theos/Classes/JDEMapView.m index 9832e40..c7ee0ca 100644 --- a/Theos/Classes/JDEMapView.m +++ b/Theos/Classes/JDEMapView.m @@ -38,7 +38,17 @@ - (void)viewDidLoad { lpgr.minimumPressDuration = .5; [_mapView addGestureRecognizer:lpgr]; //add pin at 0, 0 - _pin = [MKPointAnnotation new]; + NSString *lastCoords = [JDESettingsManager.sharedInstance spoofedLocation]; + if(lastCoords != nil){ + NSArray *coordsArray = [lastCoords componentsSeparatedByString:@";"]; + CLLocationCoordinate2D coords = { + [coordsArray[0] doubleValue], + [coordsArray[1] doubleValue]}; + + self.pin = [[MKPointAnnotation alloc] initWithCoordinate:coords]; + } else { + _pin = [MKPointAnnotation new]; + } [_mapView addAnnotation:_pin]; //Overlay Button diff --git a/Theos/Classes/JDESettingsManager.h b/Theos/Classes/JDESettingsManager.h index cb385b3..357af38 100644 --- a/Theos/Classes/JDESettingsManager.h +++ b/Theos/Classes/JDESettingsManager.h @@ -2,12 +2,10 @@ #import #import #import "../Headers/AppHaptic.h" -#import @interface JDESettingsManager : NSObject @property (strong, nonatomic) NSUserDefaults *tweakSettings; -@property (strong, nonatomic, readonly) NSString *logFile; -@property (nonatomic, readonly) BOOL logFileExists; + + (JDESettingsManager *)sharedInstance; - (NSDictionary*)cellInfoForPath:(NSIndexPath*)indexPath; @@ -16,6 +14,5 @@ - (BOOL)featureStateForTag:(NSUInteger)row; - (void)featureStateChangedTo:(BOOL)newState forTag:(NSUInteger)tag; - (NSString*)localizedStringForKey:(NSString*)key; -- (void)logString:(NSString*)string; @end diff --git a/Theos/Classes/JDESettingsManager.m b/Theos/Classes/JDESettingsManager.m index 0a0641c..40fd5b4 100644 --- a/Theos/Classes/JDESettingsManager.m +++ b/Theos/Classes/JDESettingsManager.m @@ -7,8 +7,6 @@ @interface JDESettingsManager() @property (strong, nonatomic) NSBundle *bundle; -@property (strong, nonatomic, readwrite) NSString *logFile; -@property (nonatomic, readwrite) BOOL logFileExists; @end @implementation JDESettingsManager @@ -25,9 +23,7 @@ - (id) init{ _bundle = [NSBundle bundleWithPath:@"Library/Application Support/Jodel EMPROVED.bundle"]; } //logFile stuff - NSString *docsDir = [NSHomeDirectory() stringByAppendingPathComponent:@"tmp"]; - self.logFile = [docsDir stringByAppendingPathComponent:@"JDELogs.log"]; - self.logFileExists = [[NSFileManager defaultManager] fileExistsAtPath:self.logFile]; + } return self; } @@ -136,18 +132,5 @@ - (NSString*)localizedStringForKey:(NSString*)key{ return [_bundle localizedStri - (NSString*)pathForImageWithName:(NSString*)name{ return [_bundle pathForResource:name ofType:@"png" inDirectory:@"Icons"]; } -- (void)logString:(NSString*)string{ - string = [[NSString stringWithFormat:@"[%@] ", [[NSDate now] description]] stringByAppendingString:string]; - string = [string stringByAppendingString:@"\n"]; - NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.logFile]; - if(fileHandle){ - [fileHandle seekToEndOfFile]; - [fileHandle writeData:[string dataUsingEncoding:NSUTF8StringEncoding]]; - [fileHandle closeFile]; - } - else{ - [string writeToFile:self.logFile atomically:YES encoding:NSUTF8StringEncoding error:nil]; - } -} @end \ No newline at end of file diff --git a/Theos/Classes/ThemingViewController.h b/Theos/Classes/ThemingViewController.h index 96c35e5..b530cf6 100644 --- a/Theos/Classes/ThemingViewController.h +++ b/Theos/Classes/ThemingViewController.h @@ -8,16 +8,6 @@ @end -#define main "mainColor" -#define secondary "customLightGrayColor" -#define user "meColor" -#define userDot "channelNotification" -#define channel "channelColor" -#define channelDot "declineColor" // channel and notification Dots -#define notification "notificationColor" -#define pollCell "pollBgColor" - - typedef NS_ENUM(NSUInteger, ThemeOption){ ThemeOptionMainColor, ThemeOptionSecondaryColor, diff --git a/Theos/Makefile b/Theos/Makefile index a4434f6..7300828 100644 --- a/Theos/Makefile +++ b/Theos/Makefile @@ -6,6 +6,7 @@ include $(THEOS)/makefiles/common.mk TWEAK_NAME = JodelEmproved JodelEmproved_FILES = Tweak.x JDEViewController.m $(wildcard Classes/*.m) +JodelEmproved_FILES += Utils/Logging/JELog.m JodelEmproved_CFLAGS = -fobjc-arc -Wno-unguarded-availability-new -DPACKAGE_VERSION='@"$(THEOS_PACKAGE_BASE_VERSION)"' diff --git a/Theos/Tweak.x b/Theos/Tweak.x index 08a9897..b864e83 100644 --- a/Theos/Tweak.x +++ b/Theos/Tweak.x @@ -1,4 +1,5 @@ #import "Tweak.h" +#import "Utils/Logging/JELog.h" #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending) // https://stackoverflow.com/a/5337804 @@ -11,22 +12,22 @@ - (void)viewDidLoad{ %orig; @try{ - [[JDESettingsManager sharedInstance] logString:@"Adding setting button"]; + JELog(@"Adding setting button"); MainFeedViewController *usableSelf = self; UIBarButtonItem *statsButton = [[UIBarButtonItem alloc] initWithTitle: [[JDESettingsManager sharedInstance] localizedStringForKey:@"emproved"] - style:UIBarButtonItemStyleDone target:self - action:@selector(presentJDEViewController:)]; + style:UIBarButtonItemStyleDone target:self + action:@selector(presentJDEViewController:)]; usableSelf.navigationItem.leftBarButtonItem = statsButton; } @catch(NSException *exception){ - [[JDESettingsManager sharedInstance] logString:[NSString stringWithFormat:@"Failed to add settings button: %@", exception]]; + JELog(@"Failed to add settings button: %@", exception); } } %new -(void)presentJDEViewController:(id)sender{ - [[JDESettingsManager sharedInstance] logString:@"Presenting settings VC"]; + JELog(@"Presenting settings VC"); JDEViewController *settingsVC = [JDEViewController new]; settingsVC.title = [[JDESettingsManager sharedInstance] localizedStringForKey:@"emproved"]; UINavigationController *navVC = [[UINavigationController alloc] initWithRootViewController:settingsVC]; @@ -45,7 +46,7 @@ return YES; if (action == @selector(selectAll:)) return YES; - } + } return %orig; } @@ -58,7 +59,7 @@ PictureFeedViewController *usuableSelf = self; %orig; if([[JDESettingsManager sharedInstance] featureStateForTag:0]){ - [[JDESettingsManager sharedInstance] logString:@"Adding image save button"]; + JELog(@"Adding image save button"); UIView *view = [self viewIfLoaded]; UIButton *btn = [[[JDEButtons alloc] init] buttonWithImageNamed:@"arrow.down.circle.fill"]; [btn addTarget:self action:@selector(JDEsaveImage:) forControlEvents:UIControlEventTouchUpInside]; @@ -72,7 +73,7 @@ %new - (bool)JDEsaveImage:(id)sender{ @try{ - [[JDESettingsManager sharedInstance] logString:@"Saving image"]; + JELog(@"Saving image"); UIView *view = [self view]; view = [[view subviews] firstObject]; UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0f); @@ -80,11 +81,11 @@ UIImage *capturedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); UIImageWriteToSavedPhotosAlbum(capturedImage, nil, nil, nil); - [[JDESettingsManager sharedInstance] logString:@"Saved image"]; + JELog(@"Saved image"); return YES; } @catch(NSException *exception){ - [[JDESettingsManager sharedInstance] logString:[NSString stringWithFormat:@"Failed to save image: %@", exception]]; + JELog(@"Failed to save image: %@", exception); return NO; } } @@ -94,21 +95,11 @@ //Enable Uploading From Gallery %hook ImageCaptureViewController -- (void)viewDidLoad{ - %orig; +- (void)handleGalleryTap:(id)sender{ if([[JDESettingsManager sharedInstance] featureStateForTag:1]){ - [[JDESettingsManager sharedInstance] logString:@"Adding image upload button"]; - UIView *view = [self viewIfLoaded]; - UIButton *realGalleryBtn = nil; - for(id temp in view.subviews){ - if([temp isMemberOfClass:objc_getClass("Jodel.ButtonWithBanner")]){ realGalleryBtn = temp; } - } - UIButton *btn = [[[JDEButtons alloc] init] buttonWithImageNamed:@"arrow.up.circle.fill"]; - [btn addTarget:self action:@selector(JDEuploadImage:) forControlEvents:UIControlEventTouchUpInside]; - [view addSubview:btn]; - //Constraints - [btn.trailingAnchor constraintEqualToAnchor:realGalleryBtn.safeAreaLayoutGuide.leadingAnchor constant:-20].active = YES; - [btn.bottomAnchor constraintEqualToAnchor:realGalleryBtn.safeAreaLayoutGuide.bottomAnchor constant:-7].active = YES; + [self JDEuploadImage:sender]; + }else{ + %orig; } } @@ -116,7 +107,7 @@ -(void)JDEuploadImage:(id)sender{ // IOS 14+ Only!!!! if(SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"14")){ - [[JDESettingsManager sharedInstance] logString:@"Selecting image to upload iOS 14+"]; + JELog(@"Selecting image to upload iOS 14+"); PHPickerConfiguration *config = [[PHPickerConfiguration alloc] init]; config.selectionLimit = 1; config.filter = [PHPickerFilter imagesFilter]; @@ -127,7 +118,7 @@ } // IOS 13 else { - [[JDESettingsManager sharedInstance] logString:@"Selecting image to upload iOS 13-"]; + JELog(@"Selecting image to upload iOS 13-"); UIImagePickerController *picker = [UIImagePickerController new]; picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; picker.delegate = self; @@ -144,9 +135,9 @@ [result.itemProvider loadObjectOfClass:UIImage.self completionHandler:^(UIImage* image, NSError *error){ if(error){ - [[JDESettingsManager sharedInstance] logString:[NSString stringWithFormat:@"Failed to select image iOS 14+: %@", error.description]]; + JELog(@"Failed to select image iOS 14+: %@", error.description); } else{ - [[JDESettingsManager sharedInstance] logString:@"Selected image to upload iOS 14+"]; + JELog(@"Selected image to upload iOS 14+"); [self loadImage:image]; } }]; @@ -155,7 +146,7 @@ %new - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{ [picker dismissViewControllerAnimated:YES completion:^(){ - [[JDESettingsManager sharedInstance] logString:@"Selected image to upload iOS 13-"]; + JELog(@"Selected image to upload iOS 13-"); [self loadImage:info[UIImagePickerControllerOriginalImage]]; }]; } @@ -167,7 +158,7 @@ %new - (void)loadImage:(UIImage*)image{ - [[JDESettingsManager sharedInstance] logString:@"Loading selected image"]; + JELog(@"Loading selected image"); dispatch_async(dispatch_get_main_queue(), ^{ // JDLAVCamCaptureManager is the captureManager iVar but its swift not hook-able [self captureManagerStillImageCaptured:[NSClassFromString(@"JDLAVCamCaptureManager") new] image:image]; @@ -179,7 +170,7 @@ %hook ScreenshotService - (void)madeScreenshot{ if([[JDESettingsManager sharedInstance] featureStateForTag:6]){ - [[JDESettingsManager sharedInstance] logString:@"Bypassed Screenshot"]; + JELog(@"Bypassed Screenshot"); return; } return %orig; @@ -192,7 +183,7 @@ - (id)lastStoredUserLocation{ if([[JDESettingsManager sharedInstance] featureStateForTag:2]){ NSString *spoofedLoc = [[JDESettingsManager sharedInstance] spoofedLocation]; - [[JDESettingsManager sharedInstance] logString:[NSString stringWithFormat:@"Spoofed location to %@", spoofedLoc]]; + JELog(@"Spoofed location to %@", spoofedLoc); return spoofedLoc; } return %orig; @@ -204,7 +195,7 @@ - (void)tappedSend{ if([[JDESettingsManager sharedInstance] featureStateForTag:5]){ - [[JDESettingsManager sharedInstance] logString:@"Confirm reply called"]; + JELog(@"Confirm reply called"); UIAlertController* alert = [UIAlertController alertControllerWithTitle:[[JDESettingsManager sharedInstance] localizedStringForKey:@"confirm_reply_title"] message:nil preferredStyle:UIAlertControllerStyleAlert]; @@ -225,7 +216,7 @@ - (void)downvoteTap:(id)sender{ if([[JDESettingsManager sharedInstance] featureStateForTag:4]){ - [[JDESettingsManager sharedInstance] logString:@"Confirm downvote called"]; + JELog(@"Confirm downvote called"); UIViewController *topVC = [self firstAvailableUIViewController:self]; UIAlertController* alert = [UIAlertController alertControllerWithTitle:[[JDESettingsManager sharedInstance] localizedStringForKey:@"confirm_vote_title"] message:nil @@ -242,7 +233,7 @@ } - (void)upvoteTap:(id)sender{ if([[JDESettingsManager sharedInstance] featureStateForTag:4]){ - [[JDESettingsManager sharedInstance] logString:@"Confirm upvote called"]; + JELog(@"Confirm upvote called"); UIViewController *topVC = [self firstAvailableUIViewController:self]; UIAlertController* alert = [UIAlertController alertControllerWithTitle:[[JDESettingsManager sharedInstance] localizedStringForKey:@"confirm_vote_title"] message:nil @@ -287,34 +278,38 @@ @"Jodel.JDLPostDetailsPostMediaCell"]; for(NSString *bannedClass in bannedClasses){ if([cellClass isEqualToString:bannedClass]){ - [[JDESettingsManager sharedInstance] logString:[NSString stringWithFormat:@"don't show context menu for post (%@)", cellClass]]; + JELog(@"don't show context menu for post (%@)", cellClass); return nil; } } UIAction *copy = [UIAction actionWithTitle:[[JDESettingsManager sharedInstance] localizedStringForKey:@"copy"] - image:[UIImage systemImageNamed:@"doc.on.doc"] identifier:nil handler:^(UIAction *handler) { - //Handle copying for normal and poll main feed cells - if([cellClass isEqualToString:@"Jodel.JDLFeedPostCell"] - || [cellClass isEqualToString:@"Jodel.FeedPollCell"]){ - UIPasteboard.generalPasteboard.string = [[[cell.contentView subviews][1] contentLabel] text]; - [[JDESettingsManager sharedInstance] logString:[NSString stringWithFormat:@"Successfully copied for (%@)", cellClass]]; - } - //Copying for sub posts - else if([cellClass isEqualToString:@"Jodel.JDLPostDetailsPostCell"]){ - //Change cell type to access methods interfaced methods - JDLPostDetailsPostCell *cell = [tableView cellForRowAtIndexPath:indexPath]; - UIPasteboard.generalPasteboard.string = [[cell contentLabel] text]; - [[JDESettingsManager sharedInstance] logString:[NSString stringWithFormat:@"Successfully copied for (%@)", cellClass]]; - } - else{ - [[JDESettingsManager sharedInstance] logString:[NSString stringWithFormat:@"Failed to copy for (%@)", cellClass]]; - } - }]; - - [[JDESettingsManager sharedInstance] logString:[NSString stringWithFormat:@"show context menu for post (%@)", cellClass]]; + image:[UIImage systemImageNamed:@"doc.on.doc"] identifier:nil handler:^(UIAction *handler) { + //Handle copying for normal and poll main feed cells + if([cellClass isEqualToString:@"Jodel.JDLFeedPostCell"] + || [cellClass isEqualToString:@"Jodel.FeedPollCell"]){ + UIPasteboard.generalPasteboard.string = [[[cell.contentView subviews][1] contentLabel] text]; + JELog(@"Successfully copied for (%@)", cellClass); + } + //Copying for sub posts + else if([cellClass isEqualToString:@"Jodel.JDLPostDetailsPostCell"]){ + //Change cell type to access methods interfaced methods + JDLPostDetailsPostCell *cell = [tableView cellForRowAtIndexPath:indexPath]; + UIPasteboard.generalPasteboard.string = [[cell contentLabel] text]; + JELog(@"Successfully copied for (%@)", cellClass); + } + else{ + JELog(@"Failed to copy for (%@)", cellClass); + } + }]; + + JELog(@"show context menu for post (%@)", cellClass); UIMenu *menu = [UIMenu menuWithTitle:@"" children:@[copy]]; - UIContextMenuConfiguration *menuConfig = [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil actionProvider:^(NSArray *suggestedActions) { return menu;} + UIContextMenuConfiguration *menuConfig = [UIContextMenuConfiguration + configurationWithIdentifier:nil + previewProvider:nil + actionProvider:^(NSArray *suggestedActions) + { return menu;} ]; return menuConfig; diff --git a/Theos/Utils/Logging/JELog.h b/Theos/Utils/Logging/JELog.h new file mode 100644 index 0000000..0d46988 --- /dev/null +++ b/Theos/Utils/Logging/JELog.h @@ -0,0 +1,7 @@ +#import +#import + +NSString *logFilePath(void); + +void JELog(NSString *format, ...); + diff --git a/Theos/Utils/Logging/JELog.m b/Theos/Utils/Logging/JELog.m new file mode 100644 index 0000000..b7d705f --- /dev/null +++ b/Theos/Utils/Logging/JELog.m @@ -0,0 +1,71 @@ +#import "JELog.h" +#import + +NSString *logFilePath(void){ + return [NSTemporaryDirectory() stringByAppendingPathComponent:@"JDELogs.log"]; +} + +static void writeString(__kindof NSAttributedString *string){ + + NSError *error; + NSRange stringRange = NSMakeRange(0, string.length); + NSData *stringData = [string dataFromRange:stringRange + documentAttributes:@{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType} + error:&error]; + if(stringData == nil){ + return; + } + NSURL * const logFile = [NSURL fileURLWithPath:logFilePath()]; + NSData *logsDictData = [NSData dataWithContentsOfURL:logFile]; + + + if (logsDictData == nil){ + NSDictionary *logsDict = @{@"logs": @[stringData]}; + [logsDict writeToURL:logFile atomically:YES]; + return; + } + NSMutableDictionary *logsDict = [NSPropertyListSerialization propertyListWithData:logsDictData + options:NSPropertyListMutableContainersAndLeaves + format:nil + error:&error]; + + if (error != nil){ + return; + } + [logsDict[@"logs"] addObject:stringData]; + + [logsDict writeToURL:logFile atomically:YES]; + +} + +// https://codereview.stackexchange.com/a/48207 +void JELog(NSString *format, ...){ + static NSDateFormatter* timeStampFormat; + if (!timeStampFormat) { + timeStampFormat = [[NSDateFormatter alloc] init]; + [timeStampFormat setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"]; + [timeStampFormat setTimeZone:[NSTimeZone systemTimeZone]]; + } + + NSDictionary *attrs = @{ + NSForegroundColorAttributeName: UIColor.systemYellowColor + }; + + NSString *dateString = [NSString stringWithFormat:@"[%@]", [NSDate date]]; + NSAttributedString *timestamp = [[NSAttributedString alloc] + initWithString:dateString attributes:attrs]; + + va_list vargs; + va_start(vargs, format); + NSString* formattedMessage = [[NSString alloc] initWithFormat:format arguments:vargs]; + va_end(vargs); + formattedMessage = [NSString stringWithFormat:@" %@", formattedMessage]; + NSAttributedString *attrFormattedMessage = [[NSAttributedString alloc] initWithString:formattedMessage + attributes:@{NSForegroundColorAttributeName: UIColor.labelColor}]; + + + NSMutableAttributedString *message = [[NSMutableAttributedString alloc] initWithAttributedString:timestamp]; + [message appendAttributedString:attrFormattedMessage]; + + writeString(message); +} \ No newline at end of file