【问题标题】:UISwitch does sometimes not send UIControlEventsUISwitch 有时不发送 UIControlEvents
【发布时间】:2012-08-22 19:05:52
【问题描述】:

我有一个模型对象,其中包含一个布尔标志。我想使用 UISwitch 显示标志的值。该值可以通过两种方式更改:

  1. 首先由用户通过切换开关。我为此注册了 UIControlEventTouchUpInside。我还尝试了 UIControlEventValueChanged - 它具有完全相同的效果。
  2. 其次是一些外部状态变化。我使用计时器来检查状态变化并相应地设置开关的 on 属性。

但是,在 timer 方法中设置开关的值会导致即使用户触摸了开关,有时也不会触发 touchUpInside 动作。

所以我面临以下问题:如果我在状态外部更改时在计时器中设置开关状态,我会丢失用户的一些状态更改。如果我不使用计时器,我会从用户那里获得所有状态更改。但是,我错过了所有外部状态更改。

现在我的想法已经用完了。如何实现我想要的,在模型中获取两种类型的状态更改并将它们正确反映在切换视图中?

这是一个显示问题的最小示例。我已经用一个简单的布尔标志替换了模型对象,并且在计时器中我根本不更改标志,我只是调用 setOn:animated:。我计算了操作方法的调用。像这样我可以很容易地找出错过了多少次触摸:

#import "BPAppDelegate.h"
#import "BPViewController.h"

@implementation BPAppDelegate {
    NSTimer *repeatingTimer;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    BPViewController *viewController = [[BPViewController alloc] init];
    self.window.rootViewController = viewController;
    [self.window makeKeyAndVisible];
    [self startTimer];
    return YES;
}

- (void) startTimer {
    repeatingTimer = [NSTimer scheduledTimerWithTimeInterval: 0.2
                                                      target: self.window.rootViewController
                                                    selector: @selector(timerFired:)
                                                    userInfo: nil
                                                     repeats: YES];
}

@end


#import "BPViewController.h"

