FLEXNetworkTransactionDetailTableViewController.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. //
  2. // FLEXNetworkTransactionDetailTableViewController.m
  3. // Flipboard
  4. //
  5. // Created by Ryan Olson on 2/10/15.
  6. // Copyright (c) 2015 Flipboard. All rights reserved.
  7. //
  8. #import "FLEXNetworkTransactionDetailTableViewController.h"
  9. #import "FLEXNetworkRecorder.h"
  10. #import "FLEXNetworkTransaction.h"
  11. #import "FLEXWebViewController.h"
  12. #import "FLEXImagePreviewViewController.h"
  13. #import "FLEXMultilineTableViewCell.h"
  14. #import "FLEXUtility.h"
  15. @interface FLEXNetworkDetailSection : NSObject
  16. @property (nonatomic, copy) NSString *title;
  17. @property (nonatomic, copy) NSArray *rows;
  18. @end
  19. @implementation FLEXNetworkDetailSection
  20. @end
  21. typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
  22. @interface FLEXNetworkDetailRow : NSObject
  23. @property (nonatomic, copy) NSString *title;
  24. @property (nonatomic, copy) NSString *detailText;
  25. @property (nonatomic, copy) FLEXNetworkDetailRowSelectionFuture selectionFuture;
  26. @end
  27. @implementation FLEXNetworkDetailRow
  28. @end
  29. @interface FLEXNetworkTransactionDetailTableViewController ()
  30. @property (nonatomic, copy) NSArray *sections;
  31. @end
  32. @implementation FLEXNetworkTransactionDetailTableViewController
  33. - (instancetype)initWithStyle:(UITableViewStyle)style
  34. {
  35. // Force grouped style
  36. return [super initWithStyle:UITableViewStyleGrouped];
  37. }
  38. - (void)viewDidLoad
  39. {
  40. [super viewDidLoad];
  41. [self.tableView registerClass:[FLEXMultilineTableViewCell class] forCellReuseIdentifier:kFLEXMultilineTableViewCellIdentifier];
  42. }
  43. - (void)setTransaction:(FLEXNetworkTransaction *)transaction
  44. {
  45. if (![_transaction isEqual:transaction]) {
  46. _transaction = transaction;
  47. self.title = [transaction.request.URL lastPathComponent];
  48. [self rebuildTableSections];
  49. }
  50. }
  51. - (void)setSections:(NSArray *)sections
  52. {
  53. if (![_sections isEqual:sections]) {
  54. _sections = [sections copy];
  55. [self.tableView reloadData];
  56. }
  57. }
  58. - (void)rebuildTableSections
  59. {
  60. NSMutableArray *sections = [NSMutableArray array];
  61. FLEXNetworkDetailSection *generalSection = [[self class] generalSectionForTransaction:self.transaction];
  62. if ([generalSection.rows count] > 0) {
  63. [sections addObject:generalSection];
  64. }
  65. FLEXNetworkDetailSection *requestHeadersSection = [[self class] requestHeadersSectionForTransaction:self.transaction];
  66. if ([requestHeadersSection.rows count] > 0) {
  67. [sections addObject:requestHeadersSection];
  68. }
  69. FLEXNetworkDetailSection *queryParametersSection = [[self class] queryParametersSectionForTransaction:self.transaction];
  70. if ([queryParametersSection.rows count] > 0) {
  71. [sections addObject:queryParametersSection];
  72. }
  73. FLEXNetworkDetailSection *responseHeadersSection = [[self class] responseHeadersSectionForTransaction:self.transaction];
  74. if ([responseHeadersSection.rows count] > 0) {
  75. [sections addObject:responseHeadersSection];
  76. }
  77. self.sections = sections;
  78. }
  79. #pragma mark - Table view data source
  80. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
  81. {
  82. return [self.sections count];
  83. }
  84. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  85. {
  86. FLEXNetworkDetailSection *sectionModel = [self.sections objectAtIndex:section];
  87. return [sectionModel.rows count];
  88. }
  89. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
  90. {
  91. FLEXNetworkDetailSection *sectionModel = [self.sections objectAtIndex:section];
  92. return sectionModel.title;
  93. }
  94. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  95. {
  96. FLEXMultilineTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXMultilineTableViewCellIdentifier forIndexPath:indexPath];
  97. FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
  98. cell.textLabel.attributedText = [[self class] attributedTextForRow:rowModel];
  99. cell.accessoryType = rowModel.selectionFuture ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
  100. cell.selectionStyle = rowModel.selectionFuture ? UITableViewCellSelectionStyleDefault : UITableViewCellSelectionStyleNone;
  101. return cell;
  102. }
  103. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
  104. {
  105. FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
  106. UIViewController *viewControllerToPush = nil;
  107. if (rowModel.selectionFuture) {
  108. viewControllerToPush = rowModel.selectionFuture();
  109. }
  110. if (viewControllerToPush) {
  111. [self.navigationController pushViewController:viewControllerToPush animated:YES];
  112. }
  113. [tableView deselectRowAtIndexPath:indexPath animated:YES];
  114. }
  115. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
  116. {
  117. FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
  118. NSAttributedString *attributedText = [[self class] attributedTextForRow:row];
  119. BOOL showsAccessory = row.selectionFuture != nil;
  120. return [FLEXMultilineTableViewCell preferredHeightWithAttributedText:attributedText inTableViewWidth:self.tableView.bounds.size.width style:UITableViewStyleGrouped showsAccessory:showsAccessory];
  121. }
  122. - (FLEXNetworkDetailRow *)rowModelAtIndexPath:(NSIndexPath *)indexPath
  123. {
  124. FLEXNetworkDetailSection *sectionModel = [self.sections objectAtIndex:indexPath.section];
  125. return [sectionModel.rows objectAtIndex:indexPath.row];
  126. }
  127. #pragma mark - View Configuration
  128. + (NSAttributedString *)attributedTextForRow:(FLEXNetworkDetailRow *)row
  129. {
  130. NSDictionary *titleAttributes = @{ NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue-Medium" size:12.0],
  131. NSForegroundColorAttributeName : [UIColor colorWithWhite:0.5 alpha:1.0] };
  132. NSDictionary *detailAttributes = @{ NSFontAttributeName : [FLEXUtility defaultTableViewCellLabelFont],
  133. NSForegroundColorAttributeName : [UIColor blackColor] };
  134. NSString *title = [NSString stringWithFormat:@"%@: ", row.title];
  135. NSString *detailText = row.detailText ?: @"";
  136. NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] init];
  137. [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:title attributes:titleAttributes]];
  138. [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:detailText attributes:detailAttributes]];
  139. return attributedText;
  140. }
  141. #pragma mark - Table Data Generation
  142. + (FLEXNetworkDetailSection *)generalSectionForTransaction:(FLEXNetworkTransaction *)transaction
  143. {
  144. NSMutableArray *rows = [NSMutableArray array];
  145. FLEXNetworkDetailRow *requestURLRow = [[FLEXNetworkDetailRow alloc] init];
  146. requestURLRow.title = @"Request URL";
  147. NSURL *url = transaction.request.URL;
  148. requestURLRow.detailText = url.absoluteString;
  149. requestURLRow.selectionFuture = ^{
  150. UIViewController *urlWebViewController = [[FLEXWebViewController alloc] initWithURL:url];
  151. urlWebViewController.title = url.absoluteString;
  152. return urlWebViewController;
  153. };
  154. [rows addObject:requestURLRow];
  155. FLEXNetworkDetailRow *responseBodyRow = [[FLEXNetworkDetailRow alloc] init];
  156. responseBodyRow.title = @"Response Body";
  157. NSData *responseData = [[FLEXNetworkRecorder defaultRecorder] cachedResponseBodyForTransaction:transaction];
  158. if ([responseData length] > 0) {
  159. responseBodyRow.detailText = @"tap to view";
  160. // Avoid a long lived strong reference to the response data in case we need to purge it from the cache.
  161. __weak NSData *weakResponseData = responseData;
  162. responseBodyRow.selectionFuture = ^{
  163. UIViewController *responseBodyDetailViewController = nil;
  164. NSData *strongResponseData = weakResponseData;
  165. if (strongResponseData) {
  166. responseBodyDetailViewController = [self detailViewControllerForMIMEType:transaction.response.MIMEType data:strongResponseData];
  167. } else {
  168. // FIXME (RKO): Show an alert explaining that the data was purged?
  169. }
  170. return responseBodyDetailViewController;
  171. };
  172. } else {
  173. BOOL emptyResponse = transaction.receivedDataLength == 0;
  174. responseBodyRow.detailText = emptyResponse ? @"empty" : @"purged from cache";
  175. }
  176. [rows addObject:responseBodyRow];
  177. FLEXNetworkDetailRow *requestMethodRow = [[FLEXNetworkDetailRow alloc] init];
  178. requestMethodRow.title = @"Request Method";
  179. requestMethodRow.detailText = transaction.request.HTTPMethod;
  180. [rows addObject:requestMethodRow];
  181. NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:transaction.response];
  182. if ([statusCodeString length] > 0) {
  183. FLEXNetworkDetailRow *statusCodeRow = [[FLEXNetworkDetailRow alloc] init];
  184. statusCodeRow.title = @"Status Code";
  185. statusCodeRow.detailText = statusCodeString;
  186. [rows addObject:statusCodeRow];
  187. }
  188. FLEXNetworkDetailRow *mimeTypeRow = [[FLEXNetworkDetailRow alloc] init];
  189. mimeTypeRow.title = @"MIME Type";
  190. mimeTypeRow.detailText = transaction.response.MIMEType;
  191. [rows addObject:mimeTypeRow];
  192. FLEXNetworkDetailRow *responseSizeRow = [[FLEXNetworkDetailRow alloc] init];
  193. responseSizeRow.title = @"Response Size";
  194. responseSizeRow.detailText = [NSByteCountFormatter stringFromByteCount:transaction.receivedDataLength countStyle:NSByteCountFormatterCountStyleBinary];
  195. [rows addObject:responseSizeRow];
  196. FLEXNetworkDetailRow *durationRow = [[FLEXNetworkDetailRow alloc] init];
  197. durationRow.title = @"Total Duration";
  198. durationRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.duration];
  199. [rows addObject:durationRow];
  200. FLEXNetworkDetailRow *latencyRow = [[FLEXNetworkDetailRow alloc] init];
  201. latencyRow.title = @"Latency";
  202. latencyRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.latency];
  203. [rows addObject:latencyRow];
  204. FLEXNetworkDetailSection *generalSection = [[FLEXNetworkDetailSection alloc] init];
  205. generalSection.title = @"General";
  206. generalSection.rows = rows;
  207. return generalSection;
  208. }
  209. + (FLEXNetworkDetailSection *)requestHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction
  210. {
  211. FLEXNetworkDetailSection *requestHeadersSection = [[FLEXNetworkDetailSection alloc] init];
  212. requestHeadersSection.title = @"Request Headers";
  213. requestHeadersSection.rows = [self networkDetailRowsFromDictionary:transaction.request.allHTTPHeaderFields];
  214. return requestHeadersSection;
  215. }
  216. + (FLEXNetworkDetailSection *)queryParametersSectionForTransaction:(FLEXNetworkTransaction *)transaction
  217. {
  218. NSDictionary *queryDictionary = [FLEXUtility queryDictionaryFromURL:transaction.request.URL];
  219. FLEXNetworkDetailSection *querySection = [[FLEXNetworkDetailSection alloc] init];
  220. querySection.title = @"Query Parameters";
  221. querySection.rows = [self networkDetailRowsFromDictionary:queryDictionary];
  222. return querySection;
  223. }
  224. + (FLEXNetworkDetailSection *)responseHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction
  225. {
  226. FLEXNetworkDetailSection *responseHeadersSection = [[FLEXNetworkDetailSection alloc] init];
  227. responseHeadersSection.title = @"Response Headers";
  228. if ([transaction.response isKindOfClass:[NSHTTPURLResponse class]]) {
  229. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)transaction.response;
  230. responseHeadersSection.rows = [self networkDetailRowsFromDictionary:httpResponse.allHeaderFields];
  231. }
  232. return responseHeadersSection;
  233. }
  234. + (NSArray *)networkDetailRowsFromDictionary:(NSDictionary *)dictionary
  235. {
  236. NSMutableArray *rows = [NSMutableArray arrayWithCapacity:[dictionary count]];
  237. NSArray *sortedKeys = [[dictionary allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
  238. for (NSString *key in sortedKeys) {
  239. NSString *value = [dictionary objectForKey:key];
  240. FLEXNetworkDetailRow *row = [[FLEXNetworkDetailRow alloc] init];
  241. row.title = key;
  242. row.detailText = value;
  243. [rows addObject:row];
  244. }
  245. return [rows copy];
  246. }
  247. + (UIViewController *)detailViewControllerForMIMEType:(NSString *)mimeType data:(NSData *)data
  248. {
  249. // FIXME (RKO): Don't rely on UTF8 string encoding
  250. UIViewController *detailViewController = nil;
  251. if ([mimeType isEqual:@"application/json"]) {
  252. NSString *prettyJSON = [FLEXUtility prettyJSONStringFromData:data];
  253. detailViewController = [[FLEXWebViewController alloc] initWithText:prettyJSON];
  254. } else if ([mimeType hasPrefix:@"image/"]) {
  255. UIImage *image = [UIImage imageWithData:data];
  256. detailViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
  257. } else if ([mimeType hasPrefix:@"text/"]) {
  258. NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  259. detailViewController = [[FLEXWebViewController alloc] initWithText:text];
  260. }
  261. detailViewController.title = @"Response";
  262. return detailViewController;
  263. }
  264. @end