👋

【Unity】iOS16の3本指タップ時に表示されるツールバーを無効にする

2022/10/18に公開

概要

iOS16がリリースされてから、「3本指で同時タップすると画面上部にツールバーが表示される」という新機能がスマホ音ゲーを中心に一部界隈をざわつかせています。

音ゲーのプレイ時に支障が出てしまう上、端末の設定による回避が不可能(アクセスガイドも無効)であるため、各タイトルではiOS16へのアップデートを控えるように呼びかけていました。

OS標準の動作のためアプリ側での回避が不可能だと思われていましたが、どうやらアプリ側の実装で無効にすることができるらしいです。

参考:iOS16での3本指ジェスチャーを無効にする方法 (開発者向け) – キラキュー・テック

今回はUnityでもこれを無効化できたので、その方法を共有します。

補足:iOS16.1で修正予定

実はiOS16.1のbeta版でこの問題が修正されています。

参考:iOS 16 three-finger gesture bug affects some apps and games

恐らく近いうちに正式リリースされると思われるので、急ぎでなければこれを待っても良さそうです。

対応方法

端的に言うと、ViewControllerなどでeditingInteractionConfigurationで.noneを返すようにし、そのViewControllerをfirstResponderにします。
ViewControllerでなくても、AppDelegateとかUIViewでも大丈夫かもしれません。
firstResponderとは、タッチ以外のイベントを最初に受け取るオブジェクトらしいので、キーボード入力の処理をしているUIResponderを継承したオブジェクトがあれば、それをfirstResponderにしてeditingInteractionConfigurationを実装すればいいと思います。
iOS16での3本指ジェスチャーを無効にする方法 (開発者向け) – キラキュー・テック

ViewControllerの特定のプロパティをオーバーライドすることで無効化できるようです。

Unityの場合、iOSビルド時に生成されるXCodeのプロジェクトを見てみると、UIViewControllerを継承したUnityViewControllerBaseというクラスがあります。

(以下はUnity2020.3.40f1での生成結果)

Classes/UI/UnityViewControllerBase.h
Classes/UI/UnityViewControllerBase.h
#pragma once

#import <UIKit/UIKit.h>
#import "PluginBase/UnityViewControllerListener.h"

#if PLATFORM_IOS
    #define UNITY_VIEW_CONTROLLER_BASE_CLASS UIViewController
#elif PLATFORM_TVOS
    #import <GameController/GCController.h>
    #define UNITY_VIEW_CONTROLLER_BASE_CLASS GCEventViewController
#endif

@interface UnityViewControllerBase : UNITY_VIEW_CONTROLLER_BASE_CLASS
{
    id<UnityViewControllerNotifications> _notificationDelegate;
}
- (void)viewWillLayoutSubviews;
- (void)viewDidLayoutSubviews;
- (void)viewDidDisappear:(BOOL)animated;
- (void)viewWillDisappear:(BOOL)animated;
- (void)viewDidAppear:(BOOL)animated;
- (void)viewWillAppear:(BOOL)animated;

@property (nonatomic, retain) id<UnityViewControllerNotifications> notificationDelegate;

@end

#if PLATFORM_IOS
#include "UnityViewControllerBase+iOS.h"
#elif PLATFORM_TVOS
#include "UnityViewControllerBase+tvOS.h"
#endif

// this should be used to create view controller that plays nicely with unity and account for player settings
UnityViewControllerBase* AllocUnityViewController(void);

UnityViewControllerBase* AllocUnityDefaultViewController(void);
#if UNITY_SUPPORT_ROTATION
UnityViewControllerBase* AllocUnitySingleOrientationViewController(UIInterfaceOrientation orient);
#endif
Classes/UI/UnityViewControllerBase.mm
Classes/UI/UnityViewControllerBase.mm
#import "UnityViewControllerBase.h"
#import "UnityAppController.h"
#import "UnityAppController+ViewHandling.h"

#include "OrientationSupport.h"


@implementation UnityViewControllerBase

@synthesize notificationDelegate = _notificationDelegate;

- (id)init
{
    if ((self = [super init]))
        self.modalPresentationStyle = UIModalPresentationFullScreen;
    return self;
}

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    [_notificationDelegate onViewWillLayoutSubviews];
}

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    [_notificationDelegate onViewDidLayoutSubviews];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear: animated];
    [_notificationDelegate onViewDidDisappear: animated];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear: animated];
    [_notificationDelegate onViewWillDisappear: animated];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear: animated];
    [_notificationDelegate onViewDidAppear: animated];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear: animated];
    [_notificationDelegate onViewWillAppear: animated];
}

@end