@implementation BPViewController {
    UISwitch *uiSwitch;
    BOOL value;
    int count;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    value = true;
    uiSwitch = [[UISwitch alloc] init];
    uiSwitch.on = value;
    [uiSwitch addTarget:self action:@selector(touchUpInside:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:uiSwitch];
}

- (void)touchUpInside: (UISwitch *)sender {
    count++;
    value = !value;
    NSLog(@"touchUpInside: value: %d, switch: %d, count: %d", value, sender.isOn, count);
}

- (void) timerFired: (NSTimer*) theTimer {
    NSLog(@"timerFired: value: %d, switch: %d, count: %d", value, uiSwitch.isOn, count);
    // set the value according to some external state. For the example just leave it.
    [uiSwitch setOn:value animated:false];
}

@end

【问题讨论】:

    标签: objective-c ios ios5


    【解决方案1】:

    使用UIControlEventValueChanged 确定开关何时被切换(以编程方式或由用户)。不要使用touchUpInside。当用户拖动 UISwitch 时,touchUpInside 也不能正常工作。

    另外,不要复制已经由 UISwitch 维护的属性(例如你的 value 属性)(即“on”属性)......它是多余的,只会给你带来麻烦

    【讨论】:

    • 感谢您的回答。我也试过 UIControlEventValueChanged。它具有相同的效果,即即使用户触摸了开关,也不会调用操作方法。
    • 关于旗帜。该代码只是一个最小的示例。实际上,我有一个带有布尔属性的模型对象。作为模型对象,我将其与视图分开,因为我想使用 MVC 模式。
    • 知道了,您可以在收到 UIControlEventValueChanged 通知时更新您的模型。 UIControlEventValueChanged 仅在实际切换而不是触摸开关时被调用。
    • 看看下面@jrturton 的回答,这是一个非常重要的观察结果
    • 你的答案是正确的,但我的答案并不适合评论。
    【解决方案2】:

    编辑:以下代码完全符合您的要求,如果不完全符合您的要求。如果确保计时器更改和控件的触摸更改都接收到一个动作方法。该方法每次更改只会发送一次,希望这是您想要的。

    注意切换到 UIControlEventValueChanged。

    [sw addTarget:self action:@selector(touched:) forControlEvents:UIControlEventValueChanged];
    
    - (void) timerFired: (NSTimer*) theTimer
    {
        // set the value according to some external state. For the example just leave it.
        BOOL oldOn = sw.on;
        BOOL newOn = YES;
    
        [sw setOn:newOn];
    
        if(newOn != oldOn) {
            for(id target in [sw allTargets]) {
                for(NSString *action in [sw actionsForTarget:target forControlEvent:UIControlEventValueChanged]) {
                    [target performSelector:NSSelectorFromString(action) withObject:self];
                }
            }
        }
    }
    

    编辑2: 我 [原始海报] 刚刚在您的答案中看到了新代码并进行了尝试。我不确定它是否完全符合我的要求。但是,我不确定我是否理解正确。我不想在这两种情况下都做同样的事情。

    1. 问:当用户触摸开关时,我想切换模型中的布尔值。我不一定需要以编程方式更改开关,因为无论如何它都是通过触摸它自动完成的。我想计算触摸次数以确保没有丢失。

    A:嗯,这不太可能。但是,您当然可以在操作方法中添加一个计数器,并查看您所做的点击是否等于该计数器。当用户点击开关时,“sender == theSwitch”。如果您以其他方式发送操作方法,则可以使用不同的发送方(以区分它们)。

    1. 如果在我的最小示例中获得计时器事件,我只想保留当前的布尔值。但是,我需要以编程方式设置开关状态,以便视图反映正确的模型状态。 – Bernhard Pieber 3 分钟前

    没关系 - 但你一直说你想要调用 action 方法。这段代码就是这样做的。如果你说错了,并且你不想调用 action 方法,那么删除“if”块。

    - (void)insureValueIs:(BOOL)val
    {
        BOOL oldVal = sw.on;
        if(val != oldVal) {
            for(id target in [sw allTargets]) {
                for(NSString *action in [sw actionsForTarget:target forControlEvent:UIControlEventValueChanged]) {
                    [target performSelector:NSSelectorFromString(action) withObject:self];
                }
            }
        }
    }
    

    当然,我确实想要实现相同的目标:模型状态正确反映在视图中。那有意义吗?

    坦率地说,你没有很好地描述你想要什么,我一直在回复你的 cmets,但即使现在我仍然不完全清楚。我相信您在这里拥有一切所需的信息,可以随心所欲。

    【讨论】:

    • 感谢您的回答。我知道以编程方式设置 on 属性时不会触发该操作。那不是我想要的。问题是 touchUpInside: 没有被调用,尽管用户已经触摸了开关。
    • 触摸一个开关相当于改变它的值。触摸绝对至关重要,然后我建议您在开关上添加一个透明视图,当您获得一个触摸事件时,您将其传递给开关并触发一些方法。
    • 我刚刚在您的答案中看到了新代码并进行了尝试。我不确定它是否完全符合我的要求。但是,我不确定我是否理解正确。我不想在这两种情况下都做同样的事情。
    • 1.当用户触摸开关时,我想切换模型中的布尔值。我不一定需要以编程方式更改开关,因为无论如何它都是通过触摸它来自动完成的。我想计算触摸以确保没有丢失。
    • 2.如果我在我的最小示例中获得计时器事件,我只想保留当前的布尔值。但是,我需要以编程方式设置开关状态,以便视图反映正确的模型状态。
    【解决方案3】:

    您的计时器如此频繁地触发它可能会阻止开关完成到替代值的动画转换,因此它永远不会发送 valueChanged 事件消息。您每 0.2 秒将值设置为 YES,我认为这比用户点击开关时发生的动画持续时间要快。

    【讨论】:

    • 感谢您的回答。这正是我没有使用 valueChanged 而是使用 touchUpInside: 的原因。如果开关已经具有相反的状态,则可以设置不应触发 valueChanged 的​​参数。但是,无论开关值如何,touchUpInside 都应该始终发生。
    • 如果我将计时器更改为 0.5 秒,touchUpInside 事件的删除频率会降低。但是,如果时机不好,我仍然会错过用户事件。
    • 我在 timerFired: 方法中添加了一条日志语句。有趣的是,在动画结束之前,开关的 on 状态被设置为新值。正是在这种情况发生时,不再发送操作方法。
    • 这是时机好的时候的样子: timerFired: value: 1, switch: 1, count: 0 touchUpInside: value: 0, switch: 0, count: 1 timerFired: value: 0 ,开关:0,计数:1
    【解决方案4】:

    我的原始代码在 iOS 7 中按预期工作。所以这似乎是 iOS 6 中的一个错误。

    【讨论】:

      猜你喜欢
      • 2012-11-25
      • 2017-07-26
      • 2016-09-04
      • 1970-01-01
      • 2017-02-12
      • 2014-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多