Просмотр исходного кода

FLEXArgumentInputView refactor part 2.

Rather than passing string arguments to the method calling utility, pass the actual objects or NSValue boxed primitives.
Ryan Olson лет назад: 12
Родитель
Сommit
31d9209dd8

+ 13 - 0
Classes/Editing/Argument Input Views/FLEXArgumentInputJSONObjectView.h

@@ -0,0 +1,13 @@
+//
+//  FLEXArgumentInputJSONObjectView.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 6/15/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXArgumentInputTextView.h"
+
+@interface FLEXArgumentInputJSONObjectView : FLEXArgumentInputTextView
+
+@end

+ 64 - 0
Classes/Editing/Argument Input Views/FLEXArgumentInputJSONObjectView.m

@@ -0,0 +1,64 @@
+//
+//  FLEXArgumentInputJSONObjectView.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 6/15/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXArgumentInputJSONObjectView.h"
+#import "FLEXRuntimeUtility.h"
+
+@implementation FLEXArgumentInputJSONObjectView
+
+- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
+{
+    self = [super initWithArgumentTypeEncoding:typeEncoding];
+    if (self) {
+        // Start with the numbers and punctuation keyboard since quotes, curly braces, or
+        // square brackets are likely to be the first characters type for the JSON.
+        self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
+    }
+    return self;
+}
+
+- (void)setInputOutput:(id)inputOutput
+{
+    self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:inputOutput];
+}
+
+- (id)inputOutput
+{
+    return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
+}
+
++ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
+{
+    // Must be object type.
+    BOOL supported = type[0] == '@';
+    
+    if (supported) {
+        if (value) {
+            // If there's a current value, it must be serializable to JSON
+            supported = [FLEXRuntimeUtility editableJSONStringForObject:value] != nil;
+        } else {
+            // Otherwise, see if we have more type information than just 'id'.
+            // If we do, make sure the encoding is something serializable to JSON.
+            // Properties and ivars keep more detailed type encoding information than method arguments.
+            if (strcmp(type, @encode(id)) != 0) {
+                BOOL isJSONSerializableType = NO;
+                // Note: we can't use @encode(NSString) here because that drops the string information and just goes to @encode(id).
+                isJSONSerializableType = isJSONSerializableType || strcmp(type, "@\"NSString\"") == 0;
+                isJSONSerializableType = isJSONSerializableType || strcmp(type, "@\"NSNumber\"") == 0;
+                isJSONSerializableType = isJSONSerializableType || strcmp(type, "@\"NSArray\"") == 0;
+                isJSONSerializableType = isJSONSerializableType || strcmp(type, "@\"NSDictionary\"") == 0;
+                
+                supported = isJSONSerializableType;
+            }
+        }
+    }
+    
+    return supported;
+}
+
+@end

+ 13 - 0
Classes/Editing/Argument Input Views/FLEXArgumentInputNumberView.h

@@ -0,0 +1,13 @@
+//
+//  FLEXArgumentInputNumberView.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 6/15/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXArgumentInputTextView.h"
+
+@interface FLEXArgumentInputNumberView : FLEXArgumentInputTextView
+
+@end

+ 56 - 0
Classes/Editing/Argument Input Views/FLEXArgumentInputNumberView.m

@@ -0,0 +1,56 @@
+//
+//  FLEXArgumentInputNumberView.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 6/15/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXArgumentInputNumberView.h"
+#import "FLEXRuntimeUtility.h"
+
+@implementation FLEXArgumentInputNumberView
+
+- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
+{
+    self = [super initWithArgumentTypeEncoding:typeEncoding];
+    if (self) {
+        self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
+    }
+    return self;
+}
+
+- (void)setInputOutput:(id)inputOutput
+{
+    if ([inputOutput respondsToSelector:@selector(stringValue)]) {
+        self.inputTextView.text = [inputOutput stringValue];
+    }
+}
+
+- (id)inputOutput
+{
+    return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding fromInputString:self.inputTextView.text];
+}
+
++ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
+{
+    static NSArray *primitiveTypes = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        primitiveTypes = @[@(@encode(char)),
+                           @(@encode(int)),
+                           @(@encode(short)),
+                           @(@encode(long)),
+                           @(@encode(long long)),
+                           @(@encode(unsigned char)),
+                           @(@encode(unsigned int)),
+                           @(@encode(unsigned short)),
+                           @(@encode(unsigned long)),
+                           @(@encode(unsigned long long)),
+                           @(@encode(float)),
+                           @(@encode(double))];
+    });
+    return [primitiveTypes containsObject:@(type)];
+}
+
+@end

+ 4 - 0
Classes/Editing/Argument Input Views/FLEXArgumentInputTextView.h

@@ -10,4 +10,8 @@
 
 @interface FLEXArgumentInputTextView : FLEXArgumentInputView
 
