FLEXUtility.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. //
  2. // FLEXUtility.m
  3. // Flipboard
  4. //
  5. // Created by Ryan Olson on 4/18/14.
  6. // Copyright (c) 2014 Flipboard. All rights reserved.
  7. //
  8. #import "FLEXUtility.h"
  9. #import "FLEXResources.h"
  10. #import <ImageIO/ImageIO.h>
  11. #import <zlib.h>
  12. #import <objc/runtime.h>
  13. @implementation FLEXUtility
  14. + (UIColor *)consistentRandomColorForObject:(id)object
  15. {
  16. CGFloat hue = (((NSUInteger)object >> 4) % 256) / 255.0;
  17. return [UIColor colorWithHue:hue saturation:1.0 brightness:1.0 alpha:1.0];
  18. }
  19. + (NSString *)descriptionForView:(UIView *)view includingFrame:(BOOL)includeFrame
  20. {
  21. NSString *description = [[view class] description];
  22. NSString *viewControllerDescription = [[[self viewControllerForView:view] class] description];
  23. if ([viewControllerDescription length] > 0) {
  24. description = [description stringByAppendingFormat:@" (%@)", viewControllerDescription];
  25. }
  26. if (includeFrame) {
  27. description = [description stringByAppendingFormat:@" %@", [self stringForCGRect:view.frame]];
  28. }
  29. if ([view.accessibilityLabel length] > 0) {
  30. description = [description stringByAppendingFormat:@" · %@", view.accessibilityLabel];
  31. }
  32. return description;
  33. }
  34. + (NSString *)stringForCGRect:(CGRect)rect
  35. {
  36. return [NSString stringWithFormat:@"{(%g, %g), (%g, %g)}", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height];
  37. }
  38. + (UIViewController *)viewControllerForView:(UIView *)view
  39. {
  40. UIViewController *viewController = nil;
  41. SEL viewDelSel = NSSelectorFromString([NSString stringWithFormat:@"%@ewDelegate", @"_vi"]);
  42. if ([view respondsToSelector:viewDelSel]) {
  43. #pragma clang diagnostic push
  44. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  45. viewController = [view performSelector:viewDelSel];
  46. #pragma clang diagnostic pop
  47. }
  48. return viewController;
  49. }
  50. + (UIViewController *)viewControllerForAncestralView:(UIView *)view{
  51. UIViewController *viewController = nil;
  52. SEL viewDelSel = NSSelectorFromString([NSString stringWithFormat:@"%@ewControllerForAncestor", @"_vi"]);
  53. if ([view respondsToSelector:viewDelSel]) {
  54. #pragma clang diagnostic push
  55. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  56. viewController = [view performSelector:viewDelSel];
  57. #pragma clang diagnostic pop
  58. }
  59. return viewController;
  60. }
  61. + (NSString *)detailDescriptionForView:(UIView *)view
  62. {
  63. return [NSString stringWithFormat:@"frame %@", [self stringForCGRect:view.frame]];
  64. }
  65. + (UIImage *)circularImageWithColor:(UIColor *)color radius:(CGFloat)radius
  66. {
  67. CGFloat diameter = radius * 2.0;
  68. UIGraphicsBeginImageContextWithOptions(CGSizeMake(diameter, diameter), NO, 0.0);
  69. CGContextRef imageContext = UIGraphicsGetCurrentContext();
  70. CGContextSetFillColorWithColor(imageContext, [color CGColor]);
  71. CGContextFillEllipseInRect(imageContext, CGRectMake(0, 0, diameter, diameter));
  72. UIImage *circularImage = UIGraphicsGetImageFromCurrentImageContext();
  73. UIGraphicsEndImageContext();
  74. return circularImage;
  75. }
  76. + (UIColor *)scrollViewGrayColor
  77. {
  78. return [UIColor colorWithRed:239.0/255.0 green:239.0/255.0 blue:244.0/255.0 alpha:1.0];
  79. }
  80. + (UIColor *)hierarchyIndentPatternColor
  81. {
  82. static UIColor *patternColor = nil;
  83. static dispatch_once_t onceToken;
  84. dispatch_once(&onceToken, ^{
  85. UIImage *indentationPatternImage = [FLEXResources hierarchyIndentPattern];
  86. patternColor = [UIColor colorWithPatternImage:indentationPatternImage];
  87. });
  88. return patternColor;
  89. }
  90. + (NSString *)applicationImageName
  91. {
  92. return [NSBundle mainBundle].executablePath;
  93. }
  94. + (NSString *)applicationName
  95. {
  96. return [FLEXUtility applicationImageName].lastPathComponent;
  97. }
  98. + (NSString *)safeDescriptionForObject:(id)object
  99. {
  100. // Don't assume that we have an NSObject subclass.
  101. // Check to make sure the object responds to the description methods.
  102. NSString *description = nil;
  103. if ([object respondsToSelector:@selector(debugDescription)]) {
  104. description = [object debugDescription];
  105. } else if ([object respondsToSelector:@selector(description)]) {
  106. description = [object description];
  107. }
  108. return description;
  109. }
  110. + (UIFont *)defaultFontOfSize:(CGFloat)size
  111. {
  112. return [UIFont fontWithName:@"HelveticaNeue" size:size];
  113. }
  114. + (UIFont *)defaultTableViewCellLabelFont
  115. {
  116. return [self defaultFontOfSize:12.0];
  117. }
  118. + (NSString *)stringByEscapingHTMLEntitiesInString:(NSString *)originalString
  119. {
  120. static NSDictionary *escapingDictionary = nil;
  121. static NSRegularExpression *regex = nil;
  122. static dispatch_once_t onceToken;
  123. dispatch_once(&onceToken, ^{
  124. escapingDictionary = @{ @" " : @"&nbsp;",
  125. @">" : @"&gt;",
  126. @"<" : @"&lt;",
  127. @"&" : @"&amp;",
  128. @"'" : @"&apos;",
  129. @"\"" : @"&quot;",
  130. @"«" : @"&laquo;",
  131. @"»" : @"&raquo;"
  132. };
  133. regex = [NSRegularExpression regularExpressionWithPattern:@"(&|>|<|'|\"|«|»)" options:0 error:NULL];
  134. });
  135. NSMutableString *mutableString = [originalString mutableCopy];
  136. NSArray *matches = [regex matchesInString:mutableString options:0 range:NSMakeRange(0, [mutableString length])];
  137. for (NSTextCheckingResult *result in [matches reverseObjectEnumerator]) {
  138. NSString *foundString = [mutableString substringWithRange:result.range];
  139. NSString *replacementString = escapingDictionary[foundString];
  140. if (replacementString) {
  141. [mutableString replaceCharactersInRange:result.range withString:replacementString];
  142. }
  143. }
  144. return [mutableString copy];
  145. }
  146. + (UIInterfaceOrientationMask)infoPlistSupportedInterfaceOrientationsMask
  147. {
  148. NSArray *supportedOrientations = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UISupportedInterfaceOrientations"];
  149. UIInterfaceOrientationMask supportedOrientationsMask = 0;
  150. if ([supportedOrientations containsObject:@"UIInterfaceOrientationPortrait"]) {
  151. supportedOrientationsMask |= UIInterfaceOrientationMaskPortrait;
  152. }
  153. if ([supportedOrientations containsObject:@"UIInterfaceOrientationMaskLandscapeRight"]) {
  154. supportedOrientationsMask |= UIInterfaceOrientationMaskLandscapeRight;
  155. }
  156. if ([supportedOrientations containsObject:@"UIInterfaceOrientationMaskPortraitUpsideDown"]) {
  157. supportedOrientationsMask |= UIInterfaceOrientationMaskPortraitUpsideDown;
  158. }
  159. if ([supportedOrientations containsObject:@"UIInterfaceOrientationLandscapeLeft"]) {
  160. supportedOrientationsMask |= UIInterfaceOrientationMaskLandscapeLeft;
  161. }
  162. return supportedOrientationsMask;
  163. }
  164. + (NSString *)searchBarPlaceholderText
  165. {
  166. return @"Filter";
  167. }
  168. + (BOOL)isImagePathExtension:(NSString *)extension
  169. {
  170. // https://developer.apple.com/library/ios/documentation/uikit/reference/UIImage_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40006890-CH3-SW3
  171. return [@[@"jpg", @"jpeg", @"png", @"gif", @"tiff", @"tif"] containsObject:extension];
  172. }
  173. + (UIImage *)thumbnailedImageWithMaxPixelDimension:(NSInteger)dimension fromImageData:(NSData *)data
  174. {
  175. UIImage *thumbnail = nil;
  176. CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, 0);
  177. if (imageSource) {
  178. NSDictionary *options = @{ (__bridge id)kCGImageSourceCreateThumbnailWithTransform : @YES,
  179. (__bridge id)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
  180. (__bridge id)kCGImageSourceThumbnailMaxPixelSize : @(dimension) };
  181. CGImageRef scaledImageRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)options);
  182. if (scaledImageRef) {
  183. thumbnail = [UIImage imageWithCGImage:scaledImageRef];
  184. CFRelease(scaledImageRef);
  185. }
  186. CFRelease(imageSource);
  187. }
  188. return thumbnail;
  189. }
  190. + (NSString *)stringFromRequestDuration:(NSTimeInterval)duration
  191. {
  192. NSString *string = @"0s";
  193. if (duration > 0.0) {
  194. if (duration < 1.0) {
  195. string = [NSString stringWithFormat:@"%dms", (int)(duration * 1000)];
  196. } else if (duration < 10.0) {
  197. string = [NSString stringWithFormat:@"%.2fs", duration];
  198. } else {
  199. string = [NSString stringWithFormat:@"%.1fs", duration];
  200. }
  201. }
  202. return string;
  203. }
  204. + (NSString *)statusCodeStringFromURLResponse:(NSURLResponse *)response
  205. {
  206. NSString *httpResponseString = nil;
  207. if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
  208. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
  209. NSString *statusCodeDescription = nil;
  210. if (httpResponse.statusCode == 200) {
  211. // Prefer OK to the default "no error"
  212. statusCodeDescription = @"OK";
  213. } else {
  214. statusCodeDescription = [NSHTTPURLResponse localizedStringForStatusCode:httpResponse.statusCode];
  215. }
  216. httpResponseString = [NSString stringWithFormat:@"%ld %@", (long)httpResponse.statusCode, statusCodeDescription];
  217. }
  218. return httpResponseString;
  219. }
  220. + (BOOL)isErrorStatusCodeFromURLResponse:(NSURLResponse *)response {
  221. NSIndexSet *errorStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(400, 200)];
  222. if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
  223. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
  224. return [errorStatusCodes containsIndex:httpResponse.statusCode];
  225. }
  226. return NO;
  227. }
  228. + (NSDictionary *)dictionaryFromQuery:(NSString *)query
  229. {
  230. NSMutableDictionary *queryDictionary = [NSMutableDictionary dictionary];
  231. // [a=1, b=2, c=3]
  232. NSArray *queryComponents = [query componentsSeparatedByString:@"&"];
  233. for (NSString *keyValueString in queryComponents) {
  234. // [a, 1]
  235. NSArray *components = [keyValueString componentsSeparatedByString:@"="];
  236. if ([components count] == 2) {
  237. NSString *key = [[components firstObject] stringByRemovingPercentEncoding];
  238. id value = [[components lastObject] stringByRemovingPercentEncoding];
  239. // Handle multiple entries under the same key as an array
  240. id existingEntry = queryDictionary[key];
  241. if (existingEntry) {
  242. if ([existingEntry isKindOfClass:[NSArray class]]) {
  243. value = [existingEntry arrayByAddingObject:value];
  244. } else {
  245. value = @[existingEntry, value];
  246. }
  247. }
  248. [queryDictionary setObject:value forKey:key];
  249. }
  250. }
  251. return queryDictionary;
  252. }
  253. + (NSString *)prettyJSONStringFromData:(NSData *)data
  254. {
  255. NSString *prettyString = nil;
  256. id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
  257. if ([NSJSONSerialization isValidJSONObject:jsonObject]) {
  258. prettyString = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:jsonObject options:NSJSONWritingPrettyPrinted error:NULL] encoding:NSUTF8StringEncoding];
  259. // NSJSONSerialization escapes forward slashes. We want pretty json, so run through and unescape the slashes.
  260. prettyString = [prettyString stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"];
  261. } else {
  262. prettyString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  263. }
  264. return prettyString;
  265. }
  266. + (BOOL)isValidJSONData:(NSData *)data
  267. {
  268. return [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL] ? YES : NO;
  269. }
  270. // Thanks to the following links for help with this method
  271. // http://www.cocoanetics.com/2012/02/decompressing-files-into-memory/
  272. // https://github.com/nicklockwood/GZIP
  273. + (NSData *)inflatedDataFromCompressedData:(NSData *)compressedData
  274. {
  275. NSData *inflatedData = nil;
  276. NSUInteger compressedDataLength = [compressedData length];
  277. if (compressedDataLength > 0) {
  278. z_stream stream;
  279. stream.zalloc = Z_NULL;
  280. stream.zfree = Z_NULL;
  281. stream.avail_in = (uInt)compressedDataLength;
  282. stream.next_in = (void *)[compressedData bytes];
  283. stream.total_out = 0;
  284. stream.avail_out = 0;
  285. NSMutableData *mutableData = [NSMutableData dataWithLength:compressedDataLength * 1.5];
  286. if (inflateInit2(&stream, 15 + 32) == Z_OK) {
  287. int status = Z_OK;
  288. while (status == Z_OK) {
  289. if (stream.total_out >= [mutableData length]) {
  290. mutableData.length += compressedDataLength / 2;
  291. }
  292. stream.next_out = (uint8_t *)[mutableData mutableBytes] + stream.total_out;
  293. stream.avail_out = (uInt)([mutableData length] - stream.total_out);
  294. status = inflate(&stream, Z_SYNC_FLUSH);
  295. }
  296. if (inflateEnd(&stream) == Z_OK) {
  297. if (status == Z_STREAM_END) {
  298. mutableData.length = stream.total_out;
  299. inflatedData = [mutableData copy];
  300. }
  301. }
  302. }
  303. }
  304. return inflatedData;
  305. }
  306. + (NSArray *)allWindows
  307. {
  308. BOOL includeInternalWindows = YES;
  309. BOOL onlyVisibleWindows = NO;
  310. NSArray *allWindowsComponents = @[@"al", @"lWindo", @"wsIncl", @"udingInt", @"ernalWin", @"dows:o", @"nlyVisi", @"bleWin", @"dows:"];
  311. SEL allWindowsSelector = NSSelectorFromString([allWindowsComponents componentsJoinedByString:@""]);
  312. NSMethodSignature *methodSignature = [[UIWindow class] methodSignatureForSelector:allWindowsSelector];
  313. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
  314. invocation.target = [UIWindow class];
  315. invocation.selector = allWindowsSelector;
  316. [invocation setArgument:&includeInternalWindows atIndex:2];
  317. [invocation setArgument:&onlyVisibleWindows atIndex:3];
  318. [invocation invoke];
  319. __unsafe_unretained NSArray *windows = nil;
  320. [invocation getReturnValue:&windows];
  321. return windows;
  322. }
  323. + (SEL)swizzledSelectorForSelector:(SEL)selector
  324. {
  325. return NSSelectorFromString([NSString stringWithFormat:@"_flex_swizzle_%x_%@", arc4random(), NSStringFromSelector(selector)]);
  326. }
  327. + (BOOL)instanceRespondsButDoesNotImplementSelector:(SEL)selector class:(Class)cls
  328. {
  329. if ([cls instancesRespondToSelector:selector]) {
  330. unsigned int numMethods = 0;
  331. Method *methods = class_copyMethodList(cls, &numMethods);
  332. BOOL implementsSelector = NO;
  333. for (int index = 0; index < numMethods; index++) {
  334. SEL methodSelector = method_getName(methods[index]);
  335. if (selector == methodSelector) {
  336. implementsSelector = YES;
  337. break;
  338. }
  339. }
  340. free(methods);
  341. if (!implementsSelector) {
  342. return YES;
  343. }
  344. }
  345. return NO;
  346. }
  347. + (void)replaceImplementationOfKnownSelector:(SEL)originalSelector onClass:(Class)class withBlock:(id)block swizzledSelector:(SEL)swizzledSelector
  348. {
  349. // This method is only intended for swizzling methods that are know to exist on the class.
  350. // Bail if that isn't the case.
  351. Method originalMethod = class_getInstanceMethod(class, originalSelector);
  352. if (!originalMethod) {
  353. return;
  354. }
  355. IMP implementation = imp_implementationWithBlock(block);
  356. class_addMethod(class, swizzledSelector, implementation, method_getTypeEncoding(originalMethod));
  357. Method newMethod = class_getInstanceMethod(class, swizzledSelector);
  358. method_exchangeImplementations(originalMethod, newMethod);
  359. }
  360. + (void)replaceImplementationOfSelector:(SEL)selector withSelector:(SEL)swizzledSelector forClass:(Class)cls withMethodDescription:(struct objc_method_description)methodDescription implementationBlock:(id)implementationBlock undefinedBlock:(id)undefinedBlock
  361. {
  362. if ([self instanceRespondsButDoesNotImplementSelector:selector class:cls]) {
  363. return;
  364. }
  365. IMP implementation = imp_implementationWithBlock((id)([cls instancesRespondToSelector:selector] ? implementationBlock : undefinedBlock));
  366. Method oldMethod = class_getInstanceMethod(cls, selector);
  367. if (oldMethod) {
  368. class_addMethod(cls, swizzledSelector, implementation, methodDescription.types);
  369. Method newMethod = class_getInstanceMethod(cls, swizzledSelector);
  370. method_exchangeImplementations(oldMethod, newMethod);
  371. } else {
  372. class_addMethod(cls, selector, implementation, methodDescription.types);
  373. }
  374. }
  375. @end