背景
雖然我知道iOS的控件都會有大大小小的坑,但我之前卻一直不知道連小小的UISwitch也會有那么大的坑。。。(主要是這個東西用得少,或者說項目里需要用的地方少,沒怎么注意)
事情是這樣子的,我給UISwitch添加了UIControlEventValueChanged事件,然后在一次偶然中拖了下,發(fā)現居然會一直來回跳動。于是我搜索了下,果然并不是只有我一個人。
網上的解決方案
搜索UISwitch bug
就有不少的文章,其解決方式不外乎把事件改為UIControlEventTouchUpInside
,我也嘗試了下,發(fā)現還是有問題,按住控件并且在控件外放開的時候,按鈕會切換,但并不會調用響應方法!!!
于是某個文章里又說了,再加個UIControlEventTouchUpOutside
事件。emmmmmmm... 看起來是很完美,然而并沒有什么卵用。
于是我就琢磨了,我能不能截攔事件呢? 讓我自己去處理他的事件。然后我就有了以下的解決方法。
我的方案
懂iOS的事件響應的人都知道,UIGestureRecognizer的優(yōu)先級是大于控件本身的觸摸事件的,那我能否添加個UITapGestureRecognizer來覆蓋原來的事件呢??
我嘗試了下,確實是可行的,但還是有點問題,在滑動的時候因為UITapGestureRecognizer識別失敗,還是會響應UISwitch本身的事件。
那我可不可以再增加個UIPanGestureRecognizer呢?? 我覺得是OK的,但有點麻煩了。事實上點擊已經夠用了,所以我并不打算去做滑動的處理,但滑動又有問題,那怎么辦?
根據iOS事件響應的原理,如果其有子view,并且子view是能響應事件的,那子view就會響應事件。然后我就在UISwitch上面增加了一層透明的view,讓該view去響應事件,從而阻隔了UISwitch的事件。
所以我先子類化,初始化方法如下:
- (instancetype)init
{
if (self = [super init])
{
[self setBackgroundColor:[UIColor clearColor]];
UIView *view = [[UIView alloc] init];
[view setBackgroundColor:[UIColor clearColor]];
[self addSubview:view];
[self setView:view];
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(changeState:)];
[view addGestureRecognizer:gestureRecognizer];
}
return self;
}
然后通過block回調給外面:
- (void)addBlock:(ZTSwitchBlock)block
{
[self setBlock:block];
return;
}
- (void)changeState:(UITapGestureRecognizer *)gestureRecognizer
{
ZTSwitchBlock block = [self block];
if (block)
{
block(self);
}
return;
}
可是這樣子做完后,總感覺怪怪的,好像少了什么一樣。只能點擊不能滑動嗎?
感覺也不是,因為我覺得滑動是可以做的,但沒必要,那是什么呢?。。。
在我無聊滑動我的X時候,我才想起來,沒錯,就是震動。打開系統(tǒng)的設置,在聲音與觸感
那里的底部,有個東西叫系統(tǒng)觸感反饋
,當你打開之后,你的一些操作會有輕微的震動來作為反饋,我覺得這個體驗很好,但是我已經阻斷了UISwitch本身的事件,所以它是無法響應的,也就提供不了震動反饋。不過還好,蘋果給了我們API,所以我們還是可以自己實現的。
我修改了實現,如下:
- (void)setOn:(BOOL)on animated:(BOOL)animated
{
[self vibration];
[super setOn:on animated:animated];
return;
}
- (void)changeState:(UITapGestureRecognizer *)gestureRecognizer
{
ZTSwitchBlock block = [self block];
if (block)
{
[self setNeedVibration:YES];
block(self);
[self setNeedVibration:NO];
}
return;
}
- (void)vibration
{
if ([self isNeedVibration])
{
UIImpactFeedbackGenerator *feedBackGenertor = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
[feedBackGenertor prepare];
[feedBackGenertor impactOccurred];
[self setNeedVibration:NO];
}
return;
}
當設置UISwitch狀態(tài)時,判斷是不是要震動(因為只有手動點擊才可以震動,調用方法是不需要震動的),震動后馬上重置狀態(tài)。
在我覺得已經很完美的時候,bug出現了。。。 fuck
注意下我調用block的操作,我是先設置狀態(tài),然后調用block,再重置狀態(tài),這看起來很OK,事實上一般情況下也很OK。只是,既然是造輪子,那么啥情況都有可能出現,就比如,調用block的時候,別人在block的實現里使用異步的子線程,這就導致了在沒有調用setOn:
方法就已經返回了,然后你就重置了狀態(tài)。。。 這就導致了震動永遠是無效的。
遇到這個bug,我馬上就想到了線程同步。于是乎我就開始了折騰線程同步,但事實上這是徒勞的,因為外界的block不會給我任何有關線程的東西,我也就無能為力了。
那怎么辦?? 只能眼睜睜的看著??
我覺得挺沮喪的,一個小小的控件我居然都搞不定。。。
在我懊惱之際,突然想起了系統(tǒng)異步回調的處理方法,有個專業(yè)名詞叫CompletionHandler
的block就是專門用來處理這種情況的。(更多內容請看 有一種 Block 叫 Callback,有一種 Callback 叫 CompletionHandler)
于是回調代碼改成這樣:
- (void)changeState:(UITapGestureRecognizer *)gestureRecognizer
{
ZTSwitchBlock block = [self block];
if (block)
{
[self setNeedVibration:YES];
__weak typeof(self) weakSelf = self;
block(self, ^() {
[weakSelf setNeedVibration:NO];
});
}
return;
}
調用者只要在最后調用completionHandler();
就可以了。完美的解決了我的需求。或許唯一不足的就是要求使用者調用completionHandler()
了,如果使用者忘記了,有可能會導致狀態(tài)無法恢復,在非點擊的狀態(tài)下調用setOn:
就會出現震動。
demo地址:ZTSwitchDemo
iOS OC Swift Flutter開發(fā)群 139322447