+// For subclass eyes only
+
+@property (nonatomic, strong, readonly) UITextView *inputTextView;
+
 @end

+ 0 - 13
Classes/Editing/Argument Input Views/FLEXArgumentInputTextView.m

@@ -35,19 +35,6 @@
 }
 
 
-#pragma mark - Input/Output
-
-- (void)setInputOutput:(id)inputOutput
-{
-    self.inputTextView.text = inputOutput;
-}
-
-- (id)inputOutput
-{
-    return [self.inputTextView.text copy];
-}
-
-
 #pragma mark - Layout and Sizing
 
 - (void)layoutSubviews

+ 6 - 1
Classes/Editing/Argument Input Views/FLEXArgumentInputView.h

@@ -23,7 +23,8 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
 /// To populate the filed with an initial value, set this property.
 /// To reteive the value input by the user, access the property.
 /// Primitive types and structs should/will be boxed in NSValue containers.
-@property (nonatomic, strong) id inputOutput;
+/// Concrete subclasses *must* override both the setter and getter for this property.
+@property (nonatomic) id inputOutput;
 
 /// Setting this value to large will make some argument input views increase the size of their input field(s).
 /// Useful to increase the use of space if there is only one input view on screen (i.e. for property and ivar editing).
@@ -34,6 +35,10 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
 /// If the input view has one or more text views, returns YES when one of them is focused.
 @property (nonatomic, readonly) BOOL inputViewIsFirstResponder;
 
+/// For subclasses to indicate that they can handle editing a field the give type and value.
+/// Used by FLEXArgumentInputViewFactory to create appropriate input views.
++ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value;
+
 // For subclass eyes only
 
 @property (nonatomic, strong, readonly) UILabel *titleLabel;

+ 16 - 0
Classes/Editing/Argument Input Views/FLEXArgumentInputView.m

@@ -73,6 +73,22 @@
     return NO;
 }
 
+- (void)setInputOutput:(id)inputOutput
+{
+    // Subclasses should override.
+}
+
+- (id)inputOutput
+{
+    // Subclasses should override.
+    return nil;
+}
+
++ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
+{
+    return NO;
+}
+
 
 #pragma mark - Class Helpers
 

+ 5 - 0
Classes/Editing/Argument Input Views/FLEXArgumentInputViewFactory.h

@@ -12,6 +12,11 @@
 
 @interface FLEXArgumentInputViewFactory : NSObject
 
+/// The main factory method for making argument input view subclasses that are the best fit for the type.
 + (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding;
 
+/// A way to check if we should try editing a filed given its type encoding and value.
+/// Useful when deciding whether to edit or explore a property, ivar, or NSUserDefaults value.
++ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue;
+
 @end

+ 30 - 2
Classes/Editing/Argument Input Views/FLEXArgumentInputViewFactory.m

@@ -8,13 +8,41 @@
 
 #import "FLEXArgumentInputViewFactory.h"
 #import "FLEXArgumentInputView.h"
-#import "FLEXArgumentInputTextView.h"
+#import "FLEXArgumentInputJSONObjectView.h"
+#import "FLEXArgumentInputNumberView.h"
 
 @implementation FLEXArgumentInputViewFactory
 
 + (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding
 {
-    return [[FLEXArgumentInputTextView alloc] initWithArgumentTypeEncoding:typeEncoding];
+    Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:nil];
+    if (!subclass) {
+        // Fall back to a generic FLEXArgumentInputView if we can't find a subclass that supports the type.
+        // The generic input view does not actually allow input, but it still shows the title of the field.
+        subclass = [FLEXArgumentInputView class];
+    }
+    return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding];
+}
+
++ (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
+{
+    Class argumentInputViewSubclass = nil;
+    
+    // Note that order is important here since multiple subclasses may support the same type.
+    // An example is the number subclass and the bool subclass for the type @encode(BOOL).
+    // Both work, but we'd prefer to use the bool subclass.
+    if ([FLEXArgumentInputNumberView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
+        argumentInputViewSubclass = [FLEXArgumentInputNumberView class];
+    } else if ([FLEXArgumentInputJSONObjectView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
+        argumentInputViewSubclass = [FLEXArgumentInputJSONObjectView class];
+    }
+    
+    return argumentInputViewSubclass;
+}
+
++ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
+{
+    return [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue] != nil;
 }
 
 @end

+ 9 - 9
Classes/Editing/FLEXDefaultEditorViewController.m

@@ -10,6 +10,7 @@
 #import "FLEXFieldEditorView.h"
 #import "FLEXRuntimeUtility.h"
 #import "FLEXArgumentInputView.h"
+#import "FLEXArgumentInputViewFactory.h"
 
 @interface FLEXDefaultEditorViewController ()
 
@@ -41,32 +42,31 @@
     
     self.fieldEditorView.fieldDescription = self.key;
     
-    [self updateTextFieldString];
+    FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(id)];
+    inputView.backgroundColor = self.view.backgroundColor;
+    inputView.targetSize = FLEXArgumentInputViewSizeLarge;
+    inputView.inputOutput = [self.defaults objectForKey:self.key];
+    self.fieldEditorView.argumentInputViews = @[inputView];
 }
 
 - (void)actionButtonPressed:(id)sender
 {
     [super actionButtonPressed:sender];
     
-    id value = [FLEXRuntimeUtility objectValueFromEditableString:self.firstInputView.inputOutput];
+    id value = self.firstInputView.inputOutput;
     if (value) {
         [self.defaults setObject:value forKey:self.key];
     } else {
         [self.defaults removeObjectForKey:self.key];
     }
     [self.defaults synchronize];
-    [self updateTextFieldString];
-}
 
-- (void)updateTextFieldString
-{
-    id defaultsValue = [self.defaults objectForKey:self.key];
-    self.firstInputView.inputOutput = [FLEXRuntimeUtility editiableDescriptionForObject:defaultsValue];
+    self.firstInputView.inputOutput = [self.defaults objectForKey:self.key];
 }
 
 + (BOOL)canEditDefaultWithValue:(id)currentValue
 {
-    return !currentValue || [FLEXRuntimeUtility editiableDescriptionForObject:currentValue] != nil;
+    return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:@encode(id) currentValue:currentValue];
 }
 
 @end

