├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FBTweak.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── FBTweak.xcscheme ├── FBTweak ├── FBTweak-Prefix.pch ├── FBTweak.h ├── FBTweak.m ├── FBTweakCategory.h ├── FBTweakCategory.m ├── FBTweakCollection.h ├── FBTweakCollection.m ├── FBTweakEnabled.h ├── FBTweakInline.h ├── FBTweakInline.m ├── FBTweakInlineInternal.h ├── FBTweakShakeWindow.h ├── FBTweakShakeWindow.m ├── FBTweakStore.h ├── FBTweakStore.m ├── FBTweakViewController.h ├── FBTweakViewController.m ├── _FBColorComponentCell.h ├── _FBColorComponentCell.m ├── _FBColorUtils.h ├── _FBColorUtils.m ├── _FBColorWheelCell.h ├── _FBColorWheelCell.m ├── _FBKeyboardManager.h ├── _FBKeyboardManager.m ├── _FBSliderView.h ├── _FBSliderView.m ├── _FBTweakArrayViewController.h ├── _FBTweakArrayViewController.m ├── _FBTweakBindObserver.h ├── _FBTweakBindObserver.m ├── _FBTweakCategoryViewController.h ├── _FBTweakCategoryViewController.m ├── _FBTweakCollectionViewController.h ├── _FBTweakCollectionViewController.m ├── _FBTweakColorViewController.h ├── _FBTweakColorViewController.m ├── _FBTweakColorViewControllerDataSource.h ├── _FBTweakColorViewControllerHSBDataSource.h ├── _FBTweakColorViewControllerHSBDataSource.m ├── _FBTweakColorViewControllerHexDataSource.h ├── _FBTweakColorViewControllerHexDataSource.m ├── _FBTweakColorViewControllerRGBDataSource.h ├── _FBTweakColorViewControllerRGBDataSource.m ├── _FBTweakDictionaryViewController.h ├── _FBTweakDictionaryViewController.m ├── _FBTweakTableViewCell.h └── _FBTweakTableViewCell.m ├── FBTweakExample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── FBTweakExample.xcscheme ├── FBTweakExample.xcworkspace └── contents.xcworkspacedata ├── FBTweakExample ├── FBAppDelegate.h ├── FBAppDelegate.m ├── FBTweakExample-Info.plist ├── FBTweakExample-Prefix.pch ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── en.lproj │ └── InfoPlist.strings └── main.m ├── FBTweakTests ├── FBTweakInlineTestsARC.m ├── FBTweakInlineTestsMRR.m ├── FBTweakTests-Info.plist └── en.lproj │ └── InfoPlist.strings ├── Images ├── Tweaks.gif └── Tweaks.png ├── LICENSE ├── PATENTS ├── README.md └── Tweaks.podspec /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | *.pbxuser 4 | *.perspective 5 | *.perspectivev3 6 | 7 | *.mode1v3 8 | *.mode2v3 9 | 10 | *.xcodeproj/xcuserdata/*.xcuserdatad 11 | 12 | *.xccheckout 13 | *.xcuserdatad 14 | 15 | build/ 16 | 17 | Pods 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | script: 3 | - xcodebuild test -project FBTweak.xcodeproj -scheme FBTweak -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 6" 4 | - xcodebuild archive -workspace FBTweakExample.xcworkspace -scheme FBTweakExample CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.fb.com/codeofconduct) so that you can understand what actions will and will not be tolerated. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | We want to make contributing to Tweaks as easy and transparent as 3 | possible. If you run into problems, please open an issue. We also actively welcome pull requests. 4 | 5 | ## Pull Requests 6 | 1. Fork the repo and create your branch from `master`. 7 | 2. If you've added code that should be tested, add tests 8 | 3. If you've changed APIs, update the documentation. 9 | 4. Ensure the test suite passes. 10 | 5. Make sure your code lints. 11 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 12 | 13 | ## Contributor License Agreement ("CLA") 14 | In order to accept your pull request, we need you to submit a CLA. You only need 15 | to do this once to work on any of Facebook's open source projects. 16 | 17 | Complete your CLA here: 18 | 19 | ## Issues 20 | We use GitHub issues to track public bugs. Please ensure your description is 21 | clear and has sufficient instructions to be able to reproduce the issue. 22 | 23 | ## Coding Style 24 | * 2 spaces for indentation rather than tabs 25 | 26 | ## License 27 | By contributing to Tweaks you agree that your contributions will be licensed 28 | under its BSD license. 29 | -------------------------------------------------------------------------------- /FBTweak.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FBTweak.xcodeproj/xcshareddata/xcschemes/FBTweak.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 94 | 100 | 101 | 102 | 103 | 105 | 106 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /FBTweak/FBTweak-Prefix.pch: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | -------------------------------------------------------------------------------- /FBTweak/FBTweak.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @protocol FBTweakObserver; 13 | 14 | /** 15 | @abstract Represents a possible value of a tweak. 16 | @discussion Should be able to be persisted in user defaults, 17 | except actions (represented as blocks without a currentValue). 18 | For minimum and maximum values, should implement -compare:. 19 | */ 20 | typedef id FBTweakValue; 21 | 22 | /** 23 | @abstract Represents a range of values for a numeric tweak. 24 | @discussion Use this for the -possibleValues on a tweak. 25 | */ 26 | @interface FBTweakNumericRange : NSObject 27 | 28 | /** 29 | @abstract Creates a new numeric range. 30 | @discussion This is the designated initializer. 31 | @param minimumValue The minimum value of the range. 32 | @param maximumValue The maximum value of the range. 33 | */ 34 | - (instancetype)initWithMinimumValue:(FBTweakValue)minimumValue maximumValue:(FBTweakValue)maximumValue; 35 | 36 | /** 37 | @abstract The minimum value of the range. 38 | @discussion Will always have a value. 39 | */ 40 | @property (nonatomic, strong, readwrite) FBTweakValue minimumValue; 41 | 42 | /** 43 | @abstract The maximum value of the range. 44 | @discussion Will always have a value. 45 | */ 46 | @property (nonatomic, strong, readwrite) FBTweakValue maximumValue; 47 | 48 | @end 49 | 50 | /** 51 | @abstract Represents a unique, named tweak. 52 | @discussion A tweak contains a persistent, editable value. 53 | */ 54 | @interface FBTweak : NSObject 55 | 56 | /** 57 | @abstract Creates a new tweak model. 58 | @discussion This is the designated initializer. 59 | @param identifier The identifier for the tweak. Required. 60 | */ 61 | - (instancetype)initWithIdentifier:(NSString *)identifier; 62 | 63 | /** 64 | @abstract This tweak's unique identifier. 65 | @discussion Used when reading and writing the tweak's value. 66 | */ 67 | @property (nonatomic, copy, readonly) NSString *identifier; 68 | 69 | /** 70 | @abstract The human-readable name of the tweak. 71 | @discussion Show the name when displaying the tweak. 72 | */ 73 | @property (nonatomic, copy, readwrite) NSString *name; 74 | 75 | /** 76 | @abstract If this tweak is an action, with a block value. 77 | @discussion If YES, {@ref currentValue} should not be set and 78 | {@ref defaultValue} is a block rather than a value object. 79 | */ 80 | @property (nonatomic, readonly, assign, getter = isAction) BOOL action; 81 | 82 | /** 83 | @abstract The default value of the tweak. 84 | @discussion Use this when the current value is unset. 85 | For actions, set this property to a block instead. 86 | */ 87 | @property (nonatomic, strong, readwrite) FBTweakValue defaultValue; 88 | 89 | /** 90 | @abstract The current value of the tweak. Can be nil. 91 | @discussion Changes will be propagated to disk. Enforces within 92 | possible values when changed. Must not be set on actions. 93 | */ 94 | @property (nonatomic, strong, readwrite) FBTweakValue currentValue; 95 | 96 | /** 97 | @abstract The possible values of the tweak. 98 | @discussion Optional. If nil, any value is allowed. If an 99 | FBTweakNumericRange, represents a range of numeric values. 100 | If an array or dictionary, contains all of the allowed values. 101 | Should not be set on tweaks representing actions. 102 | */ 103 | @property (nonatomic, strong, readwrite) id possibleValues; 104 | 105 | /** 106 | @abstract The minimum value of the tweak. 107 | @discussion Optional. If nil, there is no minimum. Numeric only. 108 | Should not be set on tweaks representing actions. 109 | */ 110 | @property (nonatomic, strong, readwrite) FBTweakValue minimumValue; 111 | 112 | /** 113 | @abstract The maximum value of the tweak. 114 | @discussion Optional. If nil, there is no maximum. Numeric only. 115 | Should not be set on tweaks representing actions. 116 | */ 117 | @property (nonatomic, strong, readwrite) FBTweakValue maximumValue; 118 | 119 | /** 120 | @abstract The step value of the tweak. 121 | @discussion Optional. If nil, the step value is calculated from 122 | the miniumum and maxium values. Only used for numeric tweaks. 123 | */ 124 | @property (nonatomic, strong, readwrite) FBTweakValue stepValue; 125 | 126 | /** 127 | @abstract The decimal precision value of the tweak. 128 | @discussion Optional. If nil, the precision value is calculated from 129 | the step value. Only used for numeric tweaks. 130 | */ 131 | @property (nonatomic, strong, readwrite) FBTweakValue precisionValue; 132 | 133 | /** 134 | @abstract Adds an observer to the tweak. 135 | @param observer The observer. Must not be nil. 136 | @discussion A weak reference is taken on the observer. 137 | */ 138 | - (void)addObserver:(id)observer; 139 | 140 | /** 141 | @abstract Removes an observer from the tweak. 142 | @param observer The observer to remove. Must not be nil. 143 | @discussion Optional, removing an observer isn't required. 144 | */ 145 | - (void)removeObserver:(id)observer; 146 | 147 | @end 148 | 149 | /** 150 | @abstract Responds to updates when a tweak changes. 151 | */ 152 | @protocol FBTweakObserver 153 | 154 | /** 155 | @abstract Called when a tweak's value changes. 156 | @param tweak The tweak which changed in value. 157 | */ 158 | - (void)tweakDidChange:(FBTweak *)tweak; 159 | 160 | @optional 161 | 162 | /** 163 | @abstract Called when a tweak's value will change. 164 | @param tweak The tweak which value will change. 165 | */ 166 | - (void)tweakWillChange:(FBTweak *)tweak; 167 | 168 | @end 169 | -------------------------------------------------------------------------------- /FBTweak/FBTweak.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweak.h" 11 | 12 | @implementation FBTweakNumericRange 13 | 14 | - (instancetype)initWithMinimumValue:(FBTweakValue)minimumValue maximumValue:(FBTweakValue)maximumValue 15 | { 16 | if ((self = [super init])) { 17 | NSParameterAssert(minimumValue != nil); 18 | NSParameterAssert(maximumValue != nil); 19 | 20 | _minimumValue = minimumValue; 21 | _maximumValue = maximumValue; 22 | } 23 | 24 | return self; 25 | } 26 | 27 | - (instancetype)initWithCoder:(NSCoder *)coder 28 | { 29 | FBTweakValue minimumValue = [coder decodeObjectForKey:@"minimumValue"]; 30 | FBTweakValue maximumValue = [coder decodeObjectForKey:@"maximumValue"]; 31 | self = [self initWithMinimumValue:minimumValue maximumValue:maximumValue]; 32 | 33 | return self; 34 | } 35 | 36 | - (void)encodeWithCoder:(NSCoder *)coder 37 | { 38 | [coder encodeObject:_minimumValue forKey:@"minimumValue"]; 39 | [coder encodeObject:_maximumValue forKey:@"maximumValue"]; 40 | } 41 | 42 | @end 43 | 44 | @implementation FBTweak { 45 | NSHashTable *_observers; 46 | } 47 | 48 | - (instancetype)initWithCoder:(NSCoder *)coder 49 | { 50 | NSString *identifier = [coder decodeObjectForKey:@"identifier"]; 51 | 52 | if ((self = [self initWithIdentifier:identifier])) { 53 | _name = [coder decodeObjectForKey:@"name"]; 54 | _defaultValue = [coder decodeObjectForKey:@"defaultValue"]; 55 | 56 | if ([coder containsValueForKey:@"possibleValues"]) { 57 | _possibleValues = [coder decodeObjectForKey:@"possibleValues"]; 58 | } else { 59 | // Backwards compatbility for before possibleValues was introduced. 60 | FBTweakValue minimumValue = [coder decodeObjectForKey:@"minimumValue"]; 61 | FBTweakValue maximumValue = [coder decodeObjectForKey:@"maximumValue"]; 62 | _possibleValues = [[FBTweakNumericRange alloc] initWithMinimumValue:minimumValue maximumValue:maximumValue]; 63 | } 64 | 65 | _precisionValue = [coder decodeObjectForKey:@"precisionValue"]; 66 | _stepValue = [coder decodeObjectForKey:@"stepValue"]; 67 | 68 | // Fall back to the user-defaults loaded value if current value isn't set. 69 | _currentValue = [coder decodeObjectForKey:@"currentValue"] ?: _currentValue; 70 | } 71 | 72 | return self; 73 | } 74 | 75 | - (instancetype)initWithIdentifier:(NSString *)identifier 76 | { 77 | if ((self = [super init])) { 78 | _identifier = identifier; 79 | NSData *archivedValue = [[NSUserDefaults standardUserDefaults] objectForKey:_identifier]; 80 | _currentValue = (archivedValue != nil && [archivedValue isKindOfClass:[NSData class]] ? [NSKeyedUnarchiver unarchiveObjectWithData:archivedValue] : archivedValue); 81 | } 82 | 83 | return self; 84 | } 85 | 86 | - (void)encodeWithCoder:(NSCoder *)coder 87 | { 88 | [coder encodeObject:_identifier forKey:@"identifier"]; 89 | [coder encodeObject:_name forKey:@"name"]; 90 | 91 | if (!self.isAction) { 92 | [coder encodeObject:_defaultValue forKey:@"defaultValue"]; 93 | [coder encodeObject:_possibleValues forKey:@"possibleValues"]; 94 | [coder encodeObject:_currentValue forKey:@"currentValue"]; 95 | [coder encodeObject:_precisionValue forKey:@"precisionValue"]; 96 | [coder encodeObject:_stepValue forKey:@"stepValue"]; 97 | } 98 | } 99 | 100 | - (BOOL)isAction 101 | { 102 | // NSBlock isn't a public class, walk the hierarchy for it. 103 | Class blockClass = [^{} class]; 104 | 105 | while ([blockClass superclass] != [NSObject class]) { 106 | blockClass = [blockClass superclass]; 107 | } 108 | 109 | return [_defaultValue isKindOfClass:blockClass]; 110 | } 111 | 112 | - (FBTweakValue)minimumValue 113 | { 114 | if ([_possibleValues isKindOfClass:[FBTweakNumericRange class]]) { 115 | return [(FBTweakNumericRange *)_possibleValues minimumValue]; 116 | } else { 117 | return nil; 118 | } 119 | } 120 | 121 | - (void)setMinimumValue:(FBTweakValue)minimumValue 122 | { 123 | if (minimumValue == nil) { 124 | _possibleValues = nil; 125 | } else if ([_possibleValues isKindOfClass:[FBTweakNumericRange class]]) { 126 | _possibleValues = [[FBTweakNumericRange alloc] initWithMinimumValue:minimumValue maximumValue:[(FBTweakNumericRange *)_possibleValues maximumValue]]; 127 | } else { 128 | _possibleValues = [[FBTweakNumericRange alloc] initWithMinimumValue:minimumValue maximumValue:minimumValue]; 129 | } 130 | } 131 | 132 | - (FBTweakValue)maximumValue 133 | { 134 | if ([_possibleValues isKindOfClass:[FBTweakNumericRange class]]) { 135 | return [(FBTweakNumericRange *)_possibleValues maximumValue]; 136 | } else { 137 | return nil; 138 | } 139 | } 140 | 141 | - (void)setMaximumValue:(FBTweakValue)maximumValue 142 | { 143 | if (maximumValue == nil) { 144 | _possibleValues = nil; 145 | } else if ([_possibleValues isKindOfClass:[FBTweakNumericRange class]]) { 146 | _possibleValues = [[FBTweakNumericRange alloc] initWithMinimumValue:[(FBTweakNumericRange *)_possibleValues minimumValue] maximumValue:maximumValue]; 147 | } else { 148 | _possibleValues = [[FBTweakNumericRange alloc] initWithMinimumValue:maximumValue maximumValue:maximumValue]; 149 | } 150 | } 151 | 152 | - (void)setCurrentValue:(FBTweakValue)currentValue 153 | { 154 | NSAssert(!self.isAction, @"actions cannot have non-default values"); 155 | 156 | if (_possibleValues != nil && currentValue != nil) { 157 | if ([_possibleValues isKindOfClass:[NSArray class]]) { 158 | if ([_possibleValues indexOfObject:currentValue] == NSNotFound) { 159 | currentValue = _defaultValue; 160 | } 161 | } else if ([_possibleValues isKindOfClass:[NSDictionary class]]) { 162 | if ([[_possibleValues allKeys] indexOfObject:currentValue] == NSNotFound) { 163 | currentValue = _defaultValue; 164 | } 165 | } else { 166 | FBTweakValue minimumValue = self.minimumValue; 167 | if (self.minimumValue != nil && currentValue != nil && [minimumValue compare:currentValue] == NSOrderedDescending) { 168 | currentValue = minimumValue; 169 | } 170 | 171 | FBTweakValue maximumValue = self.maximumValue; 172 | if (maximumValue != nil && currentValue != nil && [maximumValue compare:currentValue] == NSOrderedAscending) { 173 | currentValue = maximumValue; 174 | } 175 | } 176 | } 177 | 178 | if (_currentValue != currentValue) { 179 | 180 | for (id observer in [_observers setRepresentation]) { 181 | if ([observer respondsToSelector:@selector(tweakWillChange:)]) { 182 | [observer tweakWillChange:self]; 183 | } 184 | } 185 | 186 | _currentValue = currentValue; 187 | // we can't store UIColor to the plist file. That is why we archive value to the NSData. 188 | [[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:_currentValue] forKey:_identifier]; 189 | 190 | for (id observer in [_observers setRepresentation]) { 191 | [observer tweakDidChange:self]; 192 | } 193 | } 194 | } 195 | 196 | - (void)addObserver:(id)observer 197 | { 198 | if (_observers == nil) { 199 | _observers = [NSHashTable weakObjectsHashTable]; 200 | } 201 | 202 | NSAssert(observer != nil, @"observer is required"); 203 | [_observers addObject:observer]; 204 | } 205 | 206 | - (void)removeObserver:(id)observer 207 | { 208 | NSAssert(observer != nil, @"observer is required"); 209 | [_observers removeObject:observer]; 210 | } 211 | 212 | @end 213 | -------------------------------------------------------------------------------- /FBTweak/FBTweakCategory.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class FBTweakCollection; 13 | 14 | /** 15 | @abstract A named grouping of collections. 16 | */ 17 | @interface FBTweakCategory : NSObject 18 | 19 | /** 20 | @abstract Creates a tweak category. 21 | @discussion This is the designated initializer. 22 | */ 23 | - (instancetype)initWithName:(NSString *)name; 24 | 25 | /** 26 | @abstract The name of the category. 27 | */ 28 | @property (nonatomic, copy, readonly) NSString *name; 29 | 30 | /** 31 | @abstract The collections contained in this category. 32 | */ 33 | @property (nonatomic, copy, readonly) NSArray *tweakCollections; 34 | 35 | /** 36 | @abstract Fetches a collection by name. 37 | @param name The collection name to find. 38 | */ 39 | - (FBTweakCollection *)tweakCollectionWithName:(NSString *)name; 40 | 41 | /** 42 | @abstract Adds a tweak collection to the category. 43 | @param tweakCollection The tweak collection to add. 44 | */ 45 | - (void)addTweakCollection:(FBTweakCollection *)tweakCollection; 46 | 47 | /** 48 | @abstract Removes a tweak collection from the category. 49 | @param tweakCollection The tweak collection to remove. 50 | */ 51 | - (void)removeTweakCollection:(FBTweakCollection *)tweakCollection; 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /FBTweak/FBTweakCategory.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweakCategory.h" 11 | #import "FBTweakCollection.h" 12 | 13 | @implementation FBTweakCategory { 14 | NSMutableArray *_orderedCollections; 15 | NSMutableDictionary *_namedCollections; 16 | } 17 | 18 | - (instancetype)initWithCoder:(NSCoder *)coder 19 | { 20 | NSString *name = [coder decodeObjectForKey:@"name"]; 21 | 22 | if ((self = [self initWithName:name])) { 23 | _orderedCollections = [[coder decodeObjectForKey:@"collections"] mutableCopy]; 24 | 25 | for (FBTweakCollection *tweakCollection in _orderedCollections) { 26 | [_namedCollections setObject:tweakCollection forKey:tweakCollection.name]; 27 | } 28 | } 29 | 30 | return self; 31 | } 32 | 33 | - (instancetype)initWithName:(NSString *)name 34 | { 35 | if ((self = [super init])) { 36 | _name = [name copy]; 37 | 38 | _orderedCollections = [[NSMutableArray alloc] initWithCapacity:4]; 39 | _namedCollections = [[NSMutableDictionary alloc] initWithCapacity:4]; 40 | } 41 | 42 | return self; 43 | } 44 | 45 | - (void)encodeWithCoder:(NSCoder *)coder 46 | { 47 | [coder encodeObject:_name forKey:@"name"]; 48 | [coder encodeObject:_orderedCollections forKey:@"collections"]; 49 | } 50 | 51 | - (FBTweakCollection *)tweakCollectionWithName:(NSString *)name 52 | { 53 | return _namedCollections[name]; 54 | } 55 | 56 | - (NSArray *)tweakCollections 57 | { 58 | return [_orderedCollections copy]; 59 | } 60 | 61 | - (void)addTweakCollection:(FBTweakCollection *)tweakCollection 62 | { 63 | [_orderedCollections addObject:tweakCollection]; 64 | [_namedCollections setObject:tweakCollection forKey:tweakCollection.name]; 65 | } 66 | 67 | - (void)removeTweakCollection:(FBTweakCollection *)tweakCollection 68 | { 69 | [_orderedCollections removeObject:tweakCollection]; 70 | [_namedCollections removeObjectForKey:tweakCollection.name]; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /FBTweak/FBTweakCollection.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class FBTweak; 13 | 14 | /** 15 | @abstract A named collection of tweaks. 16 | */ 17 | @interface FBTweakCollection : NSObject 18 | 19 | /** 20 | @abstract Creates a tweak collection. 21 | @discussion This is the designated initializer. 22 | */ 23 | - (instancetype)initWithName:(NSString *)name; 24 | 25 | /** 26 | @abstract The name of the collection. 27 | */ 28 | @property (nonatomic, copy, readonly) NSString *name; 29 | 30 | /** 31 | @abstract The tweaks contained in this collection. 32 | */ 33 | @property (nonatomic, copy, readonly) NSArray *tweaks; 34 | 35 | /** 36 | @abstract Fetches a tweak by identifier. 37 | @param identifier The tweak identifier to find. 38 | @discussion Only search tweaks in this collection. 39 | */ 40 | - (FBTweak *)tweakWithIdentifier:(NSString *)identifier; 41 | 42 | /** 43 | @abstract Adds a tweak to the collection. 44 | @param tweak The tweak to add. 45 | */ 46 | - (void)addTweak:(FBTweak *)tweak; 47 | 48 | /** 49 | @abstract Removes a tweak from the collection. 50 | @param tweak The tweak to remove. 51 | */ 52 | - (void)removeTweak:(FBTweak *)tweak; 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /FBTweak/FBTweakCollection.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweakCollection.h" 11 | #import "FBTweak.h" 12 | 13 | @implementation FBTweakCollection { 14 | NSMutableArray *_orderedTweaks; 15 | NSMutableDictionary *_identifierTweaks; 16 | } 17 | 18 | - (instancetype)initWithCoder:(NSCoder *)coder 19 | { 20 | NSString *name = [coder decodeObjectForKey:@"name"]; 21 | 22 | if ((self = [self initWithName:name])) { 23 | _orderedTweaks = [[coder decodeObjectForKey:@"tweaks"] mutableCopy]; 24 | 25 | for (FBTweak *tweak in _orderedTweaks) { 26 | [_identifierTweaks setObject:tweak forKey:tweak.identifier]; 27 | } 28 | } 29 | 30 | return self; 31 | } 32 | 33 | - (instancetype)initWithName:(NSString *)name 34 | { 35 | if ((self = [super init])) { 36 | _name = [name copy]; 37 | 38 | _orderedTweaks = [[NSMutableArray alloc] initWithCapacity:4]; 39 | _identifierTweaks = [[NSMutableDictionary alloc] initWithCapacity:4]; 40 | } 41 | 42 | return self; 43 | } 44 | 45 | - (void)encodeWithCoder:(NSCoder *)coder 46 | { 47 | [coder encodeObject:_name forKey:@"name"]; 48 | [coder encodeObject:_orderedTweaks forKey:@"tweaks"]; 49 | } 50 | 51 | - (FBTweak *)tweakWithIdentifier:(NSString *)identifier 52 | { 53 | return _identifierTweaks[identifier]; 54 | } 55 | 56 | - (NSArray *)tweaks 57 | { 58 | return [_orderedTweaks copy]; 59 | } 60 | 61 | - (void)addTweak:(FBTweak *)tweak 62 | { 63 | [_orderedTweaks addObject:tweak]; 64 | [_identifierTweaks setObject:tweak forKey:tweak.identifier]; 65 | } 66 | 67 | - (void)removeTweak:(FBTweak *)tweak 68 | { 69 | [_orderedTweaks removeObject:tweak]; 70 | [_identifierTweaks removeObjectForKey:tweak.identifier]; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /FBTweak/FBTweakEnabled.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #ifndef FB_TWEAK_ENABLED 11 | 12 | #if DEBUG 13 | #define FB_TWEAK_ENABLED 1 14 | #else 15 | #define FB_TWEAK_ENABLED 0 16 | #endif 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /FBTweak/FBTweakInline.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweakInlineInternal.h" 11 | 12 | /** 13 | @abstract Common parameters in these macros. 14 | @param category_ The category the tweak's collection is in. Must be a constant NSString. 15 | @param collection_ The collection the tweak goes in. Must be a constant NSString. 16 | @param name_ The name of the tweak. Must be a constant NSString. 17 | @param default_ The default value of the tweak. If the user doesn't configure 18 | a custom value or the build is a release build, then the default value is used. 19 | The default value supports a variety of types, but all must be constant literals. 20 | Supported types include: BOOL, NSInteger, NSUInteger, CGFloat, NSString *, char *. 21 | @param min_ Optional, for numbers. The minimum value. Same restrictions as default. 22 | @param max_ Optional, for numbers. The maximum value. Same restrictions as default. 23 | */ 24 | 25 | /** 26 | @abstract Loads a tweak defined inline at startup. 27 | @warning If tweaks are disabled, this macro will return nil. 28 | @return A {@ref FBTweak} for the tweak that was registered at startup. 29 | */ 30 | #define FBTweakInline(category_, collection_, name_, ...) _FBTweakInline(category_, collection_, name_, __VA_ARGS__) 31 | 32 | /** 33 | @abstract Loads the value of a tweak inline. 34 | @discussion To use a tweak, use this instead of the constant value you otherwise would. 35 | To use the same tweak in two places, define a C function that returns FBTweakValue. 36 | @return The current value of the tweak, or the default value if none is set. 37 | */ 38 | #define FBTweakValue(category_, collection_, name_, ...) _FBTweakValue(category_, collection_, name_, __VA_ARGS__) 39 | 40 | /** 41 | @abstract Binds an object property to a tweak. 42 | @param object_ The object to bind to. 43 | @param property_ The property to bind. 44 | @discussion As long as the object is alive, the property will be updated to match the tweak. 45 | */ 46 | #define FBTweakBind(object_, property_, category_, collection_, name_, ...) _FBTweakBind(object_, property_, category_, collection_, name_, __VA_ARGS__) 47 | 48 | /** 49 | @abstract Performs an action on tweak selection. 50 | @param ... The last parameter is a block containing the action to run. 51 | @discussion The action does not have access to local state. It might be necessary to 52 | access global state in the block to perform actions scoped to a specific class. 53 | */ 54 | #define FBTweakAction(category_, collection_, name_, ...) _FBTweakAction(category_, collection_, name_, __VA_ARGS__) 55 | 56 | 57 | -------------------------------------------------------------------------------- /FBTweak/FBTweakInline.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweakInline.h" 11 | #import "FBTweak.h" 12 | #import "FBTweakInlineInternal.h" 13 | #import "FBTweakCollection.h" 14 | #import "FBTweakStore.h" 15 | #import "FBTweakCategory.h" 16 | 17 | #import 18 | #import 19 | #import 20 | #import 21 | #import 22 | 23 | #if FB_TWEAK_ENABLED 24 | 25 | extern NSString *_FBTweakIdentifier(fb_tweak_entry *entry) 26 | { 27 | return [NSString stringWithFormat:@"FBTweak:%@-%@-%@", *entry->category, *entry->collection, *entry->name]; 28 | } 29 | 30 | static FBTweak *_FBTweakCreateWithEntry(NSString *identifier, fb_tweak_entry *entry) 31 | { 32 | FBTweak *tweak = [[FBTweak alloc] initWithIdentifier:identifier]; 33 | tweak.name = *entry->name; 34 | 35 | if (entry->possible != NULL) { 36 | tweak.possibleValues = fb_tweak_entry_block_field(id, entry, possible); 37 | } 38 | 39 | if (strcmp(*entry->encoding, FBTweakEncodingAction) == 0) { 40 | tweak.defaultValue = *(__strong dispatch_block_t *)entry->value; 41 | } else if (strcmp(*entry->encoding, @encode(BOOL)) == 0) { 42 | tweak.defaultValue = @(fb_tweak_entry_block_field(BOOL, entry, value)); 43 | } else if (strcmp(*entry->encoding, @encode(float)) == 0) { 44 | tweak.defaultValue = [NSNumber numberWithFloat:fb_tweak_entry_block_field(float, entry, value)]; 45 | } else if (strcmp(*entry->encoding, @encode(double)) == 0) { 46 | tweak.defaultValue = [NSNumber numberWithDouble:fb_tweak_entry_block_field(double, entry, value)]; 47 | } else if (strcmp(*entry->encoding, @encode(short)) == 0) { 48 | tweak.defaultValue = [NSNumber numberWithShort:fb_tweak_entry_block_field(short, entry, value)]; 49 | } else if (strcmp(*entry->encoding, @encode(unsigned short)) == 0) { 50 | tweak.defaultValue = [NSNumber numberWithUnsignedShort:fb_tweak_entry_block_field(unsigned short, entry, value)]; 51 | } else if (strcmp(*entry->encoding, @encode(int)) == 0) { 52 | tweak.defaultValue = [NSNumber numberWithInt:fb_tweak_entry_block_field(int, entry, value)]; 53 | } else if (strcmp(*entry->encoding, @encode(unsigned int)) == 0) { 54 | tweak.defaultValue = [NSNumber numberWithUnsignedInt:fb_tweak_entry_block_field(unsigned int, entry, value)]; 55 | } else if (strcmp(*entry->encoding, @encode(long)) == 0) { 56 | tweak.defaultValue = [NSNumber numberWithLong:fb_tweak_entry_block_field(long, entry, value)]; 57 | } else if (strcmp(*entry->encoding, @encode(unsigned long)) == 0) { 58 | tweak.defaultValue = [NSNumber numberWithUnsignedLong:fb_tweak_entry_block_field(unsigned long, entry, value)]; 59 | } else if (strcmp(*entry->encoding, @encode(long long)) == 0) { 60 | tweak.defaultValue = [NSNumber numberWithLongLong:fb_tweak_entry_block_field(long long, entry, value)]; 61 | } else if (strcmp(*entry->encoding, @encode(unsigned long long)) == 0) { 62 | tweak.defaultValue = [NSNumber numberWithUnsignedLongLong:fb_tweak_entry_block_field(unsigned long long, entry, value)]; 63 | } else if (strcmp(*entry->encoding, @encode(NSInteger)) == 0) { 64 | tweak.defaultValue = [NSNumber numberWithInteger:fb_tweak_entry_block_field(NSInteger, entry, value)]; 65 | } else if (strcmp(*entry->encoding, @encode(NSUInteger)) == 0) { 66 | tweak.defaultValue = [NSNumber numberWithUnsignedInteger:fb_tweak_entry_block_field(NSUInteger, entry, value)]; 67 | } else if (*entry->encoding[0] == '[') { 68 | // Assume it's a C string. 69 | tweak.defaultValue = [NSString stringWithUTF8String:fb_tweak_entry_block_field(char *, entry, value)]; 70 | } else if (strcmp(*entry->encoding, @encode(id)) == 0) { 71 | tweak.defaultValue = fb_tweak_entry_block_field(id, entry, value); 72 | } else { 73 | NSCAssert(NO, @"Unknown encoding %s for tweak %@. Value was %p.", *entry->encoding, _FBTweakIdentifier(entry), entry->value); 74 | tweak = nil; 75 | } 76 | 77 | return tweak; 78 | } 79 | 80 | @interface _FBTweakInlineLoader : NSObject 81 | @end 82 | 83 | @implementation _FBTweakInlineLoader 84 | 85 | + (void)load 86 | { 87 | static uint32_t _tweaksLoaded = 0; 88 | if (OSAtomicTestAndSetBarrier(1, &_tweaksLoaded)) { 89 | return; 90 | } 91 | 92 | #ifdef __LP64__ 93 | typedef uint64_t fb_tweak_value; 94 | typedef struct section_64 fb_tweak_section; 95 | typedef struct mach_header_64 fb_tweak_header; 96 | #define fb_tweak_getsectbynamefromheader getsectbynamefromheader_64 97 | #else 98 | typedef uint32_t fb_tweak_value; 99 | typedef struct section fb_tweak_section; 100 | typedef struct mach_header fb_tweak_header; 101 | #define fb_tweak_getsectbynamefromheader getsectbynamefromheader 102 | #endif 103 | 104 | FBTweakStore *store = [FBTweakStore sharedInstance]; 105 | 106 | uint32_t image_count = _dyld_image_count(); 107 | for (uint32_t image_index = 0; image_index < image_count; image_index++) { 108 | const fb_tweak_header *mach_header = (const fb_tweak_header *)_dyld_get_image_header(image_index); 109 | 110 | unsigned long size; 111 | fb_tweak_entry *data = (fb_tweak_entry *)getsectiondata(mach_header, FBTweakSegmentName, FBTweakSectionName, &size); 112 | if (data == NULL) { 113 | continue; 114 | } 115 | size_t count = size / sizeof(fb_tweak_entry); 116 | for (size_t i = 0; i < count; i++) { 117 | fb_tweak_entry *entry = &data[i]; 118 | FBTweakCategory *category = [store tweakCategoryWithName:*entry->category]; 119 | if (category == nil) { 120 | category = [[FBTweakCategory alloc] initWithName:*entry->category]; 121 | [store addTweakCategory:category]; 122 | } 123 | 124 | FBTweakCollection *collection = [category tweakCollectionWithName:*entry->collection]; 125 | if (collection == nil) { 126 | collection = [[FBTweakCollection alloc] initWithName:*entry->collection]; 127 | [category addTweakCollection:collection]; 128 | } 129 | 130 | NSString *identifier = _FBTweakIdentifier(entry); 131 | if ([collection tweakWithIdentifier:identifier] == nil) { 132 | FBTweak *tweak = _FBTweakCreateWithEntry(identifier, entry); 133 | 134 | if (tweak != nil) { 135 | [collection addTweak:tweak]; 136 | } 137 | } 138 | } 139 | } 140 | } 141 | 142 | @end 143 | 144 | #endif 145 | -------------------------------------------------------------------------------- /FBTweak/FBTweakInlineInternal.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweakEnabled.h" 11 | #import "FBTweak.h" 12 | #import "FBTweakStore.h" 13 | #import "FBTweakCategory.h" 14 | #import "FBTweakCollection.h" 15 | #import "_FBTweakBindObserver.h" 16 | 17 | #if !FB_TWEAK_ENABLED 18 | 19 | #define __FBTweakDefault(default, ...) default 20 | #define _FBTweakInline(category_, collection_, name_, ...) nil 21 | #define _FBTweakValue(category_, collection_, name_, ...) (__FBTweakDefault(__VA_ARGS__, _)) 22 | #define _FBTweakBind(object_, property_, category_, collection_, name_, ...) (object_.property_ = __FBTweakDefault(__VA_ARGS__, _)) 23 | #define _FBTweakAction(category_, collection_, name_, ...) 24 | 25 | #else 26 | 27 | #ifdef __cplusplus 28 | extern "C" { 29 | #endif 30 | 31 | #define FBTweakSegmentName "__DATA" 32 | #define FBTweakSectionName "FBTweak" 33 | 34 | #define FBTweakEncodingAction "__ACTION__" 35 | 36 | typedef __unsafe_unretained NSString *FBTweakLiteralString; 37 | 38 | typedef struct { 39 | FBTweakLiteralString *category; 40 | FBTweakLiteralString *collection; 41 | FBTweakLiteralString *name; 42 | void *value; 43 | void *possible; 44 | char **encoding; 45 | } fb_tweak_entry; 46 | 47 | // cast to a pointer to a block, dereferenece said pointer, call said block 48 | #define fb_tweak_entry_block_field(type, entry, field) (*(type (^__unsafe_unretained (*))(void))(entry->field))() 49 | 50 | extern NSString *_FBTweakIdentifier(fb_tweak_entry *entry); 51 | 52 | #if __has_feature(objc_arc) 53 | #define _FBTweakRelease(x) 54 | #else 55 | #define _FBTweakRelease(x) [x release] 56 | #endif 57 | 58 | #define __FBTweakConcat_(X, Y) X ## Y 59 | #define __FBTweakConcat(X, Y) __FBTweakConcat_(X, Y) 60 | 61 | #define __FBTweakIndex(_1, _2, _3, value, ...) value 62 | #define __FBTweakIndexCount(...) __FBTweakIndex(__VA_ARGS__, 3, 2, 1) 63 | 64 | #define __FBTweakDispatch1(__withoutRange, __withRange, __withPossible, ...) __withoutRange 65 | #define __FBTweakDispatch2(__withoutRange, __withRange, __withPossible, ...) __withPossible 66 | #define __FBTweakDispatch3(__withoutRange, __withRange, __withPossible, ...) __withRange 67 | #define _FBTweakDispatch(__withoutRange, __withRange, __withPossible, ...) __FBTweakConcat(__FBTweakDispatch, __FBTweakIndexCount(__VA_ARGS__))(__withoutRange, __withRange, __withPossible) 68 | 69 | #define _FBTweakInlineWithoutRange(category_, collection_, name_, default_) \ 70 | ((^{ \ 71 | return _FBTweakInlineWithPossibleInternal(category_, collection_, name_, default_, NULL); \ 72 | })()) 73 | #define _FBTweakInlineWithRange(category_, collection_, name_, default_, min_, max_) \ 74 | ((^{ \ 75 | __attribute__((used)) static __typeof__(default_) min__ = (__typeof__(default_))min_; \ 76 | __attribute__((used)) static __typeof__(default_) max__ = (__typeof__(default_))max_; \ 77 | return _FBTweakInlineWithPossibleInternal(category_, collection_, name_, default_, [[FBTweakNumericRange alloc] initWithMinimumValue:@(min__) maximumValue:@(max__)]); \ 78 | })()) 79 | #define _FBTweakInlineWithPossible(category_, collection_, name_, default_, possible_) \ 80 | ((^{ \ 81 | return _FBTweakInlineWithPossibleInternal(category_, collection_, name_, default_, possible_); \ 82 | })()) 83 | #define _FBTweakInlineWithPossibleInternal(category_, collection_, name_, default_, possible_) \ 84 | ((^{ \ 85 | /* store the tweak data in the binary at compile time. */ \ 86 | __attribute__((used)) static FBTweakLiteralString category__ = category_; \ 87 | __attribute__((used)) static FBTweakLiteralString collection__ = collection_; \ 88 | __attribute__((used)) static FBTweakLiteralString name__ = name_; \ 89 | __attribute__((used)) static void *default__ = (__bridge void *) ^{ return default_; }; \ 90 | __attribute__((used)) static void *possible__ = (__bridge void *) ^{ return possible_; }; \ 91 | __attribute__((used)) static char *encoding__ = (char *)@encode(__typeof__(default_)); \ 92 | __attribute__((used)) __attribute__((section (FBTweakSegmentName "," FBTweakSectionName))) static fb_tweak_entry entry = \ 93 | { &category__, &collection__, &name__, (void *)&default__, (void *)&possible__, &encoding__ }; \ 94 | \ 95 | /* find the registered tweak with the given identifier. */ \ 96 | FBTweakStore *store = [FBTweakStore sharedInstance]; \ 97 | FBTweakCategory *category = [store tweakCategoryWithName:category__]; \ 98 | FBTweakCollection *collection = [category tweakCollectionWithName:collection__]; \ 99 | \ 100 | NSString *identifier = _FBTweakIdentifier(&entry); \ 101 | FBTweak *__inline_tweak = [collection tweakWithIdentifier:identifier]; \ 102 | \ 103 | return __inline_tweak; \ 104 | })()) 105 | #define _FBTweakInline(category_, collection_, name_, ...) _FBTweakDispatch(_FBTweakInlineWithoutRange, _FBTweakInlineWithRange, _FBTweakInlineWithPossible, __VA_ARGS__)(category_, collection_, name_, __VA_ARGS__) 106 | 107 | #define _FBTweakValueInternal(tweak_, category_, collection_, name_, default_) \ 108 | ((^{ \ 109 | /* returns a correctly typed version of the current tweak value */ \ 110 | FBTweakValue currentValue = tweak_.currentValue ?: tweak_.defaultValue; \ 111 | return _Generic(default_, \ 112 | float: [currentValue floatValue], \ 113 | const float: [currentValue floatValue], \ 114 | double: [currentValue doubleValue], \ 115 | const double: [currentValue doubleValue], \ 116 | short: [currentValue shortValue], \ 117 | const short: [currentValue shortValue], \ 118 | unsigned short: [currentValue unsignedShortValue], \ 119 | const unsigned short: [currentValue unsignedShortValue], \ 120 | int: [currentValue intValue], \ 121 | const int: [currentValue intValue], \ 122 | unsigned int: [currentValue unsignedIntValue], \ 123 | const unsigned int: [currentValue unsignedIntValue], \ 124 | long: [currentValue longValue], \ 125 | const long: [currentValue longValue], \ 126 | unsigned long: [currentValue unsignedLongValue], \ 127 | const unsigned long: [currentValue unsignedLongValue], \ 128 | long long: [currentValue longLongValue], \ 129 | const long long: [currentValue longLongValue], \ 130 | unsigned long long: [currentValue unsignedLongLongValue], \ 131 | const unsigned long long: [currentValue unsignedLongLongValue], \ 132 | BOOL: [currentValue boolValue], \ 133 | const BOOL: [currentValue boolValue], \ 134 | id: currentValue, \ 135 | const id: currentValue, \ 136 | /* assume char * as the default. */ \ 137 | /* constant strings are typed as char[N] */ \ 138 | /* and we can't enumerate all of those. */ \ 139 | /* luckily, we only need one fallback */ \ 140 | default: [currentValue UTF8String] \ 141 | ); \ 142 | })()) 143 | 144 | #define _FBTweakValueWithoutRange(category_, collection_, name_, default_) \ 145 | ((^{ \ 146 | FBTweak *__value_tweak = _FBTweakInlineWithoutRange(category_, collection_, name_, default_); \ 147 | return _FBTweakValueInternal(__value_tweak, category_, collection_, name_, default_); \ 148 | })()) 149 | #define _FBTweakValueWithRange(category_, collection_, name_, default_, min_, max_) \ 150 | ((^{ \ 151 | FBTweak *__value_tweak = _FBTweakInlineWithRange(category_, collection_, name_, default_, min_, max_); \ 152 | return _FBTweakValueInternal(__value_tweak, category_, collection_, name_, default_); \ 153 | })()) 154 | #define _FBTweakValueWithPossible(category_, collection_, name_, default_, possible_) \ 155 | ((^{ \ 156 | FBTweak *__value_tweak = _FBTweakInlineWithPossible(category_, collection_, name_, default_, possible_); \ 157 | return _FBTweakValueInternal(__value_tweak, category_, collection_, name_, default_); \ 158 | })()) 159 | #define _FBTweakValue(category_, collection_, name_, ...) _FBTweakDispatch(_FBTweakValueWithoutRange, _FBTweakValueWithRange, _FBTweakValueWithPossible, __VA_ARGS__)(category_, collection_, name_, __VA_ARGS__) 160 | 161 | #define _FBTweakBindWithoutRange(object_, property_, category_, collection_, name_, default_) \ 162 | ((^{ \ 163 | FBTweak *__bind_tweak = _FBTweakInlineWithoutRange(category_, collection_, name_, default_); \ 164 | _FBTweakBindInternal(object_, property_, category_, collection_, name_, default_, __bind_tweak); \ 165 | })()) 166 | #define _FBTweakBindWithRange(object_, property_, category_, collection_, name_, default_, min_, max_) \ 167 | ((^{ \ 168 | FBTweak *__bind_tweak = _FBTweakInlineWithRange(category_, collection_, name_, default_, min_, max_); \ 169 | _FBTweakBindInternal(object_, property_, category_, collection_, name_, default_, __bind_tweak); \ 170 | })()) 171 | #define _FBTweakBindWithPossible(object_, property_, category_, collection_, name_, default_, possible_) \ 172 | ((^{ \ 173 | FBTweak *__bind_tweak = _FBTweakInlineWithPossible(category_, collection_, name_, default_, possible_); \ 174 | _FBTweakBindInternal(object_, property_, category_, collection_, name_, default_, __bind_tweak); \ 175 | })()) 176 | #define _FBTweakBindInternal(object_, property_, category_, collection_, name_, default_, tweak_) \ 177 | ((^{ \ 178 | object_.property_ = _FBTweakValueInternal(tweak_, category_, collection_, name_, default_); \ 179 | _FBTweakBindObserver *observer__ = [[_FBTweakBindObserver alloc] initWithTweak:tweak_ block:^(id object__) { \ 180 | __typeof__(object_) object___ = object__; \ 181 | object___.property_ = _FBTweakValueInternal(tweak_, category_, collection_, name_, default_); \ 182 | }]; \ 183 | [observer__ attachToObject:object_]; \ 184 | })()) 185 | #define _FBTweakBind(object_, property_, category_, collection_, name_, ...) _FBTweakDispatch(_FBTweakBindWithoutRange, _FBTweakBindWithRange, _FBTweakBindWithPossible, __VA_ARGS__)(object_, property_, category_, collection_, name_, __VA_ARGS__) 186 | 187 | #define _FBTweakAction(category_, collection_, name_, ...) \ 188 | _FBTweakActionInternal(category_, collection_, name_, __COUNTER__, __VA_ARGS__) 189 | #define _FBTweakActionInternal(category_, collection_, name_, suffix_, ...) \ 190 | /* store the tweak data in the binary at compile time. */ \ 191 | __attribute__((used)) static FBTweakLiteralString __FBTweakConcat(__fb_tweak_action_category_, suffix_) = category_; \ 192 | __attribute__((used)) static FBTweakLiteralString __FBTweakConcat(__fb_tweak_action_collection_, suffix_) = collection_; \ 193 | __attribute__((used)) static FBTweakLiteralString __FBTweakConcat(__fb_tweak_action_name_, suffix_) = name_; \ 194 | __attribute__((used)) static dispatch_block_t __FBTweakConcat(__fb_tweak_action_block_, suffix_) = __VA_ARGS__; \ 195 | __attribute__((used)) static char *__FBTweakConcat(__fb_tweak_action_encoding_, suffix_) = (char *)FBTweakEncodingAction; \ 196 | __attribute__((used)) __attribute__((section (FBTweakSegmentName "," FBTweakSectionName))) static fb_tweak_entry __FBTweakConcat(__fb_tweak_action_entry_, suffix_) = { \ 197 | &__FBTweakConcat(__fb_tweak_action_category_, suffix_), \ 198 | &__FBTweakConcat(__fb_tweak_action_collection_, suffix_), \ 199 | &__FBTweakConcat(__fb_tweak_action_name_, suffix_), \ 200 | &__FBTweakConcat(__fb_tweak_action_block_, suffix_), \ 201 | NULL, \ 202 | &__FBTweakConcat(__fb_tweak_action_encoding_, suffix_), \ 203 | }; \ 204 | 205 | #ifdef __cplusplus 206 | } 207 | #endif 208 | 209 | #endif 210 | 211 | -------------------------------------------------------------------------------- /FBTweak/FBTweakShakeWindow.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | #import "FBTweakViewController.h" 13 | 14 | /** 15 | @abstract A UIWindow that automatically presents tweaks when the user shakes the device. 16 | @discussion Use this window as your app's root window to enable shaking to open tweaks. 17 | */ 18 | @interface FBTweakShakeWindow : UIWindow 19 | 20 | /** 21 | @abstract Toggles FBTweakShakeWindow's ability to respond to shake gestures. Defaults to YES 22 | */ 23 | @property (nonatomic) BOOL shakeEnabled; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /FBTweak/FBTweakShakeWindow.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweakEnabled.h" 11 | #import "FBTweakStore.h" 12 | #import "FBTweakShakeWindow.h" 13 | #import "FBTweakViewController.h" 14 | #import "_FBKeyboardManager.h" 15 | 16 | // Minimum shake time required to present tweaks on device. 17 | static CFTimeInterval _FBTweakShakeWindowMinTimeInterval = 0.4; 18 | 19 | @implementation FBTweakShakeWindow { 20 | BOOL _shaking; 21 | BOOL _active; 22 | } 23 | 24 | - (instancetype)initWithFrame:(CGRect)frame 25 | { 26 | if ((self = [super initWithFrame:frame])) { 27 | _FBTweakShakeWindowCommonInit(self); 28 | } 29 | 30 | return self; 31 | } 32 | 33 | - (instancetype)initWithCoder:(NSCoder *)coder 34 | { 35 | if ((self = [super initWithCoder:coder])) { 36 | _FBTweakShakeWindowCommonInit(self); 37 | } 38 | return self; 39 | } 40 | 41 | static void _FBTweakShakeWindowCommonInit(FBTweakShakeWindow *self) 42 | { 43 | // Maintain this state manually using notifications so Tweaks can be used in app extensions, where UIApplication is unavailable. 44 | self->_active = YES; 45 | self->_shakeEnabled = YES; 46 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_applicationWillResignActiveWithNotification:) name:UIApplicationWillResignActiveNotification object:nil]; 47 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_applicationDidBecomeActiveWithNotification:) name:UIApplicationDidBecomeActiveNotification object:nil]; 48 | } 49 | 50 | - (void)dealloc 51 | { 52 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; 53 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; 54 | } 55 | 56 | - (void)_applicationWillResignActiveWithNotification:(NSNotification *)notification 57 | { 58 | _active = NO; 59 | } 60 | 61 | - (void)_applicationDidBecomeActiveWithNotification:(NSNotification *)notification 62 | { 63 | _active = YES; 64 | } 65 | 66 | - (void)tweakViewControllerPressedDone:(FBTweakViewController *)tweakViewController 67 | { 68 | [[NSNotificationCenter defaultCenter] postNotificationName:FBTweakShakeViewControllerDidDismissNotification object:tweakViewController]; 69 | [tweakViewController.view endEditing:YES]; 70 | [tweakViewController dismissViewControllerAnimated:YES completion:NULL]; 71 | } 72 | 73 | - (void)_presentTweaks 74 | { 75 | UIViewController *visibleViewController = self.rootViewController; 76 | while (visibleViewController.presentedViewController != nil) { 77 | visibleViewController = visibleViewController.presentedViewController; 78 | } 79 | 80 | // Prevent double-presenting the tweaks view controller. 81 | if (![visibleViewController isKindOfClass:[FBTweakViewController class]]) { 82 | FBTweakStore *store = [FBTweakStore sharedInstance]; 83 | FBTweakViewController *viewController = [[FBTweakViewController alloc] initWithStore:store]; 84 | viewController.tweaksDelegate = self; 85 | [visibleViewController presentViewController:viewController animated:YES completion:NULL]; 86 | } 87 | } 88 | 89 | - (BOOL)_shouldPresentTweaks 90 | { 91 | #if TARGET_IPHONE_SIMULATOR && FB_TWEAK_ENABLED 92 | return YES; 93 | #elif FB_TWEAK_ENABLED 94 | return _shakeEnabled && _shaking && _active; 95 | #else 96 | return NO; 97 | #endif 98 | } 99 | 100 | - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event 101 | { 102 | if (motion == UIEventSubtypeMotionShake) { 103 | _shaking = YES; 104 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, _FBTweakShakeWindowMinTimeInterval * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 105 | if ([self _shouldPresentTweaks]) { 106 | [self _presentTweaks]; 107 | } 108 | }); 109 | } 110 | [super motionBegan:motion withEvent:event]; 111 | } 112 | 113 | - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event 114 | { 115 | if (motion == UIEventSubtypeMotionShake) { 116 | _shaking = NO; 117 | } 118 | [super motionEnded:motion withEvent:event]; 119 | } 120 | 121 | @end 122 | -------------------------------------------------------------------------------- /FBTweak/FBTweakStore.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class FBTweakCategory; 13 | 14 | /** 15 | @abstract The global store for tweaks. 16 | */ 17 | @interface FBTweakStore : NSObject 18 | 19 | /** 20 | @abstract Creates or returns the shared global store. 21 | */ 22 | + (instancetype)sharedInstance; 23 | 24 | /** 25 | @abstract The tweak categories in the store. 26 | */ 27 | @property (nonatomic, copy, readonly) NSArray *tweakCategories; 28 | 29 | /** 30 | @abstract Finds a tweak category by name. 31 | @param name The name of the category to find. 32 | @return The category if found, nil otherwise. 33 | */ 34 | - (FBTweakCategory *)tweakCategoryWithName:(NSString *)name; 35 | 36 | /** 37 | @abstract Registers a tweak category with the store. 38 | @param category The tweak category to register. 39 | */ 40 | - (void)addTweakCategory:(FBTweakCategory *)category; 41 | 42 | /** 43 | @abstract Removes a tweak category from the store. 44 | @param category The tweak category to remove. 45 | */ 46 | - (void)removeTweakCategory:(FBTweakCategory *)category; 47 | 48 | /** 49 | @abstract Resets all tweaks in the store. 50 | */ 51 | - (void)reset; 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /FBTweak/FBTweakStore.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweakStore.h" 11 | #import "FBTweak.h" 12 | #import "FBTweakCategory.h" 13 | #import "FBTweakCollection.h" 14 | 15 | @implementation FBTweakStore { 16 | NSMutableArray *_orderedCategories; 17 | NSMutableDictionary *_namedCategories; 18 | } 19 | 20 | + (instancetype)sharedInstance 21 | { 22 | static FBTweakStore *sharedInstance = nil; 23 | 24 | static dispatch_once_t onceToken; 25 | dispatch_once(&onceToken, ^{ 26 | sharedInstance = [[self alloc] init]; 27 | }); 28 | 29 | return sharedInstance; 30 | } 31 | 32 | - (instancetype)initWithCoder:(NSCoder *)coder 33 | { 34 | if ((self = [self init])) { 35 | _orderedCategories = [[coder decodeObjectForKey:@"categories"] mutableCopy]; 36 | 37 | for (FBTweakCategory *tweakCategory in _orderedCategories) { 38 | [_namedCategories setObject:tweakCategory forKey:tweakCategory.name]; 39 | } 40 | } 41 | 42 | return self; 43 | } 44 | 45 | - (instancetype)init 46 | { 47 | if ((self = [super init])) { 48 | _orderedCategories = [[NSMutableArray alloc] initWithCapacity:16]; 49 | _namedCategories = [[NSMutableDictionary alloc] initWithCapacity:16]; 50 | } 51 | 52 | return self; 53 | } 54 | 55 | - (void)encodeWithCoder:(NSCoder *)coder 56 | { 57 | [coder encodeObject:_orderedCategories forKey:@"categories"]; 58 | } 59 | 60 | - (NSArray *)tweakCategories 61 | { 62 | return [_orderedCategories copy]; 63 | } 64 | 65 | - (FBTweakCategory *)tweakCategoryWithName:(NSString *)name 66 | { 67 | return _namedCategories[name]; 68 | } 69 | 70 | - (void)addTweakCategory:(FBTweakCategory *)category 71 | { 72 | [_namedCategories setObject:category forKey:category.name]; 73 | [_orderedCategories addObject:category]; 74 | } 75 | 76 | - (void)removeTweakCategory:(FBTweakCategory *)category 77 | { 78 | [_namedCategories removeObjectForKey:category.name]; 79 | [_orderedCategories removeObject:category]; 80 | } 81 | 82 | - (void)reset 83 | { 84 | for (FBTweakCategory *category in self.tweakCategories) { 85 | for (FBTweakCollection *collection in category.tweakCollections) { 86 | for (FBTweak *tweak in collection.tweaks) { 87 | if (!tweak.isAction) { 88 | tweak.currentValue = nil; 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /FBTweak/FBTweakViewController.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | /** 13 | @abstract A notification posted when the FBTweakViewController is dismissed 14 | @discussion Register an observer to listen to FBTweakShakeViewControllerDidDismissNotification. 15 | The object included with the notification is the FBTweakViewController instance 16 | being dismissed. 17 | */ 18 | extern NSString *const FBTweakShakeViewControllerDidDismissNotification; 19 | 20 | @class FBTweakStore; 21 | @protocol FBTweakViewControllerDelegate; 22 | 23 | /** 24 | @abstract A view controller that shows the list of tweaks. 25 | */ 26 | @interface FBTweakViewController : UINavigationController 27 | 28 | /** 29 | @abstract Create a tweak view controller. 30 | @param store The tweak store to show. Usually +[FBTweakStore sharedInstance]. 31 | @discussion Calls -[initWithStore:category:] with a nil category. 32 | */ 33 | - (instancetype)initWithStore:(FBTweakStore *)store; 34 | 35 | /** 36 | @abstract Create a tweak view controller drilled-down to a specific category 37 | @param store The tweak store to show. Usually +[FBTweakStore sharedInstance]. 38 | @param categoryName The tweak category to drill down to. Use nil to show all categories 39 | @discussion The designated initializer. 40 | */ 41 | - (instancetype)initWithStore:(FBTweakStore *)store category:(NSString *)categoryName; 42 | 43 | /** 44 | @abstract Responds to tweak view controller actions. 45 | @discussion Named {@ref tweaksDelegate} to avoid conflicting with UINavigationController. 46 | */ 47 | @property (nonatomic, weak, readwrite) id tweaksDelegate; 48 | 49 | @end 50 | 51 | /** 52 | @abstract Responds to actions from the tweak view controller. 53 | */ 54 | @protocol FBTweakViewControllerDelegate 55 | @required 56 | 57 | /** 58 | @abstract Called when the tweak view controller pressed done. 59 | @param tweakViewController The view controller that had done pressed. 60 | @discussion The implementation should dismiss the tweak view controller. 61 | */ 62 | - (void)tweakViewControllerPressedDone:(FBTweakViewController *)tweakViewController; 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /FBTweak/FBTweakViewController.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweakStore.h" 11 | #import "FBTweakViewController.h" 12 | #import "_FBTweakCategoryViewController.h" 13 | #import "_FBTweakCollectionViewController.h" 14 | 15 | NSString *const FBTweakShakeViewControllerDidDismissNotification = @"FBTweakShakeViewControllerDidDismissNotification"; 16 | 17 | @interface FBTweakViewController () <_FBTweakCategoryViewControllerDelegate, _FBTweakCollectionViewControllerDelegate> 18 | @end 19 | 20 | @implementation FBTweakViewController { 21 | FBTweakStore *_store; 22 | } 23 | 24 | - (instancetype)initWithStore:(FBTweakStore *)store 25 | { 26 | return [self initWithStore:store category:nil]; 27 | } 28 | 29 | - (instancetype)initWithStore:(FBTweakStore *)store category:(NSString *)categoryName 30 | { 31 | if ((self = [super init])) { 32 | _store = store; 33 | 34 | _FBTweakCategoryViewController *categoryViewController = [[_FBTweakCategoryViewController alloc] initWithStore:store]; 35 | categoryViewController.delegate = self; 36 | [self pushViewController:categoryViewController animated:NO]; 37 | 38 | FBTweakCategory *category = nil; 39 | if (categoryName && (category = [store tweakCategoryWithName:categoryName])) { 40 | _FBTweakCollectionViewController *collectionViewController = [[_FBTweakCollectionViewController alloc] initWithTweakCategory:category]; 41 | collectionViewController.delegate = self; 42 | [self pushViewController:collectionViewController animated:NO]; 43 | } 44 | } 45 | 46 | return self; 47 | } 48 | 49 | - (void)tweakCategoryViewController:(_FBTweakCategoryViewController *)viewController selectedCategory:(FBTweakCategory *)category 50 | { 51 | _FBTweakCollectionViewController *collectionViewController = [[_FBTweakCollectionViewController alloc] initWithTweakCategory:category]; 52 | collectionViewController.delegate = self; 53 | [self pushViewController:collectionViewController animated:YES]; 54 | } 55 | 56 | - (void)tweakCategoryViewControllerSelectedDone:(_FBTweakCategoryViewController *)viewController 57 | { 58 | [_tweaksDelegate tweakViewControllerPressedDone:self]; 59 | } 60 | 61 | - (void)tweakCollectionViewControllerSelectedDone:(_FBTweakCollectionViewController *)viewController 62 | { 63 | [_tweaksDelegate tweakViewControllerPressedDone:self]; 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /FBTweak/_FBColorComponentCell.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class _FBColorComponentCell; 13 | 14 | @protocol _FBColorComponentCellDelegate 15 | 16 | /** 17 | @abstract Called when the component cell changes value. 18 | @param cell The cell that changed its value. 19 | @param value The new value. 20 | */ 21 | - (void)colorComponentCell:(_FBColorComponentCell *)cell didChangeValue:(CGFloat)value; 22 | 23 | @end 24 | 25 | /** 26 | @abstract A cell to edit a color component. 27 | */ 28 | @interface _FBColorComponentCell : UITableViewCell 29 | 30 | //! @abstract The title shown in the cell. 31 | @property(nonatomic, copy) NSString *title; 32 | 33 | //! @abstract The current value. The default value is 0.0. 34 | @property(nonatomic, assign) CGFloat value; 35 | 36 | //! @abstract The minimum value. The default value is 0.0. 37 | @property(nonatomic, assign) CGFloat minimumValue; 38 | 39 | //! @abstract The maximum value. The default value is 255.0. 40 | @property(nonatomic, assign) CGFloat maximumValue; 41 | 42 | //! @abstract The format string to apply for textfield value. `%.f` by default. 43 | @property(nonatomic, copy) NSString *format; 44 | 45 | //! @abstract The array of CGColorRef objects defining the color of each gradient stop on the track. 46 | @property(nonatomic, copy) NSArray *colors; 47 | 48 | //! @abstract The cell's delegate. 49 | @property(nonatomic, weak) id<_FBColorComponentCellDelegate> delegate; 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /FBTweak/_FBColorComponentCell.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "_FBColorComponentCell.h" 11 | #import "_FBSliderView.h" 12 | 13 | extern CGFloat const _FBRGBColorComponentMaxValue; 14 | static CGFloat const _FBColorComponentMargin = 5.0f; 15 | static CGFloat const _FBColorComponentTextSpacing = 10.0f; 16 | static CGFloat const _FBColorComponentTextFieldWidth = 50.0f; 17 | 18 | @interface _FBColorComponentCell () 19 | 20 | @end 21 | 22 | @implementation _FBColorComponentCell { 23 | UILabel *_label; 24 | _FBSliderView *_slider; 25 | UITextField *_textField; 26 | } 27 | 28 | - (instancetype)init 29 | { 30 | if (self = [super init]) { 31 | self.selectionStyle = UITableViewCellSelectionStyleNone; 32 | 33 | _format = @"%.f"; 34 | 35 | _label = [[UILabel alloc] init]; 36 | _label.adjustsFontSizeToFitWidth = YES; 37 | [self.contentView addSubview:_label]; 38 | 39 | _slider = [[_FBSliderView alloc] init]; 40 | _slider.maximumValue = _FBRGBColorComponentMaxValue; 41 | [self.contentView addSubview:_slider]; 42 | 43 | _textField = [[UITextField alloc] init]; 44 | _textField.textAlignment = NSTextAlignmentCenter; 45 | [_textField setKeyboardType:UIKeyboardTypeNumbersAndPunctuation]; 46 | [self.contentView addSubview:_textField]; 47 | 48 | [self setValue:0.0f]; 49 | [_slider addTarget:self action:@selector(_didChangeSliderValue:) forControlEvents:UIControlEventValueChanged]; 50 | [_textField setDelegate:self]; 51 | } 52 | 53 | return self; 54 | } 55 | 56 | - (void)setTitle:(NSString *)title 57 | { 58 | _label.text = title; 59 | } 60 | 61 | - (void)setMinimumValue:(CGFloat)minimumValue 62 | { 63 | _slider.minimumValue = minimumValue; 64 | } 65 | 66 | - (void)setMaximumValue:(CGFloat)maximumValue 67 | { 68 | _slider.maximumValue = maximumValue; 69 | } 70 | 71 | - (void)setValue:(CGFloat)value 72 | { 73 | _slider.value = value; 74 | _textField.text = [NSString stringWithFormat:_format, value]; 75 | } 76 | 77 | - (NSString *)title 78 | { 79 | return _label.text; 80 | } 81 | 82 | - (CGFloat)minimumValue 83 | { 84 | return _slider.minimumValue; 85 | } 86 | 87 | - (CGFloat)maximumValue 88 | { 89 | return _slider.maximumValue; 90 | } 91 | 92 | - (CGFloat)value 93 | { 94 | return _slider.value; 95 | } 96 | 97 | - (void)setColors:(NSArray *)colors 98 | { 99 | _slider.colors = colors; 100 | } 101 | 102 | - (NSArray *)colors 103 | { 104 | return _slider.colors; 105 | } 106 | 107 | #pragma mark - UITextFieldDelegate methods 108 | 109 | - (void)textFieldDidEndEditing:(UITextField *)textField 110 | { 111 | [self setValue:[textField.text floatValue]]; 112 | [self.delegate colorComponentCell:self didChangeValue:self.value]; 113 | } 114 | 115 | - (BOOL)textFieldShouldReturn:(UITextField *)textField 116 | { 117 | [textField resignFirstResponder]; 118 | return YES; 119 | } 120 | 121 | - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string 122 | { 123 | NSString *newString = [textField.text stringByReplacingCharactersInRange:range withString:string]; 124 | 125 | //first, check if the new string is numeric only. If not, return NO; 126 | if ([newString rangeOfCharacterFromSet:[self _invertedDecimalDigitAndDotCharacterSet]].location != NSNotFound) { 127 | return NO; 128 | } 129 | 130 | return [newString floatValue] <= _slider.maximumValue; 131 | } 132 | 133 | -(void)layoutSubviews 134 | { 135 | [super layoutSubviews]; 136 | 137 | CGFloat cellHeight = CGRectGetHeight(self.bounds); 138 | CGFloat cellWidth = CGRectGetWidth(self.bounds); 139 | CGFloat labelWidth = [_label sizeThatFits:CGSizeZero].width; 140 | _label.frame = CGRectIntegral((CGRect){_FBColorComponentMargin, _FBColorComponentMargin, labelWidth, cellHeight - 2 * _FBColorComponentMargin}); 141 | _textField.frame = CGRectIntegral((CGRect){cellWidth - _FBColorComponentMargin - _FBColorComponentTextFieldWidth, _FBColorComponentMargin, _FBColorComponentTextFieldWidth, cellHeight - 2 * _FBColorComponentMargin}); 142 | CGFloat sliderWidth = CGRectGetMinX(_textField.frame) - CGRectGetMaxX(_label.frame) - 2 * _FBColorComponentTextSpacing; 143 | _slider.frame = CGRectIntegral((CGRect){CGRectGetMaxX(_label.frame) + _FBColorComponentTextSpacing, _FBColorComponentMargin, sliderWidth, cellHeight - 2 * _FBColorComponentMargin}); 144 | } 145 | 146 | #pragma mark - Private methods 147 | 148 | - (void)_didChangeSliderValue:(_FBSliderView*)sender 149 | { 150 | [self setValue:sender.value]; 151 | [self.delegate colorComponentCell:self didChangeValue:self.value]; 152 | } 153 | 154 | - (NSCharacterSet*)_invertedDecimalDigitAndDotCharacterSet 155 | { 156 | NSMutableCharacterSet *characterSet = [NSMutableCharacterSet decimalDigitCharacterSet]; 157 | [characterSet addCharactersInString:@"."]; 158 | return [[characterSet invertedSet] copy]; 159 | } 160 | 161 | @end 162 | -------------------------------------------------------------------------------- /FBTweak/_FBColorUtils.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | typedef struct { CGFloat red, green, blue, alpha; } RGB; 13 | typedef struct { CGFloat hue, saturation, brightness, alpha; } HSB; 14 | 15 | extern CGFloat const _FBRGBColorComponentMaxValue; 16 | extern CGFloat const _FBAlphaComponentMaxValue; 17 | extern CGFloat const _FBHSBColorComponentMaxValue; 18 | extern NSUInteger const _FBRGBAColorComponentsSize; 19 | extern NSUInteger const _FBHSBAColorComponentsSize; 20 | 21 | typedef NS_ENUM(NSUInteger, _FBRGBColorComponent) { 22 | _FBRGBColorComponentRed, 23 | _FBRGBColorComponentGreed, 24 | _FBRGBColorComponentBlue, 25 | _FBRGBColorComponentAlpha, 26 | }; 27 | 28 | typedef NS_ENUM(NSUInteger, _FBHSBColorComponent) { 29 | _FBHSBColorComponentHue, 30 | _FBHSBColorComponentSaturation, 31 | _FBHSBColorComponentBrightness, 32 | _FBHSBColorComponentAlpha, 33 | }; 34 | 35 | /** 36 | @abstract Converts an RGB color value to HSV. 37 | @discussion Assumes r, g, and b are contained in the set 38 | [0, 1] and returns h, s, and b in the set [0, 1]. 39 | @param rgb The rgb color values 40 | @return The hsb color values 41 | */ 42 | extern HSB _FBRGB2HSB(RGB rgb); 43 | 44 | /** 45 | @abstract Converts an HSB color value to RGB. 46 | @discussion Assumes h, s, and b are contained in the set 47 | [0, 1] and returns r, g, and b in the set [0, 255]. 48 | @param hsb The hsb color values 49 | @return The rgb color values 50 | */ 51 | extern RGB _FBHSB2RGB(HSB hsb); 52 | 53 | /** 54 | @abstract Returns the rgb values of the color components. 55 | @param color The color value. 56 | @return The values of the color components (including alpha). 57 | */ 58 | extern RGB _FBRGBColorComponents(UIColor *color); 59 | 60 | /** 61 | @abstract Returns the color wheel's hue value according to the position, color wheel's center and radius. 62 | @param position The position in the color wheel. 63 | @param center The color wheel's center. 64 | @param radius The color wheel's radius. 65 | @return The hue value. 66 | */ 67 | extern CGFloat _FBGetColorWheelHue(CGPoint position, CGPoint center, CGFloat radius); 68 | 69 | /** 70 | @abstract Returns the color wheel's saturation value according to the position, color wheel's center and radius. 71 | @param position The position in the color wheel. 72 | @param center The color wheel's center. 73 | @param radius The color wheel's radius. 74 | @return The saturation value. 75 | */ 76 | extern CGFloat _FBGetColorWheelSaturation(CGPoint position, CGPoint center, CGFloat radius); 77 | 78 | /** 79 | @abstract Creates the color wheel with specified diameter. 80 | @param diameter The color wheel's diameter. 81 | @return The color wheel image. 82 | */ 83 | extern CGImageRef _FBCreateColorWheelImage(CGFloat diameter); 84 | -------------------------------------------------------------------------------- /FBTweak/_FBColorUtils.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "_FBColorUtils.h" 11 | 12 | CGFloat const _FBRGBColorComponentMaxValue = 255.0f; 13 | CGFloat const _FBAlphaComponentMaxValue = 100.0f; 14 | CGFloat const _FBHSBColorComponentMaxValue = 1.0f; 15 | NSUInteger const _FBRGBAColorComponentsSize = 4; 16 | NSUInteger const _FBHSBAColorComponentsSize = 4; 17 | 18 | extern HSB _FBRGB2HSB(RGB rgb) 19 | { 20 | double rd = (double) rgb.red; 21 | double gd = (double) rgb.green; 22 | double bd = (double) rgb.blue; 23 | double max = fmax(rd, fmax(gd, bd)); 24 | double min = fmin(rd, fmin(gd, bd)); 25 | double h = 0, s, b = max; 26 | 27 | double d = max - min; 28 | s = max == 0 ? 0 : d / max; 29 | 30 | if (max == min) { 31 | h = 0; // achromatic 32 | } else { 33 | if (max == rd) { 34 | h = (gd - bd) / d + (gd < bd ? 6 : 0); 35 | } else if (max == gd) { 36 | h = (bd - rd) / d + 2; 37 | } else if (max == bd) { 38 | h = (rd - gd) / d + 4; 39 | } 40 | h /= 6; 41 | } 42 | 43 | return (HSB){ .hue = h, .saturation = s, .brightness = b, .alpha = rgb.alpha }; 44 | } 45 | 46 | extern RGB _FBHSB2RGB(HSB hsb) 47 | { 48 | double r, g, b; 49 | 50 | int i = hsb.hue * 6; 51 | double f = hsb.hue * 6 - i; 52 | double p = hsb.brightness * (1 - hsb.saturation); 53 | double q = hsb.brightness * (1 - f * hsb.saturation); 54 | double t = hsb.brightness * (1 - (1 - f) * hsb.saturation); 55 | 56 | switch (i % 6){ 57 | case 0: r = hsb.brightness, g = t, b = p; break; 58 | case 1: r = q, g = hsb.brightness, b = p; break; 59 | case 2: r = p, g = hsb.brightness, b = t; break; 60 | case 3: r = p, g = q, b = hsb.brightness; break; 61 | case 4: r = t, g = p, b = hsb.brightness; break; 62 | case 5: r = hsb.brightness, g = p, b = q; break; 63 | default: r = g = b = 0; break; 64 | } 65 | 66 | return (RGB){ .red = r, .green = g, .blue = b, .alpha = hsb.alpha }; 67 | } 68 | 69 | extern RGB _FBRGBColorComponents(UIColor *color) 70 | { 71 | RGB result; 72 | CGColorSpaceModel colorSpaceModel = CGColorSpaceGetModel(CGColorGetColorSpace(color.CGColor)); 73 | if (colorSpaceModel != kCGColorSpaceModelRGB && colorSpaceModel != kCGColorSpaceModelMonochrome) { 74 | return result; 75 | } 76 | const CGFloat *components = CGColorGetComponents(color.CGColor); 77 | if (colorSpaceModel == kCGColorSpaceModelMonochrome) { 78 | result.red = result.green = result.blue = components[0]; 79 | result.alpha = components[1]; 80 | } else { 81 | result.red = components[0]; 82 | result.green = components[1]; 83 | result.blue = components[2]; 84 | result.alpha = components[3]; 85 | } 86 | return result; 87 | } 88 | 89 | extern CGFloat _FBGetColorWheelHue(CGPoint position, CGPoint center, CGFloat radius) 90 | { 91 | CGFloat dx = (CGFloat)(position.x - center.x) / radius; 92 | CGFloat dy = (CGFloat)(position.y - center.y) / radius; 93 | CGFloat d = sqrtf(dx*dx + dy*dy); 94 | CGFloat hue = 0; 95 | if (d != 0) { 96 | hue = acosf(dx / d) / M_PI / 2.0f; 97 | if (dy < 0) { 98 | hue = 1.0 - hue; 99 | } 100 | } 101 | return hue; 102 | } 103 | 104 | extern CGFloat _FBGetColorWheelSaturation(CGPoint position, CGPoint center, CGFloat radius) 105 | { 106 | CGFloat dx = (CGFloat)(position.x - center.x) / radius; 107 | CGFloat dy = (CGFloat)(position.y - center.y) / radius; 108 | return sqrtf(dx*dx + dy*dy); 109 | } 110 | 111 | extern CGImageRef _FBCreateColorWheelImage(CGFloat diameter) 112 | { 113 | CFMutableDataRef bitmapData = CFDataCreateMutable(NULL, 0); 114 | CFDataSetLength(bitmapData, diameter * diameter * 4); 115 | UInt8 * bitmap = CFDataGetMutableBytePtr(bitmapData); 116 | for (int y = 0; y < diameter; y++) { 117 | for (int x = 0; x < diameter; x++) { 118 | CGFloat hue = _FBGetColorWheelHue(CGPointMake(x, y), (CGPoint){diameter / 2, diameter / 2}, diameter / 2); 119 | CGFloat saturation = _FBGetColorWheelSaturation(CGPointMake(x, y), (CGPoint){diameter / 2, diameter / 2}, diameter / 2); 120 | CGFloat a = 0.0f; 121 | RGB rgb = {0.0f, 0.0f, 0.0f, 0.0f}; 122 | if (saturation < 1.0) { 123 | // Antialias the edge of the circle. 124 | if (saturation > 0.99) a = (1.0 - saturation) * 100; 125 | else a = 1.0; 126 | HSB hsb = {hue, saturation, 1.0f, a}; 127 | rgb = _FBHSB2RGB(hsb); 128 | } 129 | 130 | int i = 4 * (x + y * diameter); 131 | bitmap[i] = rgb.red * 0xff; 132 | bitmap[i+1] = rgb.green * 0xff; 133 | bitmap[i+2] = rgb.blue * 0xff; 134 | bitmap[i+3] = rgb.alpha * 0xff; 135 | } 136 | } 137 | 138 | CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData(bitmapData); 139 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 140 | CGImageRef imageRef = CGImageCreate(diameter, diameter, 8, 32, diameter * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaLast, dataProvider, NULL, 0, kCGRenderingIntentDefault); 141 | CGDataProviderRelease(dataProvider); 142 | CGColorSpaceRelease(colorSpace); 143 | CFRelease(bitmapData); 144 | return imageRef; 145 | } 146 | -------------------------------------------------------------------------------- /FBTweak/_FBColorWheelCell.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class _FBColorWheelCell; 13 | 14 | @protocol _FBColorWheelCellDelegate 15 | 16 | /** 17 | @abstract Called when the color wheel changes the selected color. 18 | @discussion The cell will already have been updated with the new color. 19 | */ 20 | - (void)colorWheelCellDidChangeColor:(_FBColorWheelCell *)cell; 21 | 22 | @end 23 | 24 | /** 25 | @abstract A cell to edit the hue and saturation color components. 26 | */ 27 | @interface _FBColorWheelCell : UITableViewCell 28 | 29 | //! @abstract The hue shown in the cell. 30 | @property (nonatomic, readwrite) CGFloat hue; 31 | 32 | //! @abstract The saturation shown in the cell. 33 | @property (nonatomic, readwrite) CGFloat saturation; 34 | 35 | //! @abstract The cell's delegate. 36 | @property(nonatomic, weak) id<_FBColorWheelCellDelegate> delegate; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /FBTweak/_FBColorWheelCell.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "_FBColorWheelCell.h" 11 | #import "_FBColorUtils.h" 12 | 13 | static CGFloat const _FBColorWheelDiameter = 200.0; 14 | static CGFloat const _FBColorWheelIndicatorDiameter = 33.0; 15 | 16 | @interface _FBColorWheelCell () 17 | 18 | @end 19 | 20 | @implementation _FBColorWheelCell { 21 | CALayer *_colorWheelLayer; 22 | CALayer *_indicatorLayer; 23 | CGFloat _hue; 24 | CGFloat _saturation; 25 | } 26 | 27 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 28 | { 29 | if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { 30 | _colorWheelLayer = [CALayer layer]; 31 | _colorWheelLayer.anchorPoint = (CGPoint){0.5, 0.5}; 32 | _colorWheelLayer.bounds = (CGRect){0, 0, _FBColorWheelDiameter, _FBColorWheelDiameter}; 33 | _colorWheelLayer.contents = (__bridge_transfer id)_FBCreateColorWheelImage(_FBColorWheelDiameter); 34 | [self.layer addSublayer:_colorWheelLayer]; 35 | _indicatorLayer = [self _createIndicatorLayer]; 36 | [self.layer addSublayer:_indicatorLayer]; 37 | 38 | UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(_handlePanGesture:)]; 39 | panGestureRecognizer.minimumNumberOfTouches = 1; 40 | panGestureRecognizer.maximumNumberOfTouches = 1; 41 | panGestureRecognizer.delegate = self; 42 | [self addGestureRecognizer:panGestureRecognizer]; 43 | [self setSelectionStyle:UITableViewCellSelectionStyleNone]; 44 | } 45 | return self; 46 | } 47 | 48 | - (void)setHue:(CGFloat)hue 49 | { 50 | _hue = hue; 51 | [self _setSelectedPoint:[self _selectedPoint]]; 52 | } 53 | 54 | - (void)setSaturation:(CGFloat)saturation 55 | { 56 | _saturation = saturation; 57 | [self _setSelectedPoint:[self _selectedPoint]]; 58 | } 59 | 60 | - (void)layoutSubviews 61 | { 62 | [super layoutSubviews]; 63 | 64 | _colorWheelLayer.position = (CGPoint){CGRectGetMidX(self.contentView.bounds), CGRectGetMidY(self.contentView.bounds)}; 65 | [self _setSelectedPoint:[self _selectedPoint]]; 66 | } 67 | 68 | #pragma mark - UIGestureRecognizerDelegate 69 | 70 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch 71 | { 72 | CGPoint position = [touch locationInView:self]; 73 | return [self _shouldHandleTouchAtPosition:position]; 74 | } 75 | 76 | #pragma mark - Private methods 77 | 78 | - (CALayer *)_createIndicatorLayer 79 | { 80 | UIColor *edgeColor = [UIColor colorWithWhite:0.9 alpha:0.8]; 81 | CALayer *indicatorLayer = [CALayer layer]; 82 | indicatorLayer.cornerRadius = _FBColorWheelIndicatorDiameter / 2; 83 | indicatorLayer.borderColor = edgeColor.CGColor; 84 | indicatorLayer.borderWidth = 2; 85 | indicatorLayer.backgroundColor = [UIColor whiteColor].CGColor; 86 | indicatorLayer.bounds = CGRectMake(0, 0, _FBColorWheelIndicatorDiameter, _FBColorWheelIndicatorDiameter); 87 | indicatorLayer.position = CGPointMake(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) / 2); 88 | indicatorLayer.shadowColor = [UIColor blackColor].CGColor; 89 | indicatorLayer.shadowOffset = CGSizeZero; 90 | indicatorLayer.shadowRadius = 1; 91 | indicatorLayer.shadowOpacity = 0.5f; 92 | return indicatorLayer; 93 | } 94 | 95 | - (BOOL)_shouldHandleTouchAtPosition:(CGPoint)position 96 | { 97 | CGFloat radius = _FBColorWheelDiameter / 2; 98 | CGPoint center = self.contentView.center; 99 | CGFloat dist = sqrtf((center.x - position.x) * (center.x - position.x) + (center.y - position.y) * (center.y - position.y)); 100 | return dist <= radius; 101 | } 102 | 103 | - (void)_handlePanGesture:(UIPanGestureRecognizer *)panGestureRecognizer 104 | { 105 | CGPoint position = [panGestureRecognizer locationInView:self]; 106 | if (![self _shouldHandleTouchAtPosition:position]) { 107 | return; 108 | } 109 | 110 | CGFloat radius = _FBColorWheelDiameter / 2; 111 | CGPoint center = self.contentView.center; 112 | _hue = _FBGetColorWheelHue(position, center, radius); 113 | _saturation = _FBGetColorWheelSaturation(position, center, radius); 114 | [self _setSelectedPoint:position]; 115 | [_delegate colorWheelCellDidChangeColor:self]; 116 | } 117 | 118 | - (void)_setSelectedPoint:(CGPoint)point 119 | { 120 | UIColor *selectedColor = [UIColor colorWithHue:_hue saturation:_saturation brightness:1.0f alpha:1.0f]; 121 | [CATransaction begin]; 122 | [CATransaction setDisableActions:YES]; 123 | _indicatorLayer.position = point; 124 | _indicatorLayer.backgroundColor = selectedColor.CGColor; 125 | [CATransaction commit]; 126 | } 127 | 128 | - (CGPoint)_selectedPoint 129 | { 130 | CGFloat radius = _saturation * _FBColorWheelDiameter / 2; 131 | CGFloat x = _FBColorWheelDiameter / 2 + radius * cosf(_hue * M_PI * 2.0f) + CGRectGetMinX(_colorWheelLayer.frame); 132 | CGFloat y = _FBColorWheelDiameter / 2 + radius * sinf(_hue * M_PI * 2.0f) + CGRectGetMinY(_colorWheelLayer.frame); 133 | return CGPointMake(x, y); 134 | } 135 | 136 | @end 137 | -------------------------------------------------------------------------------- /FBTweak/_FBKeyboardManager.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | /** 13 | @abstract Keyboard manager. 14 | @discussion Adjusts the content so that the target object remains visible. 15 | */ 16 | @interface _FBKeyboardManager : NSObject 17 | 18 | /** 19 | @abstract Creates a keyboard manager. 20 | @discussion This is the designated initializer. 21 | @param scrollView The that contains the content to adjust. 22 | */ 23 | - (instancetype)initWithViewScrollView:(UIScrollView *)scrollView; 24 | 25 | /** 26 | @abstract Enables the keyboard manager. The manager is enabled by default. 27 | */ 28 | - (void)enable; 29 | 30 | /** 31 | @abstract Disables the keyboard manager. 32 | */ 33 | - (void)disable; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /FBTweak/_FBKeyboardManager.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "_FBKeyboardManager.h" 11 | 12 | @implementation _FBKeyboardManager { 13 | __weak UIScrollView *_scrollView; 14 | } 15 | 16 | - (instancetype)init 17 | { 18 | return [self initWithViewScrollView:nil]; 19 | } 20 | 21 | - (instancetype)initWithViewScrollView:(UIScrollView *)scrollView 22 | { 23 | if (self = [super init]) { 24 | _scrollView = scrollView; 25 | [self enable]; 26 | } 27 | return self; 28 | } 29 | 30 | - (void)enable 31 | { 32 | [self disable]; 33 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardFrameChanged:) name:UIKeyboardWillChangeFrameNotification object:nil]; 34 | } 35 | 36 | - (void)disable 37 | { 38 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; 39 | } 40 | 41 | - (void)dealloc 42 | { 43 | [self disable]; 44 | } 45 | 46 | #pragma mark - Private methods 47 | 48 | - (void)_keyboardFrameChanged:(NSNotification *)notification 49 | { 50 | UIView *contentView = _scrollView.superview; 51 | 52 | NSDictionary *userInfo = [notification userInfo]; 53 | CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; 54 | endFrame = [contentView.window convertRect:endFrame fromWindow:nil]; 55 | endFrame = [contentView convertRect:endFrame fromView:contentView.window]; 56 | 57 | NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; 58 | UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; 59 | 60 | void (^animations)() = ^{ 61 | UIEdgeInsets contentInset = _scrollView.contentInset; 62 | contentInset.bottom = (contentView.bounds.size.height - CGRectGetMinY(endFrame)); 63 | _scrollView.contentInset = contentInset; 64 | 65 | UIEdgeInsets scrollIndicatorInsets = _scrollView.scrollIndicatorInsets; 66 | scrollIndicatorInsets.bottom = (contentView.bounds.size.height - CGRectGetMinY(endFrame)); 67 | _scrollView.scrollIndicatorInsets = scrollIndicatorInsets; 68 | }; 69 | 70 | UIViewAnimationOptions options = (curve << 16) | UIViewAnimationOptionBeginFromCurrentState; 71 | 72 | [UIView animateWithDuration:duration delay:0 options:options animations:animations completion:NULL]; 73 | } 74 | 75 | @end 76 | -------------------------------------------------------------------------------- /FBTweak/_FBSliderView.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | /** 13 | @abstract A slider with a gradient track. 14 | */ 15 | @interface _FBSliderView : UIControl 16 | 17 | /** 18 | @abstract The slider's current value. The default value is 0.0. 19 | */ 20 | @property(nonatomic, assign) CGFloat value; 21 | 22 | /** 23 | @abstract The minimum value of the slider. The default value is 0.0. 24 | */ 25 | @property(nonatomic, assign) CGFloat minimumValue; 26 | 27 | /** 28 | @abstract The maximum value of the slider. The default value is 1.0. 29 | */ 30 | @property(nonatomic, assign) CGFloat maximumValue; 31 | 32 | /** 33 | @abstract The array of CGColorRef objects defining the color of each gradient stop on the track. 34 | @discussion The location of each gradient stop is evaluated with formula: i * width_of_the_track / number_of_colors. 35 | */ 36 | @property(nonatomic, copy) NSArray *colors; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /FBTweak/_FBSliderView.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "_FBSliderView.h" 11 | 12 | static const CGFloat _FBSliderViewHeight = 28.0f; 13 | static const CGFloat _FBSliderViewThumbDimension = 28.0f; 14 | static const CGFloat _FBSliderViewTrackHeight = 3.0f; 15 | 16 | @implementation _FBSliderView { 17 | CALayer *_thumbLayer; 18 | CAGradientLayer *_trackLayer; 19 | } 20 | 21 | - (instancetype)initWithFrame:(CGRect)frame 22 | { 23 | if (self = [super initWithFrame:frame]) { 24 | _minimumValue = 0.0f; 25 | _maximumValue = 1.0f; 26 | _value = 0.0f; 27 | 28 | self.layer.delegate = self; 29 | 30 | _trackLayer = [CAGradientLayer layer]; 31 | _trackLayer.cornerRadius = _FBSliderViewTrackHeight / 2.0f; 32 | _trackLayer.startPoint = CGPointMake(0.0f, 0.5f); 33 | _trackLayer.endPoint = CGPointMake(1.0f, 0.5f); 34 | [self.layer addSublayer:_trackLayer]; 35 | 36 | _thumbLayer = [CALayer layer]; 37 | _thumbLayer.cornerRadius = _FBSliderViewHeight / 2; 38 | _thumbLayer.backgroundColor = [UIColor whiteColor].CGColor; 39 | _thumbLayer.shadowColor = [UIColor blackColor].CGColor; 40 | _thumbLayer.shadowOffset = CGSizeZero; 41 | _thumbLayer.shadowRadius = 2; 42 | _thumbLayer.shadowOpacity = 0.5f; 43 | [self.layer addSublayer:_thumbLayer]; 44 | 45 | __attribute__((objc_precise_lifetime)) id color = (__bridge id)[UIColor blueColor].CGColor; 46 | [self setColors:@[color, color]]; 47 | } 48 | return self; 49 | } 50 | 51 | - (CGSize)intrinsicContentSize 52 | { 53 | return CGSizeMake(UIViewNoIntrinsicMetric, _FBSliderViewHeight); 54 | } 55 | 56 | - (void)setValue:(CGFloat)value 57 | { 58 | if (value < _minimumValue) { 59 | _value = _minimumValue; 60 | } else if (value > _maximumValue) { 61 | _value = _maximumValue; 62 | } else { 63 | _value = value; 64 | } 65 | [self _updateThumbPositionWithValue:_value]; 66 | } 67 | 68 | - (void)setColors:(NSArray*)colors 69 | { 70 | _trackLayer.colors = colors; 71 | [self _updateLocations]; 72 | } 73 | 74 | - (NSArray*)colors 75 | { 76 | return _trackLayer.colors; 77 | } 78 | 79 | #pragma mark - UIControl touch tracking events 80 | 81 | - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event 82 | { 83 | CGPoint touchPoint = [touch locationInView:self]; 84 | if (CGRectContainsPoint(CGRectInset(_thumbLayer.frame, -10.0, -10.0), touchPoint)) { 85 | [self _setValueWithPosition:touchPoint.x]; 86 | return YES; 87 | } 88 | return NO; 89 | } 90 | 91 | - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event 92 | { 93 | CGPoint touchPoint = [touch locationInView:self]; 94 | [self _setValueWithPosition:touchPoint.x]; 95 | return YES; 96 | } 97 | 98 | - (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event 99 | { 100 | CGPoint touchPoint = [touch locationInView:self]; 101 | [self _setValueWithPosition:touchPoint.x]; 102 | } 103 | 104 | - (void)layoutSublayersOfLayer:(CALayer *)layer 105 | { 106 | if (layer == self.layer) { 107 | CGFloat height = _FBSliderViewHeight; 108 | CGFloat width = CGRectGetWidth(self.bounds); 109 | _trackLayer.bounds = CGRectMake(0, 0, width , _FBSliderViewTrackHeight); 110 | _trackLayer.position = CGPointMake(CGRectGetWidth(self.bounds) / 2, height / 2); 111 | _thumbLayer.bounds = CGRectMake(0, 0, _FBSliderViewThumbDimension, _FBSliderViewThumbDimension); 112 | [self _updateThumbPositionWithValue:_value]; 113 | } 114 | } 115 | 116 | #pragma mark - Private methods 117 | 118 | - (void)_setValueWithPosition:(CGFloat)position 119 | { 120 | CGFloat width = CGRectGetWidth(self.bounds); 121 | if (position < 0) { 122 | position = 0; 123 | } else if (position > width) { 124 | position = width; 125 | } 126 | CGFloat percentage = position / width; 127 | CGFloat value = _minimumValue + percentage * (_maximumValue - _minimumValue); 128 | [self setValue:value]; 129 | [self sendActionsForControlEvents:UIControlEventValueChanged]; 130 | } 131 | 132 | - (void)_updateLocations 133 | { 134 | NSUInteger size = [_trackLayer.colors count]; 135 | if (size == [_trackLayer.locations count]) { 136 | return; 137 | } 138 | CGFloat step = 1.0f / (size - 1); 139 | NSMutableArray *locations = [NSMutableArray array]; 140 | [locations addObject:@(0.0f)]; 141 | for (NSUInteger i = 1; i < size - 1; ++i) { 142 | [locations addObject:@(i * step)]; 143 | } 144 | [locations addObject:@(1.0f)]; 145 | _trackLayer.locations = [locations copy]; 146 | } 147 | 148 | - (void)_updateThumbPositionWithValue:(CGFloat)value 149 | { 150 | CGFloat width = CGRectGetWidth(self.bounds); 151 | if (width == 0) { 152 | return; 153 | } 154 | 155 | CGFloat percentage = (_value - _minimumValue) / (_maximumValue - _minimumValue); 156 | CGFloat position = width * percentage; 157 | 158 | [CATransaction begin]; 159 | [CATransaction setDisableActions:YES]; 160 | _thumbLayer.position = CGPointMake(position - ((position - width / 2) / (width / 2)) * _FBSliderViewThumbDimension / 2, _FBSliderViewHeight / 2); 161 | [CATransaction commit]; 162 | } 163 | 164 | @end 165 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakArrayViewController.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class FBTweak; 13 | 14 | /** 15 | @abstract Displays list of values in an array tweak. 16 | */ 17 | @interface _FBTweakArrayViewController : UIViewController 18 | 19 | /** 20 | @abstract Creates a tweak array view controller. 21 | @discussion This is the designated initializer. 22 | @param tweak The tweak the view controller is for. 23 | Must not be nil, and must have an array of possibleValues. 24 | */ 25 | - (instancetype)initWithTweak:(FBTweak *)tweak; 26 | 27 | /** 28 | @abstract The array tweak to display in the view controller. 29 | */ 30 | @property (nonatomic, strong, readonly) FBTweak *tweak; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakArrayViewController.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "_FBTweakArrayViewController.h" 11 | #import "FBTweak.h" 12 | 13 | @interface _FBTweakArrayViewController () 14 | 15 | @end 16 | 17 | @implementation _FBTweakArrayViewController { 18 | UITableView *_tableView; 19 | } 20 | 21 | - (instancetype)initWithTweak:(FBTweak *)tweak 22 | { 23 | NSParameterAssert(tweak != nil); 24 | NSParameterAssert([tweak.possibleValues isKindOfClass:[NSArray class]]); 25 | 26 | if ((self = [super init])) { 27 | _tweak = tweak; 28 | self.title = _tweak.name; 29 | } 30 | 31 | return self; 32 | } 33 | 34 | - (void)viewDidLoad 35 | { 36 | [super viewDidLoad]; 37 | 38 | _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped]; 39 | _tableView.delegate = self; 40 | _tableView.dataSource = self; 41 | _tableView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); 42 | [self.view addSubview:_tableView]; 43 | } 44 | 45 | - (void)dealloc 46 | { 47 | _tableView.delegate = nil; 48 | _tableView.dataSource = nil; 49 | } 50 | 51 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 52 | { 53 | return 1; 54 | } 55 | 56 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 57 | { 58 | return [self.tweak.possibleValues count]; 59 | } 60 | 61 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 62 | { 63 | static NSString *_FBTweakDictionaryViewControllerCellIdentifier = @"_FBTweakDictionaryViewControllerCellIdentifier"; 64 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:_FBTweakDictionaryViewControllerCellIdentifier]; 65 | if (cell == nil) { 66 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:_FBTweakDictionaryViewControllerCellIdentifier]; 67 | } 68 | 69 | FBTweakValue rowValue = self.tweak.possibleValues[indexPath.row]; 70 | NSString *stringValue = [rowValue description]; 71 | cell.textLabel.text = stringValue; 72 | 73 | cell.accessoryType = UITableViewCellAccessoryNone; 74 | FBTweakValue selectedValue = (self.tweak.currentValue ?: self.tweak.defaultValue); 75 | if ([selectedValue isEqual:rowValue]) { 76 | cell.accessoryType = UITableViewCellAccessoryCheckmark; 77 | } 78 | 79 | return cell; 80 | } 81 | 82 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 83 | { 84 | NSString *value = self.tweak.possibleValues[indexPath.row]; 85 | self.tweak.currentValue = value; 86 | [self.navigationController popViewControllerAnimated:YES]; 87 | } 88 | 89 | @end 90 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakBindObserver.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class FBTweak; 13 | 14 | /** 15 | @abstract Block to call when an update is observed. 16 | @param object The object that the observer is attached to. 17 | */ 18 | typedef void (^_FBTweakBindObserverBlock)(id object); 19 | 20 | /** 21 | @abstract Observes a tweak to issue bind updates. 22 | @discussion This is an implementation detail of {@ref FBTweakBind}. 23 | */ 24 | @interface _FBTweakBindObserver : NSObject 25 | 26 | /** 27 | @abstract Designated initializer. 28 | @param tweak The tweak to observe. 29 | @param block The block to call on change. 30 | @return A new bind observer. 31 | */ 32 | - (instancetype)initWithTweak:(FBTweak *)tweak block:(_FBTweakBindObserverBlock)block; 33 | 34 | /** 35 | @abstract Attaches to an object and deallocates with it. 36 | @discussion Useful to create a limited lifetime for the observer. 37 | */ 38 | - (void)attachToObject:(id)object; 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakBindObserver.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | #import "FBTweak.h" 13 | #import "_FBTweakBindObserver.h" 14 | 15 | @interface _FBTweakBindObserver () 16 | @end 17 | 18 | @implementation _FBTweakBindObserver { 19 | FBTweak *_tweak; 20 | _FBTweakBindObserverBlock _block; 21 | __weak id _object; 22 | } 23 | 24 | - (instancetype)initWithTweak:(FBTweak *)tweak block:(_FBTweakBindObserverBlock)block 25 | { 26 | if ((self = [super init])) { 27 | NSAssert(tweak != nil, @"tweak is required"); 28 | NSAssert(block != NULL, @"block is required"); 29 | 30 | _tweak = tweak; 31 | _block = block; 32 | 33 | [tweak addObserver:self]; 34 | } 35 | 36 | return self; 37 | } 38 | 39 | - (void)tweakDidChange:(FBTweak *)tweak 40 | { 41 | __attribute__((objc_precise_lifetime)) id strongObject = _object; 42 | 43 | if (strongObject != nil) { 44 | _block(strongObject); 45 | } 46 | } 47 | 48 | - (void)attachToObject:(id)object 49 | { 50 | NSAssert(_object == nil, @"can only attach to an object once"); 51 | NSAssert(object != nil, @"object is required"); 52 | 53 | _object = object; 54 | objc_setAssociatedObject(object, (__bridge void *)self, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 55 | } 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakCategoryViewController.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class FBTweakStore; 13 | @class FBTweakCategory; 14 | @protocol _FBTweakCategoryViewControllerDelegate; 15 | 16 | /** 17 | @abstract Displays a list of items and allows selection. 18 | */ 19 | @interface _FBTweakCategoryViewController : UIViewController 20 | 21 | /** 22 | @abstract Creates a tweak category view controller. 23 | @param store The store with the categories to show. 24 | @discussion This is the designated initializer. 25 | */ 26 | - (instancetype)initWithStore:(FBTweakStore *)store; 27 | 28 | /** 29 | @abstract The tweak store shown. 30 | */ 31 | @property (nonatomic, strong, readonly) FBTweakStore *store; 32 | 33 | /** 34 | @abstract Responds to actions from the items list. 35 | */ 36 | @property (nonatomic, weak, readwrite) id<_FBTweakCategoryViewControllerDelegate> delegate; 37 | 38 | @end 39 | 40 | @protocol _FBTweakCategoryViewControllerDelegate 41 | 42 | /** 43 | @abstract Called when a category is selected. 44 | @param viewController The view controller with the selected category. 45 | @param category The category that was selected. 46 | */ 47 | - (void)tweakCategoryViewController:(_FBTweakCategoryViewController *)viewController selectedCategory:(FBTweakCategory *)category; 48 | 49 | /** 50 | @abstract Called when done is selected. 51 | @param viewController The view controller that selected done. 52 | */ 53 | - (void)tweakCategoryViewControllerSelectedDone:(_FBTweakCategoryViewController *)viewController; 54 | 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakCategoryViewController.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweakStore.h" 11 | #import "FBTweakCategory.h" 12 | #import "_FBTweakCategoryViewController.h" 13 | #import 14 | 15 | @interface _FBTweakCategoryViewController () 16 | @end 17 | 18 | #if (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE8_0) && (!defined(__has_feature) || !__has_feature(attribute_availability_app_extension)) 19 | @interface _FBTweakCategoryViewController () 20 | @end 21 | #endif 22 | 23 | @implementation _FBTweakCategoryViewController { 24 | UITableView *_tableView; 25 | UIToolbar *_toolbar; 26 | 27 | NSArray *_sortedCategories; 28 | } 29 | 30 | - (instancetype)initWithStore:(FBTweakStore *)store 31 | { 32 | if ((self = [super init])) { 33 | self.title = @"Tweaks"; 34 | 35 | _store = store; 36 | _sortedCategories = [_store.tweakCategories sortedArrayUsingComparator:^(FBTweakCategory *a, FBTweakCategory *b) { 37 | return [a.name localizedStandardCompare:b.name]; 38 | }]; 39 | } 40 | 41 | return self; 42 | } 43 | 44 | - (void)viewDidLoad 45 | { 46 | [super viewDidLoad]; 47 | 48 | _toolbar = [[UIToolbar alloc] init]; 49 | [_toolbar sizeToFit]; 50 | CGRect toolbarFrame = _toolbar.frame; 51 | toolbarFrame.origin.y = CGRectGetMaxY(self.view.bounds) - CGRectGetHeight(toolbarFrame); 52 | _toolbar.frame = toolbarFrame; 53 | _toolbar.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin); 54 | [self.view addSubview:_toolbar]; 55 | 56 | _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain]; 57 | _tableView.delegate = self; 58 | _tableView.dataSource = self; 59 | _tableView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); 60 | [self.view insertSubview:_tableView belowSubview:_toolbar]; 61 | 62 | UIEdgeInsets contentInset = _tableView.contentInset; 63 | UIEdgeInsets scrollIndictatorInsets = _tableView.scrollIndicatorInsets; 64 | contentInset.bottom = CGRectGetHeight(_toolbar.bounds); 65 | scrollIndictatorInsets.bottom = CGRectGetHeight(_toolbar.bounds); 66 | _tableView.contentInset = contentInset; 67 | _tableView.scrollIndicatorInsets = scrollIndictatorInsets; 68 | 69 | self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Reset" style:UIBarButtonItemStylePlain target:self action:@selector(_reset)]; 70 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(_done)]; 71 | 72 | if ([MFMailComposeViewController canSendMail]) { 73 | UIBarButtonItem *exportItem = [[UIBarButtonItem alloc] initWithTitle:@"Export" style:UIBarButtonItemStyleDone target:self action:@selector(_export)]; 74 | UIBarButtonItem *flexibleSpaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; 75 | 76 | _toolbar.items = @[flexibleSpaceItem, exportItem]; 77 | } 78 | } 79 | 80 | - (void)dealloc 81 | { 82 | _tableView.delegate = nil; 83 | _tableView.dataSource = nil; 84 | } 85 | 86 | - (void)_done 87 | { 88 | [_delegate tweakCategoryViewControllerSelectedDone:self]; 89 | } 90 | 91 | - (void)_reset 92 | { 93 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 8000 94 | if ([UIAlertController class] != nil) { 95 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Are you sure?" 96 | message:@"Are you sure you want to reset your tweaks? This cannot be undone." 97 | preferredStyle:UIAlertControllerStyleAlert]; 98 | UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { 99 | // do nothing 100 | }]; 101 | [alertController addAction:cancelAction]; 102 | 103 | UIAlertAction *resetAction = [UIAlertAction actionWithTitle:@"Reset" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 104 | [_store reset]; 105 | }]; 106 | [alertController addAction:resetAction]; 107 | 108 | [self presentViewController:alertController animated:YES completion:NULL]; 109 | } else { 110 | #endif 111 | #if (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE8_0) && (!defined(__has_feature) || !__has_feature(attribute_availability_app_extension)) 112 | // This is iOS 7 or lower. We need to use UIAlertView, because UIAlertController is not available. 113 | // UIAlertView, however, is not available in app-extensions, so to allow compilation, we conditionally compile this branch only when we're not an app-extension. UIAlertController is always available in app-extensions, so this is safe. 114 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Are you sure?" 115 | message:@"Are you sure you want to reset your tweaks? This cannot be undone." 116 | delegate:self 117 | cancelButtonTitle:@"Cancel" 118 | otherButtonTitles:@"Reset", nil]; 119 | [alert show]; 120 | #endif 121 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 8000 122 | } 123 | #endif 124 | } 125 | 126 | - (void)_export 127 | { 128 | NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey]; 129 | NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; 130 | NSString *fileName = [NSString stringWithFormat:@"tweaks_%@_%@.plist", appName, version]; 131 | 132 | NSMutableData *data = [NSMutableData data]; 133 | NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; 134 | archiver.outputFormat = NSPropertyListXMLFormat_v1_0; 135 | [archiver encodeRootObject:_store]; 136 | [archiver finishEncoding]; 137 | 138 | MFMailComposeViewController *mailComposeViewController = [[MFMailComposeViewController alloc] init]; 139 | mailComposeViewController.mailComposeDelegate = self; 140 | mailComposeViewController.subject = [NSString stringWithFormat:@"%@ Tweaks (v%@)", appName, version]; 141 | [mailComposeViewController addAttachmentData:data mimeType:@"plist" fileName:fileName]; 142 | [self presentViewController:mailComposeViewController animated:YES completion:nil]; 143 | } 144 | 145 | - (void)viewWillAppear:(BOOL)animated 146 | { 147 | [super viewWillAppear:animated]; 148 | 149 | [_tableView deselectRowAtIndexPath:_tableView.indexPathForSelectedRow animated:animated]; 150 | } 151 | 152 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 153 | { 154 | return 1; 155 | } 156 | 157 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 158 | { 159 | return _sortedCategories.count; 160 | } 161 | 162 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 163 | { 164 | static NSString *_FBTweakCategoryViewControllerCellIdentifier = @"_FBTweakCategoryViewControllerCellIdentifier"; 165 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:_FBTweakCategoryViewControllerCellIdentifier]; 166 | if (cell == nil) { 167 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:_FBTweakCategoryViewControllerCellIdentifier]; 168 | } 169 | 170 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 171 | 172 | FBTweakCategory *category = _sortedCategories[indexPath.row]; 173 | cell.textLabel.text = category.name; 174 | 175 | return cell; 176 | } 177 | 178 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 179 | { 180 | FBTweakCategory *category = _sortedCategories[indexPath.row]; 181 | [_delegate tweakCategoryViewController:self selectedCategory:category]; 182 | } 183 | 184 | #if (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE8_0) && (!defined(__has_feature) || !__has_feature(attribute_availability_app_extension)) 185 | - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex 186 | { 187 | if (buttonIndex != alertView.cancelButtonIndex) { 188 | [_store reset]; 189 | } 190 | } 191 | #endif 192 | 193 | - (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error 194 | { 195 | [self dismissViewControllerAnimated:YES completion:nil]; 196 | } 197 | 198 | @end 199 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakCollectionViewController.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class FBTweakCategory; 13 | @protocol _FBTweakCollectionViewControllerDelegate; 14 | 15 | /** 16 | @abstract Displays configuration options for tweak collections. 17 | */ 18 | @interface _FBTweakCollectionViewController : UIViewController 19 | 20 | /** 21 | @abstract Create a tweak collection view controller. 22 | @param category The tweak category to show the collections in. 23 | @discussion This is the designated initializer. 24 | */ 25 | - (instancetype)initWithTweakCategory:(FBTweakCategory *)category; 26 | 27 | //! @abstract The tweak category to show the collections in. 28 | @property (nonatomic, strong, readonly) FBTweakCategory *tweakCategory; 29 | 30 | /** 31 | @abstract Responds to actions from the items list. 32 | */ 33 | @property (nonatomic, weak, readwrite) id<_FBTweakCollectionViewControllerDelegate> delegate; 34 | 35 | @end 36 | 37 | @protocol _FBTweakCollectionViewControllerDelegate 38 | 39 | /** 40 | @abstract Called when done is selected. 41 | @param viewController The view controller that selected done. 42 | */ 43 | - (void)tweakCollectionViewControllerSelectedDone:(_FBTweakCollectionViewController *)viewController; 44 | 45 | @end -------------------------------------------------------------------------------- /FBTweak/_FBTweakCollectionViewController.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweakCollection.h" 11 | #import "FBTweakCategory.h" 12 | #import "FBTweak.h" 13 | #import "_FBTweakCollectionViewController.h" 14 | #import "_FBTweakTableViewCell.h" 15 | #import "_FBTweakColorViewController.h" 16 | #import "_FBTweakDictionaryViewController.h" 17 | #import "_FBTweakArrayViewController.h" 18 | #import "_FBKeyboardManager.h" 19 | 20 | @interface _FBTweakCollectionViewController () 21 | @end 22 | 23 | @implementation _FBTweakCollectionViewController { 24 | UITableView *_tableView; 25 | NSArray *_sortedCollections; 26 | _FBKeyboardManager *_keyboardManager; 27 | } 28 | 29 | - (instancetype)initWithTweakCategory:(FBTweakCategory *)category 30 | { 31 | if ((self = [super init])) { 32 | _tweakCategory = category; 33 | self.title = _tweakCategory.name; 34 | [self _reloadData]; 35 | } 36 | 37 | return self; 38 | } 39 | 40 | - (void)viewDidLoad 41 | { 42 | [super viewDidLoad]; 43 | 44 | _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped]; 45 | _tableView.delegate = self; 46 | _tableView.dataSource = self; 47 | _tableView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); 48 | [self.view addSubview:_tableView]; 49 | 50 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(_done)]; 51 | 52 | _keyboardManager = [[_FBKeyboardManager alloc] initWithViewScrollView:_tableView]; 53 | } 54 | 55 | - (void)dealloc 56 | { 57 | _tableView.delegate = nil; 58 | _tableView.dataSource = nil; 59 | } 60 | 61 | - (void)viewWillAppear:(BOOL)animated 62 | { 63 | [super viewWillAppear:animated]; 64 | 65 | [_tableView deselectRowAtIndexPath:_tableView.indexPathForSelectedRow animated:animated]; 66 | [self _reloadData]; 67 | 68 | [_keyboardManager enable]; 69 | } 70 | 71 | - (void)viewWillDisappear:(BOOL)animated 72 | { 73 | [super viewWillDisappear:animated]; 74 | [_keyboardManager disable]; 75 | } 76 | 77 | - (void)_reloadData 78 | { 79 | _sortedCollections = [_tweakCategory.tweakCollections sortedArrayUsingComparator:^(FBTweakCollection *a, FBTweakCollection *b) { 80 | return [a.name localizedStandardCompare:b.name]; 81 | }]; 82 | [_tableView reloadData]; 83 | } 84 | 85 | - (void)_done 86 | { 87 | [_delegate tweakCollectionViewControllerSelectedDone:self]; 88 | } 89 | 90 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 91 | { 92 | return _sortedCollections.count; 93 | } 94 | 95 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 96 | { 97 | FBTweakCollection *collection = _sortedCollections[section]; 98 | return collection.tweaks.count; 99 | } 100 | 101 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section 102 | { 103 | FBTweakCollection *collection = _sortedCollections[section]; 104 | return collection.name; 105 | } 106 | 107 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 108 | { 109 | static NSString *_FBTweakCollectionViewControllerCellIdentifier = @"_FBTweakCollectionViewControllerCellIdentifier"; 110 | _FBTweakTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:_FBTweakCollectionViewControllerCellIdentifier]; 111 | if (cell == nil) { 112 | cell = [[_FBTweakTableViewCell alloc] initWithReuseIdentifier:_FBTweakCollectionViewControllerCellIdentifier]; 113 | } 114 | 115 | FBTweakCollection *collection = _sortedCollections[indexPath.section]; 116 | FBTweak *tweak = collection.tweaks[indexPath.row]; 117 | cell.tweak = tweak; 118 | 119 | return cell; 120 | } 121 | 122 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 123 | { 124 | FBTweakCollection *collection = _sortedCollections[indexPath.section]; 125 | FBTweak *tweak = collection.tweaks[indexPath.row]; 126 | if ([tweak.possibleValues isKindOfClass:[NSDictionary class]]) { 127 | _FBTweakDictionaryViewController *vc = [[_FBTweakDictionaryViewController alloc] initWithTweak:tweak]; 128 | [self.navigationController pushViewController:vc animated:YES]; 129 | } else if ([tweak.possibleValues isKindOfClass:[NSArray class]]) { 130 | _FBTweakArrayViewController *vc = [[_FBTweakArrayViewController alloc] initWithTweak:tweak]; 131 | [self.navigationController pushViewController:vc animated:YES]; 132 | } else if ([tweak.defaultValue isKindOfClass:[UIColor class]]) { 133 | _FBTweakColorViewController *vc = [[_FBTweakColorViewController alloc] initWithTweak:tweak]; 134 | [self.navigationController pushViewController:vc animated:YES]; 135 | } else if (tweak.isAction) { 136 | dispatch_block_t block = tweak.defaultValue; 137 | if (block != NULL) { 138 | block(); 139 | } 140 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 141 | } 142 | } 143 | 144 | @end 145 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakColorViewController.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class FBTweak; 13 | 14 | /** 15 | @abstract Displays a view to edit a tweak with color value. 16 | */ 17 | @interface _FBTweakColorViewController : UIViewController 18 | 19 | /** 20 | @abstract Create a RGB view controller. 21 | @param tweak The tweak with color value to edit. 22 | @discussion This is the designated initializer. 23 | */ 24 | - (instancetype)initWithTweak:(FBTweak *)tweak; 25 | 26 | //! @abstract The color tweak to display in the view controller. 27 | @property (nonatomic, strong, readonly) FBTweak *tweak; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakColorViewController.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "_FBTweakColorViewController.h" 11 | #import "_FBTweakColorViewControllerHSBDataSource.h" 12 | #import "_FBTweakColorViewControllerRGBDataSource.h" 13 | #import "_FBTweakColorViewControllerHexDataSource.h" 14 | #import "_FBKeyboardManager.h" 15 | #import "FBTweak.h" 16 | 17 | typedef NS_ENUM(NSUInteger, FBTweakColorSection) { 18 | FBTweakColorSectionRGB, 19 | FBTweakColorSectionHSB, 20 | FBTweakColorSectionHEX, 21 | }; 22 | 23 | static void *kContext = &kContext; 24 | static CGFloat const _FBTweakColorCellDefaultHeight = 44.0; 25 | static CGFloat const _FBColorWheelCellHeight = 220.0f; 26 | 27 | @interface _FBTweakColorViewController () 28 | 29 | @end 30 | 31 | @implementation _FBTweakColorViewController { 32 | NSObject<_FBTweakColorViewControllerDataSource> *_rgbDataSource; 33 | NSObject<_FBTweakColorViewControllerDataSource> *_hsbDataSource; 34 | NSObject<_FBTweakColorViewControllerDataSource> *_hexDataSource; 35 | FBTweak *_tweak; 36 | _FBKeyboardManager *_keyboardManager; 37 | UITableView *_tableView; 38 | } 39 | 40 | - (instancetype)initWithTweak:(FBTweak *)tweak 41 | { 42 | NSParameterAssert([tweak.defaultValue isKindOfClass:[UIColor class]]); 43 | if (self = [super init]) { 44 | _tweak = tweak; 45 | _rgbDataSource = [[_FBTweakColorViewControllerRGBDataSource alloc] init]; 46 | _hsbDataSource = [[_FBTweakColorViewControllerHSBDataSource alloc] init]; 47 | _hexDataSource = [[_FBTweakColorViewControllerHexDataSource alloc] init]; 48 | [_rgbDataSource addObserver:self forKeyPath:NSStringFromSelector(@selector(value)) options:NSKeyValueObservingOptionNew context:kContext]; 49 | [_hsbDataSource addObserver:self forKeyPath:NSStringFromSelector(@selector(value)) options:NSKeyValueObservingOptionNew context:kContext]; 50 | [_hexDataSource addObserver:self forKeyPath:NSStringFromSelector(@selector(value)) options:NSKeyValueObservingOptionNew context:kContext]; 51 | } 52 | return self; 53 | } 54 | 55 | - (void)dealloc 56 | { 57 | [_rgbDataSource removeObserver:self forKeyPath:NSStringFromSelector(@selector(value))]; 58 | [_hsbDataSource removeObserver:self forKeyPath:NSStringFromSelector(@selector(value))]; 59 | [_hexDataSource removeObserver:self forKeyPath:NSStringFromSelector(@selector(value))]; 60 | } 61 | 62 | - (void)viewDidLoad 63 | { 64 | [super viewDidLoad]; 65 | 66 | _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped]; 67 | _tableView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); 68 | _tableView.delegate = self; 69 | [self.view addSubview:_tableView]; 70 | 71 | _keyboardManager = [[_FBKeyboardManager alloc] initWithViewScrollView:_tableView]; 72 | 73 | UISegmentedControl *segmentedControl = [[UISegmentedControl alloc] initWithItems:@[@"RGB", @"HSB", @"HEX"]]; 74 | [segmentedControl addTarget:self action:@selector(_segmentControlDidChangeValue:) forControlEvents:UIControlEventValueChanged]; 75 | [segmentedControl sizeToFit]; 76 | self.navigationItem.titleView = segmentedControl; 77 | segmentedControl.selectedSegmentIndex = 0; 78 | [self _segmentControlDidChangeValue:segmentedControl]; 79 | } 80 | 81 | - (void)viewWillAppear:(BOOL)animated 82 | { 83 | [super viewWillAppear:animated]; 84 | [_keyboardManager enable]; 85 | } 86 | 87 | - (void)viewWillDisappear:(BOOL)animated 88 | { 89 | [super viewWillDisappear:animated]; 90 | [_keyboardManager disable]; 91 | } 92 | 93 | #pragma mark - KVO methods 94 | 95 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(NSObject<_FBTweakColorViewControllerDataSource> *)dataSource change:(NSDictionary *)change context:(void *)context 96 | { 97 | if (context != kContext) { 98 | return; 99 | } 100 | _tweak.currentValue = dataSource.value; 101 | } 102 | 103 | #pragma mark - UITableViewDelegate 104 | 105 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 106 | { 107 | if (tableView.dataSource == _hsbDataSource && indexPath.section == 1 && indexPath.row == 0) { 108 | return _FBColorWheelCellHeight; 109 | } 110 | return _FBTweakColorCellDefaultHeight; 111 | } 112 | 113 | #pragma mark - Private methods 114 | 115 | - (UIColor *)_colorValue 116 | { 117 | return _tweak.currentValue ?: _tweak.defaultValue; 118 | } 119 | 120 | - (void)_segmentControlDidChangeValue:(UISegmentedControl *)sender 121 | { 122 | _tableView.dataSource = [self dataSourceForSegementIndex:sender.selectedSegmentIndex]; 123 | [_tableView reloadData]; 124 | } 125 | 126 | - (id <_FBTweakColorViewControllerDataSource>)dataSourceForSegementIndex:(NSUInteger)index 127 | { 128 | NSObject<_FBTweakColorViewControllerDataSource> *dataSource = nil; 129 | 130 | switch (index) { 131 | case FBTweakColorSectionRGB: 132 | dataSource = _rgbDataSource; 133 | break; 134 | case FBTweakColorSectionHSB: 135 | dataSource = _hsbDataSource; 136 | break; 137 | case FBTweakColorSectionHEX: 138 | dataSource = _hexDataSource; 139 | break; 140 | default: 141 | break; 142 | } 143 | dataSource.value = [self _colorValue]; 144 | return dataSource; 145 | } 146 | 147 | @end 148 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakColorViewControllerDataSource.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | /** 13 | @abstract Provides cells for table view to edit a color value. 14 | */ 15 | @protocol _FBTweakColorViewControllerDataSource 16 | @required 17 | 18 | //! @abstract The current color value. 19 | @property(nonatomic, strong) UIColor *value; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakColorViewControllerHSBDataSource.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import "_FBTweakColorViewControllerDataSource.h" 12 | 13 | /** 14 | @abstract A data source to privide cell's for HSB table view. 15 | */ 16 | @interface _FBTweakColorViewControllerHSBDataSource : NSObject <_FBTweakColorViewControllerDataSource> 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakColorViewControllerHSBDataSource.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "_FBTweakColorViewControllerHSBDataSource.h" 11 | #import "_FBColorComponentCell.h" 12 | #import "_FBColorWheelCell.h" 13 | #import "_FBColorUtils.h" 14 | 15 | @interface _FBTweakColorViewControllerHSBDataSource () <_FBColorComponentCellDelegate, _FBColorWheelCellDelegate> 16 | 17 | @end 18 | 19 | @implementation _FBTweakColorViewControllerHSBDataSource { 20 | NSArray *_titles; 21 | NSArray *_maxValues; 22 | HSB _colorComponents; 23 | NSArray *_colorComponentCells; 24 | UITableViewCell *_colorSampleCell; 25 | _FBColorWheelCell *_colorWheelCell; 26 | } 27 | 28 | - (instancetype)init 29 | { 30 | if (self = [super init]) { 31 | _titles = @[@"H", @"S", @"B", @"A"]; 32 | _maxValues = @[ 33 | @(_FBHSBColorComponentMaxValue), 34 | @(_FBHSBColorComponentMaxValue), 35 | @(_FBHSBColorComponentMaxValue), 36 | @(_FBAlphaComponentMaxValue), 37 | ]; 38 | [self _createCells]; 39 | } 40 | return self; 41 | } 42 | 43 | - (void)setValue:(UIColor *)value 44 | { 45 | _colorComponents = _FBRGB2HSB(_FBRGBColorComponents(value)); 46 | [self _reloadData]; 47 | } 48 | 49 | - (UIColor *)value 50 | { 51 | return [UIColor colorWithHue:_colorComponents.hue saturation:_colorComponents.saturation brightness:_colorComponents.brightness alpha:_colorComponents.alpha]; 52 | } 53 | 54 | #pragma mark - _FBColorComponentCellDelegate 55 | 56 | - (void)colorComponentCell:(_FBColorComponentCell *)cell didChangeValue:(CGFloat)value 57 | { 58 | [self _setValue:cell.value forColorComponent:[_colorComponentCells indexOfObject:cell]]; 59 | [self _reloadData]; 60 | } 61 | 62 | #pragma mark - _FBColorWheelCellDelegate 63 | 64 | - (void)colorWheelCellDidChangeColor:(_FBColorWheelCell *)cell 65 | { 66 | [self _setValue:cell.hue forColorComponent:_FBHSBColorComponentHue]; 67 | [self _setValue:cell.saturation forColorComponent:_FBHSBColorComponentSaturation]; 68 | [self _reloadData]; 69 | } 70 | 71 | #pragma mark - UITableViewDataSource 72 | 73 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 74 | { 75 | return 2; // color sample + color components 76 | } 77 | 78 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 79 | { 80 | if (section == 0) { 81 | return 1; 82 | } 83 | return _colorComponentCells.count + 1; // color wheel + hsba components 84 | } 85 | 86 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 87 | { 88 | if (indexPath.section == 0) { 89 | return _colorSampleCell; 90 | } 91 | if (indexPath.row == 0) { 92 | return _colorWheelCell; 93 | } 94 | return _colorComponentCells[indexPath.row - 1]; 95 | } 96 | 97 | #pragma mark - Private methods 98 | 99 | - (void)_reloadData 100 | { 101 | _colorSampleCell.backgroundColor = self.value; 102 | _colorWheelCell.hue = _colorComponents.hue; 103 | _colorWheelCell.saturation = _colorComponents.saturation; 104 | 105 | NSArray *components = [self _colorComponentsWithHSB:_colorComponents]; 106 | for (int i = 0; i < _FBHSBAColorComponentsSize; ++i) { 107 | _FBColorComponentCell *cell = _colorComponentCells[i]; 108 | if (i == _FBRGBAColorComponentsSize - 2) { // set colors for brightness component only 109 | UIColor *tmp = [UIColor colorWithHue:_colorComponents.hue saturation:_colorComponents.saturation brightness:1.0f alpha:1.0f]; 110 | cell.colors = @[(id)[UIColor blackColor].CGColor, (id)tmp.CGColor]; 111 | } 112 | cell.value = [components[i] floatValue] * (i == _FBHSBAColorComponentsSize - 1 ? [_maxValues[i] floatValue] : 1); 113 | } 114 | } 115 | 116 | - (void)_createCells 117 | { 118 | NSArray *components = [self _colorComponentsWithHSB:_colorComponents]; 119 | NSMutableArray *tmp = [NSMutableArray array]; 120 | for (int i = 0; i < _FBHSBAColorComponentsSize; ++i) { 121 | _FBColorComponentCell *cell = [[_FBColorComponentCell alloc] init]; 122 | if (i == _FBRGBAColorComponentsSize - 2) { // set colors for brightness component only 123 | UIColor *tmp = [UIColor colorWithHue:_colorComponents.hue saturation:_colorComponents.saturation brightness:1.0f alpha:1.0f]; 124 | cell.colors = @[(id)[UIColor blackColor].CGColor, (id)tmp.CGColor]; 125 | } 126 | cell.format = i == _FBHSBAColorComponentsSize - 1 ? @"%.f" : @"%.2f"; 127 | cell.value = [components[i] floatValue] * (i == _FBHSBAColorComponentsSize - 1 ? [_maxValues[i] floatValue] : 1); 128 | cell.title = _titles[i]; 129 | cell.maximumValue = [_maxValues[i] floatValue]; 130 | cell.delegate = self; 131 | [tmp addObject:cell]; 132 | } 133 | _colorComponentCells = [tmp copy]; 134 | 135 | _colorSampleCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 136 | _colorSampleCell.backgroundColor = self.value; 137 | 138 | _colorWheelCell = [[_FBColorWheelCell alloc] init]; 139 | _colorWheelCell.delegate = self; 140 | } 141 | 142 | - (void)_setValue:(CGFloat)value forColorComponent:(_FBHSBColorComponent)colorComponent 143 | { 144 | [self willChangeValueForKey:NSStringFromSelector(@selector(value))]; 145 | switch (colorComponent) { 146 | case _FBHSBColorComponentHue: 147 | _colorComponents.hue = value; 148 | break; 149 | case _FBHSBColorComponentSaturation: 150 | _colorComponents.saturation = value; 151 | break; 152 | case _FBHSBColorComponentBrightness: 153 | _colorComponents.brightness = value; 154 | break; 155 | case _FBHSBColorComponentAlpha: 156 | _colorComponents.alpha = value / _FBAlphaComponentMaxValue; 157 | break; 158 | } 159 | [self didChangeValueForKey:NSStringFromSelector(@selector(value))]; 160 | } 161 | 162 | - (NSArray *)_colorComponentsWithHSB:(HSB)hsb 163 | { 164 | return @[@(hsb.hue), @(hsb.saturation), @(hsb.brightness), @(hsb.alpha)]; 165 | } 166 | 167 | @end 168 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakColorViewControllerHexDataSource.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import "_FBTweakColorViewControllerDataSource.h" 12 | 13 | @interface _FBTweakColorViewControllerHexDataSource : NSObject <_FBTweakColorViewControllerDataSource> 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakColorViewControllerHexDataSource.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "_FBTweakColorViewControllerHexDataSource.h" 11 | #import "_FBTweakTableViewCell.h" 12 | #import "FBTweak.h" 13 | 14 | @interface _FBTweakColorViewControllerHexDataSource () 15 | 16 | @property (nonatomic, strong) UIColor *color; 17 | @property (nonatomic, strong) FBTweak *tweak; 18 | 19 | @end 20 | 21 | @implementation _FBTweakColorViewControllerHexDataSource { 22 | UITableViewCell *_colorSampleCell; 23 | } 24 | 25 | - (instancetype)init 26 | { 27 | if (self = [super init]) { 28 | _colorSampleCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 29 | } 30 | return self; 31 | } 32 | 33 | - (void)dealloc 34 | { 35 | [self.tweak removeObserver:self]; 36 | } 37 | 38 | - (UIColor *)value 39 | { 40 | return self.color; 41 | } 42 | 43 | - (void)setValue:(UIColor *)value 44 | { 45 | _color = value; 46 | } 47 | 48 | #pragma mark - UITableViewDataSource 49 | 50 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 51 | { 52 | return 2; 53 | } 54 | 55 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 56 | { 57 | return 1; 58 | } 59 | 60 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 61 | { 62 | if (indexPath.section == 0) { 63 | _colorSampleCell.backgroundColor = self.value; 64 | return _colorSampleCell; 65 | } 66 | return [self hexCell]; 67 | } 68 | 69 | /** 70 | * By using _FBTweakTableViewCell, it allows us to internally observe changes and configure the tableViewCell without 71 | * having to create a new subclass. So while not a tweak in itself, it provides us the desired behaviour. 72 | */ 73 | - (_FBTweakTableViewCell *)hexCell 74 | { 75 | _FBTweakTableViewCell *hexCell = [[_FBTweakTableViewCell alloc] initWithReuseIdentifier:@"hexCell"]; 76 | self.tweak = [[FBTweak alloc] initWithIdentifier:@"hex"]; 77 | self.tweak.name = @"Hex Value"; 78 | self.tweak.defaultValue = @"FFFFFF"; 79 | self.tweak.currentValue = [self.class colorToHexString:self.color]; 80 | [self.tweak addObserver:self]; 81 | hexCell.tweak = self.tweak; 82 | return hexCell; 83 | } 84 | 85 | - (void)tweakDidChange:(FBTweak *)tweak 86 | { 87 | UIColor *colorFromHex = [self.class colorFromHexString:tweak.currentValue]; 88 | [self setValue:colorFromHex]; 89 | _colorSampleCell.backgroundColor = colorFromHex; 90 | } 91 | 92 | #pragma mark - hex colour converters 93 | 94 | + (NSString *)colorToHexString:(UIColor *)uiColor 95 | { 96 | CGFloat red, green, blue, alpha; 97 | [uiColor getRed:&red green:&green blue:&blue alpha:&alpha]; 98 | red = roundf(red * 255); 99 | green = roundf(green * 255); 100 | blue = round(blue * 255); 101 | NSString *hexString = [NSString stringWithFormat:@"#%02x%02x%02x", ((int)red), ((int)green), ((int)blue)]; 102 | return hexString.uppercaseString; 103 | } 104 | 105 | + (UIColor *)colorFromHexString:(NSString *)hexString 106 | { 107 | if (hexString.length == 0) { 108 | return [UIColor blackColor]; 109 | } 110 | unsigned int rgbValue = 0; 111 | NSScanner *scanner = [NSScanner scannerWithString:hexString]; 112 | [scanner setScanLocation:1]; // bypass '#' character 113 | [scanner scanHexInt:&rgbValue]; 114 | CGFloat redComponent = ((rgbValue & 0xFF0000) >> 16) / 255.0; 115 | CGFloat blueComponent = ((rgbValue & 0xFF00) >> 8) / 255.0; 116 | CGFloat greenComponent = (rgbValue & 0xFF) / 255.0; 117 | return [UIColor colorWithRed:redComponent green:greenComponent blue:blueComponent alpha:1.0]; 118 | } 119 | 120 | 121 | @end 122 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakColorViewControllerRGBDataSource.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import "_FBTweakColorViewControllerDataSource.h" 12 | 13 | /** 14 | @abstract A data source to privide cell's for RGB table view. 15 | */ 16 | @interface _FBTweakColorViewControllerRGBDataSource : NSObject <_FBTweakColorViewControllerDataSource> 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakColorViewControllerRGBDataSource.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "_FBTweakColorViewControllerRGBDataSource.h" 11 | #import "_FBColorComponentCell.h" 12 | #import "_FBColorUtils.h" 13 | 14 | @interface _FBTweakColorViewControllerRGBDataSource () <_FBColorComponentCellDelegate> 15 | 16 | @end 17 | 18 | @implementation _FBTweakColorViewControllerRGBDataSource { 19 | NSArray *_titles; 20 | NSArray *_maxValues; 21 | RGB _colorComponents; 22 | NSArray *_colorComponentCells; 23 | UITableViewCell *_colorSampleCell; 24 | } 25 | 26 | - (instancetype)init 27 | { 28 | if (self = [super init]) { 29 | _titles = @[@"R", @"G", @"B", @"A"]; 30 | _maxValues = @[ 31 | @(_FBRGBColorComponentMaxValue), 32 | @(_FBRGBColorComponentMaxValue), 33 | @(_FBRGBColorComponentMaxValue), 34 | @(_FBAlphaComponentMaxValue), 35 | ]; 36 | [self _createCells]; 37 | } 38 | return self; 39 | } 40 | 41 | - (void)setValue:(UIColor *)value 42 | { 43 | [self willChangeValueForKey:NSStringFromSelector(@selector(value))]; 44 | _colorComponents = _FBRGBColorComponents(value); 45 | [self _reloadData]; 46 | [self didChangeValueForKey:NSStringFromSelector(@selector(value))]; 47 | } 48 | 49 | - (UIColor *)value 50 | { 51 | return [UIColor colorWithRed:_colorComponents.red green:_colorComponents.green blue:_colorComponents.blue alpha:_colorComponents.alpha]; 52 | } 53 | 54 | #pragma mark - UITableViewDataSource 55 | 56 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 57 | { 58 | return 2; 59 | } 60 | 61 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 62 | { 63 | if (section == 0) { 64 | return 1; 65 | } 66 | return [_colorComponentCells count]; 67 | } 68 | 69 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 70 | { 71 | if (indexPath.section == 0) { 72 | return _colorSampleCell; 73 | } 74 | return _colorComponentCells[indexPath.row]; 75 | } 76 | 77 | #pragma mark - _FBColorComponentCellDelegate 78 | 79 | - (void)colorComponentCell:(_FBColorComponentCell*)cell didChangeValue:(CGFloat)value 80 | { 81 | [self _setValue:cell.value / cell.maximumValue forColorComponent:[_colorComponentCells indexOfObject:cell]]; 82 | [self _reloadData]; 83 | } 84 | 85 | #pragma mark - Private methods 86 | 87 | - (void)_reloadData 88 | { 89 | _colorSampleCell.backgroundColor = self.value; 90 | NSArray *components = [self _colorComponentsWithRGB:_colorComponents]; 91 | for (int i = 0; i < _FBRGBAColorComponentsSize; ++i) { 92 | _FBColorComponentCell *cell = _colorComponentCells[i]; 93 | cell.value = [components[i] floatValue] * [_maxValues[i] floatValue]; 94 | if (i < _FBRGBAColorComponentsSize - 1) { 95 | cell.colors = [self _colorsWithComponents:components colorIndex:i]; 96 | } 97 | } 98 | } 99 | 100 | - (void)_createCells 101 | { 102 | NSMutableArray *tmp = [NSMutableArray array]; 103 | NSArray *components = [self _colorComponentsWithRGB:_colorComponents]; 104 | for (int i = 0; i < _FBRGBAColorComponentsSize; ++i) { 105 | _FBColorComponentCell *cell = [[_FBColorComponentCell alloc] init]; 106 | if (i < _FBRGBAColorComponentsSize - 1) { 107 | cell.colors = [self _colorsWithComponents:components colorIndex:i]; 108 | } 109 | cell.value = [components[i] floatValue] * [_maxValues[i] floatValue]; 110 | cell.title = _titles[i]; 111 | cell.maximumValue = [_maxValues[i] floatValue]; 112 | cell.delegate = self; 113 | [tmp addObject:cell]; 114 | } 115 | _colorComponentCells = [tmp copy]; 116 | _colorSampleCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; 117 | _colorSampleCell.backgroundColor = self.value; 118 | } 119 | 120 | - (NSArray *)_colorComponentsWithRGB:(RGB)rgb 121 | { 122 | return @[@(rgb.red), @(rgb.green), @(rgb.blue), @(rgb.alpha)]; 123 | } 124 | 125 | - (NSArray *)_colorsWithComponents:(NSArray *)colorComponents colorIndex:(NSUInteger)colorIndex 126 | { 127 | CGFloat currentColorValue = [colorComponents[colorIndex] floatValue]; 128 | CGFloat colors[12]; 129 | for (NSUInteger i = 0; i < _FBRGBAColorComponentsSize; i++) 130 | { 131 | colors[i] = [colorComponents[i] floatValue]; 132 | colors[i + 4] = [colorComponents[i] floatValue]; 133 | colors[i + 8] = [colorComponents[i] floatValue]; 134 | } 135 | colors[colorIndex] = 0; 136 | colors[colorIndex + 4] = currentColorValue; 137 | colors[colorIndex + 8] = 1.0; 138 | UIColor *start = [UIColor colorWithRed:colors[0] green:colors[1] blue:colors[2] alpha:1.0f]; 139 | UIColor *middle = [UIColor colorWithRed:colors[4] green:colors[5] blue:colors[6] alpha:1.0f]; 140 | UIColor *end = [UIColor colorWithRed:colors[8] green:colors[9] blue:colors[10] alpha:1.0f]; 141 | return @[(id)start.CGColor, (id)middle.CGColor, (id)end.CGColor]; 142 | } 143 | 144 | - (void)_setValue:(CGFloat)value forColorComponent:(_FBRGBColorComponent)colorComponent 145 | { 146 | [self willChangeValueForKey:NSStringFromSelector(@selector(value))]; 147 | switch (colorComponent) { 148 | case _FBRGBColorComponentRed: 149 | _colorComponents.red = value; 150 | break; 151 | case _FBRGBColorComponentGreed: 152 | _colorComponents.green = value; 153 | break; 154 | case _FBRGBColorComponentBlue: 155 | _colorComponents.blue = value; 156 | break; 157 | case _FBRGBColorComponentAlpha: 158 | _colorComponents.alpha = value; 159 | break; 160 | } 161 | [self didChangeValueForKey:NSStringFromSelector(@selector(value))]; 162 | } 163 | 164 | @end 165 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakDictionaryViewController.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class FBTweak; 13 | 14 | /** 15 | @abstract Displays list of keys in a dictionary tweak. 16 | */ 17 | @interface _FBTweakDictionaryViewController : UIViewController 18 | 19 | /** 20 | @abstract Creates a tweak dictionary view controller. 21 | @discussion This is the designated initializer. 22 | @param tweak The tweak the view controller is for. Must 23 | not be nil, and must have a dictionary of possibleValues. 24 | */ 25 | - (instancetype)initWithTweak:(FBTweak *)tweak; 26 | 27 | /** 28 | @abstract The dictionary tweak to display in the view controller. 29 | */ 30 | @property (nonatomic, strong, readonly) FBTweak *tweak; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakDictionaryViewController.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "_FBTweakDictionaryViewController.h" 11 | #import "FBTweak.h" 12 | 13 | @interface _FBTweakDictionaryViewController () 14 | 15 | @end 16 | 17 | @implementation _FBTweakDictionaryViewController { 18 | UITableView *_tableView; 19 | } 20 | 21 | - (instancetype)initWithTweak:(FBTweak *)tweak 22 | { 23 | NSParameterAssert(tweak != nil); 24 | NSParameterAssert([tweak.possibleValues isKindOfClass:[NSDictionary class]]); 25 | 26 | if ((self = [super init])) { 27 | _tweak = tweak; 28 | self.title = _tweak.name; 29 | } 30 | 31 | return self; 32 | } 33 | 34 | - (void)viewDidLoad 35 | { 36 | [super viewDidLoad]; 37 | 38 | _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped]; 39 | _tableView.delegate = self; 40 | _tableView.dataSource = self; 41 | _tableView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); 42 | [self.view addSubview:_tableView]; 43 | } 44 | 45 | - (void)dealloc 46 | { 47 | _tableView.delegate = nil; 48 | _tableView.dataSource = nil; 49 | } 50 | 51 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 52 | { 53 | return 1; 54 | } 55 | 56 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 57 | { 58 | return [_tweak.possibleValues count]; 59 | } 60 | 61 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 62 | { 63 | static NSString *_FBTweakDictionaryViewControllerCellIdentifier = @"_FBTweakDictionaryViewControllerCellIdentifier"; 64 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:_FBTweakDictionaryViewControllerCellIdentifier]; 65 | if (cell == nil) { 66 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:_FBTweakDictionaryViewControllerCellIdentifier]; 67 | } 68 | 69 | NSArray *allKeys = [self allTweakKeys]; 70 | FBTweakValue key = allKeys[indexPath.row]; 71 | NSString *value = _tweak.possibleValues[key]; 72 | cell.textLabel.text = value; 73 | 74 | cell.accessoryType = UITableViewCellAccessoryNone; 75 | NSString *selectedKey = (_tweak.currentValue ?: _tweak.defaultValue); 76 | if ([selectedKey isEqual:key]) { 77 | cell.accessoryType = UITableViewCellAccessoryCheckmark; 78 | } 79 | 80 | return cell; 81 | } 82 | 83 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 84 | { 85 | NSArray *allKeys = [self allTweakKeys]; 86 | NSString *key = allKeys[indexPath.row]; 87 | 88 | self.tweak.currentValue = key; 89 | [self.navigationController popViewControllerAnimated:YES]; 90 | } 91 | 92 | - (NSArray *)allTweakKeys 93 | { 94 | // Sort by visible name. 95 | return [[_tweak.possibleValues allKeys] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { 96 | id value1 = _tweak.possibleValues[obj1]; 97 | id value2 = _tweak.possibleValues[obj2]; 98 | return [value1 compare:value2]; 99 | }]; 100 | } 101 | 102 | @end -------------------------------------------------------------------------------- /FBTweak/_FBTweakTableViewCell.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @class FBTweak; 13 | 14 | /** 15 | @abstract A table cell to edit a tweak. 16 | */ 17 | @interface _FBTweakTableViewCell : UITableViewCell 18 | 19 | /** 20 | @abstract Create a tweak table cell. 21 | @param reuseIdentifier The cell's reuse identifier. 22 | @discussion This is the designated initializer. 23 | */ 24 | - (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier; 25 | 26 | //! @abstract The tweak to show in the cell. 27 | @property (nonatomic, strong, readwrite) FBTweak *tweak; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /FBTweak/_FBTweakTableViewCell.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "FBTweak.h" 11 | #import "_FBTweakTableViewCell.h" 12 | 13 | static UIImage *_FBCreateColorCellsThumbnail(UIColor *color, CGSize size) { 14 | UIGraphicsBeginImageContext(size); 15 | UIBezierPath *rPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, size.width, size.height)]; 16 | [color setFill]; 17 | [rPath fill]; 18 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 19 | UIGraphicsEndImageContext(); 20 | return image; 21 | } 22 | 23 | typedef NS_ENUM(NSUInteger, _FBTweakTableViewCellMode) { 24 | _FBTweakTableViewCellModeNone = 0, 25 | _FBTweakTableViewCellModeBoolean, 26 | _FBTweakTableViewCellModeInteger, 27 | _FBTweakTableViewCellModeReal, 28 | _FBTweakTableViewCellModeString, 29 | _FBTweakTableViewCellModeAction, 30 | _FBTweakTableViewCellModeDictionary, 31 | _FBTweakTableViewCellModeArray, 32 | _FBTweakTableViewCellModeColor, 33 | }; 34 | 35 | @interface _FBTweakTableViewCell () 36 | @end 37 | 38 | @implementation _FBTweakTableViewCell { 39 | UIView *_accessoryView; 40 | 41 | _FBTweakTableViewCellMode _mode; 42 | UISwitch *_switch; 43 | UITextField *_textField; 44 | UIStepper *_stepper; 45 | } 46 | 47 | - (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier 48 | { 49 | if ((self = [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier])) { 50 | _accessoryView = [[UIView alloc] init]; 51 | 52 | _switch = [[UISwitch alloc] init]; 53 | [_switch addTarget:self action:@selector(_switchChanged:) forControlEvents:UIControlEventValueChanged]; 54 | [_accessoryView addSubview:_switch]; 55 | 56 | _textField = [[UITextField alloc] init]; 57 | _textField.textAlignment = NSTextAlignmentRight; 58 | _textField.delegate = self; 59 | [_accessoryView addSubview:_textField]; 60 | 61 | _stepper = [[UIStepper alloc] init]; 62 | [_stepper addTarget:self action:@selector(_stepperChanged:) forControlEvents:UIControlEventValueChanged]; 63 | [_accessoryView addSubview:_stepper]; 64 | 65 | self.detailTextLabel.textColor = [UIColor blackColor]; 66 | } 67 | 68 | return self; 69 | } 70 | 71 | - (void)dealloc 72 | { 73 | [_switch removeTarget:self action:@selector(_switchChanged:) forControlEvents:UIControlEventValueChanged]; 74 | _textField.delegate = nil; 75 | [_stepper removeTarget:self action:@selector(_stepperChanged:) forControlEvents:UIControlEventValueChanged]; 76 | } 77 | 78 | - (void)layoutSubviews 79 | { 80 | if (_mode == _FBTweakTableViewCellModeBoolean) { 81 | [_switch sizeToFit]; 82 | _accessoryView.bounds = _switch.bounds; 83 | } else if (_mode == _FBTweakTableViewCellModeInteger || 84 | _mode == _FBTweakTableViewCellModeReal) { 85 | [_stepper sizeToFit]; 86 | 87 | CGRect textFrame = CGRectMake(0, 0, self.bounds.size.width / 4, self.bounds.size.height); 88 | CGRect stepperFrame = CGRectMake(textFrame.size.width + 6.0, 89 | (textFrame.size.height - _stepper.bounds.size.height) / 2, 90 | _stepper.bounds.size.width, 91 | _stepper.bounds.size.height); 92 | _textField.frame = CGRectIntegral(textFrame); 93 | _stepper.frame = CGRectIntegral(stepperFrame); 94 | 95 | CGRect accessoryFrame = CGRectUnion(stepperFrame, textFrame); 96 | _accessoryView.bounds = CGRectIntegral(accessoryFrame); 97 | } else if (_mode == _FBTweakTableViewCellModeString) { 98 | CGFloat margin = CGRectGetMinX(self.textLabel.frame); 99 | CGFloat textFieldWidth = self.bounds.size.width - (margin * 3.0) - [self.textLabel sizeThatFits:CGSizeZero].width; 100 | CGRect textBounds = CGRectMake(0, 0, textFieldWidth, self.bounds.size.height); 101 | _textField.frame = CGRectIntegral(textBounds); 102 | _accessoryView.bounds = CGRectIntegral(textBounds); 103 | } else if (_mode == _FBTweakTableViewCellModeColor) { 104 | CGRect textBounds = CGRectMake(0, 0, self.bounds.size.width / 3, self.bounds.size.height); 105 | _textField.frame = CGRectIntegral(textBounds); 106 | _accessoryView.bounds = CGRectIntegral(textBounds); 107 | } else if (_mode == _FBTweakTableViewCellModeAction) { 108 | _accessoryView.bounds = CGRectZero; 109 | } 110 | 111 | // This positions the accessory view, so call it after updating its bounds. 112 | [super layoutSubviews]; 113 | } 114 | 115 | #pragma mark - Configuration 116 | 117 | - (void)setTweak:(FBTweak *)tweak 118 | { 119 | _tweak = tweak; 120 | 121 | self.textLabel.text = tweak.name; 122 | 123 | FBTweakValue value = (_tweak.currentValue ?: _tweak.defaultValue); 124 | 125 | _FBTweakTableViewCellMode mode = _FBTweakTableViewCellModeNone; 126 | if ([tweak.possibleValues isKindOfClass:[NSDictionary class]]) { 127 | mode = _FBTweakTableViewCellModeDictionary; 128 | } else if ([tweak.possibleValues isKindOfClass:[NSArray class]]) { 129 | mode = _FBTweakTableViewCellModeArray; 130 | } else if ([value isKindOfClass:[UIColor class]]) { 131 | mode = _FBTweakTableViewCellModeColor; 132 | } else if ([value isKindOfClass:[NSString class]]) { 133 | mode = _FBTweakTableViewCellModeString; 134 | } else if ([value isKindOfClass:[NSNumber class]]) { 135 | // In the 64-bit runtime, BOOL is a real boolean. 136 | // NSNumber doesn't always agree; compare both. 137 | if (strcmp([value objCType], @encode(char)) == 0 || 138 | strcmp([value objCType], @encode(_Bool)) == 0) { 139 | mode = _FBTweakTableViewCellModeBoolean; 140 | } else if (strcmp([value objCType], @encode(NSInteger)) == 0 || 141 | strcmp([value objCType], @encode(NSUInteger)) == 0 || 142 | strcmp([value objCType], @encode(int)) == 0 || 143 | strcmp([value objCType], @encode(long)) == 0) { 144 | mode = _FBTweakTableViewCellModeInteger; 145 | } else { 146 | mode = _FBTweakTableViewCellModeReal; 147 | } 148 | } else if ([_tweak isAction]) { 149 | mode = _FBTweakTableViewCellModeAction; 150 | } 151 | 152 | [self _updateMode:mode]; 153 | [self _updateValue:value primary:YES write:NO]; 154 | } 155 | 156 | - (void)_updateMode:(_FBTweakTableViewCellMode)mode 157 | { 158 | _mode = mode; 159 | 160 | self.accessoryView = _accessoryView; 161 | self.accessoryType = UITableViewCellAccessoryNone; 162 | self.detailTextLabel.text = nil; 163 | self.selectionStyle = UITableViewCellSelectionStyleNone; 164 | 165 | if (_mode == _FBTweakTableViewCellModeBoolean) { 166 | _switch.hidden = NO; 167 | _textField.hidden = YES; 168 | _stepper.hidden = YES; 169 | } else if (_mode == _FBTweakTableViewCellModeInteger) { 170 | _switch.hidden = YES; 171 | _textField.hidden = NO; 172 | _textField.keyboardType = UIKeyboardTypeNumberPad; 173 | _stepper.hidden = NO; 174 | if (_tweak.stepValue) { 175 | _stepper.stepValue = [_tweak.stepValue floatValue]; 176 | } else { 177 | _stepper.stepValue = 1.0; 178 | } 179 | 180 | if (_tweak.minimumValue != nil) { 181 | _stepper.minimumValue = [_tweak.minimumValue longLongValue]; 182 | } else { 183 | _stepper.minimumValue = [_tweak.defaultValue longLongValue] / 10.0; 184 | } 185 | 186 | if (_tweak.maximumValue != nil) { 187 | _stepper.maximumValue = [_tweak.maximumValue longLongValue]; 188 | } else { 189 | _stepper.maximumValue = [_tweak.defaultValue longLongValue] * 10.0; 190 | } 191 | } else if (_mode == _FBTweakTableViewCellModeReal) { 192 | _switch.hidden = YES; 193 | _textField.hidden = NO; 194 | _textField.keyboardType = UIKeyboardTypeDecimalPad; 195 | _stepper.hidden = NO; 196 | 197 | if (_tweak.stepValue) { 198 | _stepper.stepValue = [_tweak.stepValue floatValue]; 199 | } else { 200 | _stepper.stepValue = 1.0; 201 | } 202 | 203 | if (_tweak.minimumValue != nil) { 204 | _stepper.minimumValue = [_tweak.minimumValue doubleValue]; 205 | } else if ([_tweak.defaultValue doubleValue] == 0) { 206 | _stepper.minimumValue = -1; 207 | } else { 208 | _stepper.minimumValue = [_tweak.defaultValue doubleValue] / 10.0; 209 | } 210 | 211 | if (_tweak.maximumValue != nil) { 212 | _stepper.maximumValue = [_tweak.maximumValue doubleValue]; 213 | } else if ([_tweak.defaultValue doubleValue] == 0) { 214 | _stepper.maximumValue = 1; 215 | } else { 216 | _stepper.maximumValue = [_tweak.defaultValue doubleValue] * 10.0; 217 | } 218 | 219 | if (!_tweak.stepValue) { 220 | _stepper.stepValue = fminf(1.0, (_stepper.maximumValue - _stepper.minimumValue) / 100.0); 221 | } 222 | } else if (_mode == _FBTweakTableViewCellModeString) { 223 | _switch.hidden = YES; 224 | _textField.hidden = NO; 225 | _textField.keyboardType = UIKeyboardTypeDefault; 226 | _stepper.hidden = YES; 227 | } else if (_mode == _FBTweakTableViewCellModeAction) { 228 | _switch.hidden = YES; 229 | _textField.hidden = YES; 230 | _stepper.hidden = YES; 231 | 232 | self.accessoryView = nil; 233 | self.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 234 | self.selectionStyle = UITableViewCellSelectionStyleBlue; 235 | } else if (_mode == _FBTweakTableViewCellModeDictionary) { 236 | _switch.hidden = YES; 237 | _textField.hidden = YES; 238 | _stepper.hidden = YES; 239 | self.accessoryView = nil; 240 | self.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 241 | self.selectionStyle = UITableViewCellSelectionStyleBlue; 242 | } else if (_mode == _FBTweakTableViewCellModeArray) { 243 | _switch.hidden = YES; 244 | _textField.hidden = YES; 245 | _stepper.hidden = YES; 246 | self.accessoryView = nil; 247 | self.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 248 | self.selectionStyle = UITableViewCellSelectionStyleBlue; 249 | } else if (_mode == _FBTweakTableViewCellModeColor) { 250 | _switch.hidden = YES; 251 | _textField.hidden = YES; 252 | _stepper.hidden = YES; 253 | self.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 254 | self.accessoryView = nil; 255 | self.imageView.hidden = NO; 256 | } else { 257 | _switch.hidden = YES; 258 | _textField.hidden = YES; 259 | _stepper.hidden = YES; 260 | } 261 | 262 | [self setNeedsLayout]; 263 | [self layoutIfNeeded]; 264 | } 265 | 266 | #pragma mark - Actions 267 | 268 | - (void)_switchChanged:(UISwitch *)switch_ 269 | { 270 | [self _updateValue:@(_switch.on) primary:NO write:YES]; 271 | } 272 | 273 | - (BOOL)textFieldShouldReturn:(UITextField *)textField 274 | { 275 | [_textField resignFirstResponder]; 276 | return YES; 277 | } 278 | 279 | - (void)textFieldDidEndEditing:(UITextField *)textField 280 | { 281 | if (_mode == _FBTweakTableViewCellModeString || _mode == _FBTweakTableViewCellModeColor) { 282 | [self _updateValue:_textField.text primary:NO write:YES]; 283 | } else if (_mode == _FBTweakTableViewCellModeInteger) { 284 | NSNumber *number = @([_textField.text longLongValue]); 285 | [self _updateValue:number primary:NO write:YES]; 286 | } else if (_mode == _FBTweakTableViewCellModeReal) { 287 | NSNumber *number = @([_textField.text doubleValue]); 288 | [self _updateValue:number primary:NO write:YES]; 289 | } else { 290 | NSAssert(NO, @"unexpected type"); 291 | } 292 | } 293 | 294 | - (void)_stepperChanged:(UIStepper *)stepper 295 | { 296 | if (_mode == _FBTweakTableViewCellModeInteger) { 297 | NSNumber *number = @([@(stepper.value) longLongValue]); 298 | [self _updateValue:number primary:NO write:YES]; 299 | } else { 300 | [self _updateValue:@(stepper.value) primary:NO write:YES]; 301 | } 302 | } 303 | 304 | - (void)_updateValue:(FBTweakValue)value primary:(BOOL)primary write:(BOOL)write 305 | { 306 | if (write) { 307 | _tweak.currentValue = value; 308 | } 309 | 310 | if (_mode == _FBTweakTableViewCellModeBoolean) { 311 | if (primary) { 312 | _switch.on = [value boolValue]; 313 | } 314 | } else if (_mode == _FBTweakTableViewCellModeString) { 315 | if (primary) { 316 | _textField.text = value; 317 | } 318 | } else if (_mode == _FBTweakTableViewCellModeInteger) { 319 | if (primary) { 320 | _stepper.value = [value longLongValue]; 321 | } 322 | _textField.text = [value stringValue]; 323 | } else if (_mode == _FBTweakTableViewCellModeReal) { 324 | if (primary) { 325 | _stepper.value = [value doubleValue]; 326 | } 327 | 328 | double exp = log10(_stepper.stepValue); 329 | long precision = exp < 0 ? ceilf(fabs(exp)) : 0; 330 | 331 | if (_tweak.precisionValue) { 332 | precision = [[_tweak precisionValue] longValue]; 333 | } 334 | 335 | NSString *format = [NSString stringWithFormat:@"%%.%ldf", precision]; 336 | _textField.text = [NSString stringWithFormat:format, [value doubleValue]]; 337 | } else if (_mode == _FBTweakTableViewCellModeDictionary) { 338 | if (primary) { 339 | self.detailTextLabel.text = _tweak.possibleValues[value]; 340 | } 341 | } else if (_mode == _FBTweakTableViewCellModeArray) { 342 | if (primary) { 343 | self.detailTextLabel.text = [value description]; 344 | } 345 | } else if (_mode == _FBTweakTableViewCellModeColor) { 346 | [self.imageView setImage:_FBCreateColorCellsThumbnail(value, CGSizeMake(30, 30))]; 347 | } 348 | } 349 | 350 | @end 351 | -------------------------------------------------------------------------------- /FBTweakExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FBTweakExample.xcodeproj/xcshareddata/xcschemes/FBTweakExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /FBTweakExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /FBTweakExample/FBAppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @interface FBAppDelegate : UIResponder 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /FBTweakExample/FBAppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | #import "FBAppDelegate.h" 16 | 17 | @interface FBAppDelegate () 18 | @end 19 | 20 | @implementation FBAppDelegate { 21 | UIWindow *_window; 22 | UIViewController *_rootViewController; 23 | 24 | UILabel *_label; 25 | UIButton *_tweaksButton; 26 | FBTweak *_buttonColorTweak; 27 | FBTweak *_flipTweak; 28 | } 29 | 30 | FBTweakAction(@"Actions", @"Global", @"Hello", ^{ 31 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Hello" message:@"Global alert test." delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Done", nil]; 32 | [alert show]; 33 | }); 34 | 35 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 36 | { 37 | 38 | FBTweakAction(@"Actions", @"Scoped", @"One", ^{ 39 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Hello" message:@"Scoped alert test #1." delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Done", nil]; 40 | [alert show]; 41 | }); 42 | 43 | _window = [[FBTweakShakeWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 44 | _window.backgroundColor = [UIColor whiteColor]; 45 | [_window makeKeyAndVisible]; 46 | 47 | _rootViewController = [[UIViewController alloc] init]; 48 | _rootViewController.view.backgroundColor = [UIColor colorWithRed:FBTweakValue(@"Window", @"Color", @"Red", 0.9, 0.0, 1.0) 49 | green:FBTweakValue(@"Window", @"Color", @"Green", 0.9, 0.0, 1.0) 50 | blue:FBTweakValue(@"Window", @"Color", @"Blue", 0.9, 0.0, 1.0) 51 | alpha:1.0]; 52 | _window.rootViewController = _rootViewController; 53 | 54 | _label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, _window.bounds.size.width, _window.bounds.size.height * 0.75)]; 55 | _label.textAlignment = NSTextAlignmentCenter; 56 | _label.numberOfLines = 0; 57 | _label.userInteractionEnabled = YES; 58 | _label.backgroundColor = [UIColor clearColor]; 59 | _label.textColor = [UIColor blackColor]; 60 | _label.font = [UIFont systemFontOfSize:FBTweakValue(@"Content", @"Text", @"Size", 60.0)]; 61 | FBTweakBind(_label, text, @"Content", @"Text", @"String", @"Tweaks"); 62 | FBTweakBind(_label, textColor, @"Content", @"Text", @"Color", [UIColor blackColor]); 63 | FBTweakBind(_label, alpha, @"Content", @"Text", @"Alpha", 0.5, 0.0, 1.0); 64 | [_rootViewController.view addSubview:_label]; 65 | 66 | FBTweakBind(_rootViewController.view, backgroundColor, @"Content", @"Background", @"Color", [UIColor whiteColor]); 67 | 68 | UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(labelTapped)]; 69 | [_label addGestureRecognizer:tapRecognizer]; 70 | 71 | _flipTweak = FBTweakInline(@"Window", @"Effects", @"Upside Down", NO); 72 | [_flipTweak addObserver:self]; 73 | 74 | CGRect tweaksButtonFrame = _window.bounds; 75 | tweaksButtonFrame.origin.y = _label.bounds.size.height; 76 | tweaksButtonFrame.size.height = tweaksButtonFrame.size.height - _label.bounds.size.height; 77 | _tweaksButton = [[UIButton alloc] initWithFrame:tweaksButtonFrame]; 78 | [_tweaksButton setTitle:@"Show Tweaks" forState:UIControlStateNormal]; 79 | [_tweaksButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; 80 | [_tweaksButton addTarget:self action:@selector(buttonTapped) forControlEvents:UIControlEventTouchUpInside]; 81 | [_rootViewController.view addSubview:_tweaksButton]; 82 | 83 | FBTweak *animationDurationTweak = FBTweakInline(@"Content", @"Animation", @"Duration", 0.5); 84 | animationDurationTweak.stepValue = [NSNumber numberWithFloat:0.005f]; 85 | animationDurationTweak.precisionValue = [NSNumber numberWithFloat:3.0f]; 86 | 87 | 88 | FBTweakAction(@"Actions", @"Scoped", @"Two", ^{ 89 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Hello" message:@"Scoped alert test #2." delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Done", nil]; 90 | [alert show]; 91 | }); 92 | 93 | typedef NS_ENUM(NSUInteger, FBColor) { 94 | FBBlackColor, 95 | FBBlueColor, 96 | FBGreenColor, 97 | }; 98 | 99 | NSNumber *colorIndex = FBTweakValue(@"Content", @"Tweaks Button", @"Color", @(FBBlackColor), (@{ 100 | @(FBBlackColor) : @"Black", 101 | @(FBBlueColor) : @"Blue", 102 | @(FBGreenColor) : @"Green", 103 | })); 104 | UIColor *color = (colorIndex.integerValue == FBBlackColor ? [UIColor blackColor] : colorIndex.integerValue == FBBlueColor ? [UIColor blueColor] : [UIColor greenColor]); 105 | [_tweaksButton setTitleColor:color forState:UIControlStateNormal]; 106 | 107 | NSNumber *rotation = FBTweakValue(@"Content", @"Text", @"Rotation (radians)", @(0), (@[@(0), @(M_PI_4), @(M_PI_2)])); 108 | _label.transform = CGAffineTransformRotate(CGAffineTransformIdentity, [rotation floatValue]); 109 | 110 | return YES; 111 | } 112 | 113 | - (void)tweakDidChange:(FBTweak *)tweak 114 | { 115 | if (tweak == _flipTweak) { 116 | _window.layer.sublayerTransform = CATransform3DMakeScale(1.0, [_flipTweak.currentValue boolValue] ? -1.0 : 1.0, 1.0); 117 | } 118 | } 119 | 120 | - (void)buttonTapped 121 | { 122 | FBTweakViewController *viewController = [[FBTweakViewController alloc] initWithStore:[FBTweakStore sharedInstance]]; 123 | viewController.tweaksDelegate = self; 124 | [_window.rootViewController presentViewController:viewController animated:YES completion:NULL]; 125 | } 126 | 127 | - (void)tweakViewControllerPressedDone:(FBTweakViewController *)tweakViewController 128 | { 129 | [tweakViewController dismissViewControllerAnimated:YES completion:NULL]; 130 | } 131 | 132 | - (void)labelTapped 133 | { 134 | NSTimeInterval duration = FBTweakValue(@"Content", @"Animation", @"Duration", 0.5); 135 | [UIView animateWithDuration:duration animations:^{ 136 | CGFloat scale = FBTweakValue(@"Content", @"Animation", @"Scale", 2.0); 137 | _label.transform = CGAffineTransformMakeScale(scale, scale); 138 | } completion:^(BOOL finished) { 139 | [UIView animateWithDuration:duration animations:^{ 140 | _label.transform = CGAffineTransformIdentity; 141 | }]; 142 | }]; 143 | } 144 | 145 | @end 146 | -------------------------------------------------------------------------------- /FBTweakExample/FBTweakExample-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | com.facebook.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /FBTweakExample/FBTweakExample-Prefix.pch: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | #ifndef __IPHONE_3_0 13 | #warning "This project uses features only available in iOS SDK 3.0 and later." 14 | #endif 15 | 16 | #ifdef __OBJC__ 17 | #import 18 | #import 19 | #endif 20 | -------------------------------------------------------------------------------- /FBTweakExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "40x40", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "60x60", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "29x29", 21 | "scale" : "1x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "29x29", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "40x40", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "40x40", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "76x76", 41 | "scale" : "1x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "76x76", 46 | "scale" : "2x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /FBTweakExample/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "orientation" : "portrait", 20 | "idiom" : "ipad", 21 | "extent" : "full-screen", 22 | "minimum-system-version" : "7.0", 23 | "scale" : "1x" 24 | }, 25 | { 26 | "orientation" : "landscape", 27 | "idiom" : "ipad", 28 | "extent" : "full-screen", 29 | "minimum-system-version" : "7.0", 30 | "scale" : "1x" 31 | }, 32 | { 33 | "orientation" : "portrait", 34 | "idiom" : "ipad", 35 | "extent" : "full-screen", 36 | "minimum-system-version" : "7.0", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "orientation" : "landscape", 41 | "idiom" : "ipad", 42 | "extent" : "full-screen", 43 | "minimum-system-version" : "7.0", 44 | "scale" : "2x" 45 | } 46 | ], 47 | "info" : { 48 | "version" : 1, 49 | "author" : "xcode" 50 | } 51 | } -------------------------------------------------------------------------------- /FBTweakExample/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /FBTweakExample/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | #import "FBAppDelegate.h" 13 | 14 | int main(int argc, char * argv[]) 15 | { 16 | @autoreleasepool { 17 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([FBAppDelegate class])); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /FBTweakTests/FBTweakInlineTestsARC.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import 12 | 13 | #import "FBTweakInline.h" 14 | 15 | #if !__has_feature(objc_arc) 16 | #error ARC is required. 17 | #endif 18 | 19 | typedef NS_ENUM(unsigned long, UnsignedLongEnum) { 20 | UnsignedLongEnumOff, 21 | UnsignedLongEnumVerbose, 22 | UnsignedLongEnumInfo, 23 | UnsignedLongEnumWarn, 24 | UnsignedLongEnumError, 25 | }; 26 | 27 | @interface FBTweakTestObject : NSObject 28 | 29 | @property (nonatomic, assign, readwrite) UnsignedLongEnum unsignedLongProperty; 30 | 31 | @end 32 | 33 | @implementation FBTweakTestObject 34 | 35 | @end 36 | 37 | 38 | @interface FBTweakInlineTestsARC : XCTestCase 39 | 40 | @end 41 | 42 | @implementation FBTweakInlineTestsARC 43 | 44 | - (void)setUp 45 | { 46 | [[FBTweakStore sharedInstance] reset]; 47 | } 48 | 49 | - (void)testValueTypes 50 | { 51 | __attribute__((unused)) short testShort = FBTweakValue(@"Short", @"Short", @"Short", -1); 52 | XCTAssertEqual(testShort, (short)-1, @"Short %d", testShort); 53 | 54 | __attribute__((unused)) unsigned short testUnsignedShort = FBTweakValue(@"Unsigned Short", @"Unsigned Short", @"Unsigned Short", 1); 55 | XCTAssertEqual(testUnsignedShort, (unsigned short)1, @"Unsigned Short %d", testUnsignedShort); 56 | 57 | __attribute__((unused)) int testInt = FBTweakValue(@"Int", @"Int", @"Int", -1); 58 | XCTAssertEqual(testInt, (int)-1, @"Int %d", testInt); 59 | 60 | __attribute__((unused)) unsigned int testUnsignedInt = FBTweakValue(@"Unsigned Int", @"Unsigned Int", @"Unsigned Int", 1); 61 | XCTAssertEqual(testUnsignedInt, (unsigned int)1, @"Unsigned Int %d", testUnsignedInt); 62 | 63 | __attribute__((unused)) long testLong = FBTweakValue(@"Long", @"Long", @"Long", -1); 64 | XCTAssertEqual(testLong, (long)-1, @"Long %ld", testLong); 65 | 66 | __attribute__((unused)) unsigned long testUnsignedLong = FBTweakValue(@"Unsigned Long", @"Unsigned Long", @"Unsigned Long", 1); 67 | XCTAssertEqual(testUnsignedLong, (unsigned long)1, @"Unsigned Long %lu", testUnsignedLong); 68 | 69 | __attribute__((unused)) long long testLongLong = FBTweakValue(@"Long Long", @"Long Long", @"Long Long", -1); 70 | XCTAssertEqual(testLongLong, (long long)-1, @"Long Long %lld", testLongLong); 71 | 72 | __attribute__((unused)) unsigned long long testUnsignedLongLong = FBTweakValue(@"Unsigned Long Long", @"Unsigned Long Long", @"Unsigned Long Long", 1); 73 | XCTAssertEqual(testUnsignedLongLong, (unsigned long long)1, @"Unsigned Long Long %llu", testUnsignedLongLong); 74 | 75 | __attribute__((unused)) float testFloat = FBTweakValue(@"Float", @"Float", @"Float", 1.0); 76 | XCTAssertEqual(testFloat, (float)1.0, @"Float %f", testFloat); 77 | 78 | __attribute__((unused)) BOOL testBool = FBTweakValue(@"BOOL", @"BOOL", @"BOOL", YES); 79 | XCTAssertEqual(testBool, (BOOL)YES, @"Bool %d", testBool); 80 | 81 | __attribute__((unused)) const char *testString = FBTweakValue(@"String", @"String", @"String", "one"); 82 | XCTAssertEqual(strcmp(testString, "one"), 0, @"String %s", testString); 83 | 84 | __attribute__((unused)) NSString *testNSString = FBTweakValue(@"NSString", @"NSString", @"NSString", @"one"); 85 | XCTAssertEqualObjects(testNSString, @"one", @"NSString %@", testNSString); 86 | 87 | __attribute__((unused)) UIColor *testUIColor = FBTweakValue(@"UIColor", @"UIColor", @"UIColor", [UIColor redColor]); 88 | XCTAssertEqualObjects(testUIColor, [UIColor redColor], @"UIColor %@", testUIColor); 89 | 90 | __attribute__((unused)) NSString *testNSArray = FBTweakValue(@"NSArray", @"NSArray", @"NSArray", @"two", (@[@"one", @"two", @"three"])); 91 | XCTAssertEqualObjects(testNSArray, @"two", @"NSArray %@", testNSArray); 92 | 93 | __attribute__((unused)) NSString *testNSDictionary = FBTweakValue(@"NSDictionary", @"NSDictionary", @"NSDictionary", @"key2", (@{@"key1":@"value1", @"key2":@"value2"})); 94 | XCTAssertEqualObjects(testNSDictionary, @"key2", @"NSString %@", testNSDictionary); 95 | } 96 | 97 | - (void)testConstantValues 98 | { 99 | static const double staticConstInput = 1.0; 100 | double staticConstValue = FBTweakValue(@"Static", @"Static", @"Static", staticConstInput); 101 | XCTAssertEqual(staticConstValue, staticConstInput, @"Static %f %f", staticConstInput, staticConstValue); 102 | } 103 | 104 | // All values should be converted to the same type as the default. 105 | - (void)testMixedRangeTypes 106 | { 107 | FBTweak *mixedFloatTweak = FBTweakInline(@"Mixed Float", @"Mixed Float", @"Mixed Float", (float)1.0, (double)1.0, (long)1.0); 108 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedFloatTweak.defaultValue objCType]], @"f", @"Mixed Float Default %s", [mixedFloatTweak.defaultValue objCType]); 109 | XCTAssertEqual([mixedFloatTweak.defaultValue floatValue], (float)1.0, @"Mixed Float Default %@", mixedFloatTweak.defaultValue); 110 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedFloatTweak.minimumValue objCType]], @"f", @"Mixed Float Minimum %s", [mixedFloatTweak.minimumValue objCType]); 111 | XCTAssertEqual([mixedFloatTweak.minimumValue floatValue], (float)1.0, @"Mixed Float Minimum %@", mixedFloatTweak.minimumValue); 112 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedFloatTweak.maximumValue objCType]], @"f", @"Mixed Float Maximum %s", [mixedFloatTweak.maximumValue objCType]); 113 | XCTAssertEqual([mixedFloatTweak.maximumValue floatValue], (float)1.0, @"Mixed Float Maximum %@", mixedFloatTweak.maximumValue); 114 | 115 | FBTweak *mixedIntTweak = FBTweakInline(@"Mixed Int", @"Mixed Int", @"Mixed Int", (int)1, (char)1, (double)1); 116 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedIntTweak.defaultValue objCType]], @"i", @"Mixed Int Default %@", mixedIntTweak.defaultValue); 117 | XCTAssertEqual([mixedIntTweak.defaultValue floatValue], (int)1, @"Mixed Int Default %@", mixedIntTweak.defaultValue); 118 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedIntTweak.minimumValue objCType]], @"i", @"Mixed Int Minimum %@", mixedIntTweak.minimumValue); 119 | XCTAssertEqual([mixedIntTweak.minimumValue floatValue], (int)1, @"Mixed Int Minimum %@", mixedIntTweak.minimumValue); 120 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedIntTweak.maximumValue objCType]], @"i", @"Mixed Int Maximum %@", mixedIntTweak.maximumValue); 121 | XCTAssertEqual([mixedIntTweak.maximumValue floatValue], (int)1, @"Mixed Int Maximum %@", mixedIntTweak.maximumValue); 122 | } 123 | 124 | // Actions use variables so they can work in the global scope, test for name conflicts. 125 | - (void)testMultipleActions 126 | { 127 | FBTweakAction(@"Action", @"Action", @"One", ^{ 128 | NSLog(@"Action One"); 129 | }); 130 | 131 | FBTweakAction(@"Action", @"Action", @"Two", ^{ 132 | NSLog(@"Action Two"); 133 | }); 134 | } 135 | 136 | - (void)testBind 137 | { 138 | NSMutableURLRequest *v = [[NSMutableURLRequest alloc] init]; 139 | FBTweakBind(v, timeoutInterval, @"URL", @"Request", @"Bind", 5.0); 140 | XCTAssertEqual(v.timeoutInterval, (NSTimeInterval)5.0, @"request %@", v); 141 | 142 | FBTweak *m = FBTweakInline(@"URL", @"Request", @"Bind", 5.0); 143 | m.currentValue = @(20.0); 144 | XCTAssertEqual(v.timeoutInterval, (NSTimeInterval)20.0, @"request %@ %@", v, m); 145 | 146 | FBTweakTestObject *o = [FBTweakTestObject new]; 147 | FBTweakBind(o, unsignedLongProperty, @"Test", @"Object", @"Long", UnsignedLongEnumInfo, UnsignedLongEnumOff, UnsignedLongEnumError); 148 | XCTAssertEqual(o.unsignedLongProperty, UnsignedLongEnumInfo, @"test object: %@", @(o.unsignedLongProperty)); 149 | 150 | FBTweak *oTweak = FBTweakInline(@"Test", @"Object", @"Long", UnsignedLongEnumInfo); 151 | oTweak.currentValue = @(UnsignedLongEnumWarn); 152 | XCTAssertEqual(o.unsignedLongProperty, UnsignedLongEnumWarn, @"test object: %@", @(o.unsignedLongProperty)); 153 | } 154 | 155 | @end 156 | -------------------------------------------------------------------------------- /FBTweakTests/FBTweakInlineTestsMRR.m: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2014-present, Facebook, Inc. 3 | All rights reserved. 4 | 5 | This source code is licensed under the BSD-style license found in the 6 | LICENSE file in the root directory of this source tree. An additional grant 7 | of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import 12 | 13 | #import "FBTweakInline.h" 14 | 15 | #if __has_feature(objc_arc) 16 | #error ARC is disallowed. 17 | #endif 18 | 19 | @interface FBTweakInlineTestsMRR : XCTestCase 20 | 21 | @end 22 | 23 | @implementation FBTweakInlineTestsMRR 24 | 25 | - (void)setUp 26 | { 27 | [[FBTweakStore sharedInstance] reset]; 28 | } 29 | 30 | - (void)testValueTypes 31 | { 32 | __attribute__((unused)) short testShort = FBTweakValue(@"Short", @"Short", @"Short", -1); 33 | XCTAssertEqual(testShort, (short)-1, @"Short %d", testShort); 34 | 35 | __attribute__((unused)) unsigned short testUnsignedShort = FBTweakValue(@"Unsigned Short", @"Unsigned Short", @"Unsigned Short", 1); 36 | XCTAssertEqual(testUnsignedShort, (unsigned short)1, @"Unsigned Short %d", testUnsignedShort); 37 | 38 | __attribute__((unused)) int testInt = FBTweakValue(@"Int", @"Int", @"Int", -1); 39 | XCTAssertEqual(testInt, (int)-1, @"Int %d", testInt); 40 | 41 | __attribute__((unused)) unsigned int testUnsignedInt = FBTweakValue(@"Unsigned Int", @"Unsigned Int", @"Unsigned Int", 1); 42 | XCTAssertEqual(testUnsignedInt, (unsigned int)1, @"Unsigned Int %d", testUnsignedInt); 43 | 44 | __attribute__((unused)) long long testLongLong = FBTweakValue(@"Long Long", @"Long Long", @"Long Long", -1); 45 | XCTAssertEqual(testLongLong, (long long)-1, @"Long Long %lld", testLongLong); 46 | 47 | __attribute__((unused)) unsigned long long testUnsignedLongLong = FBTweakValue(@"Unsigned Long Long", @"Unsigned Long Long", @"Unsigned Long Long", 1); 48 | XCTAssertEqual(testUnsignedLongLong, (unsigned long long)1, @"Unsigned Long Long %llu", testUnsignedLongLong); 49 | 50 | __attribute__((unused)) float testFloat = FBTweakValue(@"Float", @"Float", @"Float", 1.0); 51 | XCTAssertEqual(testFloat, (float)1.0, @"Float %f", testFloat); 52 | 53 | __attribute__((unused)) BOOL testBool = FBTweakValue(@"BOOL", @"BOOL", @"BOOL", YES); 54 | XCTAssertEqual(testBool, (BOOL)YES, @"Bool %d", testBool); 55 | 56 | __attribute__((unused)) const char *testString = FBTweakValue(@"String", @"String", @"String", "one"); 57 | XCTAssertEqual(strcmp(testString, "one"), 0, @"String %s", testString); 58 | 59 | __attribute__((unused)) NSString *testNSString = FBTweakValue(@"NSString", @"NSString", @"NSString", @"one"); 60 | XCTAssertEqualObjects(testNSString, @"one", @"NSString %@", testNSString); 61 | 62 | __attribute__((unused)) UIColor *testUIColor = FBTweakValue(@"UIColor", @"UIColor", @"UIColor", [UIColor redColor]); 63 | XCTAssertEqualObjects([UIColor redColor], [UIColor redColor], @"UIColor %@", testUIColor); 64 | 65 | __attribute__((unused)) NSString *testNSArray = FBTweakValue(@"NSArray", @"NSArray", @"NSArray", @"two", (@[@"one", @"two", @"three"])); 66 | XCTAssertEqualObjects(testNSArray, @"two", @"NSArray %@", testNSArray); 67 | 68 | __attribute__((unused)) NSString *testNSDictionary = FBTweakValue(@"NSDictionary", @"NSDictionary", @"NSDictionary", @"key2", (@{@"key1":@"value1", @"key2":@"value2"})); 69 | XCTAssertEqualObjects(testNSDictionary, @"key2", @"NSString %@", testNSDictionary); 70 | } 71 | 72 | - (void)testConstantValues 73 | { 74 | static const double staticConstInput = 1.0; 75 | double staticConstValue = FBTweakValue(@"Static", @"Static", @"Static", staticConstInput); 76 | XCTAssertEqual(staticConstValue, staticConstInput, @"Static %f %f", staticConstInput, staticConstValue); 77 | } 78 | 79 | // All values should be converted to the same type as the default. 80 | - (void)testMixedRangeTypes 81 | { 82 | FBTweak *mixedFloatTweak = FBTweakInline(@"Mixed Float", @"Mixed Float", @"Mixed Float", (float)1.0, (double)1.0, (long)1.0); 83 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedFloatTweak.defaultValue objCType]], @"f", @"Mixed Float Default %s", [mixedFloatTweak.defaultValue objCType]); 84 | XCTAssertEqual([mixedFloatTweak.defaultValue floatValue], (float)1.0, @"Mixed Float Default %@", mixedFloatTweak.defaultValue); 85 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedFloatTweak.minimumValue objCType]], @"f", @"Mixed Float Minimum %s", [mixedFloatTweak.minimumValue objCType]); 86 | XCTAssertEqual([mixedFloatTweak.minimumValue floatValue], (float)1.0, @"Mixed Float Minimum %@", mixedFloatTweak.minimumValue); 87 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedFloatTweak.maximumValue objCType]], @"f", @"Mixed Float Maximum %s", [mixedFloatTweak.maximumValue objCType]); 88 | XCTAssertEqual([mixedFloatTweak.maximumValue floatValue], (float)1.0, @"Mixed Float Maximum %@", mixedFloatTweak.maximumValue); 89 | 90 | FBTweak *mixedIntTweak = FBTweakInline(@"Mixed Int", @"Mixed Int", @"Mixed Int", (int)1, (char)1, (double)1); 91 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedIntTweak.defaultValue objCType]], @"i", @"Mixed Int Default %@", mixedIntTweak.defaultValue); 92 | XCTAssertEqual([mixedIntTweak.defaultValue floatValue], (int)1, @"Mixed Int Default %@", mixedIntTweak.defaultValue); 93 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedIntTweak.minimumValue objCType]], @"i", @"Mixed Int Minimum %@", mixedIntTweak.minimumValue); 94 | XCTAssertEqual([mixedIntTweak.minimumValue floatValue], (int)1, @"Mixed Int Minimum %@", mixedIntTweak.minimumValue); 95 | XCTAssertEqualObjects([NSString stringWithUTF8String:[mixedIntTweak.maximumValue objCType]], @"i", @"Mixed Int Maximum %@", mixedIntTweak.maximumValue); 96 | XCTAssertEqual([mixedIntTweak.maximumValue floatValue], (int)1, @"Mixed Int Maximum %@", mixedIntTweak.maximumValue); 97 | } 98 | 99 | // Actions use variables so they can work in the global scope, test for name conflicts. 100 | - (void)testMultipleActions 101 | { 102 | FBTweakAction(@"Action", @"Action", @"One", ^{ 103 | NSLog(@"Action One"); 104 | }); 105 | 106 | FBTweakAction(@"Action", @"Action", @"Two", ^{ 107 | NSLog(@"Action Two"); 108 | }); 109 | } 110 | 111 | - (void)testBind 112 | { 113 | NSMutableURLRequest *v = [[NSMutableURLRequest alloc] init]; 114 | FBTweakBind(v, timeoutInterval, @"URL", @"Request", @"Bind", 5.0); 115 | XCTAssertEqual(v.timeoutInterval, (NSTimeInterval)5.0, @"request %@", v); 116 | 117 | FBTweak *m = FBTweakInline(@"URL", @"Request", @"Bind", 5.0); 118 | m.currentValue = @(20.0); 119 | XCTAssertEqual(v.timeoutInterval, (NSTimeInterval)20.0, @"request %@ %@", v, m); 120 | } 121 | 122 | @end 123 | -------------------------------------------------------------------------------- /FBTweakTests/FBTweakTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.facebook.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /FBTweakTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Images/Tweaks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/Tweaks/e5c78a6ca32fc58ea80399a0e9eaddd5f60e854e/Images/Tweaks.gif -------------------------------------------------------------------------------- /Images/Tweaks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/Tweaks/e5c78a6ca32fc58ea80399a0e9eaddd5f60e854e/Images/Tweaks.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For Tweaks software 4 | 5 | Copyright (c) 2014, Facebook, Inc. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights Version 2 2 | 3 | "Software" means the Tweaks software distributed by Facebook, Inc. 4 | 5 | Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software 6 | ("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable 7 | (subject to the termination provision below) license under any Necessary 8 | Claims, to make, have made, use, sell, offer to sell, import, and otherwise 9 | transfer the Software. For avoidance of doubt, no license is granted under 10 | Facebook’s rights in any patent claims that are infringed by (i) modifications 11 | to the Software made by you or any third party or (ii) the Software in 12 | combination with any software or other technology. 13 | 14 | The license granted hereunder will terminate, automatically and without notice, 15 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate 16 | directly or indirectly, or take a direct financial interest in, any Patent 17 | Assertion: (i) against Facebook or any of its subsidiaries or corporate 18 | affiliates, (ii) against any party if such Patent Assertion arises in whole or 19 | in part from any software, technology, product or service of Facebook or any of 20 | its subsidiaries or corporate affiliates, or (iii) against any party relating 21 | to the Software. Notwithstanding the foregoing, if Facebook or any of its 22 | subsidiaries or corporate affiliates files a lawsuit alleging patent 23 | infringement against you in the first instance, and you respond by filing a 24 | patent infringement counterclaim in that lawsuit against that party that is 25 | unrelated to the Software, the license granted hereunder will not terminate 26 | under section (i) of this paragraph due to such counterclaim. 27 | 28 | A "Necessary Claim" is a claim of a patent owned by Facebook that is 29 | necessarily infringed by the Software standing alone. 30 | 31 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, 32 | or contributory infringement or inducement to infringe any patent, including a 33 | cross-claim or counterclaim. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tweaks 2 | Tweaks is an easy way to fine-tune an iOS app. 3 | [![Build Status](https://travis-ci.org/facebook/Tweaks.svg?branch=master)](https://travis-ci.org/facebook/Tweaks) 4 | 5 | ![Tweaks](https://github.com/facebook/Tweaks/blob/master/Images/Tweaks.gif?raw=true) 6 | 7 | ## Why 8 | The best way to improve an app is to use it every day. Even when ideas can be tested out in advance — for example, with [Origami](http://origami.facebook.com) — it can still take some time with the app to see how it works in practice. 9 | 10 | Occasionally, it's perfect the first try. Sometimes, the idea doesn't work at all. But often, it just needs a few minor adjustments. That last case is where Tweaks fits in. Tweaks makes those small adjustments easy: with no code changes and no computer, you can try out different options and decide which works best. 11 | 12 | Some of the most useful parameters to adjust are animation timings, velocity thresholds, colors, and physics constants. At Facebook, we also use tweaks to temporarily disable new features during development. That way, the designers and engineers involved can enable it on just their devices, without getting in the way of others testing the app. 13 | 14 | Tweaks was invaluable for building [Paper](http://www.facebook.com/paper). We hope it can be useful for your app too. 15 | 16 | ## Usage 17 | Each configurable value is called a tweak. There's a few ways to set them up, found in `FBTweakInline.h`. 18 | 19 | ### Value 20 | The simplest way to create a tweak is to replace a constant with `FBTweakValue`: 21 | 22 | ```objective-c 23 | CGFloat animationDuration = FBTweakValue(@"Category", @"Group", @"Duration", 0.5); 24 | ``` 25 | 26 | The first three parameters are where the tweak is listed and what it's called, and the last one is the default value. You can pass in many types of values for the default: booleans, numbers, or strings. 27 | 28 | ```objective-c 29 | if (FBTweakValue(@"Category", @"Feature", @"Enabled", YES)) { 30 | label.text = FBTweakValue(@"Category", @"Group", @"Text", @"Tweaks example."); 31 | } 32 | ``` 33 | 34 | In release builds, the `FBTweakValue` macro expands to just the default value, so there's no performance impact. In debug builds, though, it fetches the latest value of the tweak. 35 | 36 | You can also pass a fifth parameter, which will constrain the possible values for a tweak. The fifth parameter can be an array, dictionary, or an `FBTweakNumericRange`. If it's a dictionary, the values should be strings to show in the list of choices. Arrays will show the values' `description` as choices. (Note that you have to surround array and dictionary literals with an extra set of parentheses.) 37 | 38 | ```objective-c 39 | self.initialMode = FBTweakValue(@"Header", @"Initial", @"Mode", @(FBSimpleMode), (@{ @(FBSimpleMode) : @"Simple", @(FBAdvancedMode) : @"Advanced" })); 40 | ``` 41 | 42 | For numeric tweaks (`NSInteger`, `CGFloat`, and others), you can instead pass two parameters, which constrain the value to a `FBTweakNumericRange`: 43 | 44 | ```objective-c 45 | self.red = FBTweakValue(@"Header", @"Colors", @"Red", 0.5, 0.0, 1.0); 46 | ``` 47 | 48 | ### Bind 49 | To make tweaks update live, you can use `FBTweakBind`: 50 | 51 | ```objective-c 52 | FBTweakBind(self.headerView, alpha, @"Main Screen", @"Header", @"Alpha", 0.85); 53 | ``` 54 | 55 | The first parameter is the object to bind to, and the second is the property. Whenever the tweak is changed, `self.headerView`'s `alpha` property is updated to match. A few more examples: 56 | 57 | ```objective-c 58 | FBTweakBind(audioPlayer, volume, @"Player", @"Audio", @"Volume", 0.9); 59 | FBTweakBind(webView.scrollView, scrollEnabled, @"Browser", @"Scrolling", @"Enabled", YES); 60 | ``` 61 | 62 | As with `FBTweakValue`, in release builds `FBTweakBind` expands to just setting the property to the default value. 63 | 64 | ## Action 65 | Actions let you run a (global) block when a tweak is selected. To make one, use `FBTweakAction`: 66 | 67 | ```objective-c 68 | FBTweakAction(@"Player", @"Audio", @"Volume", ^{ 69 | NSLog(@"Action selected."); 70 | }); 71 | ``` 72 | 73 | The first three parameters are the standard tweak listing information, and the last is a block to call. You can use `FBTweakAction` in any scope, but the block must be global: it can't depend on any local or instance variables (it wouldn't know which object to adjust). 74 | 75 | Actions are useful for things like launching debug UIs, checking for updates, or (if you make one that intentionally crashes) testing crash reporting. 76 | 77 | ### Tweaks UI 78 | To configure your tweaks, you need a way to show the configuration UI. There's two options for that: 79 | 80 | - Traditionally, tweaks is activated by shaking your phone. To use that, just replace your root `UIWindow` with a `FBTweakShakeWindow`. If you're using Storyboards, you can override `-window` on your app delegate: 81 | 82 | ```objective-c 83 | - (UIWindow *)window 84 | { 85 | if (!_window) { 86 | _window = [[FBTweakShakeWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 87 | } 88 | 89 | return _window; 90 | } 91 | ``` 92 | 93 | - You can present a `FBTweakViewController` from anywhere in your app. Be sure to restrict the activation UI to debug builds! 94 | 95 | ```objective-c 96 | - (void)showTweaks { 97 | FBTweakViewController *tweakVC = [[FBTweakViewController alloc] initWithStore:[FBTweakStore sharedInstance]]; 98 | tweakVC.tweaksDelegate = self; 99 | // Assuming this is in the app delegate 100 | [self.window.rootViewController presentViewController:tweakVC animated:YES completion:nil]; 101 | } 102 | 103 | - (void)tweakViewControllerPressedDone:(FBTweakViewController *)tweakViewController { 104 | [tweakViewController dismissViewControllerAnimated:YES completion:NULL]; 105 | } 106 | ``` 107 | 108 | #### Tweaks UI Dismiss Notification 109 | 110 | Alternatively, when the Tweaks UI is dismissed, you can register your notification center to listen to `FBTweakShakeViewControllerDidDismissNotification`, which can be used after importing `FBTweakViewController.h` 111 | 112 | ### Advanced 113 | You can also access the objects that make up the macros mentioned above. That can be useful for more complex scenarios, like adjusting members of a C structure. 114 | 115 | For example, to manually create a tweak: 116 | 117 | ```objective-c 118 | FBTweak *tweak = [[FBTweak alloc] initWithIdentifier:@"com.tweaks.example.advanced"]; 119 | tweak.name = @"Advanced Settings"; 120 | tweak.defaultValue = @NO; 121 | 122 | FBTweakStore *store = [FBTweakStore sharedInstance]; 123 | FBTweakCategory *category = [[FBTweakCategory alloc] initWithName:@"Settings"]; 124 | [store addTweakCategory:category]; 125 | FBTweakCollection *collection = [[FBTweakCollection alloc] initWithName:@"Enable"]; 126 | [category addTweakCollection:collection]; 127 | [collection addTweak:tweak]; 128 | 129 | [tweak addObserver:self]; 130 | ``` 131 | 132 | Then, you can watch for when the tweak changes: 133 | 134 | ```objective-c 135 | - (void)tweakDidChange:(FBTweak *)tweak 136 | { 137 | self.advancedSettingsEnabled = ![tweak.currentValue boolValue]; 138 | } 139 | ``` 140 | 141 | Also you have de ability to implement the optional method `tweakWillChange:` in order to handle the previous value of your tweak: 142 | 143 | ```objective-c 144 | - (void)tweakWillChange:(FBTweak *)tweak 145 | { 146 | NSLog(@"%@", tweak.currentValue); // Here current value is the previous value of the tweak 147 | } 148 | ``` 149 | 150 | To override when tweaks are enabled, you can define the `FB_TWEAK_ENABLED` macro. It's suggested to avoid including them when submitting to the App Store. 151 | 152 | ### Using from a Swift Project 153 | 154 | *Khan Academy's project [SwiftTweaks](http://engineering.khanacademy.org/posts/introducing-swifttweaks.htm) is designed for Swift, and might be a better choice for Swift projects.* 155 | 156 | Tweaks can be used from Swift projects. In this case the handy shortcut macros defined in `FBTweakInline.h` are not available, meaning tweaks need to be created programmatically, similar to this example: 157 | 158 | ```swift 159 | let tweak = FBTweak(identifier: "com.tweaks.example.advanced") 160 | tweak.name = "Advanced settings" 161 | tweak.defaultValue = false 162 | 163 | let collection = FBTweakCollection(name: "Enable"); 164 | collection.addTweak(tweak) 165 | 166 | let category = FBTweakCategory(name: "Settings") 167 | category.addTweakCollection(collection); 168 | 169 | let store = FBTweakStore.sharedInstance() 170 | store.addTweakCategory(category) 171 | 172 | tweak.addObserver(self) 173 | ``` 174 | 175 | After setting up a tweak you can watch for when it changes: 176 | 177 | ```swift 178 | func tweakDidChange(tweak: FBTweak!) 179 | { 180 | self.advancedSettingsEnabled = tweak.currentValue as Bool; 181 | } 182 | ``` 183 | 184 | ### How it works 185 | In debug builds, the tweak macros use `__attribute__((section))` to statically store data about each tweak in the `__FBTweak` section of the mach-o. Tweaks loads that data at startup and loads the latest values from `NSUserDefaults`. 186 | 187 | In release builds, the macros just expand to the default value. Nothing extra is included in the binary. 188 | 189 | ## Installation 190 | There are two options: 191 | 192 | 1. Tweaks is available as `Tweaks` in [CocoaPods](http://cocoapods.org). (If you have issues with custom Xcode configurations, [this comment](https://github.com/facebook/Tweaks/issues/4#issuecomment-40629741) might help.) 193 | 2. Manually add the files from `FBTweak/` into your Xcode project. Slightly simpler, but updates are also manual. 194 | 195 | Tweaks requires iOS 6 or later. 196 | 197 | There's also a demo project available. To use it, make sure to open `FBTweakExample.xcworkspace` (rather than the `.xcodeproj`) so the dependencies build correctly. 198 | 199 | ## Contributing 200 | See the CONTRIBUTING file for how to help out. 201 | 202 | ## License 203 | Tweaks is BSD-licensed. We also provide an additional patent grant. 204 | -------------------------------------------------------------------------------- /Tweaks.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'Tweaks' 3 | spec.version = '2.3.1' 4 | spec.license = { :type => 'BSD' } 5 | spec.homepage = 'https://github.com/facebook/Tweaks' 6 | spec.authors = { 'Grant Paul' => 'tweaks@grantpaul.com', 'Kimon Tsinteris' => 'kimon@mac.com' } 7 | spec.summary = 'Easily adjust parameters for iOS apps in development.' 8 | spec.source = { :git => 'https://github.com/facebook/Tweaks.git', :tag => '2.3.1' } 9 | spec.source_files = 'FBTweak/*.{h,m}' 10 | spec.requires_arc = true 11 | spec.social_media_url = 'https://twitter.com/fbOpenSource' 12 | spec.framework = 'MessageUI' 13 | 14 | spec.ios.deployment_target = '8.0' 15 | end 16 | --------------------------------------------------------------------------------