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

iOS 15でデバイスを回転させるとScreen.safeAreaが正しい値を返さないことがフォーラムで話題になっています。
>[Unity SafeArea is inconsistent between different starting rotations](https://forum.unity.com/threads/unity-safearea-is-inconsistent-between-different-starting-rotations.1173764/)

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

#iOSプラグイン

```obj-c++
#import "UtilityPlugin.h"

@implementation UtilityPlugin

+ (char *)convertUTF8StringData:(NSString *)string
{
    const char *nsStringUtf8 = string != nil ? [string UTF8String] : [@"" UTF8String];
    char* cString = (char*)malloc(strlen(nsStringUtf8) + 1);
    strcpy(cString, nsStringUtf8);
 
    return cString;
}

@end

extern "C" {
char* GetWindowSafeArea(int width, int height);
}

char* GetWindowSafeArea(int width, int height)
{
    UIWindow *window = UnityGetMainWindow();
    UIEdgeInsets windowInsets = [window safeAreaInsets];
    CGFloat scale = window.screen.scale;
    CGSize windowSize = CGSizeMake(window.bounds.size.width * scale, window.bounds.size.height * scale);
    CGSize unitySize = CGSizeMake((CGFloat)width, (CGFloat)height);
    
    if ((unitySize.width == 0.0f) || (unitySize.height == 0.0f) || (windowSize.width == 0.0f) || (windowSize.height == 0.0f))
    {
        return NULL;
    }
    
    // 画面サイズの違いによる補正値
    CGFloat cx = unitySize.width / windowSize.width;
    CGFloat cy = unitySize.height / windowSize.height;
    
    // 少数切り上げた方が元の SafeArea に近い値になる
    CGFloat top = ceilf(windowInsets.top * scale * cy);
    CGFloat bottom = ceilf(windowInsets.bottom * scale * cy);
    CGFloat right = ceilf(windowInsets.right * scale * cx);
    CGFloat left = ceilf(windowInsets.left * scale * cx);
    
    // 上下逆の座標系
    CGFloat x = left;
    CGFloat y = bottom;
    CGFloat w = unitySize.width - right - left;
    CGFloat h = unitySize.height - top - bottom;
    
    NSString *stringData = [NSString stringWithFormat:@"%f/%f/%f/%f", x, y, w, h];
    char* data = [UtilityPlugin convertUTF8StringData: stringData];
    return data;
}
```

#Unity側の読み込み

```c#
using UnityEngine;
using System.Globalization;
using System.Runtime.InteropServices;

public class UtilityPlugin : MonoBehaviour
{
#if UNITY_EDITOR

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

#else

#endif

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

#if UNITY_EDITOR

#elif UNITY_IPHONE
		string data = GetWindowSafeArea(Screen.width, Screen.height);

		if (data != null)
        {
			string[] rectArray = data.Split('/');
			if (rectArray.Length >= 4)
            {
				float x = float.Parse(rectArray[0], CultureInfo.InvariantCulture);
				float y = float.Parse(rectArray[1], CultureInfo.InvariantCulture);
				float w = float.Parse(rectArray[2], CultureInfo.InvariantCulture);
				float h = float.Parse(rectArray[3], CultureInfo.InvariantCulture);

				rect = new Rect(x, y, w, h);
			}
		}
#elif UNITY_ANDROID

#endif

        return rect;
	}
}
```

#追記
ホームバーの無い一部の機種ではこの方法で正しい値を返さない場合があることがわかりました。
Apple、あるいはUnityの正式な修正を待った方がいいかもしれません。
#追記 2
調べたところ、ホームバーの無い機種のUIWindowの返す画面サイズが、Unity側の画面サイズと異なることが原因とわかりました。
なのでフォーラムの投稿にもあるように、プラグイン側ではsafeAreaInsetsを取得するのみとし、Unity側でSafeAreaを作成するとう方法にソースコードを修正しました。
#追記 3
UIWindowとUnity側の画面サイズが異なるなら、その比率からsafeAreaInsetsを補正しなければならないはずです。
しかしホームバーの無い機種はノッチも無いので、この補正を考慮しなければならないのは、ステータスバーを表示しているか、未知の機種でのリスク予防の場合などです。
~~厳密にしたい人は考えてみて下さい。~~
なのでソースコードを修正しました。
プラグイン側にUnityの画面サイズを渡すとSafeAreaを返します。
#追記 4
下の記事によるとfloat.Parseは端末の言語によって挙動が変わるそうなので修正しました。
>[【トラブル回避】端末依存のC#処理に注意！](https://qiita.com/Gaku_Ishii/items/c44b832ee08cbc7c25c4)

#追記 5
Unityではこの問題を2022.1.0a14でFixしたと言っています。
>[[IOS 15] SCREEN.SAFEAREA.WIDTH RETURNS INCORRECT VALUE WHEN DEVICE'S ORIENTATION IS SWITCHED TO LANDSCAPE MODE](https://issuetracker.unity3d.com/issues/ios-15-screen-dot-safearea-dot-width-returns-incorrect-value-when-devices-orientation-is-switched-to-landscape-mode?_ga=2.223667352.2008697184.1638952804-1026051897.1599035905)