+ 0 - 3
Classes/Editing/FLEXFieldEditorViewController.h

@@ -15,9 +15,6 @@
 
 - (id)initWithTarget:(id)target;
 
-+ (BOOL)canEditType:(NSString *)typeEncoding currentObjectValue:(id)value;
-+ (NSString *)stringTypeEncoding;
-
 // Convenience accessor since many subclasses only use one input view
 @property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
 

+ 0 - 74
Classes/Editing/FLEXFieldEditorViewController.m

@@ -89,12 +89,6 @@
     self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
     [self.scrollView addSubview:self.fieldEditorView];
     
-    // One argument input view by default. Subclasses can configure the field editor view with more/different argument input views if needed.
-    FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:NULL];
-    inputView.backgroundColor = self.view.backgroundColor;
-    inputView.targetSize = FLEXArgumentInputViewSizeLarge;
-    self.fieldEditorView.argumentInputViews = @[inputView];
-    
     self.setterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForActionButton] style:UIBarButtonItemStyleDone target:self action:@selector(actionButtonPressed:)];
     self.navigationItem.rightBarButtonItem = self.setterButton;
 }
@@ -124,72 +118,4 @@
     return @"Set";
 }
 
-+ (BOOL)canEditType:(NSString *)typeEncoding currentObjectValue:(id)value
-{
-    // Many primitive types can always be edited (numbers and supported structs).
-    static NSArray *primitiveTypes = nil;
-    static dispatch_once_t onceToken;
-    dispatch_once(&onceToken, ^{
-        primitiveTypes = @[@(@encode(char)),
-                           @(@encode(int)),
-                           @(@encode(short)),
-                           @(@encode(long)),
-                           @(@encode(long long)),
-                           @(@encode(unsigned char)),
-                           @(@encode(unsigned int)),
-                           @(@encode(unsigned short)),
-                           @(@encode(unsigned long)),
-                           @(@encode(unsigned long long)),
-                           @(@encode(float)),
-                           @(@encode(double)),
-                           @(@encode(CGRect)),
-                           @(@encode(CGSize)),
-                           @(@encode(CGPoint)),
-                           @(@encode(CGAffineTransform)),
-                           @(@encode(NSRange)),
-                           @(@encode(UIEdgeInsets)),
-                           @(@encode(UIOffset))];
-    });
-    
-    BOOL canEdit = [primitiveTypes containsObject:typeEncoding];
-    
-    // Object types may be
-    if (!canEdit) {
-        if (value) {
-            // If the current value is non-nil and we can represent it with an editable string, then we can suppor editing.
-            canEdit = canEdit || [FLEXRuntimeUtility editiableDescriptionForObject:value] != nil;
-        } else {
-            // Also always edit types that are explicitly typed NSString, NSNumber, NSArray, or NSDictionary and are nil.
-            // This kind of type encoding is only kept by ivars and properties. Method agruments and return types drop the class from the type encoding.
-            // The editor supports populating these types through NSJSONSerilization parsing of the input string.
-            canEdit = canEdit || [typeEncoding isEqual:[self stringTypeEncoding]];
-            canEdit = canEdit || [typeEncoding isEqual:[self numberTypeEncoding]];
-            canEdit = canEdit || [typeEncoding isEqual:[self arrayTypeEncoding]];
-            canEdit = canEdit || [typeEncoding isEqual:[self dictionaryTypeEncoding]];
-        }
-    }
-    
-    return canEdit;
-}
-
-+ (NSString *)stringTypeEncoding
-{
-    return @"@\"NSString\"";
-}
-
-+ (NSString *)numberTypeEncoding
-{
-    return @"@\"NSNumber\"";
-}
-
-+ (NSString *)arrayTypeEncoding
-{
-    return @"@\"NSArray\"";
-}
-
-+ (NSString *)dictionaryTypeEncoding
-{
-    return @"@\"NSDictionary\"";
-}
-
 @end

