0

In my application there are many UISwitches and UITextFields displayed in a list of UITableViewCells.

When the user starts editing a UITextField and then taps on a UISwitch the order of the events causes the UITextField to display the value of the UISwitch because the event handler did not receive the end editing event of the UITextField.

How to ensure reliably that the UIControlEventEditingDidEnd event of the UITextField gets fired before the UIControlEventValueChanged of the UISwitch?

It leads to errors like this (value of switch displayed in text field):

UISwitch and UITextField

Steps (what should happen):

1.Tap the UISwitch to activate it

UISwitch:startEditing:switch243
UISwitch:valueChanged:{true}
UISwitch:endEditing
UIEventHandler:saveValue:switch243:{true}

2.Tap the UITextField to start editing it

UITextField:startEditing:textfield455

3.Tap the UISwitch to deactivate it

UITextField:endEditing
UISwitch:startEditing:switch243
UISwitch:valueChanged:{false}
UISwitch:endEditing
UIEventHandler:saveValue:switch243:{false}

Console log (what really happens - the UISwitch event fires before UITextField:endEditing):

UISwitch:startEditing:switch243
UISwitch:valueChanged:{true}
UISwitch:endEditing
UIEventHandler:saveValue:switch243:{true}
UITextField:startEditing:textfield455
UISwitch:startEditing:switch243
UISwitch:valueChanged:{false}
UISwitch:endEditing
UIEventHandler:saveValue:switch243:{false}
UITextField:endEditing

Implementation:

UITableViewCellWithSwitch.h:

@interface UITableViewCellWithSwitch : UITableViewCell
@property (nonatomic, strong) NSString *attributeID;
@property (nonatomic, retain) IBOutlet UISwitch *switchField;
@end

UITableViewCellWithSwitch.m:

@implementation UITableViewCellWithSwitch
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self.switchField addTarget:self
                            action:@selector(switchChanged:)
                  forControlEvents:UIControlEventValueChanged];
    }
    return self;
}
// UIControlEventValueChanged
- (void)switchChanged:(UISwitch *)sender {
    NSLog(@"UISwitch:startEditing:%@",self.attributeID);
    [self handleStartEditingForAttributeID:self.attributeID];

    NSString* newValue = sender.on==YES?@"true":@"false";
    NSLog(@"UISwitch:valueChanged:{%@}", newValue);
    [self handleValueChangeForEditedAttribute:newValue];

    NSLog(@"UISwitch:endEditing");
    [self handleEndEditingForEditedAttribute];
}
@end

UITableViewCellWithTextField.h:

@interface UITableViewCellWithTextField : UITableViewCell<UITextFieldDelegate>
@property (nonatomic, strong) NSString *attributeID;
@property (strong, nonatomic) IBOutlet UITextField *inputField;
@end

UITableViewCellWithTextField.m:

@implementation UITableViewCellWithTextField
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self.inputField addTarget:self
                            action:@selector(textFieldDidBegin:)
                  forControlEvents:UIControlEventEditingDidBegin];

        [self.inputField addTarget:self
                            action:@selector(textFieldDidChange:)
                  forControlEvents:UIControlEventEditingChanged];

        [self.inputField addTarget:self
                            action:@selector(textFieldDidEnd:)
                  forControlEvents:UIControlEventEditingDidEnd];
    }
    return self;
}
// UIControlEventEditingDidBegin
-(void) textFieldDidBegin:(UITextField *)sender {
    NSLog(@"UITextField:startEditing:%@",self.attributeID);
    [self handleStartEditingForAttributeID:self.attributeID];
}
// UIControlEventEditingChanged
-(void) textFieldDidChange:(UITextField *)sender {
    NSLog(@"UITextField:valueChanged:{%@}", sender.text);
    [self handleValueChangeForEditedAttribute:sender.text];
}
// UIControlEventEditingDidEnd
-(void) textFieldDidEnd:(UITextField *)sender {
    NSLog(@"UITextField:endEditing");
    [self handleEndEditingForEditedAttribute];
}
@end

UIEventHandler.m that aggregates all UI editing events:

-(void) handleStartEditingForAttributeID:(NSString *)attributeID { 
    // Possible solution   
    //if (self.editedAttributeID != nil && [attributeID isEqualToString:self.editedAttributeID]==NO) { // Workaround needed for UISwitch events
    //    [self handleEndEditingForActiveAttribute];
    //}
    self.editedAttributeID = attributeID;
    self.temporaryValue = nil;
}

-(void) handleValueChangeForEditedAttribute:(NSString *)newValue {
    self.temporaryValue = newValue;
}

-(void) handleEndEditingForEditedAttribute { 
    if (self.temporaryValue != nil) { // Only if value has changed
        NSLog(@"UIEventHandler:saveValue:%@:{%@}", self.editedAttributeID, self.temporaryValue);

        // Causes the view to regenerate
        // The UITextField loses first responder status and UIControlEventEditingDidEnd is gets triggered too late
        [self.storage saveValue:self.temporaryValue 
                   forAttribute:self.editedAttributeID];

        self.temporaryValue = nil;
    }
    self.editedAttributeID = nil;
}
Peter G.
  • 7,816
  • 20
  • 80
  • 154
  • 1
    You need to provide more information about what your code is doing. From what you've shown, tapping the switch to "deactivate it" does nothing other than execute the two log statements. See [mcve] – DonMag Nov 20 '18 at 14:57
  • what do you want to achieve base on Switch? – Mohammad Sadiq Nov 20 '18 at 15:18
  • @MohammadSadiq I need to call 3 events by each input field type (start editing, value changed, end editing) that are used by `UIEventHandler.m` – Peter G. Nov 20 '18 at 15:41
  • @PeterGerhat - it's really tough for anybody to offer help when you only post partial code. For example, right now you have two `textFieldDidBegin` methods? Also, nothing to indicate where `sender.attributeID` is coming from? – DonMag Nov 20 '18 at 16:24
  • @DonMag Added more details into the question. This is a question about event handling in UIKit and specifically UISwitch and UITextField. Don't want to make it too broad. – Peter G. Nov 20 '18 at 16:39
  • @PeterGerhat I just looked at your code edits after posting my answer. So the `attributeID` property is a unique ID which is the same for the text field and the switch? – Pranay Nov 20 '18 at 16:54
  • @Pranay yes that is correct, it is used as the unique identifier of the attribute that is being edited. – Peter G. Nov 20 '18 at 16:59

