井原プロダクトのBLOG

Since 2013。個人でアプリ作っています。

iPhoneの3D Touchを組み込んでみた

youtu.be

こんにちは。

年末から、ずっとSmart Metronomeのアップデートを行っておりまして、今回の変更は、収益性改善の部分と、それだけではナンなので軽めの改修を入れようかなと、メイン画面のテンポを変化させるプラスとマイナスボタンに3D Touchを組み込んでみました。3D Touchは、iPhoneXRでは採用されなかったりして、色々な憶測が飛び交っている機能なのですが、確かに硬いガラスの表面を押しているだけなのに、強く押したり弱く押したりによってアナログ的な表現ができます。但し、実際に凹んだりするわけではないので、強さのフィードバックは弾力では返ってきません。では代わりにどうするか?というとホームボタンの様にコツンという振動を返します。これによってあたかも、ボタンを押したような錯覚が起きます。

この機能は、確かに素晴らしいのですが、欠点は画面上のボタンが強く押し込めるものなのか、3D Touchを実装しているボタンなのかどうかが見た目で全くわからないことです。しかも大抵のボタンは、タップするとその機能を果たすのでユーザーはイチイチアプリ上のボタンを強く押してみたりはしません。スーパーマリオの隠れコインのように全部試すモチベーションは無いのです。なので大多数のiPhoneユーザーは、iPhoneのLEDライトが4段階に調整できる事を知らないでしょう。これでは隠しコマンドです。

f:id:ihatomo:20190114163535p:plain


というわけで、Smart Metronomeでは、バージョンアップ情報として説明動画を載せようと思っているんだけど、いざ動画作ろうと思ってもiMovieじゃダメですね。動画上に動画を重ねられないから上の動画に固定の文字を入れることはできても、タップしているという事をアニメで伝えることができない。というわけで、FinalCutProX買おうかどうか迷っているところです。動画に関しては他にもやりたい事があるので、約35KJPYの投資になってしまうけれどまぁいいかなと。

ということで、次のバージョンアップを楽しみにしていてください。尚、3D Touchは、iPhone6S以降でしたっけ。それ以前のiPhoneと何故かiPhone XRでは使えません。


上記動画のソースコード公開します。ForceTouchを搭載していない場合は、LongTapが動作する様になっています。

[2019/2/7 追記] GitHubにソース公開しました。こちらを参照ください。
blog.hatena.ne.jpihatomo.hatenablog.com

//追記ここまで

#pragma mark - plus/minus Button


@property UISelectionFeedbackGenerator *selectionFeedbackGenerator;
@property UIImpactFeedbackGenerator *impactFeedbackGenerator;

NSInteger taplevelSave;


-(void) preparePlusMinusButton {
    
    _plusButton.exclusiveTouch = YES;
    _minusButton.exclusiveTouch = YES;
    
    //ボタンタッチ
    [_plusButton addTarget:self action:@selector(buttonTappedHandlerUp:) forControlEvents:UIControlEventTouchDown];
    [_minusButton addTarget:self action:@selector(buttonTappedHandlerDown:) forControlEvents:UIControlEventTouchDown];
    
    //
    if ([self isForceTouchAvalable]){
        //forceTouchの処理設定
        [_minusButton addTarget:self action:@selector(getButonForce:withEvent: ) forControlEvents: UIControlEventAllTouchEvents];
        [_plusButton addTarget:self action:@selector(getButonForce:withEvent: ) forControlEvents: UIControlEventAllTouchEvents];
        //ボタンタッチアップ
        [_plusButton addTarget:self action:@selector(touchUpInside) forControlEvents:UIControlEventTouchUpInside];
        [_minusButton addTarget:self action:@selector(touchUpInside) forControlEvents:UIControlEventTouchUpInside];
        //FeedBack
        _selectionFeedbackGenerator = [[UISelectionFeedbackGenerator alloc] init];
        _impactFeedbackGenerator = [[UIImpactFeedbackGenerator alloc] init];
        [_selectionFeedbackGenerator prepare];
        [_impactFeedbackGenerator prepare];
    } else {
        //ボタンの長押し処理設定
        UILongPressGestureRecognizer *gestureRecognizer1 = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressedHandlerDown:)];
        [_minusButton addGestureRecognizer:gestureRecognizer1];
        UILongPressGestureRecognizer *gestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressedHandlerUp:)];
        [_plusButton addGestureRecognizer:gestureRecognizer];
    }
 

}