+ 9 - 15
Classes/Editing/FLEXIvarEditorViewController.m

@@ -10,6 +10,7 @@
 #import "FLEXFieldEditorView.h"
 #import "FLEXRuntimeUtility.h"
 #import "FLEXArgumentInputView.h"
+#import "FLEXArgumentInputViewFactory.h"
 
 @interface FLEXIvarEditorViewController ()
 
@@ -35,31 +36,24 @@
     
     self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForIvar:self.ivar];
     
-    [self updateTextFieldString];
+    FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:ivar_getTypeEncoding(self.ivar)];
+    inputView.backgroundColor = self.view.backgroundColor;
+    inputView.targetSize = FLEXArgumentInputViewSizeLarge;
+    inputView.inputOutput = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
+    self.fieldEditorView.argumentInputViews = @[inputView];
 }
 
 - (void)actionButtonPressed:(id)sender
 {
     [super actionButtonPressed:sender];
     
-    [self updateIvarFromString:self.firstInputView.inputOutput];
-    [self updateTextFieldString];
-}
-
-- (void)updateTextFieldString
-{
-    id ivarValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
-    self.firstInputView.inputOutput = [FLEXRuntimeUtility editiableDescriptionForObject:ivarValue];
-}
-
-- (void)updateIvarFromString:(NSString *)string
-{
-    [FLEXRuntimeUtility setIvar:self.ivar onObject:self.target withInputString:self.firstInputView.inputOutput];
+    [FLEXRuntimeUtility setValue:self.firstInputView.inputOutput forIvar:self.ivar onObject:self.target];
+    self.firstInputView.inputOutput = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
 }
 
 + (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value
 {
-    return [self canEditType:@(ivar_getTypeEncoding(ivar)) currentObjectValue:value];
+    return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
 }
 
 @end

+ 8 - 38
Classes/Editing/FLEXMethodCallingViewController.m

@@ -42,19 +42,12 @@
     NSMutableArray *argumentInputViews = [NSMutableArray array];
     unsigned int argumentIndex = kFLEXNumberOfImplicitArgs;
     for (NSString *methodComponent in methodComponents) {
-        FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:NULL];
-        inputView.backgroundColor = self.view.backgroundColor;
-        inputView.title = methodComponent;
-        
-        // Prepopulate the structs that we parse from strings with the default values.
-        // This shows the intended formatting which would be less clear otherwise.
         char *argumentTypeEncoding = method_copyArgumentType(self.method, argumentIndex);
-        NSValue *defaultValue = [[self class] defaultValueForEncoding:argumentTypeEncoding];
-        if (defaultValue) {
-            inputView.inputOutput = [FLEXRuntimeUtility editiableDescriptionForObject:defaultValue];
-        }
+        FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:argumentTypeEncoding];
         free(argumentTypeEncoding);
         
+        inputView.backgroundColor = self.view.backgroundColor;
+        inputView.title = methodComponent;
         [argumentInputViews addObject:inputView];
         argumentIndex++;
     }
@@ -77,12 +70,12 @@
     
     NSMutableArray *arguments = [NSMutableArray array];
     for (FLEXArgumentInputView *inputView in self.fieldEditorView.argumentInputViews) {
-        NSString *argumentString = inputView.inputOutput;
-        if (!argumentString) {
-            // Use empty string as a placeholder in the array. It will be interpreted as nil.
-            argumentString = @"";
+        id argumentValue = inputView.inputOutput;
+        if (!argumentValue) {
+            // Use NSNulls as placeholders in the array. They will be interpreted as nil arguments.
+            argumentValue = [NSNull null];
         }
-        [arguments addObject:argumentString];
+        [arguments addObject:argumentValue];
     }
     
     id returnedObject = [FLEXRuntimeUtility performSelector:method_getName(self.method) onObject:self.target withArguments:arguments error:NULL];
@@ -94,27 +87,4 @@
     }
 }
 
