You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

455 lines
17 KiB

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