読者です 読者をやめる 読者になる 読者になる

iOSコピペコーディング

できることは知ってるけど、コードってどうやって書くんだっけ?をコピペで解決

UIButtonに3DTouch判定を追加してアナログにコントロールしたい

UIButtonに3DTouchの判定を追加してアナログにズームインやズームアウトをしたり、ミサイルの発射速度をコントロールしたいと思ったので、UIButtonのactionを追加してみた。

Swiftでのサンプルコードはあるものの、Obj-CのサンプルはApple公式では用意されていないので、試してみた。

3D Touch - iOS - Apple Developer


まずは3DTouchが使えるかどうかの判定。
Swiftでやる場合はViewDidLoadでも良いっぽくサンプルプログラムもViewDidLoadに判定文が書かれているがObj-cで実装してみるとViewDidLoadでは判定ができなかった。

developer.apple.com



UIViewControllerのVIewDidLoadの時点では

self.traitCollection.forceTouchCapability

UIForceTouchCapabilityUnknown
The availability of 3D Touch is unknown. For example, if you create a view but have not yet added it to your app’s view hierarchy, the view’s trait collection has this value.

となってしまうので、ViewDidAppearまで待つ必要があるみたいだ。

正確には
- (void)traitCollectionDidChange:(UITraitCollection * _Nullable)previousTraitCollection
が来るまでは判定は出来ないと記載されている。
LifeCycleではViewDidAppearの時点では通過しているので、ViewControllerをallocした際のUIButtonにactionを追加する処理タイミングはViewDidAppearで良いと判断した。

まずはViewDidApperでForceTouchが使えるかどうかを判定し、UIButtonのすべてのTouchイベント発生時にUITouchイベントを取得するようにしてみた。
(VIewDidAppearを複数回通るようなプロジェクトの場合は回避コードが必要になると思うので適宜調整してほしい)

-(void)viewDidAppear:(BOOL)animated{

    [super viewDidAppear:animated];

    if ([self isForceTouchAvalable]) {

        [btnZoomIn addTarget:self action:@selector(getButtonForce:withEvent: ) forControlEventsUIControlEventAllTouchEvents];

        [btnZoomOut addTarget:self action:@selector(getButtonForce:withEvent: ) forControlEventsUIControlEventAllTouchEvents];

    }

}

-(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;

}

isForceTouchAvalableというMethodを作りその中で3DTouchを使えるデバイスかどうかの判定をしている。気をつけなければ行けないのはiPhone6sだから3DTouchを使えるという判定はしてはいけないことだ。
これは、設定アプリで3DTouchをOFFにしたり、感度を変更出来るようになっているからです。
判定方法としてはまずiOS9以上かどうかを判定し、ForceTouchCapability判定時にClashしないようにする。
iOS9以上であればForceTouchCapability判定を行い、Avalableであれば、3DTouchを利用可能と判定する。

あとはイベントを受け取った時に適宜3DTouchの圧力を判定して何か動作させてやればいいだろう。

- (void)getButtonForce:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    UITouch *touch = [[event allTouchesanyObject];

    double force = touch.force;

    double max = touch.maximumPossibleForce;

    double level = 0;

    if (max != 0 ) level = force/max;

    LOG(@"getButtonForce Max:%f Force:%f level:%f",max,force,level);

    //

    if (level>0.5) {

        //圧力が50%以上ならなんかをする場合はこんな感じ。

    }

    if(touch.view == btnZoomIn){//ボタンによって動作を振り分ける場合はこんな感じかな?

        NSLog(@"ZoomIn");

    }else if(touch.view == btnZoomOut){

        NSLog(@"ZoomOut");

    }

}

3DTouch非対応のデバイスとの切り分けもすれば、いろいろなシーンで使えると思う。
圧力が一定以上になった場合にアナログ的に判定をする場合はもう少し工夫が必要だけど基本的な判定はこれで行けるかな。
もちろん上記ではUIButtonの各イベントは有効なままなので、たとえばTouchUpInsideにアクションを割り当ててるけど3DTouchの場合はアナログ入力に対応するなどの場合、3DTouchが有効な場合は、TouchUpInsideの場合の処理をブロックするなどの処理は必要になるし、アクションの開始を検知圧力0.5以上とする場合は、グローバル変数などと比較してボタンの操作開始、終了を処理するMethodを用意する必要がある。

UIView上での3DTouchの圧力判定は、さらに簡単で、

-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    UITouch *touch = [[event allTouchesanyObject];

    double force = touch.force;

    double max = touch.maximumPossibleForce;

    double level = 0;

    if (max != 0 ) level = force/max;

    NSLog(@"touchesMoved Max:%f Force:%f level:%f",max,force,level);

}

これだけで、圧力は取得できる。

圧力使って面白いモノ作ろう!