diff --git a/Airpad.xcodeproj/project.pbxproj b/Airpad.xcodeproj/project.pbxproj index 2b2758e..396f390 100644 --- a/Airpad.xcodeproj/project.pbxproj +++ b/Airpad.xcodeproj/project.pbxproj @@ -52,6 +52,8 @@ CE5210F9146F422F00EAF9F9 /* DGSwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = CE5210C9146F422E00EAF9F9 /* DGSwitch.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; CE521111146F422F00EAF9F9 /* slider.png in Resources */ = {isa = PBXBuildFile; fileRef = CE5210F8146F422E00EAF9F9 /* slider.png */; }; CE9026901470CB02005070F7 /* AirbrakeView.m in Sources */ = {isa = PBXBuildFile; fileRef = CE90268F1470CB02005070F7 /* AirbrakeView.m */; }; + CEA677991477A9CE00D39569 /* AirbrakeDateView.m in Sources */ = {isa = PBXBuildFile; fileRef = CEA677981477A9CE00D39569 /* AirbrakeDateView.m */; }; + CEA677A51478867E00D39569 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEA677A41478867E00D39569 /* QuartzCore.framework */; }; CEB9A64A146D08D6008901DB /* AirbrakeModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CEB9A648146D08D6008901DB /* AirbrakeModel.xcdatamodeld */; }; CEC44B86146F568D000CB29D /* DataTableDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CEC44B85146F568D000CB29D /* DataTableDelegate.m */; }; CEC636DB146CF7FE0036FA02 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEC636DA146CF7FE0036FA02 /* UIKit.framework */; }; @@ -158,6 +160,9 @@ CE521112146F424000EAF9F9 /* DGSwitch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DGSwitch.h; sourceTree = ""; }; CE90268E1470CB02005070F7 /* AirbrakeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AirbrakeView.h; sourceTree = ""; }; CE90268F1470CB02005070F7 /* AirbrakeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AirbrakeView.m; sourceTree = ""; }; + CEA677971477A9CE00D39569 /* AirbrakeDateView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AirbrakeDateView.h; sourceTree = ""; }; + CEA677981477A9CE00D39569 /* AirbrakeDateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AirbrakeDateView.m; sourceTree = ""; }; + CEA677A41478867E00D39569 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; CEB9A649146D08D6008901DB /* AirbrakeModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AirbrakeModel.xcdatamodel; sourceTree = ""; }; CEC44B84146F568D000CB29D /* DataTableDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DataTableDelegate.h; sourceTree = ""; }; CEC44B85146F568D000CB29D /* DataTableDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DataTableDelegate.m; sourceTree = ""; }; @@ -185,6 +190,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CEA677A51478867E00D39569 /* QuartzCore.framework in Frameworks */, CE00EA8F146CF9D4004AB0EC /* MessageUI.framework in Frameworks */, CEC636DB146CF7FE0036FA02 /* UIKit.framework in Frameworks */, CEC636DD146CF7FE0036FA02 /* Foundation.framework in Frameworks */, @@ -304,6 +310,8 @@ CE5210C9146F422E00EAF9F9 /* DGSwitch.m */, CE521112146F424000EAF9F9 /* DGSwitch.h */, CE5210F8146F422E00EAF9F9 /* slider.png */, + CEA677971477A9CE00D39569 /* AirbrakeDateView.h */, + CEA677981477A9CE00D39569 /* AirbrakeDateView.m */, CEC44B84146F568D000CB29D /* DataTableDelegate.h */, CEC44B85146F568D000CB29D /* DataTableDelegate.m */, CE00E9F6146CF876004AB0EC /* AirbrakeDetailView.xib */, @@ -387,6 +395,7 @@ CEC636D9146CF7FE0036FA02 /* Frameworks */ = { isa = PBXGroup; children = ( + CEA677A41478867E00D39569 /* QuartzCore.framework */, CE00EA8E146CF9D4004AB0EC /* MessageUI.framework */, CEC636DA146CF7FE0036FA02 /* UIKit.framework */, CEC636DC146CF7FE0036FA02 /* Foundation.framework */, @@ -596,6 +605,7 @@ CE5210F9146F422F00EAF9F9 /* DGSwitch.m in Sources */, CEC44B86146F568D000CB29D /* DataTableDelegate.m in Sources */, CE9026901470CB02005070F7 /* AirbrakeView.m in Sources */, + CEA677991477A9CE00D39569 /* AirbrakeDateView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Airpad/AirbrakeDateView.h b/Airpad/AirbrakeDateView.h new file mode 100644 index 0000000..005fdd6 --- /dev/null +++ b/Airpad/AirbrakeDateView.h @@ -0,0 +1,17 @@ +// +// AirbrakeDateView.h +// Airpad +// +// Created by Conrad Irwin on 19/11/2011. +// Copyright (c) 2011 Rapportive. All rights reserved. +// + +#import +#import + +@interface AirbrakeDateView : UIView +@property (nonatomic, strong) NSDate *startDate; +@property (nonatomic, strong) NSDate *endDate; +@property (nonatomic, assign) NSInteger count; + +@end diff --git a/Airpad/AirbrakeDateView.m b/Airpad/AirbrakeDateView.m new file mode 100644 index 0000000..e41988d --- /dev/null +++ b/Airpad/AirbrakeDateView.m @@ -0,0 +1,209 @@ +// +// AirbrakeDateView.m +// Airpad +// +// Created by Conrad Irwin on 19/11/2011. +// Copyright (c) 2011 Rapportive. All rights reserved. +// + +#import "AirbrakeDateView.h" +#import "NSDate+DateISOParser.h" + +#define DAYS_TO_SHOW 35 + +@implementation AirbrakeDateView +@synthesize startDate; +@synthesize endDate; +@synthesize count; + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + // Initialization code + } + return self; +} + +- (bool) isWeekend:(NSDate*)date +{ + NSCalendar *cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; + NSDateComponents *day = [cal components:NSWeekdayCalendarUnit fromDate: date]; + return ([day weekday] == 1 || [day weekday] == 7); +} + +- (UIFont*)font +{ + return [UIFont systemFontOfSize:12]; +} + +- (NSString*) formatDate:(NSDate*)date +{ + NSDateFormatter *fmt = [[NSDateFormatter alloc] init]; + [fmt setDateFormat: @"d MMM HH:mm"]; + return[fmt stringFromDate:date]; +} +- (NSString*) getYear:(NSDate*) date +{ + NSDateFormatter *fmt = [[NSDateFormatter alloc] init]; + [fmt setDateFormat: @"Y"]; + return [fmt stringFromDate:date]; +} + +- (CGFloat) dayOffset +{ + NSCalendar *cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; + NSDateComponents *day = [cal components:NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit fromDate: [NSDate date]]; + return (((day.hour * 60.0) + day.minute) * 60.0 + day.second) / 86400.0; +} + +- (NSDate*) startOfLine +{ + return [NSDate dateWithTimeInterval: 0 - (DAYS_TO_SHOW * 86400.0) sinceDate:[NSDate date]]; +} + +- (bool) date:(NSDate*)date isSameTime:(NSDate*)other +{ + return [[self formatDate:date] isEqualToString: [self formatDate: other]]; +} + +- (bool) date:(NSDate*)date isSameDate:(NSDate*)other +{ + NSDateFormatter *fmt = [[NSDateFormatter alloc] init]; + [fmt setDateFormat: @"d mmm Y"]; + return [[fmt stringFromDate:date] isEqualToString: [fmt stringFromDate:other]]; +} + +- (NSString*) countFormatted +{ + if (self.count == 1) { + return @"(once only)"; + } else if (self.count == 2) { + return @"(twice only)"; + } else { + return [NSString stringWithFormat:@"(%i times)", count]; + } +} + +// Only override drawRect: if you perform custom drawing. +// An empty implementation adversely affects performance during animation. +- (void)drawRect:(CGRect)rect +{ + [super drawRect:rect]; + CGContextRef con = UIGraphicsGetCurrentContext(); + NSDate* startOfLine = [self startOfLine]; + CGFloat mid = [self frame].size.height * 2.5 / 4.0; + CGFloat prehistoryDistance = 16.0; + CGFloat padding = 85.0; // Enough space for a dashWidth and date to appear on the right-hand-side of the line. + CGFloat day = ([self frame].size.width - 2.0 * padding - prehistoryDistance) / (DAYS_TO_SHOW); + CGFloat offset = padding + prehistoryDistance + day * (1.0 - [self dayOffset]); + CGFloat dashTop = [self frame].size.height / 4.0; + CGFloat dashWidth = day / 3.0; + CGFloat weekBottom = [self frame].size.height * 6.0 / 8.0; + CGFloat weekendBottom = [self frame].size.height; + CGFloat bottom; + + // Draw: the horizontal line + CGContextMoveToPoint(con, padding + prehistoryDistance, mid); + CGContextAddLineToPoint(con, [self frame].size.width - padding, mid); + + // Draw: the per-day ticks + for(NSInteger i = 0; i < DAYS_TO_SHOW; i++) { + NSDate *date = [NSDate dateWithTimeInterval: ((i + 1) * 86400.0) sinceDate:startOfLine]; + if ([self isWeekend: date]) { + bottom = weekendBottom; + } else { + bottom = weekBottom; + } + CGContextMoveToPoint(con, i * day + offset, mid); + CGContextAddLineToPoint(con, i * day + offset, bottom); + } + + CGContextSetLineWidth(con, 0.5); + CGContextStrokePath(con); + + // Draw: the dotted prehistory line at the front + CGContextMoveToPoint(con, padding, mid); + CGContextAddLineToPoint(con, padding + prehistoryDistance, mid); + CGFloat dashes[] = {2.0, 2.0}; + CGContextSetLineDash(con, 0.0, dashes, 2); + CGContextSetLineWidth(con, 0.5); + CGContextStrokePath(con); + CGContextSetLineDash(con, 0.0, NULL, 0); + + if (!startDate || !endDate) { + return; + } + + // Draw: The upwards dash for start, with label + CGFloat startDistance = MAX(padding, padding + prehistoryDistance + day * ([startDate timeIntervalSinceDate:startOfLine] / 86400.0)); + NSString* startFormatted = [self formatDate: startDate]; + CGSize startSize = [startFormatted sizeWithFont:[self font]]; + + CGContextMoveToPoint(con, startDistance, mid); + CGContextAddLineToPoint(con, startDistance, dashTop); + CGContextAddLineToPoint(con, startDistance - dashWidth, dashTop); + [startFormatted drawInRect:CGRectMake(startDistance - dashWidth - startSize.width - 5.0, 0.0, startSize.width, startSize.height) withFont:[self font]]; + + if (startDistance <= padding + prehistoryDistance) { + NSString *year = [self getYear: endDate]; + CGSize yearSize = [year sizeWithFont:[self font]]; + [year drawInRect: CGRectMake(startDistance - dashWidth - 5.0 - (startSize.width + yearSize.width) / 2.0, startSize.height, yearSize.width, yearSize.height) withFont:[self font]]; + } + + // Draw: the upwards dash for end, with label + CGFloat endDistance = MAX(padding, padding + prehistoryDistance + day * ([endDate timeIntervalSinceDate:startOfLine] / 86400.0)); + NSString *endFormatted; + + if ([self date:startDate isSameTime:endDate]) { + endFormatted = [self countFormatted]; + } else { + endFormatted = [self formatDate: endDate]; + } + CGSize endSize = [endFormatted sizeWithFont:[self font]]; + CGContextMoveToPoint(con, endDistance, mid); + CGContextAddLineToPoint(con, endDistance, dashTop); + CGContextAddLineToPoint(con, endDistance + dashWidth, dashTop); + [endFormatted drawInRect:CGRectMake(endDistance + dashWidth + 5.0, 0.0, endSize.width, endSize.height) withFont:[self font]]; + + + // Draw: the occurance rate. + if ([self count] > 1 && ![self date:startDate isSameTime:endDate]) { + NSString *rateString; + if ([self count] == 2) { + rateString = @"(twice only)"; + } else if ([self date:startDate isSameDate: endDate]) { + rateString = [NSString stringWithFormat: @"(%i times)", count]; + } else { + rateString = [NSString stringWithFormat: @"(%i times, %0.2f/day)", count, count * 86400.0 / ([endDate timeIntervalSinceDate:startDate]) ]; + } + + CGSize rateSize = [rateString sizeWithFont: [self font]]; + + CGFloat midSpace = endDistance - startDistance; + CGFloat leftSpace = startDistance - startSize.width; + CGFloat rightSpace = self.frame.size.width - endDistance - endSize.width; + CGFloat ratePosition; + + // Position the rate label, either + // 1. In the middle if it fits (for awesomeness) + // 2. To the left or right, whichever there's more space (if it fits) + // 3. InĀ” the middle if it doesn't fit anywhere, for symetrical disaster. + if (rateSize.width + 10.0 < midSpace) { + ratePosition = startDistance + (midSpace - rateSize.width) / 2.0; + } else if (rateSize.width + 10.0 < leftSpace && leftSpace >= rightSpace) { + ratePosition = (leftSpace - rateSize.width) / 2.0; + } else if (rateSize.width + 10.0 < rightSpace) { + ratePosition = endDistance + (rightSpace - rateSize.width) / 2.0; + } else { + ratePosition = startDistance + (midSpace - rateSize.width) / 2.0; + } + + [rateString drawInRect:CGRectMake(ratePosition, 0.0, rateSize.width, rateSize.height) withFont:[self font]]; + } + + CGContextSetLineWidth(con, 0.5); + CGContextStrokePath(con); +} + +@end diff --git a/Airpad/AirbrakeDetailView.xib b/Airpad/AirbrakeDetailView.xib index fa99815..491e3b8 100644 --- a/Airpad/AirbrakeDetailView.xib +++ b/Airpad/AirbrakeDetailView.xib @@ -45,12 +45,13 @@ 290 {703, 197} + _NS:212 3 MQA - + 2 @@ -65,6 +66,7 @@ 292 {{248, 8}, {207, 30}} + _NS:289 NO @@ -96,6 +98,7 @@ {703, 44} + _NS:372 NO @@ -136,6 +139,7 @@ 292 {{23, 56}, {548, 93}} + _NS:345 NO @@ -144,7 +148,7 @@ NO IBIPadFramework Label - + 1 MCAwIDAAA @@ -163,39 +167,13 @@ 16 - - - 292 - {{20, 157}, {663, 35}} - - - _NS:345 - NO - YES - 7 - NO - IBIPadFramework - Label - - - 1 - 10 - - 1 - 17 - - - Helvetica - 17 - 16 - - 289 {{579, 64}, {103, 27}} - + + _NS:212 3 @@ -208,6 +186,7 @@ 292 {{15, 193}, {688, 575}} + _NS:640 @@ -241,6 +220,7 @@ 274 {{0, 193}, {703, 575}} + _NS:408 YES @@ -254,9 +234,25 @@ 10 10 + + + 292 + {{7, 155}, {689, 28}} + + + + _NS:212 + + 3 + MQA + + + IBIPadFramework + {703, 768} + 3 @@ -324,14 +320,6 @@ 32 - - - occurrenceLabel - - - - 17 - titleLabel @@ -364,6 +352,14 @@ 10 + + + dateView + + + + 50 + projectsClicked: @@ -412,12 +408,12 @@ - + @@ -454,11 +450,6 @@ - - 13 - - - 12 @@ -509,6 +500,11 @@ + + 48 + + + @@ -518,7 +514,6 @@ com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -532,15 +527,125 @@ com.apple.InterfaceBuilder.IBCocoaTouchPlugin AirbrakeView com.apple.InterfaceBuilder.IBCocoaTouchPlugin + AirbrakeDateView + com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin - 47 + 50 + + + + + AirbrakeDateView + UIView + + IBProjectSource + ./Classes/AirbrakeDateView.h + + + + AirbrakeDetailViewController + UIViewController + + id + id + id + id + + + + openClicked: + id + + + projectsClicked: + id + + + resolveStateChanged: + id + + + viewChangerChanged: + id + + + + UITextView + UITableView + AirbrakeDateView + UILabel + UIBarButtonItem + DGSwitch + UILabel + UIToolbar + UISegmentedControl + + + + backtraceText + UITextView + + + dataTable + UITableView + + + dateView + AirbrakeDateView + + + occurrenceLabel + UILabel + + + projectMenuButton + UIBarButtonItem + + + resolveSlider + DGSwitch + + + titleLabel + UILabel + + + toolbar + UIToolbar + + + viewChanger + UISegmentedControl + + + + IBProjectSource + ./Classes/AirbrakeDetailViewController.h + + + + AirbrakeView + UIView + + IBProjectSource + ./Classes/AirbrakeView.h + + + + DGSwitch + UIControl + + IBProjectSource + ./Classes/DGSwitch.h + + + - 0 IBIPadFramework YES diff --git a/Airpad/AirbrakeDetailViewController.h b/Airpad/AirbrakeDetailViewController.h index 028417c..6657f79 100644 --- a/Airpad/AirbrakeDetailViewController.h +++ b/Airpad/AirbrakeDetailViewController.h @@ -13,6 +13,7 @@ #import "AirbrakeListViewController.h" #import "DGSwitch.h" #import "DataTableDelegate.h" +#import "AirbrakeDateView.h" @interface AirbrakeDetailViewController : UIViewController @property (weak, nonatomic) IBOutlet UITableView *dataTable; @@ -24,6 +25,7 @@ @property (weak, nonatomic) AirbrakeListViewController* listView; @property (weak, nonatomic) IBOutlet UISegmentedControl *viewChanger; @property (weak, nonatomic) IBOutlet UIBarButtonItem *projectMenuButton; +@property (weak, nonatomic) IBOutlet AirbrakeDateView *dateView; @property (nonatomic, strong) DataTableDelegate *dataTableDelegate; @property (nonatomic, strong) AirbrakeUser* user; diff --git a/Airpad/AirbrakeDetailViewController.m b/Airpad/AirbrakeDetailViewController.m index 5ea9c66..2cfc0fc 100644 --- a/Airpad/AirbrakeDetailViewController.m +++ b/Airpad/AirbrakeDetailViewController.m @@ -22,6 +22,7 @@ @implementation AirbrakeDetailViewController { @synthesize occurrenceLabel; @synthesize backtraceText; @synthesize projectMenuButton; +@synthesize dateView; @synthesize listView; @synthesize viewChanger; @@ -100,6 +101,7 @@ - (void)viewDidUnload [self setResolveSlider:nil]; [self setDataTable:nil]; [self setViewChanger:nil]; + [self setDateView:nil]; [super viewDidUnload]; // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; @@ -190,6 +192,10 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N [resolveSlider setOn: [user.currentAirbrake.isResolved boolValue]]; [titleLabel setText: user.currentAirbrake.errorMessage]; [occurrenceLabel setText: [self occurrenceDescription]]; + [dateView setStartDate: user.currentAirbrake.earliestSeenAt]; + [dateView setEndDate: user.currentAirbrake.latestSeenAt]; + [dateView setCount: [user.currentAirbrake.noticesCount integerValue]]; + [dateView setNeedsDisplay]; [dataTable reloadData]; } else if (context == @"projectFilter") { diff --git a/Airpad/Airpad-Info.plist b/Airpad/Airpad-Info.plist index 28fa5b3..e1e2b0a 100644 --- a/Airpad/Airpad-Info.plist +++ b/Airpad/Airpad-Info.plist @@ -11,7 +11,7 @@ CFBundleIconFiles CFBundleIdentifier - com.jelzo.test.${PRODUCT_NAME:rfc1034identifier} + com.rapportive.conrad.${PRODUCT_NAME:rfc1034identifier} CFBundleInfoDictionaryVersion 6.0 CFBundleName