FLEXUtility.m 18 KB

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