// ***** 通常のタップ時の処理 *****
-(void)buttonTappedHandlerUp:(UIButton *)button
{
    if (_tempo < 999 ){
        _tempo ++;
        if (_tempo == 300){
            [self divideSet_withoutSound:1];
        }
    }
    [self tempoSet:(float)_tempo];
}


-(void)buttonTappedHandlerDown:(UIButton *)button
{
    if (_tempo > 999){
        _tempo = 999;
    }
    if (_tempo > 1){
        _tempo --;
    }
    [self tempoSet:(float)_tempo];
}


// ***** 長押し関連処理 *****
-(void)longPressedHandlerUp:(UILongPressGestureRecognizer *)gestureRecognizer
{
    //NSLog(@"%s",__func__);
    switch (gestureRecognizer.state) {
        case UIGestureRecognizerStateBegan://長押しを検知開始
        {
            buttonCounter = 0;
            taplevelSave = 0;
            float interval = 0.1;
            NSLog(@"UIGestureRecognizerStateBegan");
            
            buttonTimer = [NSTimer
                           scheduledTimerWithTimeInterval:interval
                           target:self
                           selector:@selector(tempoUpForLogTap)
                           userInfo:nil
                           repeats:YES
                           ];
            [buttonTimer fire];
            
        }
            break;
        case UIGestureRecognizerStateEnded://長押し終了時
        {
            [Metronome change:self.tempo];
            if (_tempo > 299 || divide !=1){
                [self divideSet_withoutSound:1];
            }
            [buttonTimer invalidate];
            //値の保存
            NSUserDefaults *temposave = [NSUserDefaults standardUserDefaults];
            [temposave setFloat:_tempo forKey:@"tempo"];
            buttonCounter = 0;
        }
            break;
        default:
            break;
    }
}


-(void)longPressedHandlerDown:(UILongPressGestureRecognizer *)gestureRecognizer
{
    //NSLog(@"%s",__func__);
    switch (gestureRecognizer.state) {
        case UIGestureRecognizerStateBegan://長押しを検知開始
        {
            buttonCounter = 0;
            taplevelSave = 0;
            float interval = 0.1;
            NSLog(@"UIGestureRecognizerStateBegan");
            
            buttonTimer = [NSTimer
                           scheduledTimerWithTimeInterval:interval
                           target:self
                           selector:@selector(tempoDownForLongTap)
                           userInfo:nil
                           repeats:YES
                           ];
            [buttonTimer fire];
            
        }
            break;
        case UIGestureRecognizerStateEnded://長押し終了時
        {
            [Metronome change:self.tempo];
            [buttonTimer invalidate];
            //値の保存
            NSUserDefaults *temposave = [NSUserDefaults standardUserDefaults];
            [temposave setFloat:_tempo forKey:@"tempo"];
            buttonCounter = 0;
        }
            break;
        default:
            break;
    }
}

-(void)tempoUpForLogTap{
    //最初の5回はゆっくり、その後早く
    if (buttonCounter < 20){
        if (_tempo < 999) {
            _tempo = _tempo + 1;
        }
    } else if (buttonCounter < 65){
        if (_tempo < 997) {
            _tempo = _tempo + 2;
        } else if (_tempo < 999){
            _tempo ++;
        }
    } else {
        if (_tempo < 995) {
            _tempo = _tempo + 5;
        } else if (_tempo < 999){
            _tempo ++;
        }
    }
    buttonCounter ++;
    [self tempoSetEnd];
    
}

-(void)tempoDownForLongTap{
    //最初の5回はゆっくり、その後早く
    if (buttonCounter < 20){
        if (_tempo > 1){
            _tempo --;
        }
    } else if (buttonCounter < 65){
        if (_tempo > 2) {
            _tempo = _tempo - 2;
        } else if (_tempo > 1){
            _tempo --;
        }
    } else {
        if (_tempo > 5) {
            _tempo = _tempo - 5;
        } else if (_tempo > 1){
            _tempo --;
        }
    }
    buttonCounter ++;
    [self tempoSetEnd];
}


-(void) tempoSetEnd {
    int tempoInt = (int) _tempo;
    _tempo = tempoInt;
    NSString *tempotext = [NSString stringWithFormat:@"%d", tempoInt];
    _tempoLabel.text = tempotext;
    [self tempoMarkSet:_tempo];
    [Metronome change:self.tempo];
    //sliderのセット
    _pendlumSlider.value = _tempo;
}


//以下はfourceTouchの処理

