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.

470 lines
17 KiB

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