-+ (NSValue *)defaultValueForEncoding:(const char *)typeEncoding
-{
-    NSValue *value = nil;
-    
-    if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
-        value = [NSValue valueWithCGRect:CGRectZero];
-    } else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
-        value = [NSValue valueWithCGPoint:CGPointZero];
-    } else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
-        value = [NSValue valueWithCGSize:CGSizeZero];
-    } else if (strcmp(typeEncoding, @encode(CGAffineTransform)) == 0) {
-        value = [NSValue valueWithCGAffineTransform:CGAffineTransformIdentity];
-    } else if (strcmp(typeEncoding, @encode(NSRange)) == 0) {
-        value = [NSValue valueWithRange:NSMakeRange(0, 0)];
-    } else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
-        value = [NSValue valueWithUIEdgeInsets:UIEdgeInsetsZero];
-    } else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
-        value = [NSValue valueWithUIOffset:UIOffsetZero];
-    }
-    
-    return value;
-}
-
 @end

+ 13 - 15
Classes/Editing/FLEXPropertyEditorViewController.m

@@ -10,6 +10,7 @@
 #import "FLEXRuntimeUtility.h"
 #import "FLEXFieldEditorView.h"
 #import "FLEXArgumentInputView.h"
+#import "FLEXArgumentInputViewFactory.h"
 
 @interface FLEXPropertyEditorViewController ()
 
@@ -37,33 +38,30 @@
     id currentValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
     self.setterButton.enabled = [[self class] canEditProperty:self.property currentValue:currentValue];
     
-    [self updateArgumentInputView];
+    const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:self.property] UTF8String];
+    FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:typeEncoding];
+    inputView.backgroundColor = self.view.backgroundColor;
+    inputView.targetSize = FLEXArgumentInputViewSizeLarge;
+    inputView.inputOutput = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
+    self.fieldEditorView.argumentInputViews = @[inputView];
 }
 
 - (void)actionButtonPressed:(id)sender
 {
     [super actionButtonPressed:sender];
     
-    [self updatePropertyFromString:self.firstInputView.inputOutput];
-    [self updateArgumentInputView];
-}
-
-- (void)updateArgumentInputView
-{
-    id propertyValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
-    self.firstInputView.inputOutput = [FLEXRuntimeUtility editiableDescriptionForObject:propertyValue];
-}
-
-- (void)updatePropertyFromString:(NSString *)string
-{
+    id userInputObject = self.firstInputView.inputOutput;
+    NSArray *arguments = userInputObject ? @[userInputObject] : nil;
     SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:self.property];
-    NSArray *arguments = string ? @[string] : nil;
     [FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:NULL];
+    
+    self.firstInputView.inputOutput = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
 }
 
 + (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value
 {
-    BOOL canEditType = [self canEditType:[FLEXRuntimeUtility typeEncodingForProperty:property] currentObjectValue:value];
+    const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:property] UTF8String];
+    BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
     BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property];
     return canEditType && !isReadonly;
 }

+ 4 - 3
Classes/Utility/FLEXRuntimeUtility.h

@@ -26,7 +26,7 @@ extern const unsigned int kFLEXNumberOfImplicitArgs;
 // Ivar Helpers
 + (NSString *)prettyNameForIvar:(Ivar)ivar;
 + (id)valueForIvar:(Ivar)ivar onObject:(id)object;
-+ (void)setIvar:(Ivar)ivar onObject:(id)object withInputString:(NSString *)inputString;
++ (void)setValue:(id)value forIvar:(Ivar)ivar onObject:(id)object;
 
 // Method Helpers
 + (NSString *)prettyNameForMethod:(Method)method isClassMethod:(BOOL)isClassMethod;
@@ -34,7 +34,8 @@ extern const unsigned int kFLEXNumberOfImplicitArgs;
 
 // Method Calling/Field Editing
 + (id)performSelector:(SEL)selector onObject:(id)object withArguments:(NSArray *)arguments error:(NSError * __autoreleasing *)error;
-+ (NSString *)editiableDescriptionForObject:(id)object;
-+ (id)objectValueFromEditableString:(NSString *)string;
++ (NSString *)editableJSONStringForObject:(id)object;
++ (id)objectValueFromEditableJSONString:(NSString *)string;
++ (NSValue *)valueForNumberWithObjCType:(const char *)typeEncoding fromInputString:(NSString *)inputString;
 
 @end

+ 97 - 113
Classes/Utility/FLEXRuntimeUtility.m

@@ -25,7 +25,8 @@ static NSString *const kFLEXUtilityAttributeOldStyleTypeEncoding = @"t";
 static NSString *const FLEXRuntimeUtilityErrorDomain = @"FLEXRuntimeUtilityErrorDomain";
 typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
     FLEXRuntimeUtilityErrorCodeDoesNotRecognizeSelector = 0,
-    FLEXRuntimeUtilityErrorCodeInvocationFailed = 1
+    FLEXRuntimeUtilityErrorCodeInvocationFailed = 1,
+    FLEXRuntimeUtilityErrorCodeArgumentTypeMismatch = 2
 };
 
 // Arguments 0 and 1 are self and _cmd always