-(void)getButonForce : (NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //fourceTouchされたときに呼ばれる
    UITouch *touch = [[event allTouches] anyObject];
    float force = touch.force;
    float max = touch.maximumPossibleForce;
    float level = 0;
    if (max !=0) {
        level = force/max;
    }
    if (taplevelSave == 0){
        //buttonCounterのクリア
        buttonCounter = 0;
        float interval = 0.08;
        NSLog(@"%@", touch.view);
        if (touch.view == _minusButton){
            buttonTimer = [NSTimer
                           scheduledTimerWithTimeInterval:interval
                           target:self
                           selector:@selector(tempoDownForForceTap)
                           userInfo:nil
                           repeats:YES
                           ];
            [buttonTimer fire];
            buttonCounter ++;
            taplevelSave = level;
            //NSLog (@"Force Tapped %ld",(long)buttonCounter);
        } else if (touch.view == _plusButton) {
            buttonTimer = [NSTimer
                           scheduledTimerWithTimeInterval:interval
                           target:self
                           selector:@selector(tempoUpForForceTap)
                           userInfo:nil
                           repeats:YES
                           ];
            [buttonTimer fire];
            buttonCounter ++;
            taplevelSave = level;
        }
    }
    if (level > 0.7){
        //最強に押されている状態
        if (taplevelSave !=2){
            [UIView animateWithDuration:0.1f
                             animations:^{
                                 touch.view.transform = CGAffineTransformMakeScale(1.4, 1.4);
                             }];
            [_impactFeedbackGenerator impactOccurred];
            taplevelSave = 2;
        }
    } else if (level > 0.03) {
        //軽く押されている状態
        if (taplevelSave !=1){
            if (taplevelSave  == 2){
                [_impactFeedbackGenerator impactOccurred];

            }
            [UIView animateWithDuration:0.1f
                             animations:^{
                                 touch.view.transform = CGAffineTransformMakeScale(1.2, 1.2);
                             }];
            taplevelSave = 1;
        }
    } else {
        //ほとんど触ってるだけ(止める)
        [self touchUpInside];
        taplevelSave = 0;
    }
}

-(void) tempoUpForForceTap  {
    //NSLog (@"Tempo Up %ld",(long)taplevelSave);
    if (buttonCounter < 4){
        //最初の0.5秒は何もしない
        buttonCounter ++;
    } else {
        int x = 0;
        //tapの強さによって加速度を変える
        if (taplevelSave == 1){
            x=1;
        } else if (taplevelSave == 2){
            x = 10;
        }
        if (_tempo < 999 - x ){
            if (buttonCounter % 2 == 0) _tempo = _tempo + x;
            if (x==10){
                [_selectionFeedbackGenerator selectionChanged];
            }
        } else if (_tempo < 999) {
            _tempo = _tempo + 1;
        }
        buttonCounter ++;
        [self tempoSetEnd];
    }
}

-(void) tempoDownForForceTap  {
    //NSLog (@"Tempo Up %ld",(long)taplevelSave);
    if (buttonCounter < 4){
        //最初の0.5秒は何もしない
        buttonCounter ++;
    } else {
        int x = 0;
        //tapの強さによって加速度を変える
        if (taplevelSave == 1){
            x=1;
        } else if (taplevelSave == 2){
            x = 10;
        }
        if (_tempo > 1 + x ){
            if (buttonCounter % 2 == 0) _tempo = _tempo - x;
            if (x==10){
                [_selectionFeedbackGenerator selectionChanged];
            }
        } else if (_tempo > 1) {
            _tempo = _tempo - 1;
        }
        buttonCounter ++;
        [self tempoSetEnd];
    }
}


-(void)touchUpInside  {
    //NSLog (@"ボタンから指を離した");
    if ([buttonTimer isValid]){
        [buttonTimer invalidate];
    }
    [UIView animateWithDuration:0.1f
                     animations:^{
                         self->_plusButton.transform = CGAffineTransformMakeScale(1.0, 1.0);
                         self->_minusButton.transform = CGAffineTransformMakeScale(1.0, 1.0);

                     }];
    
    //値の保存
    NSUserDefaults *temposave = [NSUserDefaults standardUserDefaults];
    [temposave setFloat:_tempo forKey:@"tempo"];
    buttonCounter = 0;
}


-(BOOL)isForceTouchAvalable{
    NSString *strSystemVer = [UIDevice currentDevice].systemVersion;
    NSLog(@"version: %@", strSystemVer);
    double version = strSystemVer.doubleValue;
    if (version >= 9.0) {
        NSLog(@"ForceTouchCapability %zd",self.traitCollection.forceTouchCapability);
        if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {
            return YES;
        }
    }
    return NO;
}