2 Answers2

1

If I understand correctly, the problem you're having is when the switch value is changed while a textfield is the first responder, then your textfield's text gets updated to the value of the switch.

A UITextField's didEndEditing: event only happens if the textfield resigns first responder. If all you want to do is to make sure that the textfield ends editing when the switch value changes, you should send the endEditing: message to the active textfield when the switch receives a UIControlEventValueChanged event.

Now how you call the endEditing: message on the text field depends on the structure of your classes. You could have a designated initializer method on your table view cell class where you pass an instance of the UITextField corresponding to the UISwitch that controls the text field. Keep a weak reference to the textfield instance and then call endEditing: when the switch value changes. Or you could simply try to call [self endEditing:YES]; on the UITableViewCellWithSwitch when switchChanged: event is fired or [self.superview endEditing:YES];. I prefer the former solution rather than the latter since the latter is more of a hack than a proper solution.

UPDATE:

After reviewing your code, the reason for the error you've mentioned in your question

It leads to errors like this (value of switch displayed in text field):

is the following piece of code:

- (void)switchChanged:(UISwitch *)sender {
    NSLog(@"UISwitch:startEditing:%@",self.attributeID);
    [self handleStartEditingForAttributeID:self.attributeID];

    NSString* newValue = sender.on==YES?@"true":@"false";
    NSLog(@"UISwitch:valueChanged:{%@}", newValue);
    [self handleValueChangeForEditedAttribute:newValue];

    NSLog(@"UISwitch:endEditing");
    [self handleEndEditingForEditedAttribute];
}

You are calling the handleValueChangeForEditedAttribute: with the switch's value for the attribute that is only supposed to hold the textfield's value in reality. And in your UIEventHandler class you are updating your data access object (DAO) with the switch value in the handleEndEditingForEditedAttribute method. Change your switchChanged: method's logic to something like this:

- (void)switchChanged:(UISwitch *)sender {
    if (sender.on == YES) {
        [self handleStartEditingForAttributeID:self.attributeID];
    } else {
        [self handleEndEditingForEditedAttribute];
    }
}

And in your UIEventHandler class, uncomment the commented lines in your post that say "possible solution". That should ensure any previous changes to get saved before storing values for the new attributeID.

-(void) handleStartEditingForAttributeID:(NSString *)attributeID { 
    // Possible solution   
    if (self.editedAttributeID != nil && [attributeID isEqualToString:self.editedAttributeID]==NO) { // Workaround needed for UISwitch events
        [self handleEndEditingForActiveAttribute];
    }
    self.editedAttributeID = attributeID;
    self.temporaryValue = nil;
}
Pranay
  • 846
  • 6
  • 10
  • That is correct `[self.storage saveValue]` causes the whole view to regenerate, which causes the `UITextField` to lose first responder status and trigger the `UIControlEventEditingDidEnd` when it is too late. I am starting to think that due to the architecture of my application the best place to solve it is in the `UIEventHandler.m` (see possible solution). – Peter G. Nov 20 '18 at 17:02
  • 1
    @PeterGerhat You're right. Check out my updated answer. I updated your `switchChanged:` – Pranay Nov 20 '18 at 17:07
  • Seems like you didn't get the idea how `switchChanged:` should work. It has to trigger the `startEditing`, `valueChanged` and `endEditing` events in that sequence for the `UIEventHandler` to work properly. I use this event model across many different UI input components so I can't adapt it for `UISwitch` exclusively. – Peter G. Nov 22 '18 at 08:28
  • @PeterGerhat Ah got it. Well if this design is universal across your app I'd say your best bet is the solution you posted below or calling `[self.view endEditing:YES]` in your table view cell or its parent view to trigger the `UITextField`'s did end editing event. – Pranay Nov 26 '18 at 16:19
0

My best solution yet was to solve the problem in UIEventHandler.m. If at the time of calling startEditing the endEditing event wasn't triggered yet it gets called from the UIEventHandler.

-(void) handleStartEditingForAttributeID:(NSString *)attributeID { 
    // Possible solution   
    if (self.editedAttributeID != nil && [attributeID isEqualToString:self.editedAttributeID]==NO) { // Workaround needed for UISwitch events
        [self handleEndEditingForActiveAttribute];
    }
    self.editedAttributeID = attributeID;
    self.temporaryValue = nil;
}

-(void) handleValueChangeForEditedAttribute:(NSString *)newValue {
    self.temporaryValue = newValue;
}

-(void) handleEndEditingForEditedAttribute { 
    if (self.temporaryValue != nil) { // Only if value has changed
        NSLog(@"UIEventHandler:saveValue:%@:{%@}", self.editedAttributeID, self.temporaryValue);

        // Causes the view to regenerate
        // The UITextField loses first responder status and UIControlEventEditingDidEnd is gets triggered too late
        [self.storage saveValue:self.temporaryValue 
                   forAttribute:self.editedAttributeID];

        self.temporaryValue = nil;
    }
    self.editedAttributeID = nil;
}
Peter G.
  • 7,816
  • 20
  • 80
  • 154