@@ -199,27 +200,31 @@ const unsigned int kFLEXNumberOfImplicitArgs = 2;
     } else {
         ptrdiff_t offset = ivar_getOffset(ivar);
         void *pointer = (__bridge void *)object + offset;
-        value = [self valueForPrimitivePointer:pointer objcType:type];
+        value = [self valueForPrimitivePointer:pointer objCType:type];
     }
     return value;
 }
 
-+ (void)setIvar:(Ivar)ivar onObject:(id)object withInputString:(NSString *)inputString
++ (void)setValue:(id)value forIvar:(Ivar)ivar onObject:(id)object
 {
     const char *typeEncodingCString = ivar_getTypeEncoding(ivar);
     if (typeEncodingCString[0] == '@') {
-        // Object - use NSJSONSerialization
-        id ivarValue = [self objectValueFromEditableString:inputString];
-        object_setIvar(object, ivar, ivarValue);
-    } else {
-        // Primitive - try to parse a number or struct from the string based on the type encoding
+        object_setIvar(object, ivar, value);
+    } else if ([value isKindOfClass:[NSValue class]]) {
+        // Primitive - unbox the NSValue.
+        NSValue *valueValue = (NSValue *)value;
+        
+        // Make sure that the box contained the correct type.
+        NSAssert(strcmp([valueValue objCType], typeEncodingCString) == 0, @"Type encoding mismatch (value: %s; ivar: %s) in setting ivar named: %s on object: %@", [valueValue objCType], typeEncodingCString, ivar_getName(ivar), object);
+        
         NSUInteger bufferSize = 0;
         NSGetSizeAndAlignment(typeEncodingCString, &bufferSize, NULL);
-        void *buffer = malloc(bufferSize);
-        [self getPrimitiveValue:buffer withObjCType:typeEncodingCString fromInputString:inputString];
+        void *buffer = calloc(bufferSize, 1);
+        [valueValue getValue:buffer];
         ptrdiff_t offset = ivar_getOffset(ivar);
         void *pointer = (__bridge void *)object + offset;
         memcpy(pointer, buffer, bufferSize);
+        free(buffer);
     }
 }
 
@@ -288,24 +293,34 @@ const unsigned int kFLEXNumberOfImplicitArgs = 2;
     NSUInteger numberOfArguments = [methodSignature numberOfArguments];
     for (NSUInteger argumentIndex = kFLEXNumberOfImplicitArgs; argumentIndex < numberOfArguments; argumentIndex++) {
         NSUInteger argumentsArrayIndex = argumentIndex - kFLEXNumberOfImplicitArgs;
-        NSString *argumentString = [arguments count] > argumentsArrayIndex ? [arguments objectAtIndex:argumentsArrayIndex] : nil;
-        const char *typeEncodingCString = [methodSignature getArgumentTypeAtIndex:argumentIndex];
-        if (typeEncodingCString[0] == @encode(id)[0] || typeEncodingCString[0] == @encode(Class)[0]) {
-            // Object
-            id argumentObject = [self objectValueFromEditableString:argumentString];
-            [invocation setArgument:&argumentObject atIndex:argumentIndex];
-        } else if (strcmp(typeEncodingCString, @encode(const char *)) == 0 || strcmp(typeEncodingCString, @encode(char *)) == 0) {
-            // C string
-            const char *argumentCString = [argumentString UTF8String];
-            [invocation setArgument:&argumentCString atIndex:argumentIndex];
-        } else {
-            // Primitive number or struct
-            NSUInteger bufferSize = 0;
-            NSGetSizeAndAlignment(typeEncodingCString, &bufferSize, NULL);
-            void *buffer = malloc(bufferSize);
-            [self getPrimitiveValue:buffer withObjCType:typeEncodingCString fromInputString:argumentString];
-            [invocation setArgument:buffer atIndex:argumentIndex];
-            free(buffer);
+        id argumentObject = [arguments count] > argumentsArrayIndex ? [arguments objectAtIndex:argumentsArrayIndex] : nil;
+        
+        // NSNull in the arguments array can be passed as a placeholder to indicate nil. We only need to set the argument if it will be non-nil.
+        if (argumentObject && ![argumentObject isKindOfClass:[NSNull class]]) {
+            const char *typeEncodingCString = [methodSignature getArgumentTypeAtIndex:argumentIndex];
+            if (typeEncodingCString[0] == @encode(id)[0] || typeEncodingCString[0] == @encode(Class)[0]) {
+                // Object
+                [invocation setArgument:&argumentObject atIndex:argumentIndex];
+            } else if ([argumentObject isKindOfClass:[NSValue class]]){
+                // Primitive boxed in NSValue
+                NSValue *argumentValue = (NSValue *)argumentObject;
+                
+                // Ensure that the type encoding on the NSValue matches the type encoding of the argument in the method signature
+                if (strcmp([argumentValue objCType], typeEncodingCString) != 0) {
+                    if (error) {
+                        NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Type encoding mismatch for agrument at index %lu. Value type: %s; Method argument type: %s.", (unsigned long)argumentsArrayIndex, [argumentValue objCType], typeEncodingCString]};
+                        *error = [NSError errorWithDomain:FLEXRuntimeUtilityErrorDomain code:FLEXRuntimeUtilityErrorCodeArgumentTypeMismatch userInfo:userInfo];
+                    }
+                    return nil;
+                }
+                
+                NSUInteger bufferSize = 0;
+                NSGetSizeAndAlignment(typeEncodingCString, &bufferSize, NULL);
+                void *buffer = calloc(bufferSize, 1);
+                [argumentValue getValue:buffer];
+                [invocation setArgument:buffer atIndex:argumentIndex];
+                free(buffer);
+            }
         }
     }
     
@@ -336,7 +351,7 @@ const unsigned int kFLEXNumberOfImplicitArgs = 2;
             void *returnValue = malloc([methodSignature methodReturnLength]);
             if (returnValue) {
                 [invocation getReturnValue:returnValue];
-                returnObject = [self valueForPrimitivePointer:returnValue objcType:returnType];
+                returnObject = [self valueForPrimitivePointer:returnValue objCType:returnType];
                 free(returnValue);
             }
         }
