Technology Musings

May 29, 2013

Snippets / Adding a "Copy" context menu to a disabled UITextfield in iOS

JB

I have a project which has a lot of fields which, at any moment, may be enabled or disabled.  However, even when they are disabled, I want to show a context menu.  It turns out that no one has a solution, so I hacked one together for myself.  This code is adapted from a working project, but I have not tested the version presented here, but let me know if you have problems.  

The general problem you run into is this: if your UITextField is disabled, then it cannot receive touch events.  Therefore, it cannot fire the menu, even if you try to do so explicitly using Gesture recognizers.  The normal answer is to simply not allow any editing on the field.  However, while this prevents you from editing the field, it still *draws* the textfield as active.  We want it to *look* inactive as well, which you only get by disabling it.

So, what I did was to create a subclass of UITextField (JBTextField) which has a subview (a specialized subclass of UIView - JBTextFieldResponderView) that has a gesture recognizer attached.  We override hitTest on JBTextField to do this: if JBTextField is disabled, then we delegate hitTest to our JBTextFieldResponderView subclass.

As I said, this is from another project, which had a whole bunch of other stuff mixed in.  I have tried to separate out the code for you here, but I don't guarantee that it will work out-of-the-box.

@interface JBTextFieldResponderView : UIView
@property (strong, nonatomic) UIGestureRecognizer *contextGestureRecognizer;
@end

@interface JBTextField : UITextField
@property (strong, nonatomic) JBTextFieldResponderView *contextGestureRecognizerViewForDisabled;
@end
@implementation JBTextFieldResponderView
@synthesize contextGestureRecognizer = _contextGestureRecognizer;

-(id) initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];

  // Add the gesture recognizer
  self.contextGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestureDidFire:)];
  [self addGestureRecognizer:self.contextGestureRecognizer];

  return self;
}

// First-level of response, filters out some noise
-(IBAction) longPressGestureDidFire:(id)sender {
  UILongPressGestureRecognizer *recognizer = sender;
  if(recognizer.state == UIGestureRecognizerStateBegan) { // Only fire once
    [self initiateContextMenu:sender];
  }
}

// Second level of response - actually trigger the menu
-(IBAction) initiateContextMenu:(id)sender {
  [self becomeFirstResponder]; // So the menu will be active.  We can't set the Text field to be first responder -- doesn't work if it is disabled
  UIMenuController *menu = [UIMenuController sharedMenuController];
  [menu setTarget:self.bounds inView:self];
  [menu setMenuVisible:YES animated:YES];
}

// The menu will automatically add a "Copy" command if it sees a "copy:" method.  
// See UIResponderStandardEditActions to see what other commands we can add through methods.
-(IBAction) copy:(id)sender {
  UIPasteboard *pb = [UIPasteboard generalPasteboard];
  UITextField *tf = (UITextField *)self.superview;
  [pb setString: tf.text];
}

// Normally a UIView doesn't want to become a first responder.  This forces the issue.
-(BOOL) canBecomeFirstResponder {
  return YES;
}

@end

@implementation JBTextField
@synthesize contextGestureRecognizerViewForDisabled = _contextGestureRecognizerViewForDisabled;

/* Add the recognizer view no matter which path this is initialized through */

-(id) initWithCoder:(NSCoder *)aDecoder {
  self = [super initWithCoder:aDecoder];
  [self addDisabledRecognizer];
  return self;
}

-(id) initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  [self addDisabledRecognizer];
  return self;
}

// This creates the view and adds it at the end of the view chain so it doesn't interfere, but is still present */
-(void) addDisabledRecognizer {
  self.contextGestureViewForDisabled = [[JBTextFieldResponderView alloc] initWithFrame:self.bounds];
  self.contextGestureViewForDisabled.userInteractionEnabled = NO;
  [self addSubview:self.contextGestureViewForDisabled];
}

-(void) setEnabled:(BOOL) enabled {
  [super setEnabled:enabled];
  self.contextGestureViewForDisabled.userInteractionEnabled = !enabled;
}

// This is where the magic happens
-(UIView *) hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  if(self.enabled) {
    // If we are enabled, respond normally
    return [super hitTest:point withEvent:event];
  } else {
    // If we are disabled, let our specialized view determine how to respond
    UIView *v = [self.contextGestureViewForDisabled hitTest:point withEvent:event];
  }
}

@end

Now, in interface builder, we can enabled copy on disabled fields just by setting the class to JBTextField, or, if built programmatically, by replacing [UITextField alloc] with [JBTextField alloc].

I hope this helps!

Additional Resources:

 

 

 

May 09, 2013

Platforms / My Beef with Model Validators in Rails

JB

When I first started using Rails, it was a dream come true.  I estimate that I reduced the amount of code I was writing by 60-75%.  This is probably still true.  However, as I've gotten to know the platform better, there are some philosophical issues that I have with Rails.  Sometimes it is more the Rails community and its conventions rather than rails itself, but nonetheless, it continually manifests itself.

My current beef - validators!  Having written lots and lots of code that interacts with lots and lots of systems, I have to say that you should never, ever, ever put in model validators.  Ever.  Validation is a process issue, not a data one.  In theory it should be a data issue, but it never ever works out that way.

Here's what happens.  Let's say I have a telephone number validator for a model.  It makes sure the numbers are in the format (918) 245-2234.  If not, it gives a nice error message.  However, now let's connect that same model to an external system.  Let's now say that the author of the external system was not as picky about phone number formats.  What happens now?  Well, every record that gets migrated from that system that isn't formatted according to your desires will now get kicked out!  And it might not even show up during testing!  In addition, your pretty error messages mean practically nothing on system->system communication.  So, what you have is the system randomly breaking down.

This causes even more problems in long-term maintenance.  I've had many bugs pop up because someone added a validator that broke several processes.  It worked in all of their tests, but they weren't thinking about all of the ways the system was used.  For instance, we decided to lock down the characters used in our usernames a little more.  The programmer decided to implement it as a validator.  Well, the problem is that several users already had usernames that didn't match the validator.  So what happened?  Those records just stopped saving.  Not just when there was someone who was at the terminal, where they could see the error message, but every job started erroring out because of users who had faulty names.

Because of these issues, I *always* do validation in my controller, and *never* in the model.  It is true that there are probably workarounds for all of these issues, but the point is that they are *not* that exceptional!  They are everywhere!  What would be nice is to have validators which could be run optionally, in specific situations.  So, a controller could declare that it was using :data_entry validations, and then the model would only run those validators.  So, it would look something like this:

In the model:

validates_presence_of :email, :for => [:data_entry]

In the controller:

uses_validators :data_entry

One of the issues I have found with dynamic languages such as ruby is that errors arise when "magic" is sprinkled in unexpected places.  Ruby magic is awesome, but when I do something simple and it won't save, it is sometimes hard to even locate where the error is.  Explicitly declaring which validator set you are using allows programmers to more easily see what they are doing, and only apply magic code where it goes.

Someday I'll write a package like this --- when I get extra time :)

May 07, 2013

Platforms /

Why the tabs-vs-spaces debate matters:

Inconsistent indentation: 2 spaces were used for indentation, but the rest of the document was indented using 1 tab.

Everyone - indent with tabs.  Then you can control the indentation using your editor's settings.  DO NOT indent with spaces.  And DO NOT use any programming language or configuration system that cares about indentation.