FLEXUtility.m 18 KB

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