SystemInfo.operatingSystemからバージョンを取得するときの注意（iPadOS15.1以降のiPad端末の挙動について）

[PONOS Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ponos)の2日目の記事です。

昨日は@nissy_gpさんの[「UnityのGC AllocateとWaitForSeconds」](https://qiita.com/nissy_gp/private/7823afcd6b1db0b75887)でした。

# はじめに

Unityの`SystemInfo.operatingSystem`をご存知でしょうか。
アプリの実行環境のOS情報を取得するプロパティであり、OS名とバージョンを「iOS 15.1」のようなフォーマットの文字列で返してくれます。
[SystemInfo-operatingSystem - Unity スクリプトリファレンス](https://docs.unity3d.com/ja/2019.4/ScriptReference/SystemInfo-operatingSystem.html)

今回は、`iPadOS 15.1`以降のiPad端末で`SystemInfo.operatingSystem`を使用していて発生した問題について共有させていただきます。

なお、筆者の手元で今回確認した端末のOSバージョンが`iPadOS 15.1`でしたので、そちらのバージョンについてのみ記述しています。
`iPadOS 15.0 ~ 15.0.2`での動作については未確認ですが、同様の挙動をする可能性があります。

また、確認時のビルド環境は`Unity 2019.4.32f1`および`Xcode 13.0`です。

# 発生した問題

## 前提

`SystemInfo.operatingSystem`をiOS環境で呼ぶと、「iOS 15.1」のように**「iOS」**から始まる文字列を返してくれます。
これは、端末の種類がiPhoneであってもiPadであっても同様の挙動でした。
（ちなみに、`iOS 10`未満の場合は**「iPhone OS」**から始まる文字列を返していましたが、今回は特に触れません）

私の携わっていたプロジェクトでは、この`SystemInfo.operatingSystem`から取得できる文字列を利用して、
以下のようなコードでiOSのバージョンを取得して一部機能の利用可否を判定していました。

```cs
/// <summary>
/// 端末のiOSのバージョンを取得。
/// </summary>
/// <returns>iOSのバージョン。</returns>
static Version GetiOSVersion()
{
    var operatingSystem = SystemInfo.operatingSystem;

    // 「iOS」から始まる文字列が返ってくる前提で、「iOS」部分を削除してバージョンの部分のみを残す。
    // "iOS 15.1" -> "15.1"
    var versionString = operatingSystem.Replace("iOS ", "");

    // バージョンの文字列をSystem.Versionオブジェクトにパースする。
    Version version;
    if (Version.TryParse(versionString, out version))
    {
        return version;
    }

    return null;
}
```
```cs
#if UNITY_IOS

var version = GetiOSVersion();
if(version == null)
{
    return false;
}

return 13 <= version.majarVersion; // iOS 13以降であれば機能が利用できる。

#endif 
```

## iPadOS 15.1で問題が発生

アプリの`iOS 15`対応を進めていたある日、
**「iPadOS 15.1のiPad端末で検証しているが、該当機能が有効化されていない」**という不具合報告が飛び込んできました。
「該当機能については手を入れていないはずなのに不思議だな…」と思いつつ調査を進めていくと、衝撃の事実が。

なんと、**「`iPadOS 15.1`を搭載したiPad端末」において`SystemInfo.operatingSystem`が
「iPadOS 15.1」のような「iPadOS」から始まる文字列を返している**ことが判明したのです…！

先ほど上で紹介した`GetiOSVersion()`メソッドは`SystemInfo.operatingSystem`の前半部分が「iOS」でないと、
バージョンの文字列を抽出することができないので、その後の`System.Version.TryParse()`に失敗します。
`GetiOSVersion()`メソッドがバージョン情報を返せなかったために、利用可能なバージョンかどうかの判定を行うことができず、
該当機能が「利用不可」として処理されてしまっていた、というわけです…。

`iPadOS 15.1`上の`SystemInfo.operatingSystem`の挙動に対応するため、`GetiOSVersion()`メソッドを修正することになりました。

## iPadOSから始まる文字列に対応するための修正

修正方法としてすぐに思いついたのは、**「iOS」と「iPadOS」の両方を`Replace()`で削除する方法**でした。

```cs
// 「iOS」部分を削除してバージョン文字列の部分のみを残す。
var versionString = operatingSystem.Replace("iOS ", "");
// 「iPadOS」部分を削除してバージョン文字列の部分のみを残す。
versionString = operatingSystem.Replace("iPadOS ", "");
```

しかし、この修正の場合、今後のバージョンでまた前半の文字列が変化した場合に再度コードを修正する必要が生じてしまいます。

これ以上、前半の文字列の内容に振り回されたくはないため、**正規表現でバージョン情報を取得する方法**で修正することにしました。

```cs
/// <summary>
/// 端末のiOSのバージョンを取得。
/// </summary>
/// <returns>iOSのバージョン。</returns>
static Version GetiOSVersion()
{
    var operatingSystem = SystemInfo.operatingSystem;

    // 「数字」から始まり、「.(ピリオド)」と「数字」が連続する文字列を探し出す。
    var regex = new System.Text.RegularExpressions.Regex("([0-9]+)(\\.[0-9]+)*");
    var match = regex.Match(operatingSystemString);
    if (!match.Success)
    {
        return null;
    }

    Version version;
    if (Version.TryParse(match.Value, out version))
    {
        return version;
    }

    return null;
}
```

これなら、前半の文字列が「iOS」でも「iPhoneOS」でも「iPadOS」でもバージョン情報を抽出することができます！

# まとめ

`SystemInfo.operatingSystem`を利用してOSバージョンを判定している場合、
判定方法によっては新しいOSバージョンがリリースされたときに動作不備が発生することがありますので注意しましょう。

明日の担当も私@e73ryoです。