iOS 15でデバイスを回転させるとScreen.safeAreaが正しい値を返さないことがフォーラムで話題になっています。
Unity SafeArea is inconsistent between different starting rotations
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プラグイン
#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側の読み込み
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は端末の言語によって挙動が変わるそうなので修正しました。
#追記 5
Unityではこの問題を2022.1.0a14でFixしたと言っています。