@@ -345,31 +360,11 @@ const unsigned int kFLEXNumberOfImplicitArgs = 2;
     return returnObject;
 }
 
-+ (NSString *)editiableDescriptionForObject:(id)object
++ (NSString *)editableJSONStringForObject:(id)object
 {
     NSString *editableDescription = nil;
     
-    if ([object isKindOfClass:[NSValue class]]) {
-        NSValue *value = (NSValue *)object;
-        const char *typeEncoding = [value objCType];
-        if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
-            editableDescription = NSStringFromCGRect([value CGRectValue]);
-        } else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
-            editableDescription = NSStringFromCGSize([value CGSizeValue]);
-        } else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
-            editableDescription = NSStringFromCGPoint([value CGPointValue]);
-        } else if (strcmp(typeEncoding, @encode(CGAffineTransform)) == 0) {
-            editableDescription = NSStringFromCGAffineTransform([value CGAffineTransformValue]);
-        } else if (strcmp(typeEncoding, @encode(NSRange)) == 0) {
-            editableDescription = NSStringFromRange([value rangeValue]);
-        } else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
-            editableDescription = NSStringFromUIEdgeInsets([value UIEdgeInsetsValue]);
-        } else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
-            editableDescription = NSStringFromUIOffset([value UIOffsetValue]);
-        }
-    }
-    
-    if (object && !editableDescription) {
+    if (object) {
         // This is a hack to use JSON serialzation for our editable objects.
         // NSJSONSerialization doesn't allow writing fragments - the top level object must be an array or dictionary.
         // We always wrap the object inside an array and then strip the outter square braces off the final string.
@@ -383,7 +378,7 @@ const unsigned int kFLEXNumberOfImplicitArgs = 2;
     return editableDescription;
 }
 
-+ (id)objectValueFromEditableString:(NSString *)string
++ (id)objectValueFromEditableJSONString:(NSString *)string
 {
     id value = nil;
     // nil for empty string/whitespace
@@ -393,6 +388,55 @@ const unsigned int kFLEXNumberOfImplicitArgs = 2;
     return value;
 }
 
++ (NSValue *)valueForNumberWithObjCType:(const char *)typeEncoding fromInputString:(NSString *)inputString
+{
+    NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
+    [formatter setNumberStyle:NSNumberFormatterDecimalStyle];
+    NSNumber *number = [formatter numberFromString:inputString];
+    
+    // Make sure we box the number with the correct type encoding so it can be propperly unboxed later via getValue:
+    NSValue *value = nil;
+    if (strcmp(typeEncoding, @encode(char)) == 0) {
+        char primitiveValue = [number charValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    } else if (strcmp(typeEncoding, @encode(int)) == 0) {
+        int primitiveValue = [number intValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    } else if (strcmp(typeEncoding, @encode(short)) == 0) {
+        short primitiveValue = [number shortValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    } else if (strcmp(typeEncoding, @encode(long)) == 0) {
+        long primitiveValue = [number longValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    } else if (strcmp(typeEncoding, @encode(long long)) == 0) {
+        long long primitiveValue = [number longLongValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    } else if (strcmp(typeEncoding, @encode(unsigned char)) == 0) {
+        unsigned char primitiveValue = [number unsignedCharValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    } else if (strcmp(typeEncoding, @encode(unsigned int)) == 0) {
+        unsigned int primitiveValue = [number unsignedIntValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    } else if (strcmp(typeEncoding, @encode(unsigned short)) == 0) {
+        unsigned short primitiveValue = [number unsignedShortValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    } else if (strcmp(typeEncoding, @encode(unsigned long)) == 0) {
+        unsigned long primitiveValue = [number unsignedLongValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    } else if (strcmp(typeEncoding, @encode(unsigned long long)) == 0) {
+        unsigned long long primitiveValue = [number unsignedLongValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    } else if (strcmp(typeEncoding, @encode(float)) == 0) {
+        float primitiveValue = [number floatValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    } else if (strcmp(typeEncoding, @encode(double)) == 0) {
+        double primitiveValue = [number doubleValue];
+        value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
+    }
+    
+    return value;
+}
+
 
 #pragma mark - Internal Helpers
 
@@ -510,7 +554,7 @@ const unsigned int kFLEXNumberOfImplicitArgs = 2;
     return encodingString;
 }
 
-+ (NSValue *)valueForPrimitivePointer:(void *)pointer objcType:(const char *)type
++ (NSValue *)valueForPrimitivePointer:(void *)pointer objCType:(const char *)type
 {
     // CASE marcro inspired by https://www.mikeash.com/pyblog/friday-qa-2013-02-08-lets-build-key-value-coding.html
 #define CASE(ctype, selectorpart) \
@@ -543,64 +587,4 @@ const unsigned int kFLEXNumberOfImplicitArgs = 2;
     return value;
 }
 
-+ (void)getPrimitiveValue:(void *)value withObjCType:(const char *)typeEncoding fromInputString:(NSString *)inputString
-{
-    if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
-        *(CGRect *)value = CGRectFromString(inputString);
-    } else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
-        *(CGSize *)value = CGSizeFromString(inputString);
-    } else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
-        *(CGPoint *)value = CGPointFromString(inputString);
-    } else if (strcmp(typeEncoding, @encode(CGAffineTransform)) == 0) {
-        *(CGAffineTransform *)value = CGAffineTransformFromString(inputString);
-    } else if (strcmp(typeEncoding, @encode(NSRange)) == 0) {
-        *(NSRange *)value = NSRangeFromString(inputString);
-    } else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
-        *(UIEdgeInsets *)value = UIEdgeInsetsFromString(inputString);
-    } else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
-        *(UIOffset *)value = UIOffsetFromString(inputString);
-    } else {
-        NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
-        [formatter setNumberStyle:NSNumberFormatterDecimalStyle];
-        NSNumber *number = [formatter numberFromString:inputString];
-        
-        if (strcmp(typeEncoding, @encode(char)) == 0) {
-            // Special case - ask for the string's BOOL value if we couldn't parse a number.
-            // This allows us to take YES/NO/true/false input.
-            if (number) {
-                *(char *)value = [number charValue];
-            } else {
-                *(char *)value = [inputString boolValue];
-            }
-        } else if (strcmp(typeEncoding, @encode(int)) == 0) {
-            *(int *)value = [number intValue];
-        } else if (strcmp(typeEncoding, @encode(short)) == 0) {
-            *(short *)value = [number shortValue];
-        } else if (strcmp(typeEncoding, @encode(long)) == 0) {
-            *(long *)value = [number longValue];
-        } else if (strcmp(typeEncoding, @encode(long long)) == 0) {
-            *(long long *)value = [number longLongValue];
-        } else if (strcmp(typeEncoding, @encode(unsigned char)) == 0) {
-            *(unsigned char *)value = [number unsignedCharValue];
-        } else if (strcmp(typeEncoding, @encode(unsigned int)) == 0) {
-            *(unsigned int *)value = [number unsignedIntValue];
-        } else if (strcmp(typeEncoding, @encode(unsigned short)) == 0) {
-            *(unsigned short *)value = [number unsignedShortValue];
-        } else if (strcmp(typeEncoding, @encode(unsigned long)) == 0) {
-            *(unsigned long *)value = [number unsignedLongValue];
-        } else if (strcmp(typeEncoding, @encode(unsigned long long)) == 0) {
-            *(unsigned long long *)value = [number unsignedLongLongValue];
-        } else if (strcmp(typeEncoding, @encode(float)) == 0) {
-            *(float *)value = [number floatValue];
-        } else if (strcmp(typeEncoding, @encode(double)) == 0) {
-            *(double *)value = [number doubleValue];
-        } else {
-            // If we didn't match one of the supported types, fill the buffer with zeros.
-            NSUInteger bytes = 0;
-            NSGetSizeAndAlignment(typeEncoding, &bytes, NULL);
-            memset(value, 0, bytes);
-        }
-    }
-}
-
 @end