UnityViewControllerBase* AllocUnityDefaultViewController()
{
    return [UnityDefaultViewController alloc];
}

#if UNITY_SUPPORT_ROTATION
UnityViewControllerBase* AllocUnitySingleOrientationViewController(UIInterfaceOrientation orient)
{
    switch (orient)
    {
        case UIInterfaceOrientationPortrait:            return [UnityPortraitOnlyViewController alloc];
        case UIInterfaceOrientationPortraitUpsideDown:  return [UnityPortraitUpsideDownOnlyViewController alloc];
        case UIInterfaceOrientationLandscapeLeft:       return [UnityLandscapeLeftOnlyViewController alloc];
        case UIInterfaceOrientationLandscapeRight:      return [UnityLandscapeRightOnlyViewController alloc];

        default:                                        assert(false && "bad UIInterfaceOrientation provided");
    }
    return nil;
}

#endif

UnityViewControllerBase* AllocUnityViewController()
{
#if UNITY_SUPPORT_ROTATION
    if (UnityShouldAutorotate())
        return AllocUnityDefaultViewController();

    UIInterfaceOrientation orient = ConvertToIosScreenOrientation((ScreenOrientation)UnityRequestedScreenOrientation());
    return AllocUnitySingleOrientationViewController(orient);
#else
    return AllocUnityDefaultViewController();
#endif
}

どうやらこれがUnity内で標準的に使われている模様なので、これらにパッチを当てる事で対応が可能です。
以下のようなパッチファイルを用意してみました。

Patches/disable_toolbar.patch
diff -urN Classes/UI/UnityViewControllerBase.h Classes/UI/UnityViewControllerBase.h
--- Classes/UI/UnityViewControllerBase.h
+++ Classes/UI/UnityViewControllerBase.h
@@ -19,6 +19,8 @@
 - (void)viewDidDisappear:(BOOL)animated;
 - (void)viewWillDisappear:(BOOL)animated;
 - (void)viewDidAppear:(BOOL)animated;
+- (BOOL)canBecomeFirstResponder;
+- (UIEditingInteractionConfiguration)editingInteractionConfiguration;
 - (void)viewWillAppear:(BOOL)animated;
 
 @property (nonatomic, retain) id<UnityViewControllerNotifications> notificationDelegate;
diff -urN Classes/UI/UnityViewControllerBase.mm Classes/UI/UnityViewControllerBase.mm
--- Classes/UI/UnityViewControllerBase.mm
+++ Classes/UI/UnityViewControllerBase.mm
@@ -43,9 +43,20 @@
 - (void)viewDidAppear:(BOOL)animated
 {
     [super viewDidAppear: animated];
+    [self becomeFirstResponder];
     [_notificationDelegate onViewDidAppear: animated];
 }
 
+- (BOOL)canBecomeFirstResponder
+{
+    return YES;
+}
+
+- (UIEditingInteractionConfiguration)editingInteractionConfiguration
+{
+    return UIEditingInteractionConfigurationNone;
+}
+
 - (void)viewWillAppear:(BOOL)animated
 {
     [super viewWillAppear: animated];

このパッチをPostProcessBuildなどで適用すると良さそうです。
Macでビルドを走らすので、zshでパッチコマンドを実行します。

[PostProcessBuild]
static void PostProcessBuildForIos(BuildTarget buildTarget, string outputPath)
{
    if (buildTarget == BuildTarget.iOS)
    {
        // NOTE: iOS16以降で3本指でタップした時にツールバーが表示されてしまう問題へのパッチ対応
        var patchFilePath = UnityEngine.Application.dataPath.Replace("Assets", "") + "Patches/disable_toolbar.patch";

        var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "/bin/zsh",
                WorkingDirectory = outputPath,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                Arguments = $"-c 'patch -p0 < {patchFilePath}'",
            }
        };

        process.Start();
        process.WaitForExit();

        if (process.ExitCode == 0)
        {
            Debug.Log(process.StandardOutput.ReadToEnd());
        }
        else
        {
            Debug.LogError(process.StandardError.ReadToEnd());
            if (UnityEngine.Application.isBatchMode)
            {
                EditorApplication.Exit(1);
            }
        }
    }
}

おわりに

iOSネイティブの開発には疎いので、手探りの雰囲気で対応してみましたが、なんらか間違ったことをしている可能性がありそうなので、参考程度に見てもらえると幸いです。

幸いこの問題はiOS16.1で対応されそうで良かったですが、今後iOSの仕様変更でまたこういった問題が起こる可能性はありそうです。
今回のように、ビルド後のコードに直接パッチを当てるという手段が使えるというのは覚えておいて損は無さそうです。

Discussion