├── .gitignore ├── FCUtilities.podspec ├── FCUtilities ├── CALayer+FCUtilities.h ├── CALayer+FCUtilities.m ├── FCBasics.h ├── FCBasics.m ├── FCCache.h ├── FCCache.m ├── FCConcurrentMutableDictionary.h ├── FCConcurrentMutableDictionary.m ├── FCExtensionPipe.h ├── FCExtensionPipe.m ├── FCNetworkImageLoader.h ├── FCNetworkImageLoader.m ├── FCOpenInChromeActivity.h ├── FCOpenInChromeActivity.m ├── FCOpenInSafariActivity.h ├── FCOpenInSafariActivity.m ├── FCPickerViewController.h ├── FCPickerViewController.m ├── FCReachability.h ├── FCReachability.m ├── FCSheetView.h ├── FCSheetView.m ├── FCSimpleKeychain.h ├── FCTwitterAuthorization.h ├── FCTwitterAuthorization.m ├── FCiOS11TableViewAnimationBugfix.h ├── NSArray+FCUtilities.h ├── NSArray+FCUtilities.m ├── NSData+FCUtilities.h ├── NSData+FCUtilities.m ├── NSOperationQueue+FCUtilities.h ├── NSOperationQueue+FCUtilities.m ├── NSString+FCUtilities.h ├── NSString+FCUtilities.m ├── NSURL+FCUtilities.h ├── NSURL+FCUtilities.m ├── NSURLSession+FCUtilities.h ├── NSURLSession+FCUtilities.m ├── UIBarButtonItem+FCUtilities.h ├── UIBarButtonItem+FCUtilities.m ├── UIColor+FCUtilities.h ├── UIColor+FCUtilities.m ├── UIDevice+FCUtilities.h ├── UIDevice+FCUtilities.m ├── UIImage+FCUtilities.h └── UIImage+FCUtilities.m ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | */build/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | *.xccheckout 19 | 20 | #CocoaPods 21 | Pods 22 | -------------------------------------------------------------------------------- /FCUtilities.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'FCUtilities' 3 | s.version = '0.1.0' 4 | s.summary = 'Assorted common iOS utilities.' 5 | s.homepage = 'https://github.com/marcoarment/FCUtilities' 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.author = { 'Marco Arment' => 'arment@marco.org' } 8 | s.source = { :git => 'https://github.com/marcoarment/FCUtilities.git', :tag => s.version.to_s } 9 | s.source_files = 'FCUtilities/*.{h,m}' 10 | s.requires_arc = true 11 | s.ios.deployment_target = '7.0' 12 | end 13 | -------------------------------------------------------------------------------- /FCUtilities/CALayer+FCUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+FCUtilities.h 3 | // Overcast 4 | // 5 | // Created by Marco Arment on 10/30/17. 6 | // Copyright © 2017 Marco Arment. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | 13 | @interface CALayer (FCUtilities) 14 | 15 | - (void)fc_removeAnimationsRecursive; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /FCUtilities/CALayer+FCUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+FCUtilities.m 3 | // Overcast 4 | // 5 | // Created by Marco Arment on 10/30/17. 6 | // Copyright © 2017 Marco Arment. All rights reserved. 7 | // 8 | 9 | #import "CALayer+FCUtilities.h" 10 | 11 | @implementation CALayer (FCUtilities) 12 | 13 | - (void)_fc_removeAnimationsRecursiveInner 14 | { 15 | for (CALayer *sublayer in self.sublayers) [sublayer _fc_removeAnimationsRecursiveInner]; 16 | [self removeAllAnimations]; 17 | } 18 | 19 | - (void)fc_removeAnimationsRecursive 20 | { 21 | [CATransaction begin]; 22 | [self _fc_removeAnimationsRecursiveInner]; 23 | [CATransaction commit]; 24 | } 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /FCUtilities/FCBasics.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCBasics.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | @import UIKit; 7 | 8 | #define user_defaults_get_bool(key) [[NSUserDefaults standardUserDefaults] boolForKey:key] 9 | #define user_defaults_get_int(key) ((int) [[NSUserDefaults standardUserDefaults] integerForKey:key]) 10 | #define user_defaults_get_double(key) [[NSUserDefaults standardUserDefaults] doubleForKey:key] 11 | #define user_defaults_get_string(key) fc_safeString([[NSUserDefaults standardUserDefaults] stringForKey:key]) 12 | #define user_defaults_get_array(key) [[NSUserDefaults standardUserDefaults] arrayForKey:key] 13 | #define user_defaults_get_object(key) [[NSUserDefaults standardUserDefaults] objectForKey:key] 14 | 15 | #define user_defaults_set_bool(key, b) { [[NSUserDefaults standardUserDefaults] setBool:b forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; } 16 | #define user_defaults_set_int(key, i) { [[NSUserDefaults standardUserDefaults] setInteger:i forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; } 17 | #define user_defaults_set_double(key, d) { [[NSUserDefaults standardUserDefaults] setDouble:d forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; } 18 | #define user_defaults_set_string(key, s) { [[NSUserDefaults standardUserDefaults] setObject:s forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; } 19 | #define user_defaults_set_array(key, a) { [[NSUserDefaults standardUserDefaults] setObject:a forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; } 20 | #define user_defaults_set_object(key, o) { [[NSUserDefaults standardUserDefaults] setObject:o forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; } 21 | 22 | #define APP_DISPLAY_NAME [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"] 23 | #define APP_VERSION [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"] 24 | #define APP_BUILD_NUMBER [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"] 25 | 26 | inline __attribute__((always_inline)) NSString *fc_safeString(NSString *str) 27 | { 28 | return str && [str isKindOfClass:NSString.class] ? str : @""; 29 | } 30 | 31 | inline __attribute__((always_inline)) NSString *fc_safeStringCopy(NSString *str) 32 | { 33 | return str && [str isKindOfClass:NSString.class] ? [NSString stringWithFormat:@"%@", str] : @""; 34 | } 35 | 36 | inline __attribute__((always_inline)) NSString *fc_dictionaryValueToString(NSObject *cfObj) 37 | { 38 | if ([cfObj isKindOfClass:[NSString class]]) return (NSString *)cfObj; 39 | else return [(NSNumber *)cfObj stringValue]; 40 | } 41 | 42 | // If we're currently on the main thread, run block() sync, otherwise dispatch block() async to main thread. 43 | void fc_executeOnMainThread(void (^block)(void)); 44 | 45 | inline __attribute((always_inline)) uint64_t fc_random_int64(void) 46 | { 47 | uint64_t urandom; 48 | if (0 != SecRandomCopyBytes(kSecRandomDefault, sizeof(uint64_t), (uint8_t *) (&urandom))) { 49 | arc4random_stir(); 50 | urandom = ( ((uint64_t) arc4random()) << 32) | (uint64_t) arc4random(); 51 | } 52 | return urandom; 53 | } 54 | 55 | inline __attribute((always_inline)) uint32_t fc_random_int32(void) 56 | { 57 | uint32_t urandom; 58 | if (0 != SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *) (&urandom))) { 59 | arc4random_stir(); 60 | urandom = (uint32_t) arc4random(); 61 | } 62 | return urandom; 63 | } 64 | 65 | inline __attribute__((always_inline)) CGRect fc_safeCGRectInset(CGRect rect, CGFloat dx, CGFloat dy) 66 | { 67 | CGFloat dx2 = 2.0f * dx, dy2 = 2.0f * dy; 68 | if (rect.size.width < dx2) { 69 | rect.origin.x = rect.size.width / 2.0f; 70 | rect.size.width = 0; 71 | } else { 72 | rect.origin.x += dx; 73 | rect.size.width -= dx2; 74 | } 75 | 76 | if (rect.size.height < dy2) { 77 | rect.origin.y = rect.size.height / 2.0f; 78 | rect.size.height = 0; 79 | } else { 80 | rect.origin.y += dy; 81 | rect.size.height -= dy2; 82 | } 83 | 84 | return rect; 85 | } 86 | 87 | inline __attribute__((always_inline)) CGRect fc_aspectFitRect(CGRect outerRect, CGSize innerSize) { 88 | 89 | // the width and height ratios of the rects 90 | CGFloat wRatio = outerRect.size.width/innerSize.width; 91 | CGFloat hRatio = outerRect.size.height/innerSize.height; 92 | 93 | // calculate scaling ratio based on the smallest ratio. 94 | CGFloat ratio = (wRatio < hRatio)? wRatio:hRatio; 95 | 96 | // The x-offset of the inner rect as it gets centered 97 | CGFloat xOffset = (outerRect.size.width-(innerSize.width*ratio))*0.5; 98 | 99 | // The y-offset of the inner rect as it gets centered 100 | CGFloat yOffset = (outerRect.size.height-(innerSize.height*ratio))*0.5; 101 | 102 | // aspect fitted origin and size 103 | CGPoint innerRectOrigin = {xOffset+outerRect.origin.x, yOffset+outerRect.origin.y}; 104 | innerSize = (CGSize) {innerSize.width*ratio, innerSize.height*ratio}; 105 | 106 | return (CGRect){innerRectOrigin, innerSize}; 107 | } 108 | 109 | -------------------------------------------------------------------------------- /FCUtilities/FCBasics.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCBasics.m 3 | // Overcast 4 | // 5 | // Created by Marco Arment on 6/10/16. 6 | // Copyright © 2016 Marco Arment. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "FCBasics.h" 11 | 12 | static dispatch_once_t fc_mainThreadOnceToken; 13 | void fc_executeOnMainThread(void (^block)(void)) 14 | { 15 | dispatch_once(&fc_mainThreadOnceToken, ^{ 16 | dispatch_queue_set_specific(dispatch_get_main_queue(), &fc_mainThreadOnceToken, &fc_mainThreadOnceToken, NULL); 17 | }); 18 | 19 | if (dispatch_get_specific(&fc_mainThreadOnceToken) == &fc_mainThreadOnceToken) { 20 | block(); 21 | } else { 22 | dispatch_async(dispatch_get_main_queue(), block); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /FCUtilities/FCCache.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCCache.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | 8 | @interface FCCache : NSObject 9 | 10 | - (id)objectForKey:(id)key; 11 | - (void)setObject:(id)obj forKey:(id)key; 12 | - (void)removeObjectForKey:(id)key; 13 | - (void)removeAllObjects; 14 | 15 | @property (nonatomic) NSUInteger itemLimit; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /FCUtilities/FCCache.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCCache.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "FCCache.h" 7 | #ifdef TARGET_OS_IPHONE 8 | @import UIKit; 9 | #endif 10 | 11 | @interface FCCache () { 12 | NSUInteger limit; 13 | } 14 | @property (nonatomic) NSMutableDictionary *backingStore; 15 | @property (nonatomic) dispatch_queue_t queue; 16 | @end 17 | 18 | @implementation FCCache 19 | 20 | - (instancetype)init 21 | { 22 | if ( (self = [super init]) ) { 23 | self.backingStore = [NSMutableDictionary dictionary]; 24 | #if TARGET_OS_IPHONE && ! TARGET_OS_WATCH 25 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 26 | #endif 27 | self.queue = dispatch_queue_create("FCCache", DISPATCH_QUEUE_SERIAL); // compatible with concurrent as well via barriers 28 | } 29 | return self; 30 | } 31 | 32 | - (void)dealloc { [NSNotificationCenter.defaultCenter removeObserver:self]; } 33 | 34 | - (NSUInteger)itemLimit { return limit; } 35 | 36 | - (void)setItemLimit:(NSUInteger)itemLimit 37 | { 38 | limit = itemLimit; 39 | dispatch_barrier_async(_queue, ^{ 40 | if (limit && _backingStore.count >= limit) [_backingStore removeAllObjects]; 41 | }); 42 | } 43 | 44 | - (id)objectForKey:(id)key 45 | { 46 | if (! key) return nil; 47 | __block id value; 48 | dispatch_sync(_queue, ^{ value = [_backingStore objectForKey:key]; }); 49 | return value; 50 | } 51 | 52 | - (void)setObject:(id)obj forKey:(id)key 53 | { 54 | if (! obj || ! key) return; 55 | dispatch_barrier_async(_queue, ^{ 56 | if (limit && _backingStore.count >= limit) [_backingStore removeAllObjects]; 57 | [_backingStore setObject:obj forKey:key]; 58 | }); 59 | } 60 | 61 | - (void)removeObjectForKey:(id)key 62 | { 63 | if (! key) return; 64 | dispatch_barrier_async(_queue, ^{ [_backingStore removeObjectForKey:key]; }); 65 | } 66 | 67 | - (void)removeAllObjects 68 | { 69 | dispatch_barrier_async(_queue, ^{ [_backingStore removeAllObjects]; }); 70 | } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /FCUtilities/FCConcurrentMutableDictionary.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCConcurrentMutableDictionary.h 3 | // 4 | // Created by Marco Arment on 1/22/15. 5 | // Copyright (c) 2015 Marco Arment. See included LICENSE file. 6 | // 7 | 8 | #import 9 | 10 | @interface FCConcurrentMutableDictionary : NSObject 11 | 12 | @property (nonatomic, readonly) NSUInteger count; 13 | @property (nonatomic, readonly) NSDictionary *dictionarySnapshot; 14 | 15 | + (instancetype)dictionary; 16 | 17 | - (id)objectForKey:(id)key; 18 | - (id)objectForKeyedSubscript:(id)key; 19 | - (void)setObject:(id)object forKey:(id)key; 20 | - (void)setObject:(id)object forKeyedSubscript:(id)key; 21 | - (void)removeObjectForKey:(id)key; 22 | - (void)removeAllObjects; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /FCUtilities/FCConcurrentMutableDictionary.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCConcurrentMutableDictionary.m 3 | // 4 | // Created by Marco Arment on 1/22/15. 5 | // Copyright (c) 2015 Marco Arment. See included LICENSE file. 6 | // 7 | 8 | #import "FCConcurrentMutableDictionary.h" 9 | 10 | @interface FCConcurrentMutableDictionary () 11 | @property (nonatomic) NSMutableDictionary *backingStore; 12 | @property (nonatomic) dispatch_queue_t queue; 13 | @end 14 | 15 | @implementation FCConcurrentMutableDictionary 16 | 17 | + (instancetype)dictionary { return [[self alloc] init]; } 18 | 19 | - (instancetype)init 20 | { 21 | if ( (self = [super init]) ) { 22 | self.backingStore = [NSMutableDictionary dictionary]; 23 | self.queue = dispatch_queue_create("FCConcurrentMutableDictionary", DISPATCH_QUEUE_CONCURRENT); 24 | } 25 | return self; 26 | } 27 | 28 | - (NSDictionary *)dictionarySnapshot 29 | { 30 | __block NSDictionary *dict; 31 | dispatch_sync(_queue, ^{ dict = [self.backingStore copy]; }); 32 | return dict; 33 | } 34 | 35 | - (NSUInteger)count 36 | { 37 | __block NSUInteger count; 38 | dispatch_sync(_queue, ^{ count = _backingStore.count; }); 39 | return count; 40 | } 41 | 42 | - (id)objectForKey:(id)key 43 | { 44 | __block id value; 45 | dispatch_sync(_queue, ^{ value = [_backingStore objectForKey:key]; }); 46 | return value; 47 | } 48 | 49 | - (id)objectForKeyedSubscript:(id)key 50 | { 51 | __block id value; 52 | dispatch_sync(_queue, ^{ value = [_backingStore objectForKeyedSubscript:key]; }); 53 | return value; 54 | } 55 | 56 | - (void)setObject:(id)object forKey:(id)key 57 | { 58 | dispatch_barrier_async(_queue, ^{ [_backingStore setObject:object forKey:key]; }); 59 | } 60 | 61 | - (void)setObject:(id)object forKeyedSubscript:(id)key 62 | { 63 | dispatch_barrier_async(_queue, ^{ [_backingStore setObject:object forKeyedSubscript:key]; }); 64 | } 65 | 66 | - (void)removeObjectForKey:(id)key 67 | { 68 | dispatch_barrier_async(_queue, ^{ [_backingStore removeObjectForKey:key]; }); 69 | } 70 | 71 | - (void)removeAllObjects 72 | { 73 | dispatch_barrier_async(_queue, ^{ [_backingStore removeAllObjects]; }); 74 | } 75 | 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /FCUtilities/FCExtensionPipe.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCExtensionPipe.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | // A basic way for members of a shared iOS App Group container to pass messages to each other. 6 | // 7 | // Any given pipe identifier should be one-way within a process, and each pipe should only have one writer, e.g.: 8 | // 9 | // - the master app can write to a pipe named e.g. "fromApp" but should not read from it, while extensions can read it 10 | // - an extension could write to a pipe named e.g. "fromWatchKit" that the master app reads from but doesn't write 11 | // 12 | #import 13 | 14 | @interface FCExtensionPipe : NSObject 15 | 16 | // Receive messages by retaining one of these. remotePipeIdentifier must be filename-safe. 17 | - (instancetype)initWithAppGroupIdentifier:(NSString *)appGroupID remotePipeIdentifier:(NSString *)remotePipeID target:(__weak id)target action:(SEL)actionTakingNSDictionary; 18 | 19 | // Send messages statically. pipeIdentifier must be filename-safe. 20 | + (BOOL)sendMessageToAppGroupIdentifier:(NSString *)appGroupIdentifier pipeIdentifier:(NSString *)pipeIdentifier userInfo:(NSDictionary *)userInfo; 21 | 22 | 23 | @property (nonatomic, weak) id target; 24 | @property (nonatomic) SEL action; 25 | 26 | @property (nonatomic, readonly) NSDictionary *lastMessage; // For optional conveniences only. Retention not guaranteed. 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /FCUtilities/FCExtensionPipe.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCExtensionPipe.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "FCExtensionPipe.h" 7 | #include 8 | 9 | static void notificationCenterCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) 10 | { 11 | FCExtensionPipe *instance = (__bridge FCExtensionPipe *) observer; 12 | if (instance && [instance isKindOfClass:FCExtensionPipe.class]) { 13 | __strong id target = instance.target; 14 | #pragma clang diagnostic push 15 | #pragma clang diagnostic ignored "-Warc-performSelector-leaks" 16 | [target performSelector:instance.action withObject:instance.lastMessage]; 17 | #pragma clang diagnostic pop 18 | } 19 | } 20 | 21 | 22 | @interface FCExtensionPipe () 23 | @property (nonatomic) NSString *filename; 24 | @end 25 | 26 | @implementation FCExtensionPipe 27 | 28 | - (instancetype)initWithAppGroupIdentifier:(NSString *)appGroupID remotePipeIdentifier:(NSString *)remotePipeID target:(__weak id)target action:(SEL)actionTakingNSDictionary 29 | { 30 | if ( (self = [super init]) ) { 31 | self.target = target; 32 | self.action = actionTakingNSDictionary; 33 | self.filename = [self.class filenameForAppGroupIdentifier:appGroupID sourceIdentifier:remotePipeID]; 34 | 35 | CFStringRef cfStrID = (__bridge CFStringRef) [NSString stringWithFormat:@"%@.%@", appGroupID, remotePipeID]; 36 | CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *) self, notificationCenterCallback, cfStrID, NULL, CFNotificationSuspensionBehaviorDeliverImmediately); 37 | } 38 | return self; 39 | } 40 | 41 | - (void)dealloc 42 | { 43 | CFNotificationCenterRemoveEveryObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *) self); 44 | } 45 | 46 | - (NSDictionary *)lastMessage { return [NSDictionary dictionaryWithContentsOfFile:self.filename]; } 47 | 48 | + (NSString *)filenameForAppGroupIdentifier:(NSString *)appGroupID sourceIdentifier:(NSString *)s 49 | { 50 | return [[NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:appGroupID].path stringByAppendingPathComponent:[NSString stringWithFormat:@"FCExtensionPipe-%@", s]]; 51 | } 52 | 53 | + (BOOL)sendMessageToAppGroupIdentifier:(NSString *)appGroupID pipeIdentifier:(NSString *)pipeID userInfo:(NSDictionary *)userInfo 54 | { 55 | NSError *error; 56 | NSData *data = [NSPropertyListSerialization dataWithPropertyList:userInfo format:NSPropertyListBinaryFormat_v1_0 options:0 error:&error]; 57 | if (! data) { 58 | [[[NSException alloc] initWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"FCExtensionPipe data error: %@", error.localizedDescription] userInfo:nil] raise]; 59 | } 60 | 61 | if (! [data writeToFile:[self filenameForAppGroupIdentifier:appGroupID sourceIdentifier:pipeID] atomically:YES]) { 62 | return NO; 63 | } 64 | 65 | CFStringRef cfStrID = (__bridge CFStringRef) [NSString stringWithFormat:@"%@.%@", appGroupID, pipeID]; 66 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), cfStrID, NULL, NULL, YES); 67 | return YES; 68 | } 69 | 70 | @end 71 | 72 | -------------------------------------------------------------------------------- /FCUtilities/FCNetworkImageLoader.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCNetworkImageLoader.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | @import UIKit; 8 | 9 | @interface FCNetworkImageLoader : NSObject 10 | 11 | // Optional 12 | + (void)setCellularPolicyHandler:(BOOL (^ _Nullable)(void))returnIsCellularAllowed; 13 | 14 | // Optional. Will be called from a background queue, so be careful with UI* calls. 15 | // Use the fc_decodedImageFromData:… methods in UIImage+FCUtilities.h instead of UIImage-based processing or rendering. 16 | + (void)setFetchedImageDecoder:(UIImage * _Nullable (^ _Nullable)(NSData * _Nonnull imageData))block; 17 | 18 | // Optional. Called after each completed request to report its data usage. 19 | + (void)setDataTransferHandler:(void (^ _Nullable)(int64_t totalBytesTransferred, int64_t cellularBytesTransferred))dataTransferHandler; 20 | 21 | + (void)loadImageAtURL:(NSURL * _Nonnull)url intoImageView:(UIImageView * _Nonnull)imageView placeholderImage:(UIImage * _Nullable)placeholder; 22 | + (void)loadImageAtURL:(NSURL * _Nonnull)url intoImageView:(UIImageView * _Nonnull)imageView placeholderImage:(UIImage * _Nullable)placeholder cachePolicy:(NSURLRequestCachePolicy)cachePolicy; 23 | + (void)loadImageAtURL:(NSURL * _Nonnull)url intoImageView:(UIImageView * _Nonnull)imageView placeholderImage:(UIImage * _Nullable)placeholder cachePolicy:(NSURLRequestCachePolicy)cachePolicy imageTransformer:(UIImage * _Nonnull (^ _Nullable)(UIImage * _Nonnull image, CGSize imageViewSize))imageTransformer; 24 | 25 | + (void)cancelLoadForImageView:(UIImageView * _Nonnull)imageView; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /FCUtilities/FCNetworkImageLoader.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCNetworkImageLoader.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "FCNetworkImageLoader.h" 7 | #import "UIImage+FCUtilities.h" 8 | #import 9 | 10 | @interface UIImageView (FCNetworkImageLoader) 11 | @property (nonatomic, strong) NSURLSessionTask *fcNetworkImageLoader_downloadTask; 12 | @end 13 | 14 | #import 15 | @implementation UIImageView (FCNetworkImageLoader) 16 | @dynamic fcNetworkImageLoader_downloadTask; 17 | - (NSURLSessionTask *)fcNetworkImageLoader_downloadTask { return objc_getAssociatedObject(self, @selector(fcNetworkImageLoader_downloadTask)); } 18 | - (void)setFcNetworkImageLoader_downloadTask:(NSURLSessionTask *)downloadTask 19 | { 20 | objc_setAssociatedObject(self, @selector(fcNetworkImageLoader_downloadTask), downloadTask, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 21 | } 22 | @end 23 | 24 | @interface FCNetworkImageLoader () { 25 | @public 26 | os_unfair_lock writeLock; 27 | } 28 | @property (nonatomic) NSURLSession *session; 29 | @property (nonatomic) dispatch_queue_t decodeQueue; 30 | @property (nonatomic, copy) BOOL (^cellularPolicyHandler)(void); 31 | @property (nonatomic, copy) UIImage *(^fetchedImageDecoder)(NSData *imageData); 32 | @property (nonatomic, copy) void (^dataTransferHandler)(int64_t totalBytesTransferred, int64_t cellularBytesTransferred); 33 | + (instancetype)sharedInstance; 34 | @end 35 | 36 | @implementation FCNetworkImageLoader 37 | 38 | + (instancetype)sharedInstance 39 | { 40 | static FCNetworkImageLoader *instance; 41 | static dispatch_once_t onceToken; 42 | dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); 43 | return instance; 44 | } 45 | 46 | + (void)setCellularPolicyHandler:(BOOL (^)(void))returnIsCellularAllowed 47 | { 48 | FCNetworkImageLoader.sharedInstance.cellularPolicyHandler = returnIsCellularAllowed; 49 | } 50 | 51 | + (void)setFetchedImageDecoder:(UIImage * (^)(NSData *imageData))block 52 | { 53 | FCNetworkImageLoader.sharedInstance.fetchedImageDecoder = block; 54 | } 55 | 56 | + (void)setDataTransferHandler:(void (^)(int64_t totalBytesTransferred, int64_t cellularBytesTransferred))dataTransferHandler 57 | { 58 | FCNetworkImageLoader.sharedInstance.dataTransferHandler = dataTransferHandler; 59 | } 60 | 61 | - (instancetype)init 62 | { 63 | if ( (self = [super init]) ) { 64 | writeLock = OS_UNFAIR_LOCK_INIT; 65 | self.decodeQueue = dispatch_queue_create("FCNetworkImageLoader-decode", DISPATCH_QUEUE_CONCURRENT); 66 | self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil]; 67 | } 68 | return self; 69 | } 70 | 71 | - (void)URLSession:(NSURLSession * _Nonnull)session dataTask:(NSURLSessionDataTask * _Nonnull)dataTask willCacheResponse:(NSCachedURLResponse * _Nonnull)proposedResponse completionHandler:(void (^ _Nonnull)(NSCachedURLResponse * _Nullable cachedResponse))completionHandler 72 | { 73 | // Force all valid responses to be cacheable 74 | NSInteger httpStatus = ((NSHTTPURLResponse *)proposedResponse.response).statusCode; 75 | if (httpStatus >= 200 && httpStatus < 300) { 76 | proposedResponse = [[NSCachedURLResponse alloc] initWithResponse:proposedResponse.response data:proposedResponse.data userInfo:proposedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; 77 | } 78 | completionHandler(proposedResponse); 79 | } 80 | 81 | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics 82 | { 83 | int64_t bytesTransferred = 0; 84 | int64_t cellularBytesTransferred = 0; 85 | for (NSURLSessionTaskTransactionMetrics *tm in metrics.transactionMetrics) { 86 | int64_t total = tm.countOfRequestHeaderBytesSent + tm.countOfRequestBodyBytesSent + tm.countOfResponseHeaderBytesReceived + tm.countOfResponseBodyBytesReceived; 87 | bytesTransferred += total; 88 | if (tm.isCellular) cellularBytesTransferred += total; 89 | } 90 | 91 | if (self.dataTransferHandler) self.dataTransferHandler(bytesTransferred, cellularBytesTransferred); 92 | } 93 | 94 | 95 | + (void)loadImageAtURL:(NSURL *)url intoImageView:(UIImageView *)imageView placeholderImage:(UIImage *)placeholder 96 | { 97 | [self loadImageAtURL:url intoImageView:imageView placeholderImage:placeholder cachePolicy:NSURLRequestUseProtocolCachePolicy imageTransformer:nil]; 98 | } 99 | 100 | + (void)loadImageAtURL:(NSURL *)url intoImageView:(UIImageView *)imageView placeholderImage:(UIImage *)placeholder cachePolicy:(NSURLRequestCachePolicy)cachePolicy 101 | { 102 | [self loadImageAtURL:url intoImageView:imageView placeholderImage:placeholder cachePolicy:cachePolicy imageTransformer:nil]; 103 | } 104 | 105 | + (void)loadImageAtURL:(NSURL *)url intoImageView:(UIImageView *)imageView placeholderImage:(UIImage *)placeholder cachePolicy:(NSURLRequestCachePolicy)cachePolicy imageTransformer:(UIImage * _Nonnull (^ _Nullable)(UIImage * _Nonnull image, CGSize imageViewSize))imageTransformer 106 | { 107 | [FCNetworkImageLoader.sharedInstance _loadImageAtURL:url intoImageView:imageView placeholderImage:placeholder cachePolicy:cachePolicy imageTransformer:imageTransformer]; 108 | } 109 | 110 | - (void)_loadImageAtURL:(NSURL *)url intoImageView:(UIImageView *)imageView placeholderImage:(UIImage *)placeholder cachePolicy:(NSURLRequestCachePolicy)cachePolicy imageTransformer:(UIImage * _Nonnull (^ _Nullable)(UIImage * _Nonnull image, CGSize imageViewSize))imageTransformer 111 | { 112 | os_unfair_lock_lock((os_unfair_lock * _Nonnull) &(writeLock)); 113 | 114 | NSURLSessionTask *alreadyDownloadingTask = imageView.fcNetworkImageLoader_downloadTask; 115 | BOOL alreadyDownloadingThisURL = alreadyDownloadingTask && [alreadyDownloadingTask.originalRequest.URL isEqual:url]; 116 | 117 | if (alreadyDownloadingTask && ! alreadyDownloadingThisURL) { 118 | [alreadyDownloadingTask cancel]; 119 | imageView.fcNetworkImageLoader_downloadTask = nil; 120 | } 121 | 122 | __weak typeof(self) weakSelf = self; 123 | __weak UIImageView *weakImageView = imageView; 124 | 125 | if (placeholder && ! alreadyDownloadingThisURL) imageView.image = placeholder; 126 | 127 | NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url cachePolicy:cachePolicy timeoutInterval:30]; 128 | BOOL (^cellularHandler)(void) = FCNetworkImageLoader.sharedInstance.cellularPolicyHandler; 129 | if (cellularHandler) req.allowsCellularAccess = cellularHandler(); 130 | 131 | NSURLSessionDataTask *task = [FCNetworkImageLoader.sharedInstance.session dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { 132 | dispatch_async(dispatch_get_main_queue(), ^{ 133 | CGSize imageViewSize = imageView.bounds.size; 134 | 135 | __strong typeof(self) strongSelf = weakSelf; 136 | __strong UIImageView *strongImageView = weakImageView; 137 | if (! strongSelf || ! strongImageView || ! data || ! response || error || ! [strongImageView.fcNetworkImageLoader_downloadTask.originalRequest.URL isEqual:url]) return; 138 | 139 | dispatch_async(strongSelf.decodeQueue, ^{ 140 | UIImage *(^imageDecoder)(NSData *image) = FCNetworkImageLoader.sharedInstance.fetchedImageDecoder; 141 | UIImage *image = imageDecoder ? imageDecoder(data) : [UIImage fc_decodedImageFromData:data]; 142 | if (! image) return; 143 | 144 | os_unfair_lock_lock((os_unfair_lock * _Nonnull) &writeLock); 145 | BOOL current = [strongImageView.fcNetworkImageLoader_downloadTask.originalRequest.URL isEqual:url]; 146 | os_unfair_lock_unlock((os_unfair_lock * _Nonnull) &writeLock); 147 | if (current) { 148 | UIImage *imageToDisplay = image; 149 | if (imageTransformer) imageToDisplay = imageTransformer(imageToDisplay, imageViewSize); 150 | dispatch_async(dispatch_get_main_queue(), ^{ 151 | strongImageView.image = imageToDisplay; 152 | }); 153 | } 154 | }); 155 | }); 156 | }]; 157 | imageView.fcNetworkImageLoader_downloadTask = task; 158 | [task resume]; 159 | os_unfair_lock_unlock((os_unfair_lock * _Nonnull) &writeLock); 160 | } 161 | 162 | + (void)cancelLoadForImageView:(UIImageView *)imageView 163 | { 164 | os_unfair_lock_lock((os_unfair_lock * _Nonnull) &(FCNetworkImageLoader.sharedInstance->writeLock)); 165 | NSURLSessionTask *existingTask = imageView.fcNetworkImageLoader_downloadTask; 166 | if (existingTask) { [existingTask cancel]; imageView.fcNetworkImageLoader_downloadTask = nil; } 167 | os_unfair_lock_unlock((os_unfair_lock * _Nonnull) &(FCNetworkImageLoader.sharedInstance->writeLock)); 168 | } 169 | 170 | @end 171 | -------------------------------------------------------------------------------- /FCUtilities/FCOpenInChromeActivity.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCOpenInChromeActivity.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | 8 | FOUNDATION_EXPORT NSString *const FCActivityTypeOpenInChrome; 9 | 10 | @interface FCOpenInChromeActivity : UIActivity 11 | 12 | - (instancetype)initWithSourceName:(NSString *)xCallbackSource successCallbackURL:(NSURL *)xCallbackURL; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /FCUtilities/FCOpenInChromeActivity.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCOpenInChromeActivity.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "FCOpenInChromeActivity.h" 7 | 8 | NSString *const FCActivityTypeOpenInChrome = @"FCActivityTypeOpenInChrome"; 9 | 10 | @interface FCOpenInChromeActivity () 11 | @property (nonatomic, copy) NSString *callbackSource; 12 | @property (nonatomic) NSURL *URL; 13 | @property (nonatomic) NSURL *successCallbackURL; 14 | @end 15 | 16 | @implementation FCOpenInChromeActivity 17 | 18 | + (NSString *)conservativelyPercentEscapeString:(NSString *)str 19 | { 20 | static NSMutableCharacterSet *allowedCharacters = nil; 21 | if (! allowedCharacters) { 22 | allowedCharacters = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; 23 | [allowedCharacters removeCharactersInString:@"?=&+:;@/$!'()\",*"]; 24 | } 25 | return [str stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters]; 26 | } 27 | 28 | - (instancetype)initWithSourceName:(NSString *)xCallbackSource successCallbackURL:(NSURL *)xCallbackURL 29 | { 30 | if ( (self = [super init]) ) { 31 | self.callbackSource = xCallbackSource; 32 | self.successCallbackURL = xCallbackURL; 33 | } 34 | return self; 35 | } 36 | 37 | - (NSString *)activityType { return FCActivityTypeOpenInChrome; } 38 | - (NSString *)activityTitle { return NSLocalizedString(@"Open in Chrome", NULL); } 39 | 40 | - (UIImage *)activityImage 41 | { 42 | return [self.class chromeLogoWithHeight:33]; 43 | } 44 | 45 | + (UIImage *)chromeLogoWithHeight:(CGFloat)outputHeight 46 | { 47 | CGSize outputSize = CGSizeMake(outputHeight, outputHeight); 48 | UIGraphicsBeginImageContextWithOptions(outputSize, NO, UIScreen.mainScreen.scale); 49 | 50 | CGContextRef ctx = UIGraphicsGetCurrentContext(); 51 | CGContextSaveGState(ctx); 52 | CGFloat scale = outputHeight / 31.0f; 53 | CGContextConcatCTM(ctx, CGAffineTransformMakeScale(scale, scale)); 54 | 55 | [UIColor.blackColor setStroke]; 56 | 57 | UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(0.5, 0.5, 30, 30)]; 58 | [ovalPath stroke]; 59 | 60 | UIBezierPath* oval2Path = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(9.5, 9.5, 12, 12)]; 61 | [oval2Path stroke]; 62 | 63 | UIBezierPath* bezierPath = [UIBezierPath bezierPath]; 64 | [bezierPath moveToPoint: CGPointMake(16.5, 9.5)]; 65 | [bezierPath addCurveToPoint: CGPointMake(29.5, 9.5) controlPoint1: CGPointMake(28.5, 9.5) controlPoint2: CGPointMake(29.5, 9.5)]; 66 | [bezierPath stroke]; 67 | 68 | UIBezierPath* bezier2Path = [UIBezierPath bezierPath]; 69 | [bezier2Path moveToPoint: CGPointMake(20.5, 18.5)]; 70 | [bezier2Path addLineToPoint: CGPointMake(14.5, 30.5)]; 71 | [bezier2Path stroke]; 72 | 73 | UIBezierPath* bezier3Path = [UIBezierPath bezierPath]; 74 | [bezier3Path moveToPoint: CGPointMake(9.5, 17.5)]; 75 | [bezier3Path addLineToPoint: CGPointMake(3.5, 6.5)]; 76 | [bezier3Path stroke]; 77 | 78 | CGContextRestoreGState(ctx); 79 | UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext(); 80 | UIGraphicsEndImageContext(); 81 | return finalImage; 82 | } 83 | 84 | - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems 85 | { 86 | if (! [UIApplication.sharedApplication canOpenURL:[NSURL URLWithString:@"googlechrome-x-callback://"]]) return NO; 87 | 88 | for (id item in activityItems) { 89 | if ([item isKindOfClass:NSString.class]) { 90 | NSURL *u = [NSURL URLWithString:item]; 91 | if (u && ! u.isFileURL) return YES; 92 | } else if ([item isKindOfClass:NSURL.class]) { 93 | if (! ((NSURL *)item).isFileURL) return YES; 94 | } 95 | } 96 | 97 | return NO; 98 | } 99 | 100 | - (void)prepareWithActivityItems:(NSArray *)activityItems 101 | { 102 | NSURL *stringURL = nil; 103 | NSURL *URL = nil; 104 | for (id item in activityItems) { 105 | if ([item isKindOfClass:NSString.class]) { 106 | NSURL *u = [NSURL URLWithString:item]; 107 | if (u && ! u.isFileURL) stringURL = u; 108 | } else if (! URL && [item isKindOfClass:NSURL.class]) { 109 | if (! ((NSURL *)item).isFileURL) URL = item; 110 | } 111 | } 112 | 113 | self.URL = URL ?: stringURL; 114 | } 115 | 116 | - (void)performActivity 117 | { 118 | [UIApplication.sharedApplication openURL:[NSURL URLWithString:[NSString stringWithFormat: 119 | @"googlechrome-x-callback://x-callback-url/open/?url=%@&x-success=%@&x-source=%@", 120 | [self.class conservativelyPercentEscapeString:self.URL.absoluteString], 121 | [self.class conservativelyPercentEscapeString:(self.successCallbackURL ? self.successCallbackURL.absoluteString : @"")], 122 | [self.class conservativelyPercentEscapeString:(self.callbackSource ?: @"")] 123 | ]] options:@{} completionHandler:^(BOOL success) { 124 | [self activityDidFinish:success]; 125 | }]; 126 | } 127 | 128 | @end 129 | -------------------------------------------------------------------------------- /FCUtilities/FCOpenInSafariActivity.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCOpenInSafariActivity.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | 8 | extern NSString *const FCActivityTypeOpenInSafari; 9 | 10 | @interface FCOpenInSafariActivity : UIActivity 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /FCUtilities/FCOpenInSafariActivity.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCOpenInSafariActivity.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "FCOpenInSafariActivity.h" 7 | 8 | NSString *const FCActivityTypeOpenInSafari = @"FCActivityTypeOpenInSafari"; 9 | 10 | @interface FCOpenInSafariActivity () 11 | @property (nonatomic) NSURL *URL; 12 | @end 13 | 14 | @implementation FCOpenInSafariActivity 15 | 16 | - (NSString *)activityType { return FCActivityTypeOpenInSafari; } 17 | - (NSString *)activityTitle { return NSLocalizedString(@"Open in Safari", NULL); } 18 | - (UIImage *)activityImage { return [UIImage systemImageNamed:@"safari" withConfiguration:[UIImageSymbolConfiguration configurationWithPointSize:23.0f]]; } 19 | 20 | - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems 21 | { 22 | for (id item in activityItems) { 23 | if ([item isKindOfClass:NSString.class]) { 24 | NSURL *u = [NSURL URLWithString:item]; 25 | if (u && ! u.isFileURL) return YES; 26 | } else if ([item isKindOfClass:NSURL.class]) { 27 | if (! ((NSURL *)item).isFileURL) return YES; 28 | } 29 | } 30 | 31 | return NO; 32 | } 33 | 34 | - (void)prepareWithActivityItems:(NSArray *)activityItems 35 | { 36 | NSURL *stringURL = nil; 37 | NSURL *URL = nil; 38 | for (id item in activityItems) { 39 | if ([item isKindOfClass:NSString.class]) { 40 | NSURL *u = [NSURL URLWithString:item]; 41 | if (u && ! u.isFileURL) stringURL = u; 42 | } else if (! URL && [item isKindOfClass:NSURL.class]) { 43 | if (! ((NSURL *)item).isFileURL) URL = item; 44 | } 45 | } 46 | 47 | self.URL = URL ?: stringURL; 48 | } 49 | 50 | - (void)performActivity 51 | { 52 | [UIApplication.sharedApplication openURL:self.URL options:@{} completionHandler:^(BOOL success) { 53 | [self activityDidFinish:success]; 54 | }]; 55 | } 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /FCUtilities/FCPickerViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCPickerViewController.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | 8 | @interface FCPickerViewController : UITableViewController 9 | 10 | - (instancetype)initWithTitle:(NSString *)title items:(NSArray *)items itemLabelBlock:(NSString *(^)(id item))itemLabelBlock pickedBlock:(void (^)(NSUInteger idx))pickedBlock currentSelection:(NSUInteger)currentSelection; 11 | 12 | // Subclasses may override these to e.g. customize appearance or register a different cell class 13 | - (void)configureTableView:(UITableView *)tableView withCellReuseIdentifier:(NSString *)cellReuseIdentifier; 14 | - (void)configureCell:(UITableViewCell *)cell withItem:(id)item; 15 | - (UIView *)viewForSectionHeaderWithTitle:(NSString *)title width:(CGFloat)width; 16 | 17 | @end 18 | 19 | 20 | @interface FCPickerViewSectionBreak : NSObject 21 | 22 | - (instancetype)initWithSectionTitle:(NSString *)title; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /FCUtilities/FCPickerViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCPickerViewController.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "FCPickerViewController.h" 7 | 8 | 9 | @interface FCPickerViewSectionBreak () 10 | @property (nonatomic, copy) NSString *title; 11 | @end 12 | @implementation FCPickerViewSectionBreak 13 | - (instancetype)initWithSectionTitle:(NSString *)title 14 | { 15 | if ( (self = [super init]) ) { 16 | self.title = title; 17 | } 18 | return self; 19 | } 20 | @end 21 | 22 | 23 | #define kCellReuseIdentifier @"FCPickerCell" 24 | 25 | @interface FCPickerViewController () { 26 | NSUInteger _currentSelection; 27 | NSArray *itemsBySection; 28 | NSArray *allItems; 29 | NSArray *sectionTitles; 30 | NSIndexPath *indexPathOfSelectedCell; 31 | } 32 | @property (nonatomic, copy) NSString *(^itemLabelBlock)(id item); 33 | @property (nonatomic, copy) void (^pickedBlock)(NSUInteger idx); 34 | @end 35 | 36 | @implementation FCPickerViewController 37 | 38 | - (instancetype)initWithTitle:(NSString *)title items:(NSArray *)items itemLabelBlock:(NSString *(^)(id item))itemLabelBlock pickedBlock:(void (^)(NSUInteger idx))pickedBlock currentSelection:(NSUInteger)currentSelection 39 | { 40 | if ( (self = [super initWithStyle:UITableViewStyleInsetGrouped]) ) { 41 | 42 | NSMutableArray *filteredItems = [NSMutableArray array]; 43 | NSMutableArray *sections = [NSMutableArray array]; 44 | NSMutableArray *currentSection = [NSMutableArray array]; 45 | NSMutableArray *titlesBySection = [NSMutableArray arrayWithObject:@""]; 46 | [sections addObject:currentSection]; 47 | for (id item in items) { 48 | if ([item isKindOfClass:FCPickerViewSectionBreak.class]) { 49 | currentSection = [NSMutableArray array]; 50 | [sections addObject:currentSection]; 51 | [titlesBySection addObject:((FCPickerViewSectionBreak *) item).title ?: @""]; 52 | } else { 53 | [filteredItems addObject:item]; 54 | [currentSection addObject:item]; 55 | } 56 | } 57 | allItems = [filteredItems copy]; 58 | itemsBySection = [sections copy]; 59 | sectionTitles = [titlesBySection copy]; 60 | 61 | self.title = title; 62 | self.itemLabelBlock = itemLabelBlock; 63 | self.pickedBlock = pickedBlock; 64 | _currentSelection = currentSelection; 65 | } 66 | return self; 67 | } 68 | 69 | - (void)viewDidLoad 70 | { 71 | [super viewDidLoad]; 72 | [self.tableView registerClass:UITableViewCell.class forCellReuseIdentifier:kCellReuseIdentifier]; 73 | [self configureTableView:self.tableView withCellReuseIdentifier:kCellReuseIdentifier]; 74 | } 75 | 76 | // For subclass overriding 77 | - (void)configureTableView:(UITableView *)tableView withCellReuseIdentifier:(NSString *)cellReuseIdentifier 78 | { 79 | } 80 | 81 | // For subclass overriding 82 | - (void)configureCell:(UITableViewCell *)cell withItem:(id)item 83 | { 84 | cell.textLabel.text = self.itemLabelBlock ? self.itemLabelBlock(item) : ([item isKindOfClass:NSString.class] ? (NSString *) item : [item description]); 85 | } 86 | 87 | // For subclass overriding 88 | - (UIView *)viewForSectionHeaderWithTitle:(NSString *)title width:(CGFloat)width 89 | { 90 | return nil; 91 | } 92 | 93 | #pragma mark - Table view data source 94 | 95 | - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section 96 | { 97 | return [self tableView:tableView viewForHeaderInSection:section].bounds.size.height ?: 36.0f; 98 | } 99 | 100 | - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section 101 | { 102 | NSString *titleStr = sectionTitles[section]; 103 | if (! titleStr.length) return nil; 104 | 105 | CGFloat tableWidth = tableView.bounds.size.width; 106 | if (tableView.style == UITableViewStyleInsetGrouped) tableWidth -= 40.0f; 107 | 108 | return [self viewForSectionHeaderWithTitle:sectionTitles[section] width:tableWidth]; 109 | } 110 | 111 | 112 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return itemsBySection.count; } 113 | 114 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return itemsBySection[section].count; } 115 | 116 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 117 | { 118 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellReuseIdentifier forIndexPath:indexPath]; 119 | id item = itemsBySection[indexPath.section][indexPath.row]; 120 | [self configureCell:cell withItem:item]; 121 | 122 | if ([allItems indexOfObject:item] == _currentSelection) { 123 | cell.accessoryType = UITableViewCellAccessoryCheckmark; 124 | indexPathOfSelectedCell = [indexPath copy]; 125 | } else { 126 | cell.accessoryType = UITableViewCellAccessoryNone; 127 | } 128 | 129 | return cell; 130 | } 131 | 132 | #pragma mark - Table view delegate 133 | 134 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 135 | { 136 | if ([indexPath isEqual:indexPathOfSelectedCell]) { 137 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 138 | return; 139 | } 140 | 141 | [tableView cellForRowAtIndexPath:indexPathOfSelectedCell].accessoryType = UITableViewCellAccessoryNone; 142 | [tableView cellForRowAtIndexPath:indexPath].accessoryType = UITableViewCellAccessoryCheckmark; 143 | indexPathOfSelectedCell = indexPath; 144 | 145 | dispatch_async(dispatch_get_main_queue(), ^(void){ 146 | self.pickedBlock([allItems indexOfObject:itemsBySection[indexPath.section][indexPath.row]]); 147 | [self.navigationController popViewControllerAnimated:YES]; 148 | }); 149 | } 150 | 151 | @end 152 | -------------------------------------------------------------------------------- /FCUtilities/FCReachability.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCReachability.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | 8 | extern NSString * const FCReachabilityChangedNotification; 9 | extern NSString * const FCReachabilityOnlineNotification; 10 | 11 | @interface FCReachability : NSObject 12 | 13 | + (instancetype)sharedInstance; 14 | - (BOOL)internetConnectionIsOfflineForError:(NSError *)error; 15 | 16 | @property (nonatomic, readonly) BOOL isOnline; 17 | @property (nonatomic, readonly) BOOL isUnrestricted; // Wi-Fi, Ethernet, etc. not marked by Low Data Mode or "expensive" (tethering, etc.) 18 | @property (nonatomic, readonly) BOOL isCellular; // consider using isExpensive instead, which is semantically usually more correct 19 | @property (nonatomic, readonly) BOOL isExpensive; // includes cellular and tethering 20 | @property (nonatomic, readonly) BOOL isConstrained; // Low Data Mode 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /FCUtilities/FCReachability.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCReachability.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "FCReachability.h" 7 | @import Network; 8 | #import 9 | 10 | NSString * const FCReachabilityChangedNotification = @"FCReachabilityChangedNotification"; 11 | NSString * const FCReachabilityOnlineNotification = @"FCReachabilityOnlineNotification"; 12 | 13 | @interface FCReachability () { 14 | nw_path_monitor_t monitor; 15 | dispatch_queue_t queue; 16 | BOOL wasOnline; 17 | BOOL wasCellular; 18 | BOOL wasExpensive; 19 | BOOL wasConstrained; 20 | atomic_bool isSettingInitialState; 21 | } 22 | @property (nonatomic) BOOL isOnline; 23 | @property (nonatomic) BOOL isCellular; 24 | @property (nonatomic) BOOL isExpensive; 25 | @property (nonatomic) BOOL isConstrained; 26 | @property (nonatomic) BOOL isUnrestricted; 27 | @end 28 | 29 | @implementation FCReachability 30 | 31 | + (instancetype)sharedInstance 32 | { 33 | static dispatch_once_t onceToken; 34 | static FCReachability *g_inst = nil; 35 | dispatch_once(&onceToken, ^{ 36 | g_inst = [[FCReachability alloc] init]; 37 | }); 38 | return g_inst; 39 | } 40 | 41 | - (instancetype)init 42 | { 43 | if ( (self = [super init]) ) { 44 | dispatch_queue_attr_t attrs = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, DISPATCH_QUEUE_PRIORITY_DEFAULT); 45 | queue = dispatch_queue_create("FCReachability", attrs); 46 | 47 | // initial state: assume online, but don't assume Wi-Fi until we know 48 | self.isOnline = wasOnline = YES; 49 | self.isCellular = wasCellular = YES; 50 | self.isExpensive = wasExpensive = YES; 51 | self.isConstrained = wasConstrained = YES; 52 | self.isUnrestricted = NO; 53 | 54 | monitor = nw_path_monitor_create(); 55 | nw_path_monitor_set_queue(monitor, queue); 56 | 57 | __weak typeof(self) weakSelf = self; 58 | nw_path_monitor_set_update_handler(monitor, ^(nw_path_t path) { 59 | __strong typeof(self) strongSelf = weakSelf; 60 | if (! strongSelf) return; 61 | 62 | BOOL isOnline = nw_path_get_status(path) == nw_path_status_satisfied; 63 | strongSelf.isOnline = isOnline; 64 | strongSelf.isCellular = strongSelf.isOnline && nw_path_uses_interface_type(path, nw_interface_type_cellular); 65 | strongSelf.isExpensive = strongSelf.isOnline && nw_path_is_expensive(path); 66 | strongSelf.isConstrained = strongSelf.isOnline && nw_path_is_constrained(path); 67 | 68 | strongSelf.isUnrestricted = isOnline && ! (strongSelf.isCellular || strongSelf.isExpensive || strongSelf.isConstrained); 69 | 70 | BOOL onlineChanged = (strongSelf->wasOnline != isOnline); 71 | BOOL statusChanged = onlineChanged || (strongSelf.isCellular != strongSelf->wasCellular) || (strongSelf.isExpensive != strongSelf->wasExpensive) || (strongSelf.isConstrained != strongSelf->wasConstrained); 72 | strongSelf->wasOnline = isOnline; 73 | strongSelf->wasCellular = strongSelf.isCellular; 74 | strongSelf->wasExpensive = strongSelf.isExpensive; 75 | strongSelf->wasConstrained = strongSelf.isConstrained; 76 | 77 | if (statusChanged && ! atomic_load(&strongSelf->isSettingInitialState)) { 78 | dispatch_async(dispatch_get_main_queue(), ^{ 79 | [NSNotificationCenter.defaultCenter postNotificationName:FCReachabilityChangedNotification object:strongSelf userInfo:nil]; 80 | if (onlineChanged && isOnline) { 81 | [NSNotificationCenter.defaultCenter postNotificationName:FCReachabilityOnlineNotification object:strongSelf userInfo:nil]; 82 | } 83 | }); 84 | } 85 | }); 86 | 87 | atomic_store(&isSettingInitialState, true); 88 | nw_path_monitor_start(monitor); 89 | dispatch_sync(queue, ^{ }); // wait for initial state if it's queued synchronously in nw_path_monitor_start, which seems true 90 | atomic_store(&isSettingInitialState, false); 91 | } 92 | return self; 93 | } 94 | 95 | - (BOOL)internetConnectionIsOfflineForError:(NSError *)error 96 | { 97 | return (error && (error.code == NSURLErrorNotConnectedToInternet || error.code == NSURLErrorNetworkConnectionLost || error.code == NSURLErrorDataNotAllowed)); 98 | } 99 | 100 | @end 101 | -------------------------------------------------------------------------------- /FCUtilities/FCSheetView.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCSheetView.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | 8 | 9 | @interface FCSheetView : UIView 10 | 11 | - (instancetype)initWithContentView:(UIView *)contentView; 12 | - (void)presentInView:(UIView *)view; 13 | - (void)presentInView:(UIView *)view extraAnimations:(void (^)(void))animations extraDismissAnimations:(void (^)(void))dismissAnimations; 14 | - (void)dismissAnimated:(BOOL)animated completion:(void (^)(void))completionBlock; 15 | 16 | + (void)dismissAllAnimated:(BOOL)animated; 17 | 18 | @property (nonatomic, copy) void (^dismissAction)(void); 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /FCUtilities/FCSheetView.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCSheetView.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #define kSlideInAnimationDuration 0.40f 7 | #define kSlideOutAnimationDuration 0.25f 8 | #define kExtraHeightForBottomOverlap 40 9 | 10 | #import "FCSheetView.h" 11 | 12 | #define FCSheetViewForceDismissNotification @"FCSheetViewForceDismissNotification" 13 | 14 | @interface FCSheetView () 15 | @property (nonatomic) UIButton *dismissButton; 16 | @property (nonatomic) UIView *contentContainer; 17 | @property (nonatomic) UIToolbar *blurToolbar; 18 | @property (nonatomic) CALayer *blurLayer; 19 | @property (nonatomic) UIView *blurView; 20 | @property (nonatomic, copy) void (^dismissAnimations)(void); 21 | @property (nonatomic) BOOL presented; 22 | @end 23 | 24 | @implementation FCSheetView 25 | 26 | + (void)dismissAllAnimated:(BOOL)animated 27 | { 28 | [NSNotificationCenter.defaultCenter postNotificationName:FCSheetViewForceDismissNotification object:@(animated)]; 29 | } 30 | 31 | - (instancetype)initWithContentView:(UIView *)contentView 32 | { 33 | if ( (self = [super init]) ) { 34 | self.presented = NO; 35 | self.accessibilityViewIsModal = YES; 36 | 37 | CGRect contentContainerFrame = contentView.bounds; 38 | contentContainerFrame.size.height += kExtraHeightForBottomOverlap; 39 | self.contentContainer = [[UIView alloc] initWithFrame:contentContainerFrame]; 40 | self.contentContainer.backgroundColor = UIColor.clearColor; 41 | self.contentContainer.autoresizesSubviews = YES; 42 | self.contentContainer.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 43 | 44 | contentContainerFrame.origin = CGPointZero; 45 | self.blurToolbar = [[UIToolbar alloc] initWithFrame:contentContainerFrame]; 46 | self.blurLayer = self.blurToolbar.layer; 47 | self.blurView = [UIView new]; 48 | self.blurView.userInteractionEnabled = NO; 49 | self.contentContainer.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 50 | [_blurView.layer addSublayer:_blurLayer]; 51 | [self.contentContainer addSubview:_blurView]; 52 | 53 | CGRect innerContentFrame = contentView.bounds; 54 | innerContentFrame.origin = CGPointZero; 55 | contentView.frame = innerContentFrame; 56 | [self.contentContainer addSubview:contentView]; 57 | 58 | self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 59 | self.autoresizesSubviews = YES; 60 | self.opaque = NO; 61 | self.backgroundColor = [UIColor clearColor]; 62 | self.dismissButton = [UIButton buttonWithType:UIButtonTypeCustom]; 63 | [self.dismissButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchDown]; 64 | 65 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(dismissByNotification:) name:FCSheetViewForceDismissNotification object:nil]; 66 | } 67 | return self; 68 | } 69 | 70 | - (void)dealloc { [NSNotificationCenter.defaultCenter removeObserver:self]; } 71 | 72 | - (void)dismissByNotification:(NSNotification *)n 73 | { 74 | if (self.presented) [self dismissAnimated:((NSNumber *) n.object).boolValue completion:NULL]; 75 | } 76 | 77 | - (void)dismissAnimated:(BOOL)animated completion:(void (^)(void))completionBlock 78 | { 79 | if (animated) { 80 | [UIView animateWithDuration:kSlideOutAnimationDuration animations:^{ 81 | CGRect contentFrame = _contentContainer.bounds; 82 | contentFrame.origin.y = self.bounds.size.height; 83 | _contentContainer.frame = contentFrame; 84 | self.backgroundColor = [UIColor clearColor]; 85 | 86 | if (self.dismissAnimations) self.dismissAnimations(); 87 | } completion:^(BOOL finished) { 88 | [self removeFromSuperview]; 89 | UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); 90 | if (self.dismissAction) self.dismissAction(); 91 | if (completionBlock) completionBlock(); 92 | }]; 93 | } else { 94 | [self removeFromSuperview]; 95 | UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); 96 | if (self.dismissAction) self.dismissAction(); 97 | if (completionBlock) completionBlock(); 98 | } 99 | } 100 | 101 | - (void)dismiss 102 | { 103 | [self dismissAnimated:YES completion:NULL]; 104 | } 105 | 106 | - (BOOL)accessibilityPerformEscape 107 | { 108 | [self dismiss]; 109 | return YES; 110 | } 111 | 112 | - (void)presentInView:(UIView *)view 113 | { 114 | self.presented = YES; 115 | [self presentInView:view extraAnimations:nil extraDismissAnimations:nil]; 116 | } 117 | 118 | - (void)presentInView:(UIView *)view extraAnimations:(void (^)(void))animations extraDismissAnimations:(void (^)(void))dismissAnimations 119 | { 120 | if (! view.window) [[NSException exceptionWithName:NSInvalidArgumentException reason:@"FCSheetView host view must be in a window" userInfo:nil] raise]; 121 | 122 | self.presented = YES; 123 | self.dismissAnimations = dismissAnimations; 124 | 125 | CGRect masterFrame = view.window.bounds; 126 | self.frame = masterFrame; 127 | [view.window addSubview:self]; 128 | 129 | CGRect dismissFrame = masterFrame; 130 | dismissFrame.size.height = masterFrame.size.height - (_contentContainer.bounds.size.height - kExtraHeightForBottomOverlap); 131 | 132 | self.dismissButton.frame = dismissFrame; 133 | self.dismissButton.accessibilityLabel = NSLocalizedString(@"Back", @"FCSheetView dismiss-button accessibility label"); 134 | [self addSubview:self.dismissButton]; 135 | 136 | __block CGRect contentFrame = masterFrame; 137 | contentFrame.size.height = _contentContainer.bounds.size.height; 138 | contentFrame.origin.y = masterFrame.size.height; 139 | _contentContainer.frame = contentFrame; 140 | [self addSubview:_contentContainer]; 141 | self.tintColor = view.window.tintColor; 142 | 143 | [UIView animateWithDuration:kSlideInAnimationDuration delay:0 144 | usingSpringWithDamping:0.66f initialSpringVelocity:0.9f 145 | options:UIViewAnimationOptionCurveEaseInOut 146 | animations:^{ 147 | contentFrame.origin.y = masterFrame.size.height - (_contentContainer.bounds.size.height - kExtraHeightForBottomOverlap); 148 | _contentContainer.frame = contentFrame; 149 | self.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.25f]; 150 | if (animations) animations(); 151 | } 152 | completion:^(BOOL finished) { 153 | UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); 154 | } 155 | ]; 156 | } 157 | 158 | 159 | @end 160 | -------------------------------------------------------------------------------- /FCUtilities/FCSimpleKeychain.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCSimpleKeychain.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #ifndef FCSimpleKeychain_h 7 | #define FCSimpleKeychain_h 8 | 9 | 10 | @import Security; 11 | 12 | static __inline__ __attribute__((always_inline)) NSString *fc_groupKeychainStringForKey(NSString *key, BOOL *outKeychainError, NSString *accessGroup) 13 | { 14 | CFDataRef data = nil; 15 | NSMutableDictionary *params = [@{ 16 | (__bridge id) kSecClass : (__bridge id) kSecClassGenericPassword, 17 | (__bridge id) kSecAttrService : [[NSBundle.mainBundle.infoDictionary objectForKey:(NSString *)kCFBundleIdentifierKey] stringByAppendingFormat:@".%@", key], 18 | (__bridge id) kSecReturnData : (__bridge id) kCFBooleanTrue, 19 | } mutableCopy]; 20 | if (accessGroup) params[(__bridge id) kSecAttrAccessGroup] = accessGroup; 21 | 22 | OSStatus err = SecItemCopyMatching((__bridge CFDictionaryRef) params, (CFTypeRef *) &data); 23 | if (err != errSecSuccess && err != errSecItemNotFound) { 24 | NSLog(@"Keychain error: SecItemCopyMatching failed for key %@: %d", key, (int) err); 25 | if (outKeychainError) *outKeychainError = YES; 26 | return nil; 27 | } 28 | 29 | if (outKeychainError) *outKeychainError = NO; 30 | if (! data) return nil; 31 | return [[NSString alloc] initWithData:(__bridge_transfer NSData *)data encoding:NSUTF8StringEncoding]; 32 | } 33 | 34 | static __inline__ __attribute__((always_inline)) BOOL fc_deleteGroupKeychainStringForKey(NSString *key, NSString *accessGroup) 35 | { 36 | NSMutableDictionary *params = [@{ 37 | (__bridge id) kSecClass : (__bridge id) kSecClassGenericPassword, 38 | (__bridge id) kSecAttrService : [[NSBundle.mainBundle.infoDictionary objectForKey:(NSString *)kCFBundleIdentifierKey] stringByAppendingFormat:@".%@", key], 39 | } mutableCopy]; 40 | if (accessGroup) params[(__bridge id) kSecAttrAccessGroup] = accessGroup; 41 | return errSecSuccess == SecItemDelete((__bridge CFDictionaryRef) params); 42 | } 43 | 44 | static __inline__ __attribute__((always_inline)) BOOL fc_setGroupKeychainStringForKeyWithAccessibility(NSString *key, NSString *value, CFTypeRef accessibility, NSString *accessGroup) 45 | { 46 | if (! value) return fc_deleteGroupKeychainStringForKey(key, accessGroup); 47 | 48 | NSMutableDictionary *query = [@{ 49 | (__bridge id) kSecClass : (__bridge id) kSecClassGenericPassword, 50 | (__bridge id) kSecAttrService : [[NSBundle.mainBundle.infoDictionary objectForKey:(NSString *)kCFBundleIdentifierKey] stringByAppendingFormat:@".%@", key], 51 | (__bridge id) kSecAttrAccessible : (__bridge id) accessibility, 52 | (__bridge id) kSecValueData : [value dataUsingEncoding:NSUTF8StringEncoding] 53 | } mutableCopy]; 54 | if (accessGroup) query[(__bridge id) kSecAttrAccessGroup] = accessGroup; 55 | 56 | OSStatus err = SecItemAdd((__bridge CFDictionaryRef) query, NULL); 57 | if (err == errSecDuplicateItem && fc_deleteGroupKeychainStringForKey(key, accessGroup)) err = SecItemAdd((__bridge CFDictionaryRef) query, NULL); 58 | if (err != errSecSuccess) NSLog(@"Keychain error: SecItemAdd failed for key %@: %d", key, (int) err); 59 | return err == errSecSuccess; 60 | } 61 | 62 | static __inline__ __attribute__((always_inline)) BOOL fc_setGroupKeychainStringForKey(NSString *key, NSString *value, NSString *accessGroup) 63 | { 64 | return fc_setGroupKeychainStringForKeyWithAccessibility(key, value, kSecAttrAccessibleAfterFirstUnlock, accessGroup); 65 | } 66 | 67 | static __inline__ __attribute__((always_inline)) NSString *fc_keychainStringForKey(NSString *key, BOOL *outKeychainError) 68 | { 69 | return fc_groupKeychainStringForKey(key, outKeychainError, nil); 70 | } 71 | 72 | static __inline__ __attribute__((always_inline)) BOOL fc_deleteKeychainStringForKey(NSString *key) 73 | { 74 | return fc_deleteGroupKeychainStringForKey(key, nil); 75 | } 76 | 77 | static __inline__ __attribute__((always_inline)) BOOL fc_setKeychainStringForKeyWithAccessibility(NSString *key, NSString *value, CFTypeRef accessibility) 78 | { 79 | return fc_setGroupKeychainStringForKeyWithAccessibility(key, value, accessibility, nil); 80 | } 81 | 82 | static __inline__ __attribute__((always_inline)) BOOL fc_setKeychainStringForKey(NSString *key, NSString *value) 83 | { 84 | return fc_setGroupKeychainStringForKeyWithAccessibility(key, value, kSecAttrAccessibleAfterFirstUnlock, nil); 85 | } 86 | 87 | #endif 88 | -------------------------------------------------------------------------------- /FCUtilities/FCTwitterAuthorization.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCTwitterAuthorization.h 3 | // Created by Marco Arment on 7/11/17. 4 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 5 | // 6 | 7 | #import 8 | 9 | @interface FCTwitterCredentials : NSObject 10 | @property (nonatomic, readonly) NSString *token; 11 | @property (nonatomic, readonly) NSString *secret; 12 | @property (nonatomic, readonly) NSString *username; 13 | @end 14 | 15 | /* USAGE 16 | 1. Add a unique URL scheme to Info.plist starting with "twitterkit-", e.g. "twitterkit-123notetaker" 17 | 2. Pass that as callbackURLScheme when using authorizeWithConsumerKey:... 18 | 3. In the app delegate's application:openURL:options:, if the URL scheme matches step 1's, pass it to [FCTwitterAuthorization callbackURLReceived:] 19 | 4. Add the URL scheme "twitterauth" to LSApplicationQueriesSchemes in Info.plist 20 | 5. Ensure your app's Settings on https://apps.twitter.com/ include a valid web Callback URL (which won't be used in this flow) and Callback Locking is OFF. 21 | */ 22 | 23 | @interface FCTwitterAuthorization : NSObject 24 | 25 | + (BOOL)isTwitterAppInstalled; 26 | + (void)authorizeWithConsumerKey:(NSString *)key consumerSecret:(NSString *)secret callbackURLScheme:(NSString *)scheme completion:(void (^)(FCTwitterCredentials *credentials))completion; 27 | 28 | + (void)cancel; 29 | 30 | + (void)callbackURLReceived:(NSURL *)url; 31 | 32 | @end 33 | 34 | -------------------------------------------------------------------------------- /FCUtilities/FCTwitterAuthorization.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCTwitterAuthorization.m 3 | // Created by Marco Arment on 7/11/17. 4 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 5 | // 6 | 7 | #import "FCTwitterAuthorization.h" 8 | 9 | @interface FCTwitterCredentials () 10 | @property (nonatomic) NSString *token; 11 | @property (nonatomic) NSString *secret; 12 | @property (nonatomic) NSString *username; 13 | @end 14 | 15 | @implementation FCTwitterCredentials 16 | + (instancetype)credentialsWithToken:(NSString *)token secret:(NSString *)secret username:(NSString *)username 17 | { 18 | FCTwitterCredentials *creds = [FCTwitterCredentials new]; 19 | creds.token = token ?: @""; 20 | creds.secret = secret ?: @""; 21 | creds.username = username ?: @""; 22 | return creds.token.length && creds.secret.length ? creds : nil; 23 | } 24 | @end 25 | 26 | @interface FCTwitterAuthorization () 27 | @property (nonatomic, copy) void (^completion)(FCTwitterCredentials *credentials); 28 | @property (nonatomic, copy) NSString *consumerKey; 29 | @property (nonatomic, copy) NSString *consumerSecret; 30 | @property (nonatomic, copy) NSString *callbackURLScheme; 31 | @end 32 | 33 | static FCTwitterAuthorization *g_currentInstance = NULL; 34 | 35 | @implementation FCTwitterAuthorization 36 | 37 | + (BOOL)isTwitterAppInstalled { return [UIApplication.sharedApplication canOpenURL:[NSURL URLWithString:@"twitterauth://authorize"]]; } 38 | 39 | + (void)authorizeWithConsumerKey:(NSString *)key consumerSecret:(NSString *)secret callbackURLScheme:(NSString *)scheme completion:(void (^)(FCTwitterCredentials *credentials))completion 40 | { 41 | [self cancel]; 42 | if ( (g_currentInstance = [[self alloc] initWithConsumerKey:key consumerSecret:secret callbackURLScheme:scheme completion:completion]) ) { 43 | [g_currentInstance authorize]; 44 | } else { 45 | if (completion) completion(nil); 46 | } 47 | } 48 | 49 | + (void)cancel 50 | { 51 | if (g_currentInstance) { 52 | [g_currentInstance finishWithCredentials:nil]; 53 | g_currentInstance = nil; 54 | } 55 | } 56 | 57 | - (instancetype)initWithConsumerKey:(NSString *)key consumerSecret:(NSString *)secret callbackURLScheme:(NSString *)scheme completion:(void (^)(FCTwitterCredentials *credentials))completion 58 | { 59 | if ( (self = [super init]) ) { 60 | self.consumerKey = key; 61 | self.consumerSecret = secret; 62 | self.completion = completion; 63 | self.callbackURLScheme = scheme; 64 | } 65 | return self; 66 | } 67 | 68 | + (void)callbackURLReceived:(NSURL *)url { if (g_currentInstance) [g_currentInstance callbackURLReceived:url]; } 69 | - (void)callbackURLReceived:(NSURL *)url 70 | { 71 | NSString *queryString = url.host; 72 | 73 | NSMutableDictionary *params = [NSMutableDictionary dictionary]; 74 | for (NSString *pair in [queryString componentsSeparatedByString:@"&"]) { 75 | NSArray *parts = [pair componentsSeparatedByString:@"="]; 76 | if (parts.count < 1 || parts[0].length < 1) continue; 77 | params[parts[0].stringByRemovingPercentEncoding] = parts.lastObject; 78 | } 79 | 80 | [self finishWithCredentials:[FCTwitterCredentials credentialsWithToken:params[@"token"] secret:params[@"secret"] username:params[@"username"]]]; 81 | } 82 | 83 | - (void)authorize 84 | { 85 | NSURL *appAuthURL = [NSURL URLWithString:[NSString stringWithFormat: 86 | @"twitterauth://authorize?consumer_key=%@&consumer_secret=%@&oauth_callback=%@", 87 | self.consumerKey, self.consumerSecret, self.callbackURLScheme 88 | ]]; 89 | 90 | if ([UIApplication.sharedApplication canOpenURL:appAuthURL]) { 91 | [UIApplication.sharedApplication openURL:appAuthURL options:@{} completionHandler:^(BOOL success) { 92 | if (! success) [self finishWithCredentials:nil]; 93 | }]; 94 | } else { 95 | [self finishWithCredentials:nil]; 96 | } 97 | } 98 | 99 | - (void)finishWithCredentials:(FCTwitterCredentials *)creds 100 | { 101 | void (^completion)(FCTwitterCredentials *credentials) = self.completion; 102 | if (completion) completion(creds); 103 | self.completion = nil; 104 | g_currentInstance = nil; 105 | } 106 | 107 | @end 108 | 109 | -------------------------------------------------------------------------------- /FCUtilities/FCiOS11TableViewAnimationBugfix.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCiOS11TableViewAnimationBugfix.h 3 | // Overcast 4 | // 5 | // Created by Marco Arment on 10/30/17. 6 | // Copyright © 2017 Marco Arment. All rights reserved. 7 | // 8 | 9 | #ifndef FCiOS11TableViewAnimationBugfix_h 10 | #define FCiOS11TableViewAnimationBugfix_h 11 | 12 | inline __attribute__((always_inline)) void fc_iOS11TableViewAnimationBugfix(UIView *view) 13 | { 14 | dispatch_async(dispatch_get_main_queue(), ^{ 15 | [view.layer fc_removeAnimationsRecursive]; 16 | }); 17 | } 18 | 19 | 20 | #endif /* FCiOS11TableViewAnimationBugfix_h */ 21 | -------------------------------------------------------------------------------- /FCUtilities/NSArray+FCUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSArray+FCUtilities.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | 8 | @interface NSArray (FCUtilities) 9 | 10 | - (NSArray *)fc_filteredArrayUsingBlock:(BOOL (^)(id obj, NSUInteger idx, BOOL *stop))keepBlock; 11 | - (NSArray *)fc_arrayWithCorrespondingObjectsFromBlock:(id (^)(id obj))newObjectFromObjectBlock; 12 | - (id)fc_randomObject; 13 | - (id)fc_safeObjectAtIndex:(NSUInteger)idx; 14 | 15 | @end 16 | 17 | 18 | @interface NSMutableArray (FCUtilities) 19 | 20 | - (void)fc_moveObjectAtIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /FCUtilities/NSArray+FCUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSArray+FCUtilities.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "NSArray+FCUtilities.h" 7 | 8 | @implementation NSArray (FCUtilities) 9 | 10 | - (NSArray *)fc_filteredArrayUsingBlock:(BOOL (^)(id obj, NSUInteger idx, BOOL *stop))keepBlock 11 | { 12 | return [self objectsAtIndexes:[self indexesOfObjectsPassingTest:keepBlock]]; 13 | } 14 | 15 | - (NSArray *)fc_arrayWithCorrespondingObjectsFromBlock:(id (^)(id obj))newObjectFromObjectBlock 16 | { 17 | NSMutableArray *outArray = [NSMutableArray arrayWithCapacity:self.count]; 18 | for (id obj in self) [outArray addObject:newObjectFromObjectBlock(obj)]; 19 | return outArray; 20 | } 21 | 22 | - (id)fc_randomObject 23 | { 24 | if (! self.count) return nil; 25 | return self[arc4random_uniform((uint32_t) self.count)]; 26 | } 27 | 28 | - (id)fc_safeObjectAtIndex:(NSUInteger)idx 29 | { 30 | return idx < self.count ? [self objectAtIndex:idx] : nil; 31 | } 32 | 33 | @end 34 | 35 | 36 | @implementation NSMutableArray (FCUtilities) 37 | 38 | - (void)fc_moveObjectAtIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex 39 | { 40 | id object = [self objectAtIndex:fromIndex]; 41 | [self removeObjectAtIndex:fromIndex]; 42 | [self insertObject:object atIndex:toIndex]; 43 | } 44 | 45 | @end 46 | 47 | -------------------------------------------------------------------------------- /FCUtilities/NSData+FCUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSData+FCUtilities.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | 7 | #import 8 | @import Security; 9 | // Also requires libz to be linked, but "@import libz;" doesn't work, presumably because it's not a full-fledged framework 10 | 11 | @interface NSData (FCUtilities) 12 | 13 | + (NSData *)fc_randomDataWithLength:(NSUInteger)length; 14 | - (NSData *)fc_deflatedData; 15 | - (NSData *)fc_inflatedDataWithHeader:(BOOL)headerPresent; // pass NO for raw deflate data without a gzip header, such as PHP's gzdeflate() output 16 | - (NSString *)fc_stringValue; 17 | - (NSString *)fc_hexString; 18 | - (NSString *)fc_URLSafeBase64EncodedString; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /FCUtilities/NSData+FCUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSData+FCUtilities.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "NSData+FCUtilities.h" 7 | #import 8 | #import 9 | 10 | @implementation NSData (FCUtilities) 11 | 12 | + (NSData *)fc_randomDataWithLength:(NSUInteger)length 13 | { 14 | NSMutableData *data = [NSMutableData dataWithLength:length]; 15 | if (0 != SecRandomCopyBytes(kSecRandomDefault, length, (uint8_t *) data.mutableBytes)) return nil; 16 | return [data copy]; 17 | } 18 | 19 | - (NSString *)fc_stringValue 20 | { 21 | return [[NSString alloc] initWithBytes:[self bytes] length:[self length] encoding:NSUTF8StringEncoding]; 22 | } 23 | 24 | - (NSString *)fc_hexString 25 | { 26 | NSMutableString *stringBuffer = [NSMutableString stringWithCapacity:([self length] * 2)]; 27 | const unsigned char *dataBuffer = [self bytes]; 28 | int i; 29 | for (i = 0; i < [self length]; ++i) { 30 | [stringBuffer appendFormat:@"%02lx", (unsigned long)dataBuffer[i]]; 31 | } 32 | return [stringBuffer copy]; 33 | } 34 | 35 | - (NSString *)fc_URLSafeBase64EncodedString 36 | { 37 | NSString *str = [self base64EncodedStringWithOptions:0]; 38 | str = [str stringByReplacingOccurrencesOfString:@"+" withString:@"-"]; 39 | str = [str stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; 40 | str = [str stringByReplacingOccurrencesOfString:@"=" withString:@""]; 41 | return str; 42 | } 43 | 44 | // Deflate functions adapted from: 45 | // http://code.google.com/p/google-toolbox-for-mac/source/browse/trunk/Foundation/GTMNSData%2Bzlib.m?r=5 46 | 47 | - (NSData *)fc_deflatedData 48 | { 49 | z_stream strm; 50 | bzero(&strm, sizeof(z_stream)); 51 | strm.avail_in = (unsigned int) self.length; 52 | strm.next_in = (unsigned char *) self.bytes; 53 | if (Z_OK != deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15, 8, Z_DEFAULT_STRATEGY)) return nil; 54 | 55 | NSMutableData *result = [NSMutableData dataWithCapacity:(self.length / 10)]; 56 | unsigned char output[1024]; 57 | int retCode; 58 | do { 59 | strm.avail_out = 1024; 60 | strm.next_out = output; 61 | retCode = deflate(&strm, Z_FINISH); 62 | if ( (retCode != Z_OK) && (retCode != Z_STREAM_END) ) { 63 | deflateEnd(&strm); 64 | return nil; 65 | } 66 | 67 | unsigned gotBack = 1024 - strm.avail_out; 68 | if (gotBack > 0) [result appendBytes:output length:gotBack]; 69 | } while (retCode == Z_OK); 70 | 71 | deflateEnd(&strm); 72 | return result; 73 | } 74 | 75 | - (NSData *)fc_inflatedDataWithHeader:(BOOL)headerPresent 76 | { 77 | z_stream strm; 78 | bzero(&strm, sizeof(z_stream)); 79 | strm.avail_in = (int) self.length; 80 | strm.next_in = (unsigned char *) self.bytes; 81 | if (Z_OK != inflateInit2(&strm, (headerPresent ? 47 : -MAX_WBITS))) return nil; 82 | 83 | NSMutableData *result = [NSMutableData dataWithCapacity:(self.length * 2)]; 84 | unsigned char output[1024]; 85 | int retCode; 86 | do { 87 | strm.avail_out = 1024; 88 | strm.next_out = output; 89 | retCode = inflate(&strm, Z_NO_FLUSH); 90 | if ((retCode != Z_OK) && (retCode != Z_STREAM_END)) { 91 | inflateEnd(&strm); 92 | return nil; 93 | } 94 | 95 | unsigned gotBack = 1024 - strm.avail_out; 96 | if (gotBack > 0) [result appendBytes:output length:gotBack]; 97 | } while (retCode == Z_OK); 98 | 99 | inflateEnd(&strm); 100 | return result; 101 | } 102 | 103 | @end 104 | -------------------------------------------------------------------------------- /FCUtilities/NSOperationQueue+FCUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSOperationQueue+FCUtilities.h 3 | // Overcast 4 | // 5 | // Created by Marco Arment on 10/6/18. 6 | // Copyright © 2018 Marco Arment. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface FCBlockOperation : NSOperation 12 | 13 | + (instancetype)operationWithBlock:(void (^)(void))block; 14 | @property (nonatomic, readonly) void (^block)(void); 15 | 16 | @end 17 | 18 | 19 | @interface NSOperationQueue (FCUtilities) 20 | 21 | - (void)fc_addOperationWithBlock:(void (^)(void))block waitUntilFinished:(BOOL)wait; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /FCUtilities/NSOperationQueue+FCUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSOperationQueue+FCUtilities.m 3 | // Overcast 4 | // 5 | // Created by Marco Arment on 10/6/18. 6 | // Copyright © 2018 Marco Arment. All rights reserved. 7 | // 8 | 9 | #import "NSOperationQueue+FCUtilities.h" 10 | 11 | @interface FCBlockOperation () 12 | @property (nonatomic, copy) void (^block)(void); 13 | @end 14 | 15 | @implementation FCBlockOperation 16 | + (instancetype)operationWithBlock:(void (^)(void))block 17 | { 18 | FCBlockOperation *op = [self new]; 19 | op.block = block; 20 | return op; 21 | } 22 | 23 | - (void)main { if (self.block && ! self.isCancelled) self.block(); } 24 | @end 25 | 26 | 27 | @implementation NSOperationQueue (FCUtilities) 28 | 29 | - (void)fc_addOperationWithBlock:(void (^)(void))block waitUntilFinished:(BOOL)wait 30 | { 31 | [self addOperations:@[ [FCBlockOperation operationWithBlock:block] ] waitUntilFinished:wait]; 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /FCUtilities/NSString+FCUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+FCUtilities.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | 8 | @interface NSString (FCUtilities) 9 | 10 | - (NSString *)fc_URLEncodedString; 11 | - (NSString *)fc_summarizeToLength:(int)length withEllipsis:(BOOL)ellipsis; 12 | 13 | - (NSString *)fc_substringAfter:(NSString *)needle fromEnd:(BOOL)reverse; 14 | - (NSString *)fc_substringBefore:(NSString *)needle fromEnd:(BOOL)reverse; 15 | - (NSString *)fc_substringBetween:(NSString *)leftCap and:(NSString *)rightCap; 16 | 17 | - (NSString *)fc_trimSubstringFromStart:(NSString *)needle; 18 | - (NSString *)fc_trimSubstringFromEnd:(NSString *)needle; 19 | - (NSString *)fc_trimSubstringFromBothEnds:(NSString *)needle; 20 | 21 | - (BOOL)fc_contains:(NSString *)needle; 22 | 23 | - (NSString *)fc_HTMLEncodedString; 24 | - (NSString *)fc_hexString; 25 | - (NSString *)fc_URLSafeBase64EncodedString; 26 | 27 | - (NSString *)fc_stringWithNormalizedWhitespace; 28 | 29 | - (NSString *)fc_stringByReplacingMatches:(NSRegularExpression *)regex usingBlock:(NSString *(^)(NSTextCheckingResult *match, NSArray *captureGroups))replacementBlock; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /FCUtilities/NSString+FCUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+FCUtilities.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "NSString+FCUtilities.h" 7 | #import 8 | 9 | @implementation NSString (FCUtilities) 10 | 11 | - (NSString *)fc_URLEncodedString 12 | { 13 | NSMutableCharacterSet *allowedCharacters = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; 14 | [allowedCharacters removeCharactersInString:@"?=&+:;@/$!'()\",*"]; 15 | return [self stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters]; 16 | } 17 | 18 | - (NSString *)fc_HTMLEncodedString 19 | { 20 | #if IS_MAC 21 | CFStringRef cs = CFXMLCreateStringByEscapingEntities(kCFAllocatorDefault, (CFStringRef) self, NULL); 22 | NSString *str = [NSString stringWithString:(NSString *) cs]; 23 | CFRelease(cs); 24 | return str; 25 | #else 26 | NSString *h = [self stringByReplacingOccurrencesOfString:@"&" withString:@"&"]; 27 | h = [h stringByReplacingOccurrencesOfString:@"\"" withString:@"""]; 28 | h = [h stringByReplacingOccurrencesOfString:@"<" withString:@"<"]; 29 | h = [h stringByReplacingOccurrencesOfString:@">" withString:@">"]; 30 | return h; 31 | #endif 32 | } 33 | 34 | - (NSString *)fc_stringWithNormalizedWhitespace 35 | { 36 | NSCharacterSet *whitespaceSet = NSCharacterSet.whitespaceAndNewlineCharacterSet; 37 | NSMutableString *outputString = [NSMutableString string]; 38 | NSScanner *scanner = [[NSScanner alloc] initWithString:self]; 39 | while (! scanner.isAtEnd) { 40 | NSString *segment = NULL; 41 | [scanner scanUpToCharactersFromSet:whitespaceSet intoString:&segment]; 42 | [scanner scanCharactersFromSet:whitespaceSet intoString:NULL]; 43 | if (segment) { [outputString appendString:segment]; [outputString appendString:@" "]; } 44 | } 45 | 46 | return [outputString stringByTrimmingCharactersInSet:whitespaceSet]; 47 | } 48 | 49 | - (NSString *)fc_summarizeToLength:(int)length withEllipsis:(BOOL)ellipsis 50 | { 51 | NSString *str = self; 52 | if ([str length] > length) { 53 | str = [str substringToIndex:length]; 54 | 55 | // Find last space, trim to it 56 | NSRange offset = [str rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet] options:NSBackwardsSearch range:NSMakeRange(0, length)]; 57 | if (offset.location == NSNotFound) offset.location = length; 58 | str = [NSString stringWithFormat:@"%@%@", [str substringToIndex:offset.location], ellipsis ? @"\xE2\x80\xA6" : @""]; 59 | } 60 | return str; 61 | } 62 | 63 | - (NSString *)fc_substringAfter:(NSString *)needle fromEnd:(BOOL)reverse 64 | { 65 | NSRange r = [self rangeOfString:needle options:(reverse ? NSBackwardsSearch : 0)]; 66 | if (r.location == NSNotFound) return self; 67 | return [self substringFromIndex:(r.location + r.length)]; 68 | } 69 | 70 | - (NSString *)fc_substringBefore:(NSString *)needle fromEnd:(BOOL)reverse 71 | { 72 | NSRange r = [self rangeOfString:needle options:(reverse ? NSBackwardsSearch : 0)]; 73 | if (r.location == NSNotFound) return self; 74 | return [self substringToIndex:r.location]; 75 | } 76 | 77 | - (NSString *)fc_substringBetween:(NSString *)leftCap and:(NSString *)rightCap 78 | { 79 | return [[self fc_substringAfter:leftCap fromEnd:NO] fc_substringBefore:rightCap fromEnd:NO]; 80 | } 81 | 82 | - (BOOL)fc_contains:(NSString *)needle 83 | { 84 | return ([self rangeOfString:needle].location != NSNotFound); 85 | } 86 | 87 | - (NSString *)fc_trimSubstringFromStart:(NSString *)needle 88 | { 89 | NSInteger nlen = [needle length]; 90 | NSString *ret = self; 91 | while ([ret hasPrefix:needle]) ret = [ret substringFromIndex:nlen]; 92 | return ret; 93 | } 94 | 95 | - (NSString *)fc_trimSubstringFromEnd:(NSString *)needle 96 | { 97 | NSInteger nlen = [needle length]; 98 | NSString *ret = self; 99 | while ([ret hasSuffix:needle]) ret = [ret substringToIndex:([ret length] - nlen)]; 100 | return ret; 101 | } 102 | 103 | - (NSString *)fc_trimSubstringFromBothEnds:(NSString *)needle 104 | { 105 | return [[self fc_trimSubstringFromStart:needle] fc_trimSubstringFromEnd:needle]; 106 | } 107 | 108 | - (NSString *)fc_hexString 109 | { 110 | const char *utf8 = [self UTF8String]; 111 | NSMutableString *hex = [NSMutableString string]; 112 | while ( *utf8 ) [hex appendFormat:@"%02X" , *utf8++ & 0x00FF]; 113 | return [NSString stringWithFormat:@"%@", hex]; 114 | } 115 | 116 | - (NSString *)fc_URLSafeBase64EncodedString 117 | { 118 | NSString *str = [[self dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0]; 119 | str = [str stringByReplacingOccurrencesOfString:@"+" withString:@"-"]; 120 | str = [str stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; 121 | str = [str stringByReplacingOccurrencesOfString:@"=" withString:@""]; 122 | return str; 123 | } 124 | 125 | - (NSString *)fc_stringByReplacingMatches:(NSRegularExpression *)regex usingBlock:(NSString *(^)(NSTextCheckingResult *match, NSArray *captureGroups))replacementBlock 126 | { 127 | if (! replacementBlock) return self; 128 | 129 | NSUInteger numCaptureGroups = regex.numberOfCaptureGroups; 130 | NSMutableString *mutableString = [self mutableCopy]; 131 | NSInteger offset = 0; 132 | for (NSTextCheckingResult *result in [regex matchesInString:self options:0 range:NSMakeRange(0, self.length)]) { 133 | NSMutableArray *captureGroups = [NSMutableArray arrayWithCapacity:numCaptureGroups + 1]; 134 | for (NSUInteger g = 0; g <= numCaptureGroups; g++) { 135 | NSRange captureRange = [result rangeAtIndex:g]; 136 | captureGroups[g] = captureRange.location == NSNotFound ? @"" : [self substringWithRange:captureRange]; 137 | } 138 | 139 | NSRange resultRange = result.range; 140 | resultRange.location += offset; 141 | NSString *replacement = replacementBlock(result, captureGroups); 142 | [mutableString replaceCharactersInRange:resultRange withString:replacement]; 143 | offset += (replacement.length - resultRange.length); 144 | } 145 | return mutableString; 146 | } 147 | 148 | @end 149 | -------------------------------------------------------------------------------- /FCUtilities/NSURL+FCUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+FCUtilities.h 3 | // Pods 4 | // 5 | // Created by Marco Arment on 5/9/14. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface NSURL (FCUtilities) 12 | 13 | - (NSDictionary *)fc_queryComponents; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /FCUtilities/NSURL+FCUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+FCUtilities.m 3 | // Pods 4 | // 5 | // Created by Marco Arment on 5/9/14. 6 | // 7 | // 8 | 9 | #import "NSURL+FCUtilities.h" 10 | 11 | @implementation NSURL (FCUtilities) 12 | 13 | - (NSDictionary *)fc_queryComponents 14 | { 15 | NSString *query = self.query; 16 | if (! query || ! query.length) return @{ }; 17 | 18 | NSMutableDictionary *decoded = [NSMutableDictionary dictionary]; 19 | for (NSString *pair in [query componentsSeparatedByString:@"&"]) { 20 | NSArray *parts = [pair componentsSeparatedByString:@"="]; 21 | if (! parts.count || ! [parts.firstObject length]) continue; 22 | 23 | if (parts.count == 1) { 24 | decoded[parts[0].stringByRemovingPercentEncoding] = @""; 25 | } else if (parts.count == 2) { 26 | decoded[parts[0].stringByRemovingPercentEncoding] = parts[1].stringByRemovingPercentEncoding; 27 | } 28 | } 29 | 30 | return decoded; 31 | } 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /FCUtilities/NSURLSession+FCUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSURLSession+FCUtilities.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | 8 | @interface NSURLSession (FCUtilities) 9 | 10 | - (NSData * _Nullable)fc_sendSynchronousRequest:(NSURLRequest * _Nonnull)request returningResponse:(NSURLResponse * _Nullable __autoreleasing * _Nullable)response error:(NSError * _Nullable __autoreleasing * _Nullable)error; 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /FCUtilities/NSURLSession+FCUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSURLSession+FCUtilities.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "NSURLSession+FCUtilities.h" 7 | 8 | @implementation NSURLSession (FCUtilities) 9 | 10 | - (NSData * _Nullable)fc_sendSynchronousRequest:(NSURLRequest * _Nonnull)request returningResponse:(NSURLResponse * _Nullable __autoreleasing * _Nullable)response error:(NSError * _Nullable __autoreleasing * _Nullable)error 11 | { 12 | dispatch_semaphore_t done = dispatch_semaphore_create(0); 13 | __block NSData *data = nil; 14 | 15 | BOOL captureResponse = (response != nil); 16 | BOOL captureError = (error != nil); 17 | 18 | [[self dataTaskWithRequest:request completionHandler:^(NSData * _Nullable gotData, NSURLResponse * _Nullable gotResponse, NSError * _Nullable gotError) { 19 | data = gotData; 20 | if (captureResponse) *response = gotResponse; 21 | if (captureError) *error = [gotError copy]; 22 | dispatch_semaphore_signal(done); 23 | }] resume]; 24 | 25 | dispatch_semaphore_wait(done, DISPATCH_TIME_FOREVER); 26 | return data; 27 | } 28 | 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /FCUtilities/UIBarButtonItem+FCUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIBarButtonItem+FCUtilities.h 3 | // Overcast 4 | // 5 | // Created by Marco Arment on 10/18/16. 6 | // Copyright © 2016 Marco Arment. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIBarButtonItem (FCUtilities) 12 | 13 | + (instancetype)fc_fixedSpaceItemWithWidth:(CGFloat)width; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /FCUtilities/UIBarButtonItem+FCUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIBarButtonItem+FCUtilities.m 3 | // Overcast 4 | // 5 | // Created by Marco Arment on 10/18/16. 6 | // Copyright © 2016 Marco Arment. All rights reserved. 7 | // 8 | 9 | #import "UIBarButtonItem+FCUtilities.h" 10 | 11 | @implementation UIBarButtonItem (FCUtilities) 12 | 13 | + (instancetype)fc_fixedSpaceItemWithWidth:(CGFloat)width 14 | { 15 | UIBarButtonItem *item = [[self alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:NULL]; 16 | item.width = width; 17 | return item; 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /FCUtilities/UIColor+FCUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+FCUtilities.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | 8 | //#define SUPPORT_DUMPING_COLOR_VALUES 9 | 10 | #ifdef SUPPORT_DUMPING_COLOR_VALUES 11 | #import 12 | #import "NSString+FCUtilities.h" 13 | #endif 14 | 15 | #define fc_UIColorFromRGB(r, g, b) [UIColor colorWithRed:r/255.0f green:g/255.0f blue:b/255.0f alpha:1.0f] 16 | #define fc_UIColorFromRGBA(r, g, b, a) [UIColor colorWithRed:r/255.0f green:g/255.0f blue:b/255.0f alpha:a] 17 | #define fc_UIColorFromHexInt(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0] 18 | 19 | @interface UIColor (FCUtilities) 20 | 21 | - (UIColor * _Nonnull)fc_colorByModifyingRGBA:(void (^ _Nonnull)(CGFloat * _Nonnull red, CGFloat * _Nonnull green, CGFloat * _Nonnull blue, CGFloat *_Nonnull alpha))modifyingBlock; 22 | - (UIColor * _Nonnull)fc_colorByModifyingHSBA:(void (^ _Nonnull)(CGFloat * _Nonnull hue, CGFloat * _Nonnull saturation, CGFloat *_Nonnull brightness, CGFloat * _Nonnull alpha))modifyingBlock; 23 | - (NSString * _Nonnull)fc_CSSColor; 24 | + (UIColor * _Nullable)fc_colorWithHexString:(NSString * _Nullable)hexString; 25 | - (UIColor * _Nullable)fc_colorByBlendingWithColor:(UIColor * _Nullable)color2; 26 | - (UIColor * _Nullable)fc_opaqueColorByBlendingWithBackgroundColor:(UIColor * _Nonnull)backgroundColor; 27 | 28 | - (CGFloat)fc_apcaContrastAgainstBackgroundColor:(UIColor * _Nonnull)backgroundColor; 29 | - (UIColor * _Nonnull)fc_colorWithMinimumAPCAContrast:(CGFloat)minContrast againstBackgroundColor:(UIColor * _Nonnull)backgroundColor changed:(out BOOL * _Nullable)outColorDidChange; 30 | 31 | #ifdef SUPPORT_DUMPING_COLOR_VALUES 32 | #if TARGET_OS_IOS 33 | + (void)fc_dumpSystemColorValues; 34 | #endif 35 | #endif 36 | 37 | #pragma mark - Persistent, cross-platform system colors 38 | 39 | // These methods provide the standard values for UIColor.systemRedColor, etc. in dark/light themes, 40 | // but hard-coded so they can be used on platforms such as watchOS that lack these color constants, 41 | // and to easily provide overridable theme settings (e.g. dark app theme while OS is in light mode) 42 | // while still using the standard system colors. 43 | 44 | // An alternative to UIUserInterfaceStyle that defaults to "light" and is available on all platforms 45 | typedef NS_ENUM(NSInteger, FCUserInterfaceStyle) { 46 | FCUserInterfaceStyleLight = 0, 47 | FCUserInterfaceStyleDark = 1, 48 | }; 49 | 50 | // Color identifiers are strings of the format: "name#t:r,g,b,a#t:r,g,b,a…" or "#t:r,g,b,a#t:r,g,b,a…" 51 | // name (optional): the dynamic systemWhateverColor if set by the fc_system*Color methods below 52 | // Followed by a sequence of at least one of these: 53 | // #: delimiter 54 | // t: FCUserInterfaceStyle as integer 55 | // r, g, b, a: float values for the color components 56 | // 57 | // For example, systemOrangeColor is represented as: 58 | // "systemOrangeColor#0:1,0.584314,0,1#1:1,0.623529,0.039216,1" 59 | // 60 | // These identifiers can be used to store and retrieve e.g. user color settings while retaining dynamic 61 | // color changes between themes. If a name isn't recognized, the RGBA values are used as a fallback. 62 | // If a recognized name is used, any RGBA values that follow it are ignored. 63 | // 64 | - (NSString * _Nullable)fc_colorIdentifier; 65 | + (UIColor * _Nullable)fc_colorFromIdentifier:(NSString * _Nullable)string theme:(FCUserInterfaceStyle)theme; 66 | 67 | // Generated by SUPPORT_DUMPING_COLOR_VALUES 68 | + (UIColor * _Nullable)fc_systemColorWithName:(NSString * _Nonnull)name theme:(FCUserInterfaceStyle)theme; 69 | + (UIColor * _Nonnull)fc_systemRedColorWithTheme:(FCUserInterfaceStyle)theme; 70 | + (UIColor * _Nonnull)fc_systemGreenColorWithTheme:(FCUserInterfaceStyle)theme; 71 | + (UIColor * _Nonnull)fc_systemBlueColorWithTheme:(FCUserInterfaceStyle)theme; 72 | + (UIColor * _Nonnull)fc_labelColorWithTheme:(FCUserInterfaceStyle)theme; 73 | + (UIColor * _Nonnull)fc_systemGrayColorWithTheme:(FCUserInterfaceStyle)theme; 74 | + (UIColor * _Nonnull)fc_systemBackgroundColorWithTheme:(FCUserInterfaceStyle)theme; 75 | + (UIColor * _Nonnull)fc_secondarySystemGroupedBackgroundColorWithTheme:(FCUserInterfaceStyle)theme; 76 | + (UIColor * _Nonnull)fc_secondaryLabelColorWithTheme:(FCUserInterfaceStyle)theme; 77 | + (UIColor * _Nonnull)fc_separatorColorWithTheme:(FCUserInterfaceStyle)theme; 78 | + (UIColor * _Nonnull)fc_linkColorWithTheme:(FCUserInterfaceStyle)theme; 79 | + (UIColor * _Nonnull)fc_tertiarySystemFillColorWithTheme:(FCUserInterfaceStyle)theme; 80 | + (UIColor * _Nonnull)fc_systemFillColorWithTheme:(FCUserInterfaceStyle)theme; 81 | + (UIColor * _Nonnull)fc_secondarySystemFillColorWithTheme:(FCUserInterfaceStyle)theme; 82 | + (UIColor * _Nonnull)fc_secondarySystemBackgroundColorWithTheme:(FCUserInterfaceStyle)theme; 83 | + (UIColor * _Nonnull)fc_tertiarySystemBackgroundColorWithTheme:(FCUserInterfaceStyle)theme; 84 | + (UIColor * _Nonnull)fc_systemGroupedBackgroundColorWithTheme:(FCUserInterfaceStyle)theme; 85 | + (UIColor * _Nonnull)fc_tertiarySystemGroupedBackgroundColorWithTheme:(FCUserInterfaceStyle)theme; 86 | + (UIColor * _Nonnull)fc_systemOrangeColorWithTheme:(FCUserInterfaceStyle)theme; 87 | + (UIColor * _Nonnull)fc_tertiaryLabelColorWithTheme:(FCUserInterfaceStyle)theme; 88 | + (UIColor * _Nonnull)fc_systemYellowColorWithTheme:(FCUserInterfaceStyle)theme; 89 | + (UIColor * _Nonnull)fc_systemPinkColorWithTheme:(FCUserInterfaceStyle)theme; 90 | + (UIColor * _Nonnull)fc_systemMintColorWithTheme:(FCUserInterfaceStyle)theme; 91 | + (UIColor * _Nonnull)fc_systemCyanColorWithTheme:(FCUserInterfaceStyle)theme; 92 | + (UIColor * _Nonnull)fc_systemTealColorWithTheme:(FCUserInterfaceStyle)theme; 93 | + (UIColor * _Nonnull)fc_systemPurpleColorWithTheme:(FCUserInterfaceStyle)theme; 94 | + (UIColor * _Nonnull)fc_systemIndigoColorWithTheme:(FCUserInterfaceStyle)theme; 95 | + (UIColor * _Nonnull)fc_systemBrownColorWithTheme:(FCUserInterfaceStyle)theme; 96 | + (UIColor * _Nonnull)fc_quaternaryLabelColorWithTheme:(FCUserInterfaceStyle)theme; 97 | + (UIColor * _Nonnull)fc_placeholderTextColorWithTheme:(FCUserInterfaceStyle)theme; 98 | + (UIColor * _Nonnull)fc_opaqueSeparatorColorWithTheme:(FCUserInterfaceStyle)theme; 99 | + (UIColor * _Nonnull)fc_quaternarySystemFillColorWithTheme:(FCUserInterfaceStyle)theme; 100 | + (UIColor * _Nonnull)fc_systemGray2ColorWithTheme:(FCUserInterfaceStyle)theme; 101 | + (UIColor * _Nonnull)fc_systemGray3ColorWithTheme:(FCUserInterfaceStyle)theme; 102 | + (UIColor * _Nonnull)fc_systemGray4ColorWithTheme:(FCUserInterfaceStyle)theme; 103 | + (UIColor * _Nonnull)fc_systemGray5ColorWithTheme:(FCUserInterfaceStyle)theme; 104 | + (UIColor * _Nonnull)fc_systemGray6ColorWithTheme:(FCUserInterfaceStyle)theme; 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /FCUtilities/UIColor+FCUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+FCUtilities.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "UIColor+FCUtilities.h" 7 | #import 8 | 9 | static void *UIColorFCUtilitiesIdentifierKey = &UIColorFCUtilitiesIdentifierKey; 10 | 11 | @implementation UIColor (FCUtilities) 12 | 13 | - (UIColor * _Nonnull)fc_withSystemName:(NSString * _Nullable)string 14 | { 15 | objc_setAssociatedObject(self, UIColorFCUtilitiesIdentifierKey, string, OBJC_ASSOCIATION_COPY); 16 | return self; 17 | } 18 | 19 | - (NSString * _Nullable)fc_colorIdentifier 20 | { 21 | NSMutableDictionary *colorsToRepresentAsRGBA = [NSMutableDictionary dictionary]; 22 | 23 | NSString *systemName = (NSString *) objc_getAssociatedObject(self, UIColorFCUtilitiesIdentifierKey); 24 | if (systemName) { 25 | NSArray *validThemeValues = @[ 26 | @(FCUserInterfaceStyleLight), 27 | @(FCUserInterfaceStyleDark), 28 | ]; 29 | 30 | for (NSNumber *themeNum in validThemeValues) { 31 | UIColor *c = [self.class fc_systemColorWithName:systemName theme:themeNum.integerValue]; 32 | if (c) colorsToRepresentAsRGBA[themeNum] = c; 33 | } 34 | } 35 | if (! colorsToRepresentAsRGBA.count) colorsToRepresentAsRGBA[@(FCUserInterfaceStyleLight)] = self; 36 | 37 | __block BOOL hasAnyComponents = NO; 38 | NSMutableString *idStr = (systemName ?: @"").mutableCopy; 39 | [colorsToRepresentAsRGBA enumerateKeysAndObjectsUsingBlock:^(NSNumber *themeNum, UIColor *color, BOOL *stop) { 40 | CGFloat r, g, b, a; 41 | if (! [color fc_getRed:&r green:&g blue:&b alpha:&a]) return; 42 | hasAnyComponents = YES; 43 | [idStr appendFormat:@"#%d:%g,%g,%g,%g", themeNum.intValue, r, g, b, a]; 44 | }]; 45 | 46 | return hasAnyComponents ? idStr : nil; 47 | } 48 | 49 | + (UIColor * _Nullable)fc_colorFromIdentifier:(NSString * _Nullable)string theme:(FCUserInterfaceStyle)theme 50 | { 51 | if (! string) return nil; 52 | 53 | NSScanner *scanner = [NSScanner scannerWithString:string]; 54 | scanner.charactersToBeSkipped = [NSCharacterSet characterSetWithCharactersInString:@",:"]; 55 | NSString *systemName = NULL; 56 | 57 | if ([scanner scanCharactersFromSet:NSCharacterSet.alphanumericCharacterSet intoString:&systemName] && systemName.length > 0) { 58 | UIColor *systemColor = [self fc_systemColorWithName:systemName theme:theme]; 59 | if (systemColor) return systemColor; 60 | } 61 | 62 | // If it's not a recognized system color, fall back to RGBA values 63 | NSMutableDictionary *colorsByTheme = [NSMutableDictionary dictionary]; 64 | while ([scanner scanString:@"#" intoString:NULL]) { 65 | NSInteger themeID; 66 | float r = 0, g = 0, b = 0, a = 0; 67 | if ([scanner scanInteger:&themeID] && [scanner scanFloat:&r] && [scanner scanFloat:&g] && [scanner scanFloat:&b] && [scanner scanFloat:&a]) { 68 | colorsByTheme[@(themeID)] = [UIColor colorWithRed:r green:g blue:b alpha:a]; 69 | } 70 | } 71 | 72 | UIColor *colorInTheme = colorsByTheme[@(theme)]; 73 | if (! colorInTheme) colorInTheme = colorsByTheme[@(FCUserInterfaceStyleLight)]; // Fall back to light if color exact theme isn't present 74 | if (! colorInTheme) colorInTheme = colorsByTheme.allValues.firstObject; // If exact theme AND light theme aren't set, fall back to any theme present 75 | return colorInTheme ? [colorInTheme fc_withSystemName:systemName] : nil; 76 | } 77 | 78 | - (BOOL)fc_getRed:(CGFloat *)red green:(CGFloat *)green blue:(CGFloat *)blue alpha:(CGFloat *)alpha { 79 | if ([self getRed:red green:green blue:blue alpha:alpha]) return YES; 80 | if ([self getWhite:red alpha:alpha]) { 81 | if (green) *green = *red; 82 | if (blue) *blue = *red; 83 | return YES; 84 | } 85 | return NO; 86 | } 87 | 88 | - (UIColor *)fc_colorByModifyingRGBA:(void (^)(CGFloat *red, CGFloat *green, CGFloat *blue, CGFloat *alpha))modifyingBlock 89 | { 90 | CGFloat red, green, blue, alpha; 91 | 92 | if (! [self fc_getRed:&red green:&green blue:&blue alpha:&alpha]) { 93 | [[NSException exceptionWithName:NSInvalidArgumentException reason:@"Color is not in a decomposable format" userInfo:nil] raise]; 94 | } 95 | 96 | modifyingBlock(&red, &green, &blue, &alpha); 97 | return [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; 98 | } 99 | 100 | - (UIColor *)fc_colorByModifyingHSBA:(void (^)(CGFloat *hue, CGFloat *saturation, CGFloat *brightness, CGFloat *alpha))modifyingBlock 101 | { 102 | CGFloat hue, saturation, brightness, alpha; 103 | 104 | if (! [self getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha]) { 105 | if ([self getWhite:&brightness alpha:&alpha]) { 106 | hue = 0; 107 | saturation = 0; 108 | } else { 109 | [[NSException exceptionWithName:NSInvalidArgumentException reason:@"Color is not in a decomposable format" userInfo:nil] raise]; 110 | } 111 | } 112 | 113 | modifyingBlock(&hue, &saturation, &brightness, &alpha); 114 | hue = MAX(0.0f, MIN(1.0f, hue)); 115 | saturation = MAX(0.0f, MIN(1.0f, saturation)); 116 | brightness = MAX(0.0f, MIN(1.0f, brightness)); 117 | alpha = MAX(0.0f, MIN(1.0f, alpha)); 118 | return [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:alpha]; 119 | } 120 | 121 | - (CGFloat)fc_apcaLuminance /* "Y" from https://github.com/Myndex/SAPC-APCA/ and https://www.w3.org/WAI/GL/task-forces/silver/wiki/Visual_Contrast_of_Text_Subgroup/APCA_model */ 122 | { 123 | CGFloat r, g, b, a; 124 | [self fc_getRed:&r green:&g blue:&b alpha:&a]; 125 | return powf(r / 1.0f, 2.4f) * 0.2126729f + powf(g / 1.0f, 2.4f) * 0.7151522f + powf(b / 1.0f, 2.4f) * 0.0721750f; 126 | } 127 | 128 | - (CGFloat)fc_apcaContrastAgainstBackgroundColor:(UIColor *)backgroundColor 129 | { 130 | UIColor *textColor = [self fc_opaqueColorByBlendingWithBackgroundColor:backgroundColor]; 131 | CGFloat textY = textColor.fc_apcaLuminance; 132 | CGFloat backgroundY = backgroundColor.fc_apcaLuminance; 133 | 134 | // adapted from https://github.com/Myndex/SAPC-APCA/blob/master/src/JS/SAPC_0_98G_4g_minimal.js 135 | 136 | const CGFloat clampThreshold = 0.22f; 137 | if (textY <= clampThreshold) { textY += powf(clampThreshold - textY, 1.414f); } 138 | if (backgroundY <= clampThreshold) { textY += powf(clampThreshold - backgroundY, 1.414f); } 139 | 140 | if (ABS(backgroundY - textY) < 0.0005f) return 0; 141 | 142 | CGFloat sapc = backgroundY > textY ? powf(backgroundY, 0.56f) - powf(textY, 0.57f) : powf(backgroundY, 0.65f) - powf(textY, 0.62f); 143 | 144 | if (sapc < 0.001f) return 0; 145 | if (sapc < 0.035991f) return sapc - sapc * 27.7847239587675f * 0.027f; 146 | return sapc - 0.027f; 147 | } 148 | 149 | - (UIColor * _Nonnull)fc_colorWithMinimumAPCAContrast:(CGFloat)minContrast againstBackgroundColor:(UIColor *)backgroundColor changed:(out BOOL *)outColorDidChange 150 | { 151 | if (outColorDidChange) *outColorDidChange = NO; 152 | BOOL adjustmentDirectionDarken = (backgroundColor.fc_apcaLuminance > self.fc_apcaLuminance); 153 | UIColor *color = self; 154 | CGFloat lastContrast = 0.0f; 155 | CGFloat contrast; 156 | while ( (contrast = [color fc_apcaContrastAgainstBackgroundColor:backgroundColor]) < minContrast) { 157 | if (contrast == lastContrast) { /* not improving anymore; bail out */ return color; } 158 | color = [color fc_colorByModifyingHSBA:^(CGFloat * _Nonnull hue, CGFloat * _Nonnull saturation, CGFloat * _Nonnull brightness, CGFloat * _Nonnull alpha) { 159 | if (adjustmentDirectionDarken) { 160 | *brightness *= 0.975f; 161 | *saturation *= 1.1f; 162 | } else { 163 | *brightness *= 1.05f; 164 | *saturation *= 0.9f; 165 | } 166 | }]; 167 | if (outColorDidChange) *outColorDidChange = YES; 168 | lastContrast = contrast; 169 | } 170 | return color; 171 | } 172 | 173 | - (NSString *)fc_CSSColor 174 | { 175 | CGFloat r, g, b, a; 176 | if ([self fc_getRed:&r green:&g blue:&b alpha:&a]) { 177 | return [NSString stringWithFormat:@"rgba(%d, %d, %d, %g)", (int) (r * 255.0f), (int) (g * 255.0f), (int) (b * 255.0f), a]; 178 | } else { 179 | [[NSException exceptionWithName:NSInvalidArgumentException reason:@"Cannot convert this color space to CSS color" userInfo:nil] raise]; 180 | return nil; 181 | } 182 | } 183 | 184 | + (UIColor *)fc_colorWithHexString:(NSString *)hexString 185 | { 186 | unsigned hexNum; 187 | if (! [[NSScanner scannerWithString:hexString] scanHexInt:&hexNum]) return nil; 188 | return fc_UIColorFromHexInt(hexNum); 189 | } 190 | 191 | - (UIColor *)fc_opaqueColorByBlendingWithBackgroundColor:(UIColor *)backgroundColor 192 | { 193 | return [backgroundColor fc_colorByBlendingWithColor:self]; 194 | } 195 | 196 | // adaptation of https://stackoverflow.com/a/18903483/30480 197 | - (UIColor *)fc_colorByBlendingWithColor:(UIColor *)color2 198 | { 199 | CGFloat r1, g1, b1, a1, r2, g2, b2, a2; 200 | 201 | if (! [self fc_getRed:&r1 green:&g1 blue:&b1 alpha:&a1]) return nil; 202 | if (! [color2 fc_getRed:&r2 green:&g2 blue:&b2 alpha:&a2]) return nil; 203 | 204 | CGFloat beta = 1.0f - a2; 205 | 206 | CGFloat r = r1 * beta + r2 * a2; 207 | CGFloat g = g1 * beta + g2 * a2; 208 | CGFloat b = b1 * beta + b2 * a2; 209 | //CGFloat a = a1 * beta + a2 * a2; 210 | return [UIColor colorWithRed:r green:g blue:b alpha:a1]; 211 | } 212 | 213 | #ifdef SUPPORT_DUMPING_COLOR_VALUES 214 | #if TARGET_OS_IOS 215 | + (void)fc_dumpSystemColorValues 216 | { 217 | NSMutableString *hFile = @"+ (UIColor * _Nullable)fc_systemColorWithName:(NSString * _Nonnull)name theme:(FCUserInterfaceStyle)theme;\n".mutableCopy; 218 | NSMutableString *cFile = @"".mutableCopy; 219 | NSMutableString *swiftUIFile = @"import SwiftUI\n\nextension Color {\n".mutableCopy; 220 | 221 | NSMutableArray *systemColorMethodNames = [NSMutableArray array]; 222 | int unsigned numMethods; 223 | Method *methods = class_copyMethodList(objc_getMetaClass("UIColor"), &numMethods); 224 | for (int i = 0; i < numMethods; i++) { 225 | NSString *methodName = NSStringFromSelector(method_getName(methods[i])); 226 | if ( 227 | ! [methodName hasPrefix:@"_"] && 228 | ! [methodName hasPrefix:@"fc_"] && 229 | [methodName hasSuffix:@"Color"] && 230 | ( 231 | [methodName hasPrefix:@"system"] || [methodName containsString:@"System"] || 232 | [methodName hasPrefix:@"label"] || [methodName containsString:@"Label"] || 233 | [methodName isEqualToString:@"separatorColor"] || [methodName isEqualToString:@"opaqueSeparatorColor"] || 234 | [methodName isEqualToString:@"linkColor"] || [methodName isEqualToString:@"placeholderTextColor"] 235 | ) && 236 | ! [methodName hasPrefix:@"external"] && ! [methodName hasPrefix:@"mail"] && // private APIs 237 | ! [methodName containsString:@"Tint"] && // private APIs 238 | ! [methodName containsString:@"systemLight"] && ! [methodName containsString:@"systemDark"] && // private APIs 239 | ! [methodName containsString:@"systemWhite"] && ! [methodName containsString:@"systemBlack"] && 240 | ! [methodName containsString:@"systemMid"] && ! [methodName containsString:@"systemExtraLightGray"] // private APIs 241 | ) { 242 | [systemColorMethodNames addObject:methodName]; 243 | } 244 | } 245 | free(methods); 246 | 247 | NSMutableString *byNameMethodBody = @"".mutableCopy; 248 | 249 | for (NSString *name in systemColorMethodNames) { 250 | UIColor *color = [UIColor performSelector:NSSelectorFromString(name)]; 251 | if (! [color isKindOfClass:UIColor.class]) continue; 252 | 253 | [hFile appendFormat:@"+ (UIColor * _Nonnull)fc_%@WithTheme:(FCUserInterfaceStyle)theme;\n", name]; 254 | 255 | CGFloat darkR, darkG, darkB, darkA, lightR, lightG, lightB, lightA; 256 | UIColor *darkColor = [color resolvedColorWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]]; 257 | UIColor *lightColor = [color resolvedColorWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]]; 258 | if (! [darkColor fc_getRed:&darkR green:&darkG blue:&darkB alpha:&darkA] || ! [lightColor fc_getRed:&lightR green:&lightG blue:&lightB alpha:&lightA]) continue; 259 | 260 | [cFile appendFormat:@"+ (UIColor * _Nonnull)fc_%@WithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:%0.6ff green:%0.6ff blue:%0.6ff alpha:%0.6ff] : [UIColor colorWithRed:%0.6ff green:%0.6ff blue:%0.6ff alpha:%0.6ff]) fc_withSystemName:@\"%@\"]; }\n", name, darkR, darkG, darkB, darkA, lightR, lightG, lightB, lightA, name]; 261 | 262 | [byNameMethodBody appendFormat:@" if ([name isEqualToString:@\"%@\"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:%0.6ff green:%0.6ff blue:%0.6ff alpha:%0.6ff] : [UIColor colorWithRed:%0.6ff green:%0.6ff blue:%0.6ff alpha:%0.6ff]) fc_withSystemName:@\"%@\"]; }\n", name, darkR, darkG, darkB, darkA, lightR, lightG, lightB, lightA, name]; 263 | 264 | [swiftUIFile appendFormat:@" static func %@(_ scheme: ColorScheme) -> Color { scheme == .dark ? Color(red: %g, green: %g, blue: %g, opacity: %g) : Color(red: %g, green: %g, blue: %g, opacity: %g) }\n", 265 | [name fc_substringBefore:@"Color" fromEnd:YES], darkR, darkG, darkB, darkA, lightR, lightG, lightB, lightA 266 | ]; 267 | } 268 | 269 | [cFile appendFormat:@"\n+ (UIColor * _Nullable)fc_systemColorWithName:(NSString * _Nonnull)name theme:(FCUserInterfaceStyle)theme {\n%@\n return nil;\n}\n", byNameMethodBody]; 270 | 271 | [swiftUIFile appendString:@"}\n"]; 272 | 273 | NSError *error = NULL; 274 | [hFile writeToFile:@"/tmp/UIColor+FCUtilities+SystemColors.h" atomically:NO encoding:NSUTF8StringEncoding error:&error]; 275 | if (error) [[NSException exceptionWithName:@"ColorDumpFailed" reason:error.localizedDescription userInfo:nil] raise]; 276 | 277 | [cFile writeToFile:@"/tmp/UIColor+FCUtilities+SystemColors.c" atomically:NO encoding:NSUTF8StringEncoding error:NULL]; 278 | if (error) [[NSException exceptionWithName:@"ColorDumpFailed" reason:error.localizedDescription userInfo:nil] raise]; 279 | 280 | [swiftUIFile writeToFile:@"/tmp/Color+FCUtilities+SystemColors.swift" atomically:NO encoding:NSUTF8StringEncoding error:NULL]; 281 | if (error) [[NSException exceptionWithName:@"ColorDumpFailed" reason:error.localizedDescription userInfo:nil] raise]; 282 | } 283 | #endif 284 | #endif 285 | 286 | // Generated by SUPPORT_DUMPING_COLOR_VALUES 287 | + (UIColor * _Nonnull)fc_systemRedColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:1.000000f green:0.270588f blue:0.227451f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:0.231373f blue:0.188235f alpha:1.000000f]) fc_withSystemName:@"systemRedColor"]; } 288 | + (UIColor * _Nonnull)fc_systemGreenColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.188235f green:0.819608f blue:0.345098f alpha:1.000000f] : [UIColor colorWithRed:0.203922f green:0.780392f blue:0.349020f alpha:1.000000f]) fc_withSystemName:@"systemGreenColor"]; } 289 | + (UIColor * _Nonnull)fc_systemBlueColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.039216f green:0.517647f blue:1.000000f alpha:1.000000f] : [UIColor colorWithRed:0.000000f green:0.478431f blue:1.000000f alpha:1.000000f]) fc_withSystemName:@"systemBlueColor"]; } 290 | + (UIColor * _Nonnull)fc_labelColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:1.000000f green:1.000000f blue:1.000000f alpha:1.000000f] : [UIColor colorWithRed:0.000000f green:0.000000f blue:0.000000f alpha:1.000000f]) fc_withSystemName:@"labelColor"]; } 291 | + (UIColor * _Nonnull)fc_systemGrayColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.556863f green:0.556863f blue:0.576471f alpha:1.000000f] : [UIColor colorWithRed:0.556863f green:0.556863f blue:0.576471f alpha:1.000000f]) fc_withSystemName:@"systemGrayColor"]; } 292 | + (UIColor * _Nonnull)fc_systemBackgroundColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.000000f green:0.000000f blue:0.000000f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:1.000000f blue:1.000000f alpha:1.000000f]) fc_withSystemName:@"systemBackgroundColor"]; } 293 | + (UIColor * _Nonnull)fc_secondarySystemGroupedBackgroundColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.109804f green:0.109804f blue:0.117647f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:1.000000f blue:1.000000f alpha:1.000000f]) fc_withSystemName:@"secondarySystemGroupedBackgroundColor"]; } 294 | + (UIColor * _Nonnull)fc_secondaryLabelColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.921569f green:0.921569f blue:0.960784f alpha:0.600000f] : [UIColor colorWithRed:0.235294f green:0.235294f blue:0.262745f alpha:0.600000f]) fc_withSystemName:@"secondaryLabelColor"]; } 295 | + (UIColor * _Nonnull)fc_separatorColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.329412f green:0.329412f blue:0.345098f alpha:0.600000f] : [UIColor colorWithRed:0.235294f green:0.235294f blue:0.262745f alpha:0.290000f]) fc_withSystemName:@"separatorColor"]; } 296 | + (UIColor * _Nonnull)fc_linkColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.035294f green:0.517647f blue:1.000000f alpha:1.000000f] : [UIColor colorWithRed:0.000000f green:0.478431f blue:1.000000f alpha:1.000000f]) fc_withSystemName:@"linkColor"]; } 297 | + (UIColor * _Nonnull)fc_tertiarySystemFillColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.462745f green:0.462745f blue:0.501961f alpha:0.240000f] : [UIColor colorWithRed:0.462745f green:0.462745f blue:0.501961f alpha:0.120000f]) fc_withSystemName:@"tertiarySystemFillColor"]; } 298 | + (UIColor * _Nonnull)fc_systemFillColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.470588f green:0.470588f blue:0.501961f alpha:0.360000f] : [UIColor colorWithRed:0.470588f green:0.470588f blue:0.501961f alpha:0.200000f]) fc_withSystemName:@"systemFillColor"]; } 299 | + (UIColor * _Nonnull)fc_secondarySystemFillColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.470588f green:0.470588f blue:0.501961f alpha:0.320000f] : [UIColor colorWithRed:0.470588f green:0.470588f blue:0.501961f alpha:0.160000f]) fc_withSystemName:@"secondarySystemFillColor"]; } 300 | + (UIColor * _Nonnull)fc_secondarySystemBackgroundColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.109804f green:0.109804f blue:0.117647f alpha:1.000000f] : [UIColor colorWithRed:0.949020f green:0.949020f blue:0.968627f alpha:1.000000f]) fc_withSystemName:@"secondarySystemBackgroundColor"]; } 301 | + (UIColor * _Nonnull)fc_tertiarySystemBackgroundColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.172549f green:0.172549f blue:0.180392f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:1.000000f blue:1.000000f alpha:1.000000f]) fc_withSystemName:@"tertiarySystemBackgroundColor"]; } 302 | + (UIColor * _Nonnull)fc_systemGroupedBackgroundColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.000000f green:0.000000f blue:0.000000f alpha:1.000000f] : [UIColor colorWithRed:0.949020f green:0.949020f blue:0.968627f alpha:1.000000f]) fc_withSystemName:@"systemGroupedBackgroundColor"]; } 303 | + (UIColor * _Nonnull)fc_tertiarySystemGroupedBackgroundColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.172549f green:0.172549f blue:0.180392f alpha:1.000000f] : [UIColor colorWithRed:0.949020f green:0.949020f blue:0.968627f alpha:1.000000f]) fc_withSystemName:@"tertiarySystemGroupedBackgroundColor"]; } 304 | + (UIColor * _Nonnull)fc_systemOrangeColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:1.000000f green:0.623529f blue:0.039216f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:0.584314f blue:0.000000f alpha:1.000000f]) fc_withSystemName:@"systemOrangeColor"]; } 305 | + (UIColor * _Nonnull)fc_tertiaryLabelColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.921569f green:0.921569f blue:0.960784f alpha:0.300000f] : [UIColor colorWithRed:0.235294f green:0.235294f blue:0.262745f alpha:0.300000f]) fc_withSystemName:@"tertiaryLabelColor"]; } 306 | + (UIColor * _Nonnull)fc_systemYellowColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:1.000000f green:0.839216f blue:0.039216f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:0.800000f blue:0.000000f alpha:1.000000f]) fc_withSystemName:@"systemYellowColor"]; } 307 | + (UIColor * _Nonnull)fc_systemPinkColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:1.000000f green:0.215686f blue:0.372549f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:0.176471f blue:0.333333f alpha:1.000000f]) fc_withSystemName:@"systemPinkColor"]; } 308 | + (UIColor * _Nonnull)fc_systemMintColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.388235f green:0.901961f blue:0.886275f alpha:1.000000f] : [UIColor colorWithRed:0.000000f green:0.780392f blue:0.745098f alpha:1.000000f]) fc_withSystemName:@"systemMintColor"]; } 309 | + (UIColor * _Nonnull)fc_systemCyanColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.392157f green:0.823529f blue:1.000000f alpha:1.000000f] : [UIColor colorWithRed:0.196078f green:0.678431f blue:0.901961f alpha:1.000000f]) fc_withSystemName:@"systemCyanColor"]; } 310 | + (UIColor * _Nonnull)fc_systemTealColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.250980f green:0.784314f blue:0.878431f alpha:1.000000f] : [UIColor colorWithRed:0.188235f green:0.690196f blue:0.780392f alpha:1.000000f]) fc_withSystemName:@"systemTealColor"]; } 311 | + (UIColor * _Nonnull)fc_systemPurpleColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.749020f green:0.352941f blue:0.949020f alpha:1.000000f] : [UIColor colorWithRed:0.686275f green:0.321569f blue:0.870588f alpha:1.000000f]) fc_withSystemName:@"systemPurpleColor"]; } 312 | + (UIColor * _Nonnull)fc_systemIndigoColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.368627f green:0.360784f blue:0.901961f alpha:1.000000f] : [UIColor colorWithRed:0.345098f green:0.337255f blue:0.839216f alpha:1.000000f]) fc_withSystemName:@"systemIndigoColor"]; } 313 | + (UIColor * _Nonnull)fc_systemBrownColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.674510f green:0.556863f blue:0.407843f alpha:1.000000f] : [UIColor colorWithRed:0.635294f green:0.517647f blue:0.368627f alpha:1.000000f]) fc_withSystemName:@"systemBrownColor"]; } 314 | + (UIColor * _Nonnull)fc_quaternaryLabelColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.921569f green:0.921569f blue:0.960784f alpha:0.160000f] : [UIColor colorWithRed:0.235294f green:0.235294f blue:0.262745f alpha:0.180000f]) fc_withSystemName:@"quaternaryLabelColor"]; } 315 | + (UIColor * _Nonnull)fc_placeholderTextColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.921569f green:0.921569f blue:0.960784f alpha:0.300000f] : [UIColor colorWithRed:0.235294f green:0.235294f blue:0.262745f alpha:0.300000f]) fc_withSystemName:@"placeholderTextColor"]; } 316 | + (UIColor * _Nonnull)fc_opaqueSeparatorColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.219608f green:0.219608f blue:0.227451f alpha:1.000000f] : [UIColor colorWithRed:0.776471f green:0.776471f blue:0.784314f alpha:1.000000f]) fc_withSystemName:@"opaqueSeparatorColor"]; } 317 | + (UIColor * _Nonnull)fc_quaternarySystemFillColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.462745f green:0.462745f blue:0.501961f alpha:0.180000f] : [UIColor colorWithRed:0.454902f green:0.454902f blue:0.501961f alpha:0.080000f]) fc_withSystemName:@"quaternarySystemFillColor"]; } 318 | + (UIColor * _Nonnull)fc_systemGray2ColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.388235f green:0.388235f blue:0.400000f alpha:1.000000f] : [UIColor colorWithRed:0.682353f green:0.682353f blue:0.698039f alpha:1.000000f]) fc_withSystemName:@"systemGray2Color"]; } 319 | + (UIColor * _Nonnull)fc_systemGray3ColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.282353f green:0.282353f blue:0.290196f alpha:1.000000f] : [UIColor colorWithRed:0.780392f green:0.780392f blue:0.800000f alpha:1.000000f]) fc_withSystemName:@"systemGray3Color"]; } 320 | + (UIColor * _Nonnull)fc_systemGray4ColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.227451f green:0.227451f blue:0.235294f alpha:1.000000f] : [UIColor colorWithRed:0.819608f green:0.819608f blue:0.839216f alpha:1.000000f]) fc_withSystemName:@"systemGray4Color"]; } 321 | + (UIColor * _Nonnull)fc_systemGray5ColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.172549f green:0.172549f blue:0.180392f alpha:1.000000f] : [UIColor colorWithRed:0.898039f green:0.898039f blue:0.917647f alpha:1.000000f]) fc_withSystemName:@"systemGray5Color"]; } 322 | + (UIColor * _Nonnull)fc_systemGray6ColorWithTheme:(FCUserInterfaceStyle)theme { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.109804f green:0.109804f blue:0.117647f alpha:1.000000f] : [UIColor colorWithRed:0.949020f green:0.949020f blue:0.968627f alpha:1.000000f]) fc_withSystemName:@"systemGray6Color"]; } 323 | 324 | + (UIColor * _Nullable)fc_systemColorWithName:(NSString * _Nonnull)name theme:(FCUserInterfaceStyle)theme { 325 | if ([name isEqualToString:@"systemRedColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:1.000000f green:0.270588f blue:0.227451f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:0.231373f blue:0.188235f alpha:1.000000f]) fc_withSystemName:@"systemRedColor"]; } 326 | if ([name isEqualToString:@"systemGreenColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.188235f green:0.819608f blue:0.345098f alpha:1.000000f] : [UIColor colorWithRed:0.203922f green:0.780392f blue:0.349020f alpha:1.000000f]) fc_withSystemName:@"systemGreenColor"]; } 327 | if ([name isEqualToString:@"systemBlueColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.039216f green:0.517647f blue:1.000000f alpha:1.000000f] : [UIColor colorWithRed:0.000000f green:0.478431f blue:1.000000f alpha:1.000000f]) fc_withSystemName:@"systemBlueColor"]; } 328 | if ([name isEqualToString:@"labelColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:1.000000f green:1.000000f blue:1.000000f alpha:1.000000f] : [UIColor colorWithRed:0.000000f green:0.000000f blue:0.000000f alpha:1.000000f]) fc_withSystemName:@"labelColor"]; } 329 | if ([name isEqualToString:@"systemGrayColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.556863f green:0.556863f blue:0.576471f alpha:1.000000f] : [UIColor colorWithRed:0.556863f green:0.556863f blue:0.576471f alpha:1.000000f]) fc_withSystemName:@"systemGrayColor"]; } 330 | if ([name isEqualToString:@"systemBackgroundColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.000000f green:0.000000f blue:0.000000f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:1.000000f blue:1.000000f alpha:1.000000f]) fc_withSystemName:@"systemBackgroundColor"]; } 331 | if ([name isEqualToString:@"secondarySystemGroupedBackgroundColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.109804f green:0.109804f blue:0.117647f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:1.000000f blue:1.000000f alpha:1.000000f]) fc_withSystemName:@"secondarySystemGroupedBackgroundColor"]; } 332 | if ([name isEqualToString:@"secondaryLabelColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.921569f green:0.921569f blue:0.960784f alpha:0.600000f] : [UIColor colorWithRed:0.235294f green:0.235294f blue:0.262745f alpha:0.600000f]) fc_withSystemName:@"secondaryLabelColor"]; } 333 | if ([name isEqualToString:@"separatorColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.329412f green:0.329412f blue:0.345098f alpha:0.600000f] : [UIColor colorWithRed:0.235294f green:0.235294f blue:0.262745f alpha:0.290000f]) fc_withSystemName:@"separatorColor"]; } 334 | if ([name isEqualToString:@"linkColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.035294f green:0.517647f blue:1.000000f alpha:1.000000f] : [UIColor colorWithRed:0.000000f green:0.478431f blue:1.000000f alpha:1.000000f]) fc_withSystemName:@"linkColor"]; } 335 | if ([name isEqualToString:@"tertiarySystemFillColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.462745f green:0.462745f blue:0.501961f alpha:0.240000f] : [UIColor colorWithRed:0.462745f green:0.462745f blue:0.501961f alpha:0.120000f]) fc_withSystemName:@"tertiarySystemFillColor"]; } 336 | if ([name isEqualToString:@"systemFillColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.470588f green:0.470588f blue:0.501961f alpha:0.360000f] : [UIColor colorWithRed:0.470588f green:0.470588f blue:0.501961f alpha:0.200000f]) fc_withSystemName:@"systemFillColor"]; } 337 | if ([name isEqualToString:@"secondarySystemFillColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.470588f green:0.470588f blue:0.501961f alpha:0.320000f] : [UIColor colorWithRed:0.470588f green:0.470588f blue:0.501961f alpha:0.160000f]) fc_withSystemName:@"secondarySystemFillColor"]; } 338 | if ([name isEqualToString:@"secondarySystemBackgroundColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.109804f green:0.109804f blue:0.117647f alpha:1.000000f] : [UIColor colorWithRed:0.949020f green:0.949020f blue:0.968627f alpha:1.000000f]) fc_withSystemName:@"secondarySystemBackgroundColor"]; } 339 | if ([name isEqualToString:@"tertiarySystemBackgroundColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.172549f green:0.172549f blue:0.180392f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:1.000000f blue:1.000000f alpha:1.000000f]) fc_withSystemName:@"tertiarySystemBackgroundColor"]; } 340 | if ([name isEqualToString:@"systemGroupedBackgroundColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.000000f green:0.000000f blue:0.000000f alpha:1.000000f] : [UIColor colorWithRed:0.949020f green:0.949020f blue:0.968627f alpha:1.000000f]) fc_withSystemName:@"systemGroupedBackgroundColor"]; } 341 | if ([name isEqualToString:@"tertiarySystemGroupedBackgroundColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.172549f green:0.172549f blue:0.180392f alpha:1.000000f] : [UIColor colorWithRed:0.949020f green:0.949020f blue:0.968627f alpha:1.000000f]) fc_withSystemName:@"tertiarySystemGroupedBackgroundColor"]; } 342 | if ([name isEqualToString:@"systemOrangeColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:1.000000f green:0.623529f blue:0.039216f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:0.584314f blue:0.000000f alpha:1.000000f]) fc_withSystemName:@"systemOrangeColor"]; } 343 | if ([name isEqualToString:@"tertiaryLabelColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.921569f green:0.921569f blue:0.960784f alpha:0.300000f] : [UIColor colorWithRed:0.235294f green:0.235294f blue:0.262745f alpha:0.300000f]) fc_withSystemName:@"tertiaryLabelColor"]; } 344 | if ([name isEqualToString:@"systemYellowColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:1.000000f green:0.839216f blue:0.039216f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:0.800000f blue:0.000000f alpha:1.000000f]) fc_withSystemName:@"systemYellowColor"]; } 345 | if ([name isEqualToString:@"systemPinkColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:1.000000f green:0.215686f blue:0.372549f alpha:1.000000f] : [UIColor colorWithRed:1.000000f green:0.176471f blue:0.333333f alpha:1.000000f]) fc_withSystemName:@"systemPinkColor"]; } 346 | if ([name isEqualToString:@"systemMintColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.388235f green:0.901961f blue:0.886275f alpha:1.000000f] : [UIColor colorWithRed:0.000000f green:0.780392f blue:0.745098f alpha:1.000000f]) fc_withSystemName:@"systemMintColor"]; } 347 | if ([name isEqualToString:@"systemCyanColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.392157f green:0.823529f blue:1.000000f alpha:1.000000f] : [UIColor colorWithRed:0.196078f green:0.678431f blue:0.901961f alpha:1.000000f]) fc_withSystemName:@"systemCyanColor"]; } 348 | if ([name isEqualToString:@"systemTealColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.250980f green:0.784314f blue:0.878431f alpha:1.000000f] : [UIColor colorWithRed:0.188235f green:0.690196f blue:0.780392f alpha:1.000000f]) fc_withSystemName:@"systemTealColor"]; } 349 | if ([name isEqualToString:@"systemPurpleColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.749020f green:0.352941f blue:0.949020f alpha:1.000000f] : [UIColor colorWithRed:0.686275f green:0.321569f blue:0.870588f alpha:1.000000f]) fc_withSystemName:@"systemPurpleColor"]; } 350 | if ([name isEqualToString:@"systemIndigoColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.368627f green:0.360784f blue:0.901961f alpha:1.000000f] : [UIColor colorWithRed:0.345098f green:0.337255f blue:0.839216f alpha:1.000000f]) fc_withSystemName:@"systemIndigoColor"]; } 351 | if ([name isEqualToString:@"systemBrownColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.674510f green:0.556863f blue:0.407843f alpha:1.000000f] : [UIColor colorWithRed:0.635294f green:0.517647f blue:0.368627f alpha:1.000000f]) fc_withSystemName:@"systemBrownColor"]; } 352 | if ([name isEqualToString:@"quaternaryLabelColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.921569f green:0.921569f blue:0.960784f alpha:0.160000f] : [UIColor colorWithRed:0.235294f green:0.235294f blue:0.262745f alpha:0.180000f]) fc_withSystemName:@"quaternaryLabelColor"]; } 353 | if ([name isEqualToString:@"placeholderTextColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.921569f green:0.921569f blue:0.960784f alpha:0.300000f] : [UIColor colorWithRed:0.235294f green:0.235294f blue:0.262745f alpha:0.300000f]) fc_withSystemName:@"placeholderTextColor"]; } 354 | if ([name isEqualToString:@"opaqueSeparatorColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.219608f green:0.219608f blue:0.227451f alpha:1.000000f] : [UIColor colorWithRed:0.776471f green:0.776471f blue:0.784314f alpha:1.000000f]) fc_withSystemName:@"opaqueSeparatorColor"]; } 355 | if ([name isEqualToString:@"quaternarySystemFillColor"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.462745f green:0.462745f blue:0.501961f alpha:0.180000f] : [UIColor colorWithRed:0.454902f green:0.454902f blue:0.501961f alpha:0.080000f]) fc_withSystemName:@"quaternarySystemFillColor"]; } 356 | if ([name isEqualToString:@"systemGray2Color"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.388235f green:0.388235f blue:0.400000f alpha:1.000000f] : [UIColor colorWithRed:0.682353f green:0.682353f blue:0.698039f alpha:1.000000f]) fc_withSystemName:@"systemGray2Color"]; } 357 | if ([name isEqualToString:@"systemGray3Color"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.282353f green:0.282353f blue:0.290196f alpha:1.000000f] : [UIColor colorWithRed:0.780392f green:0.780392f blue:0.800000f alpha:1.000000f]) fc_withSystemName:@"systemGray3Color"]; } 358 | if ([name isEqualToString:@"systemGray4Color"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.227451f green:0.227451f blue:0.235294f alpha:1.000000f] : [UIColor colorWithRed:0.819608f green:0.819608f blue:0.839216f alpha:1.000000f]) fc_withSystemName:@"systemGray4Color"]; } 359 | if ([name isEqualToString:@"systemGray5Color"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.172549f green:0.172549f blue:0.180392f alpha:1.000000f] : [UIColor colorWithRed:0.898039f green:0.898039f blue:0.917647f alpha:1.000000f]) fc_withSystemName:@"systemGray5Color"]; } 360 | if ([name isEqualToString:@"systemGray6Color"]) { return [(theme == FCUserInterfaceStyleDark ? [UIColor colorWithRed:0.109804f green:0.109804f blue:0.117647f alpha:1.000000f] : [UIColor colorWithRed:0.949020f green:0.949020f blue:0.968627f alpha:1.000000f]) fc_withSystemName:@"systemGray6Color"]; } 361 | 362 | return nil; 363 | } 364 | 365 | @end 366 | -------------------------------------------------------------------------------- /FCUtilities/UIDevice+FCUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+FCUtilities.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | 8 | // Using CPU detection responsibly: 9 | // 10 | // - Assume unknown CPUs are *better* than all known CPUs. 11 | // - Enable all features, effects, animations, etc. by default. 12 | // - Only reduce functionality/effects on specific CPUs you've tested and found to be too slow. 13 | // 14 | typedef NS_ENUM(NSInteger, FCDeviceCPUClass) { 15 | FCDeviceCPUClassA4 = 0, 16 | FCDeviceCPUClassA5, 17 | FCDeviceCPUClassA6, 18 | FCDeviceCPUClassA7, 19 | FCDeviceCPUClassA8, 20 | FCDeviceCPUClassA9, 21 | FCDeviceCPUClassA10, 22 | FCDeviceCPUClassA11, 23 | FCDeviceCPUClassUnknown 24 | }; 25 | 26 | // Using radio detection responsibly: 27 | // 28 | // - Having a cellular radio doesn't mean that it's enabled, or that your app is allowed to use it. 29 | // - Having a cellular radio doesn't mean that Wi-Fi is enabled. 30 | // - Assume "Unknown" could have either, both, or none. (Enable all features, controls, etc.) 31 | 32 | typedef NS_ENUM(NSInteger, FCDeviceRadioType) { 33 | FCDeviceRadioTypeWiFiOnly = 0, 34 | FCDeviceRadioTypeCellular, 35 | FCDeviceRadioTypeUnknown 36 | }; 37 | 38 | @interface UIDevice (FCUtilities) 39 | 40 | - (FCDeviceCPUClass)fc_CPUClass; 41 | - (FCDeviceRadioType)fc_radioType; 42 | - (BOOL)fc_systemVersionIsAtLeast:(NSString *)versionString; // e.g. "7.0" or "7.0.4" 43 | - (NSString *)fc_modelIdentifier; // e.g. "iPhone5,1" 44 | - (NSString *)fc_modelHumanIdentifier; // e.g. "iPhone 4S" 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /FCUtilities/UIDevice+FCUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+FCUtilities.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "UIDevice+FCUtilities.h" 7 | #import 8 | #include 9 | #include 10 | 11 | static NSString *fcModelHumanIdentifier = NULL; 12 | static FCDeviceCPUClass fcCPUClass; 13 | static FCDeviceRadioType fcRadioType; 14 | 15 | @implementation UIDevice (FCUtilities) 16 | 17 | - (BOOL)fc_systemVersionIsAtLeast:(NSString *)versionString 18 | { 19 | return [[UIDevice currentDevice].systemVersion compare:versionString options:NSNumericSearch] != NSOrderedAscending; 20 | } 21 | 22 | - (NSString *)fc_modelIdentifier 23 | { 24 | size_t size; 25 | sysctlbyname("hw.machine", NULL, &size, NULL, 0); 26 | char *name = malloc(size); 27 | sysctlbyname("hw.machine", name, &size, NULL, 0); 28 | NSString *machine = [NSString stringWithCString:name encoding:NSUTF8StringEncoding]; 29 | free(name); 30 | return machine; 31 | } 32 | 33 | - (FCDeviceCPUClass)fc_CPUClass 34 | { 35 | if (! fcModelHumanIdentifier) [self fc_modelHumanIdentifier]; 36 | return fcCPUClass; 37 | } 38 | 39 | - (FCDeviceRadioType)fc_radioType 40 | { 41 | if (! fcModelHumanIdentifier) [self fc_modelHumanIdentifier]; 42 | return fcRadioType; 43 | } 44 | 45 | - (NSString *)fc_modelHumanIdentifier 46 | { 47 | if (fcModelHumanIdentifier) return fcModelHumanIdentifier; 48 | 49 | NSString *mid = self.fc_modelIdentifier; 50 | 51 | if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { 52 | if ([mid isEqualToString:@"iPad2,1"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad 2"); } 53 | if ([mid isEqualToString:@"iPad2,2"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad 2"); } 54 | if ([mid isEqualToString:@"iPad2,3"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad 2"); } 55 | if ([mid isEqualToString:@"iPad2,4"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad 2"); } 56 | 57 | if ([mid isEqualToString:@"iPad3,1"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad 3"); } 58 | if ([mid isEqualToString:@"iPad3,2"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad 3"); } 59 | if ([mid isEqualToString:@"iPad3,3"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad 3"); } 60 | 61 | if ([mid isEqualToString:@"iPad3,4"]) { fcCPUClass = FCDeviceCPUClassA6; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad 4"); } 62 | if ([mid isEqualToString:@"iPad3,5"]) { fcCPUClass = FCDeviceCPUClassA6; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad 4"); } 63 | if ([mid isEqualToString:@"iPad3,6"]) { fcCPUClass = FCDeviceCPUClassA6; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad 4"); } 64 | 65 | if ([mid isEqualToString:@"iPad6,11"]) { fcCPUClass = FCDeviceCPUClassA9; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad 5"); } 66 | if ([mid isEqualToString:@"iPad6,12"]) { fcCPUClass = FCDeviceCPUClassA9; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad 5"); } 67 | 68 | if ([mid isEqualToString:@"iPad2,5"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad Mini"); } 69 | if ([mid isEqualToString:@"iPad2,6"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Mini"); } 70 | if ([mid isEqualToString:@"iPad2,7"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Mini"); } 71 | 72 | if ([mid isEqualToString:@"iPad4,1"]) { fcCPUClass = FCDeviceCPUClassA7; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad Air"); } 73 | if ([mid isEqualToString:@"iPad4,2"]) { fcCPUClass = FCDeviceCPUClassA7; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Air"); } 74 | if ([mid isEqualToString:@"iPad4,3"]) { fcCPUClass = FCDeviceCPUClassA7; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Air"); } // China 75 | 76 | if ([mid isEqualToString:@"iPad4,4"]) { fcCPUClass = FCDeviceCPUClassA7; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad Mini 2"); } 77 | if ([mid isEqualToString:@"iPad4,5"]) { fcCPUClass = FCDeviceCPUClassA7; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Mini 2"); } 78 | if ([mid isEqualToString:@"iPad4,6"]) { fcCPUClass = FCDeviceCPUClassA7; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Mini 2"); } // China 79 | 80 | if ([mid isEqualToString:@"iPad4,7"]) { fcCPUClass = FCDeviceCPUClassA7; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad Mini 3"); } 81 | if ([mid isEqualToString:@"iPad4,8"]) { fcCPUClass = FCDeviceCPUClassA7; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Mini 3"); } 82 | if ([mid isEqualToString:@"iPad4,9"]) { fcCPUClass = FCDeviceCPUClassA7; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Mini 3"); } // China 83 | 84 | if ([mid isEqualToString:@"iPad5,1"]) { fcCPUClass = FCDeviceCPUClassA8; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad Mini 4"); } 85 | if ([mid isEqualToString:@"iPad5,2"]) { fcCPUClass = FCDeviceCPUClassA8; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Mini 4"); } 86 | 87 | if ([mid isEqualToString:@"iPad5,3"]) { fcCPUClass = FCDeviceCPUClassA8; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad Air 2"); } 88 | if ([mid isEqualToString:@"iPad5,4"]) { fcCPUClass = FCDeviceCPUClassA8; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Air 2"); } 89 | 90 | if ([mid isEqualToString:@"iPad6,7"]) { fcCPUClass = FCDeviceCPUClassA9; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad Pro 12.9-inch"); } 91 | if ([mid isEqualToString:@"iPad6,8"]) { fcCPUClass = FCDeviceCPUClassA9; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Pro 12.9-inch"); } 92 | 93 | if ([mid isEqualToString:@"iPad6,3"]) { fcCPUClass = FCDeviceCPUClassA9; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad Pro 9.7-inch"); } 94 | if ([mid isEqualToString:@"iPad6,4"]) { fcCPUClass = FCDeviceCPUClassA9; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Pro 9.7-inch"); } 95 | 96 | if ([mid isEqualToString:@"iPad7,1"]) { fcCPUClass = FCDeviceCPUClassA10; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad Pro 12.9-inch (2nd gen)"); } 97 | if ([mid isEqualToString:@"iPad7,2"]) { fcCPUClass = FCDeviceCPUClassA10; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Pro 12.9-inch (2nd gen)"); } 98 | if ([mid isEqualToString:@"iPad7,3"]) { fcCPUClass = FCDeviceCPUClassA10; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPad Pro 10.5-inch"); } 99 | if ([mid isEqualToString:@"iPad7,4"]) { fcCPUClass = FCDeviceCPUClassA10; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPad Pro 10.5-inch"); } 100 | 101 | fcCPUClass = FCDeviceCPUClassUnknown; 102 | fcRadioType = FCDeviceRadioTypeUnknown; 103 | return (fcModelHumanIdentifier = @"iPad"); 104 | } else { 105 | if ([mid isEqualToString:@"iPhone3,1"]) { fcCPUClass = FCDeviceCPUClassA4; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 4"); } 106 | if ([mid isEqualToString:@"iPhone3,2"]) { fcCPUClass = FCDeviceCPUClassA4; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 4"); } 107 | if ([mid isEqualToString:@"iPhone3,3"]) { fcCPUClass = FCDeviceCPUClassA4; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 4"); } 108 | 109 | if ([mid isEqualToString:@"iPhone4,1"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 4S"); } 110 | if ([mid isEqualToString:@"iPhone4,1*"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 4S"); } 111 | 112 | if ([mid isEqualToString:@"iPhone5,1"]) { fcCPUClass = FCDeviceCPUClassA6; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 5"); } 113 | if ([mid isEqualToString:@"iPhone5,2"]) { fcCPUClass = FCDeviceCPUClassA6; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 5"); } 114 | 115 | if ([mid isEqualToString:@"iPhone5,3"]) { fcCPUClass = FCDeviceCPUClassA6; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 5c"); } 116 | if ([mid isEqualToString:@"iPhone5,4"]) { fcCPUClass = FCDeviceCPUClassA6; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 5c"); } 117 | 118 | if ([mid isEqualToString:@"iPhone6,1"]) { fcCPUClass = FCDeviceCPUClassA7; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 5s"); } 119 | if ([mid isEqualToString:@"iPhone6,2"]) { fcCPUClass = FCDeviceCPUClassA7; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 5s"); } 120 | 121 | if ([mid isEqualToString:@"iPhone7,1"]) { fcCPUClass = FCDeviceCPUClassA8; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 6 Plus"); } 122 | if ([mid isEqualToString:@"iPhone7,1*"]) { fcCPUClass = FCDeviceCPUClassA8; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 6 Plus"); } // China 123 | if ([mid isEqualToString:@"iPhone7,2"]) { fcCPUClass = FCDeviceCPUClassA8; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 6"); } 124 | if ([mid isEqualToString:@"iPhone7,2*"]) { fcCPUClass = FCDeviceCPUClassA8; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 6"); } // China 125 | 126 | if ([mid isEqualToString:@"iPhone8,1"]) { fcCPUClass = FCDeviceCPUClassA9; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 6s"); } 127 | if ([mid isEqualToString:@"iPhone8,2"]) { fcCPUClass = FCDeviceCPUClassA9; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 6s Plus"); } 128 | 129 | if ([mid isEqualToString:@"iPhone8,4"]) { fcCPUClass = FCDeviceCPUClassA9; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone SE"); } 130 | 131 | if ([mid isEqualToString:@"iPhone9,1"]) { fcCPUClass = FCDeviceCPUClassA10; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 7"); } 132 | if ([mid isEqualToString:@"iPhone9,3"]) { fcCPUClass = FCDeviceCPUClassA10; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 7"); } 133 | if ([mid isEqualToString:@"iPhone9,2"]) { fcCPUClass = FCDeviceCPUClassA10; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 7 Plus"); } 134 | if ([mid isEqualToString:@"iPhone9,4"]) { fcCPUClass = FCDeviceCPUClassA10; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 7 Plus"); } 135 | 136 | if ([mid isEqualToString:@"iPhone10,1"]) { fcCPUClass = FCDeviceCPUClassA11; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 8"); } 137 | if ([mid isEqualToString:@"iPhone10,4"]) { fcCPUClass = FCDeviceCPUClassA11; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 8"); } 138 | if ([mid isEqualToString:@"iPhone10,2"]) { fcCPUClass = FCDeviceCPUClassA11; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 8 Plus"); } 139 | if ([mid isEqualToString:@"iPhone10,5"]) { fcCPUClass = FCDeviceCPUClassA11; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone 8 Plus"); } 140 | if ([mid isEqualToString:@"iPhone10,3"]) { fcCPUClass = FCDeviceCPUClassA11; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone X"); } 141 | if ([mid isEqualToString:@"iPhone10,6"]) { fcCPUClass = FCDeviceCPUClassA11; fcRadioType = FCDeviceRadioTypeCellular; return (fcModelHumanIdentifier = @"iPhone X"); } 142 | 143 | if ([mid isEqualToString:@"iPod5,1"]) { fcCPUClass = FCDeviceCPUClassA5; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPod 5G"); } 144 | if ([mid isEqualToString:@"iPod7,1"]) { fcCPUClass = FCDeviceCPUClassA8; fcRadioType = FCDeviceRadioTypeWiFiOnly; return (fcModelHumanIdentifier = @"iPod 6G"); } 145 | 146 | fcCPUClass = FCDeviceCPUClassUnknown; 147 | fcRadioType = FCDeviceRadioTypeUnknown; 148 | return (fcModelHumanIdentifier = @"iPhone"); 149 | } 150 | } 151 | 152 | @end 153 | -------------------------------------------------------------------------------- /FCUtilities/UIImage+FCUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+FCUtilities.h 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import 7 | #import 8 | 9 | @interface UIImage (FCUtilities) 10 | 11 | // Decoding and resizing image data, usable from any thread or queue 12 | + (UIImage * _Nullable)fc_decodedImageFromData:(NSData * _Nonnull)data; 13 | + (UIImage * _Nullable)fc_decodedImageFromData:(NSData * _Nonnull)data resizedToMaxOutputDimension:(int)outputDimension; 14 | + (UIImage * _Nullable)fc_decodedImageFromData:(NSData * _Nonnull)data resizedToMaxOutputDimension:(int)outputDimension maxSourceBytes:(int)maxSourceBytes maxSourceDimension:(int)maxSourceDimension onlyIfCommonSourceFormat:(BOOL)onlyIfCommonSourceFormat; 15 | 16 | - (void)fc_enumeratePixelsUsingBlock:(void (^ _Nonnull)(NSUInteger x, NSUInteger y, UInt8 r, UInt8 g, UInt8 b, UInt8 a))callback; 17 | - (float)fc_similarityToImageOfSameSize:(UIImage * _Nonnull)otherImage; 18 | 19 | // Masked images are resource images that you provide in black (or any color) on a transparent background. 20 | // Only their transparency values are used -- they're effectively just masks. 21 | // 22 | // On load, you can make the opaque portions of the source image any color you want. 23 | // Useful when declaring your interface colors programatically or supporting multiple color schemes. 24 | 25 | + (UIImage * _Nullable)fc_maskedImageNamed:(NSString * _Nonnull)name color:(UIColor * _Nonnull)color; 26 | - (UIImage * _Nullable)fc_maskedImageWithColor:(UIColor * _Nonnull)color; 27 | 28 | 29 | // Convenience methods for using solid colors where UIKit wants images 30 | 31 | + (UIImage * _Nonnull)fc_stretchableImageWithSolidColor:(UIColor * _Nonnull)solidColor; 32 | + (UIImage * _Nonnull)fc_solidColorImageWithSize:(CGSize)size color:(UIColor * _Nonnull)solidColor; 33 | + (UIImage * _Nonnull)fc_solidColorImageWithSize:(CGSize)size scale:(CGFloat)scale color:(UIColor * _Nonnull)solidColor; 34 | 35 | // Basic effects 36 | 37 | - (UIImage * _Nonnull)fc_desaturatedImage; 38 | - (UIImage * _Nonnull)fc_tintedImageUsingColor:(UIColor * _Nonnull)tintColor; 39 | - (UIImage * _Nonnull)fc_imageWithRoundedCornerRadius:(CGFloat)cornerRadius; 40 | - (UIImage * _Nonnull)fc_imageWithJonyIveRoundedCornerRadius:(CGFloat)cornerRadius; 41 | - (UIImage * _Nonnull)fc_imageWithJonyIveRoundedCornerRadius:(CGFloat)cornerRadius borderColor:(UIColor * _Nonnull)borderColor borderWidth:(CGFloat)borderWidth; 42 | - (UIImage * _Nonnull)fc_imageWithJonyIveRoundedCornerRadius:(CGFloat)cornerRadius borderColor:(UIColor * _Nonnull)borderColor borderWidth:(CGFloat)borderWidth backgroundColor:(UIColor * _Nullable)backgroundColor; 43 | - (UIImage * _Nonnull)fc_imagePaddedWithColor:(UIColor * _Nonnull)color insets:(UIEdgeInsets)insets; 44 | 45 | 46 | // Creation of new images (or annotation of existing ones) by using Quartz drawing commands: 47 | 48 | + (UIImage * _Nonnull)fc_imageWithSize:(CGSize)size drawing:(void (^ _Nonnull)(void))drawingCommands; 49 | - (UIImage * _Nonnull)fc_imageWithAdditionalDrawing:(void (^ _Nonnull)(void))drawingCommands; 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /FCUtilities/UIImage+FCUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+FCUtilities.m 3 | // Part of FCUtilities by Marco Arment. See included LICENSE file for BSD license. 4 | // 5 | 6 | #import "UIImage+FCUtilities.h" 7 | @import UniformTypeIdentifiers; 8 | 9 | @implementation UIImage (FCUtilities) 10 | 11 | + (UIImage * _Nullable)fc_decodedImageFromData:(NSData * _Nonnull)data 12 | { 13 | return [self fc_decodedImageFromData:data resizedToMaxOutputDimension:0 maxSourceBytes:0 maxSourceDimension:0 onlyIfCommonSourceFormat:NO]; 14 | } 15 | 16 | + (UIImage * _Nullable)fc_decodedImageFromData:(NSData * _Nonnull)data resizedToMaxOutputDimension:(int)outputDimension 17 | { 18 | return [self fc_decodedImageFromData:data resizedToMaxOutputDimension:outputDimension maxSourceBytes:0 maxSourceDimension:0 onlyIfCommonSourceFormat:NO]; 19 | } 20 | + (UIImage * _Nullable)fc_decodedImageFromData:(NSData * _Nonnull)data resizedToMaxOutputDimension:(int)outputDimension maxSourceBytes:(int)maxSourceBytes maxSourceDimension:(int)maxSourceDimension onlyIfCommonSourceFormat:(BOOL)onlyIfCommonSourceFormat 21 | { 22 | if (! data.length) return nil; 23 | if (maxSourceBytes > 0 && data.length > maxSourceBytes) return nil; 24 | 25 | CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef) data, (__bridge CFDictionaryRef) @{ 26 | ((__bridge NSString *) kCGImageSourceShouldCache) : @NO 27 | }); 28 | if (! imageSource) return nil; 29 | 30 | if (onlyIfCommonSourceFormat) { 31 | // JPEG and PNG only to avoid huge CPU/RAM usage when using more-obscure, less-optimized formats like JPEG 2000 32 | CFStringRef uti = CGImageSourceGetType(imageSource); 33 | UTType *utt = uti ? [UTType typeWithIdentifier:(__bridge NSString * _Nonnull)(uti)] : nil; 34 | if (! utt || ( 35 | ! [utt conformsToType:UTTypeJPEG] && 36 | ! [utt conformsToType:UTTypePNG] && 37 | ! [utt conformsToType:UTTypeGIF] 38 | )) { 39 | CFRelease(imageSource); 40 | return nil; 41 | } 42 | } 43 | 44 | CFDictionaryRef dictRef = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); 45 | if (! dictRef) { 46 | CFRelease(imageSource); 47 | return nil; 48 | } 49 | 50 | NSDictionary *dict = (__bridge NSDictionary *)dictRef; 51 | int sourceWidth = [dict[(__bridge NSString *) kCGImagePropertyPixelWidth] intValue]; 52 | int sourceHeight = [dict[(__bridge NSString *) kCGImagePropertyPixelHeight] intValue]; 53 | CFRelease(dictRef); 54 | 55 | if (maxSourceDimension > 0 && ( 56 | sourceWidth <= 0 || sourceHeight <= 0 || sourceWidth > maxSourceDimension || sourceHeight > maxSourceDimension 57 | )) { 58 | CFRelease(imageSource); 59 | return nil; 60 | } 61 | 62 | CGImageRef decodedImage; 63 | UIImage *outputImage = nil; 64 | if (outputDimension > 0 && MAX(sourceWidth, sourceHeight) > outputDimension) { 65 | decodedImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef) @{ 66 | ((__bridge NSString *) kCGImageSourceCreateThumbnailFromImageAlways) : @YES, 67 | ((__bridge NSString *) kCGImageSourceShouldCacheImmediately) : @YES, 68 | ((__bridge NSString *) kCGImageSourceCreateThumbnailWithTransform) : @YES, 69 | ((__bridge NSString *) kCGImageSourceThumbnailMaxPixelSize) : @(outputDimension), 70 | }); 71 | } else { 72 | decodedImage = CGImageSourceCreateImageAtIndex(imageSource, 0, (__bridge CFDictionaryRef) @{ 73 | ((__bridge NSString *) kCGImageSourceShouldCacheImmediately) : @YES, 74 | }); 75 | } 76 | 77 | if (decodedImage) { 78 | outputImage = [UIImage imageWithCGImage:decodedImage]; 79 | CFRelease(decodedImage); 80 | } 81 | 82 | CFRelease(imageSource); 83 | return outputImage; 84 | } 85 | 86 | + (UIImage *)fc_stretchableImageWithSolidColor:(UIColor *)solidColor 87 | { 88 | UIGraphicsBeginImageContext(CGSizeMake(1, 1)); 89 | CGRect drawRect = CGRectMake(0, 0, 1, 1); 90 | [solidColor set]; 91 | UIRectFill(drawRect); 92 | UIImage *drawnImage = UIGraphicsGetImageFromCurrentImageContext(); 93 | UIGraphicsEndImageContext(); 94 | return [drawnImage stretchableImageWithLeftCapWidth:0 topCapHeight:0]; 95 | } 96 | 97 | + (UIImage *)fc_solidColorImageWithSize:(CGSize)size scale:(CGFloat)scale color:(UIColor *)solidColor 98 | { 99 | UIGraphicsBeginImageContextWithOptions(size, YES, scale); 100 | CGRect drawRect = CGRectMake(0, 0, size.width, size.height); 101 | [solidColor set]; 102 | UIRectFill(drawRect); 103 | UIImage *drawnImage = UIGraphicsGetImageFromCurrentImageContext(); 104 | UIGraphicsEndImageContext(); 105 | return drawnImage; 106 | } 107 | 108 | + (UIImage *)fc_solidColorImageWithSize:(CGSize)size color:(UIColor *)solidColor 109 | { 110 | UIGraphicsBeginImageContext(size); 111 | CGRect drawRect = CGRectMake(0, 0, size.width, size.height); 112 | [solidColor set]; 113 | UIRectFill(drawRect); 114 | UIImage *drawnImage = UIGraphicsGetImageFromCurrentImageContext(); 115 | UIGraphicsEndImageContext(); 116 | return drawnImage; 117 | } 118 | 119 | - (UIImage *)fc_maskedImageWithColor:(UIColor *)color 120 | { 121 | CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); 122 | UIGraphicsBeginImageContextWithOptions(rect.size, NO, self.scale); 123 | CGContextRef c = UIGraphicsGetCurrentContext(); 124 | [self drawInRect:rect]; 125 | CGContextSetFillColorWithColor(c, [color CGColor]); 126 | CGContextSetBlendMode(c, kCGBlendModeSourceAtop); 127 | CGContextFillRect(c, rect); 128 | UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); 129 | UIGraphicsEndImageContext(); 130 | return result; 131 | } 132 | 133 | + (UIImage *)fc_maskedImageNamed:(NSString *)name color:(UIColor *)color 134 | { 135 | UIImage *image = [UIImage imageNamed:name]; 136 | CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height); 137 | UIGraphicsBeginImageContextWithOptions(rect.size, NO, image.scale); 138 | CGContextRef c = UIGraphicsGetCurrentContext(); 139 | [image drawInRect:rect]; 140 | CGContextSetFillColorWithColor(c, [color CGColor]); 141 | CGContextSetBlendMode(c, kCGBlendModeSourceAtop); 142 | CGContextFillRect(c, rect); 143 | UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); 144 | UIGraphicsEndImageContext(); 145 | return result; 146 | } 147 | 148 | - (UIImage *)fc_desaturatedImage 149 | { 150 | CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); 151 | UIGraphicsBeginImageContextWithOptions(rect.size, NO, self.scale); 152 | CGContextRef c = UIGraphicsGetCurrentContext(); 153 | [self drawInRect:rect]; 154 | CGContextSetFillColorWithColor(c, [UIColor blackColor].CGColor); 155 | CGContextSetBlendMode(c, kCGBlendModeSaturation); 156 | CGContextFillRect(c, rect); 157 | UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); 158 | UIGraphicsEndImageContext(); 159 | return result; 160 | } 161 | 162 | - (UIImage *)fc_tintedImageUsingColor:(UIColor *)tintColor 163 | { 164 | UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); 165 | CGRect drawRect = CGRectMake(0, 0, self.size.width, self.size.height); 166 | [self drawInRect:drawRect]; 167 | [tintColor set]; 168 | UIRectFillUsingBlendMode(drawRect, kCGBlendModeSourceAtop); 169 | UIImage *tintedImage = UIGraphicsGetImageFromCurrentImageContext(); 170 | UIGraphicsEndImageContext(); 171 | return [UIImage imageWithCGImage:tintedImage.CGImage scale:self.scale orientation:UIImageOrientationUp]; 172 | } 173 | 174 | + (UIImage *)fc_imageWithSize:(CGSize)size drawing:(void (^)(void))drawingCommands 175 | { 176 | UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale); 177 | drawingCommands(); 178 | UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext(); 179 | UIGraphicsEndImageContext(); 180 | return finalImage; 181 | } 182 | 183 | - (UIImage *)fc_imageWithAdditionalDrawing:(void (^)(void))drawingCommands 184 | { 185 | UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); 186 | CGRect drawRect = CGRectMake(0, 0, self.size.width, self.size.height); 187 | CGContextClearRect(UIGraphicsGetCurrentContext(), drawRect); 188 | [self drawInRect:drawRect]; 189 | drawingCommands(); 190 | UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext(); 191 | UIGraphicsEndImageContext(); 192 | return finalImage; 193 | } 194 | 195 | - (UIImage *)fc_imageWithRoundedCornerRadius:(CGFloat)cornerRadius 196 | { 197 | CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); 198 | UIGraphicsBeginImageContextWithOptions(rect.size, NO, self.scale); 199 | [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius] addClip]; 200 | [self drawInRect:rect]; 201 | UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); 202 | UIGraphicsEndImageContext(); 203 | return result; 204 | } 205 | 206 | - (UIImage *)fc_imageWithJonyIveRoundedCornerRadius:(CGFloat)cornerRadius 207 | { 208 | CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); 209 | UIGraphicsBeginImageContextWithOptions(rect.size, NO, self.scale); 210 | [[UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(cornerRadius, cornerRadius)] addClip]; 211 | [self drawInRect:rect]; 212 | UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); 213 | UIGraphicsEndImageContext(); 214 | return result; 215 | } 216 | 217 | - (UIImage *)fc_imageWithJonyIveRoundedCornerRadius:(CGFloat)borderCornerRadius borderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth 218 | { 219 | return [self fc_imageWithJonyIveRoundedCornerRadius:borderCornerRadius borderColor:borderColor borderWidth:borderWidth backgroundColor:nil]; 220 | } 221 | 222 | - (UIImage *)fc_imageWithJonyIveRoundedCornerRadius:(CGFloat)borderCornerRadius borderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth backgroundColor:(UIColor *)backgroundColor 223 | { 224 | CGFloat halfBorderWidth = borderWidth / 2.0f; 225 | CGRect outputRect = CGRectMake(0, 0, self.size.width, self.size.height); 226 | CGRect imageRect = CGRectInset(outputRect, borderWidth, borderWidth); 227 | CGRect borderRect = CGRectInset(outputRect, halfBorderWidth, halfBorderWidth); 228 | UIGraphicsBeginImageContextWithOptions(outputRect.size, NO, self.scale); 229 | CGFloat imageCornerRadius = MAX(0, borderCornerRadius - borderWidth); 230 | 231 | CGContextRef ctx = UIGraphicsGetCurrentContext(); 232 | CGContextSaveGState(ctx); 233 | [[UIBezierPath bezierPathWithRoundedRect:imageRect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(imageCornerRadius, imageCornerRadius)] addClip]; 234 | if (backgroundColor) { 235 | [backgroundColor setFill]; 236 | [[UIBezierPath bezierPathWithRect:outputRect] fill]; 237 | } 238 | [self drawInRect:imageRect]; 239 | CGContextRestoreGState(ctx); 240 | 241 | UIBezierPath *borderPath = [UIBezierPath bezierPathWithRoundedRect:borderRect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(borderCornerRadius, borderCornerRadius)]; 242 | borderPath.lineWidth = borderWidth; 243 | [borderColor setStroke]; 244 | [borderPath stroke]; 245 | 246 | UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); 247 | UIGraphicsEndImageContext(); 248 | return result; 249 | } 250 | 251 | - (UIImage * _Nonnull)fc_imagePaddedWithColor:(UIColor * _Nonnull)color insets:(UIEdgeInsets)insets 252 | { 253 | // Only ever adds size to the image, doesn't remove it 254 | insets.top = ABS(insets.top); 255 | insets.bottom = ABS(insets.bottom); 256 | insets.left = ABS(insets.left); 257 | insets.right = ABS(insets.right); 258 | 259 | CGSize originalSize = self.size; 260 | CGSize newSize = CGSizeMake(originalSize.width + (insets.left + insets.right), originalSize.height + (insets.top + insets.bottom)); 261 | return [self.class fc_imageWithSize:newSize drawing:^{ 262 | CGRect entireImageRect = CGRectMake(0, 0, newSize.width, newSize.height); 263 | [color setFill]; 264 | [[UIBezierPath bezierPathWithRect:entireImageRect] fill]; 265 | [self drawInRect:UIEdgeInsetsInsetRect(entireImageRect, insets)]; 266 | }]; 267 | } 268 | 269 | - (void)fc_enumeratePixelsUsingBlock:(void (^ _Nonnull)(NSUInteger x, NSUInteger y, UInt8 r, UInt8 g, UInt8 b, UInt8 a))callback 270 | { 271 | // Adapted from 272 | // http://stackoverflow.com/questions/448125/how-to-get-pixel-data-from-a-uiimage-cocoa-touch-or-cgimage-core-graphics 273 | 274 | CGImageRef imageRef = self.CGImage; 275 | NSUInteger width = CGImageGetWidth(imageRef); 276 | NSUInteger height = CGImageGetHeight(imageRef); 277 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 278 | unsigned char *rawData = (unsigned char *) calloc(height * width * 4, sizeof(unsigned char)); 279 | NSUInteger bytesPerPixel = 4; 280 | NSUInteger bytesPerRow = bytesPerPixel * width; 281 | NSUInteger bitsPerComponent = 8; 282 | CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); 283 | CGColorSpaceRelease(colorSpace); 284 | CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); 285 | CGContextRelease(context); 286 | 287 | for (NSUInteger x = 0; x < width; x++) { 288 | for (NSUInteger y = 0; y < width; y++) { 289 | NSUInteger byteIndex = (bytesPerRow * y) + x * bytesPerPixel; 290 | UInt8 red = rawData[byteIndex]; 291 | UInt8 green = rawData[byteIndex + 1]; 292 | UInt8 blue = rawData[byteIndex + 2]; 293 | UInt8 alpha = rawData[byteIndex + 3]; 294 | callback(x, y, red, green, blue, alpha); 295 | } 296 | } 297 | 298 | free(rawData); 299 | } 300 | 301 | - (float)fc_similarityToImageOfSameSize:(UIImage * _Nonnull)otherImage 302 | { 303 | CGImageRef imageRef1 = self.CGImage, imageRef2 = otherImage.CGImage; 304 | NSUInteger width = CGImageGetWidth(imageRef1); 305 | NSUInteger height = CGImageGetHeight(imageRef1); 306 | 307 | if (CGImageGetWidth(imageRef2) != width || CGImageGetHeight(imageRef2) != height) { 308 | [[NSException exceptionWithName:NSInvalidArgumentException reason:@"Images must be the same size" userInfo:nil] raise]; return 0; 309 | } 310 | 311 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 312 | unsigned char *rawData1 = (unsigned char *) calloc(height * width * 4, sizeof(unsigned char)); 313 | unsigned char *rawData2 = (unsigned char *) calloc(height * width * 4, sizeof(unsigned char)); 314 | NSUInteger bytesPerPixel = 4; 315 | NSUInteger bytesPerRow = bytesPerPixel * width; 316 | NSUInteger bitsPerComponent = 8; 317 | CGContextRef context1 = CGBitmapContextCreate(rawData1, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); 318 | CGContextRef context2 = CGBitmapContextCreate(rawData2, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); 319 | CGColorSpaceRelease(colorSpace); 320 | CGContextDrawImage(context1, CGRectMake(0, 0, width, height), imageRef1); 321 | CGContextDrawImage(context2, CGRectMake(0, 0, width, height), imageRef2); 322 | CGContextRelease(context1); 323 | CGContextRelease(context2); 324 | 325 | NSUInteger totalDifferentPixels = 0; 326 | float totalDifference = 0.0f; 327 | for (NSUInteger x = 0; x < width; x++) { 328 | for (NSUInteger y = 0; y < width; y++) { 329 | NSUInteger byteIndex = (bytesPerRow * y) + x * bytesPerPixel; 330 | NSUInteger differenceSquare = 331 | MAX( 332 | ABS(rawData1[byteIndex] - rawData2[byteIndex]), 333 | MAX( 334 | ABS(rawData1[byteIndex + 1] - rawData2[byteIndex + 1]), 335 | ABS(rawData1[byteIndex + 2] - rawData2[byteIndex + 2]) 336 | ) 337 | ) 338 | ; 339 | 340 | differenceSquare *= differenceSquare; 341 | totalDifference += MIN(1.0f, (float) differenceSquare / 256.0f); 342 | if (differenceSquare) totalDifferentPixels++; 343 | } 344 | } 345 | 346 | free(rawData1); 347 | free(rawData2); 348 | 349 | return totalDifferentPixels ? 1.0f - (totalDifference / (float) totalDifferentPixels) : 1.0f; 350 | } 351 | 352 | 353 | @end 354 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 marcoarment 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FCUtilities 2 | =========== 3 | 4 | Common iOS utilities that I've needed for my apps. Hopefully some are useful for yours. 5 | --------------------------------------------------------------------------------