【Unity】iOSでウィンドウからSafeAreaを取得するプラグインを作る


iOS 15でデバイスを回転させるとScreen.safeAreaが正しい値を返さないことがフォーラムで話題になっています。

Unity SafeArea is inconsistent between different starting rotations

Screen.safeAreaが正しい値を返さないのは何年も前からある現象で、UnityとしてはもうFixしたと言ってみたり、また再発したりを繰り返しています。
原因はUIViewのsafeAreaInsetsが正しい値を返さないことにあり、UIWindowのsafeAreaInsetsだと正しかったりします。
どうもウィンドウからビューにイベントの伝達がうまくいってないっぽく、これはUnityの責任というより、Appleの責任ではないかとも思われます。
UnityでiOSビルドしたプロジェクトを探すと、UnityView.mmというファイルのComputeSafeArea(UIView* view)という関数でsafeAreaを取得しているようですが、毎回ここを書き換えるのも面倒です。
なのでフォーラムの投稿にもありますが、UIWindowからsafeAreaを取得するネイティブプラグインを作って対処することを考えます。

iOSプラグイン

extern "C"
{
char* convertNSStringToCString(const NSString* nsString);
char* GetWindowSafeArea();
}

char* convertNSStringToCString(const NSString* nsString)
{
    if (nsString == NULL)
        return NULL;

    const char* nsStringUtf8 = [nsString UTF8String];
    char* cString = (char*)malloc(strlen(nsStringUtf8) + 1);
    strcpy(cString, nsStringUtf8);

    return cString;
}

char* GetWindowSafeArea()
{
    UIEdgeInsets insets = UIEdgeInsetsZero;
    UIWindow *window = UnityGetMainWindow();
    CGRect bounds = window.bounds;
    CGFloat scale = window.screen.scale;

    if (@available(iOS 11.0, *)) {
        insets = window.safeAreaInsets;
    }

    // 上下逆の座標系
    CGFloat x = insets.left * scale;
    CGFloat y = insets.bottom * scale;
    CGFloat w = (bounds.size.width - insets.left - insets.right) * scale;
    CGFloat h = (bounds.size.height - insets.top - insets.bottom) * scale;
    CGRect safeArea = CGRectMake(x, y, w, h);

    NSString *stringData = [NSString stringWithFormat:@"%f/%f/%f/%f", safeArea.origin.x, safeArea.origin.y, safeArea.size.width, safeArea.size.height];
    char* data = convertNSStringToCString(stringData);

    return data;
}

Unity側の読み込み

using UnityEngine;
using System.Runtime.InteropServices;

public class UtilityPlugin : MonoBehaviour
{
#if UNITY_EDITOR

#elif UNITY_IPHONE
    [DllImport("__Internal")]
    private static extern string GetWindowSafeArea();
#elif UNITY_ANDROID

#else

#endif

    public static Rect GetSafeArea()
    {
        Rect rect = Screen.safeArea;

#if UNITY_EDITOR

#elif UNITY_IPHONE
        string data = GetWindowSafeArea();

        if (data != null)
        {
            string[] rectArray = data.Split('/');
            if (rectArray.Length >= 4)
            {
                float x = float.Parse(rectArray[0]);
                float y = float.Parse(rectArray[1]);
                float w = float.Parse(rectArray[2]);
                float h = float.Parse(rectArray[3]);
                rect = new Rect(x, y, w, h);
            }
        }
#elif UNITY_ANDROID

#endif

        return rect;
    }
}