はじめに

pointfreeのswift-issue-reportingというライブラリを眺めていたらXcodeの紫色の警告を出す方法を知ったので記録も兼ねて記事にしました。(参考

Xcodeの紫色の警告は、Runtime Warningというもので、ビルドが成功してアプリが起動した後の実行時に警告が出るというものです。UIの状態をメインスレッドの外で更新した際に出るのはよく見かけると思います。

このRuntime Warningの表示方法は明確にドキュメント化されていないようですが、pointfreeのUnobtrusive Runtime Warnings for Librariesという記事を読んで、表示方法が分かったのでその方法を記事を元に記録します。

なお、記事にもあるようにこの方法はAppleが明示的に示した方法ではないので、AppStoreに提出するアプリでは使用しないようにした方が良いです。

何が嬉しいのか

開発者がRuntimeWarningを出す目的は、ランタイムでアプリの不具合に気付けるようにすることです。他の手法として、ブレークポイントや assertなどがあります。

ユースケースとして、ライブラリ開発者が正しくない使い方での利用が発生したことを利用側に伝えることを考えます。その手法として、ブレークポイントや assertの2つを考えます。

しかし、ブレークポイントはライブラリ提供側ではなく利用側で設定する必要があるというデメリットがあります。また、assertは実際にアプリの不具合に気づけるものの、アプリケーションもクラッシュしてしまうため、開発の妨げになるケースが考えられます。

RuntimeWarningを使うとライブラリ提供側でアプリの不具合を利用側に伝えることができ、アプリケーションもクラッシュしないため、上記のデメリットを解決しつつ、ライブラリの正しくない使い方に即座に気づくことが可能です。

RuntimeWarningの表示方法

結論

  • os_logを使い、subsystemにcom.apple.runtime-issuesを指定する
  • dsoにはSwiftUIのフレームワークのベースアドレスを指定する

下記のようなクラスを作れば再利用して使えますが、もっと簡単にswift-issue-reportingのreportIssue関数を使うことで実現できます。


actor RuntimeWarning {
    var info = Dl_info()

    init() {
        dladdr(
            dlsym(
                dlopen(
                    nil,
                    RTLD_LAZY
                ),
                """
                $sxSg7SwiftUI8CommandsA2bCRzlAbCP4body4BodyQzvgTW
                """
            ),
            &info
        )
    }

    func log(message: StaticString) {
        os_log(
            .fault,
            dso: info.dli_fbase,
            log: OSLog(
                subsystem: "com.apple.runtime-issues",
                category: "ReportRuntimeWarningSample"
            ),
            message
        )
    }
}

詳細

RuntimeWarningの表示方法は、os_logを使うことで実現できます。subsystemにcom.apple.runtime-issuesを指定することが一つのポイントです。 それに加えて、dso(dynamic shared object)というパラメータを指定することで、RuntimeWarningを表示することができます。

os_log(
    .fault,
    dso: /* ここにdsoを指定する */,
    log: OSLog(
        subsystem: "com.apple.runtime-issues",
        category: "ReportRuntimeWarningSample"
    ),
    message // StringではなくStaticStringを指定する
)

このdsoは記事で参照されている元ツイートが消えている関係で詳細は分からなかったのですが、dynamic libraryを特定するために使われているようで、SwiftUIのフレームワークに梱包されているシンボルのポインタを取得する必要があるようです。

このdlopendlsymはpointfreeがXCTFailをTesting以外の場所でも使えるようにした際にも使われているようです。(参考

SwiftUIのフレームワークのシンボルは下記のコマンドで元記事では取得していました。

nm -g /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/SwiftUI.tbd

これを実行すると自分の手元では72400行の出力が出ました(膨大)。この中からどれか一つのシンボルを選びます。

出力行の数がたくさんあるところ
dlsym(
    dlopen(
        nil,
        RTLD_LAZY
    ),
    // ここに取得したシンボルを指定する
    """
    $sxSg7SwiftUI8CommandsA2bCRzlAbCP4body4BodyQzvgTW
    """
)

ここまでで、SwiftUIに含まれるシンボルのアドレスが分かったので、あとはdladdrを使ってDl_info構造体に値を入れます。

dladdr(
    dlsym(
        dlopen(nil, RTLD_LAZY),
        """
        $sxSg7SwiftUI8CommandsA2bCRzlAbCP4body4BodyQzvgTW
        """
    ),
    &info
)

これでos_logのdsoパラメータに値を指定できるようになります。

os_log(
    .fault,
    dso: info.dli_fbase,
    log: OSLog(
        subsystem: "com.apple.runtime-issues",
        category: "ReportRuntimeWarningSample"
    ),
    "RuntimeWarningが表示されました。"
)
Xcode上でRuntimeWarningとして表示されているところ

最後に

ランタイム警告の表示方法として、pointfreeのswift-issue-reportingというライブラリがあります。この実装ではos_logを活用し、subsystemにcom.apple.runtime-issuesを指定し、dsoにSwiftUIのフレームワークのベースアドレスを指定することで実現していました。 assertやブレークポイントの代替案として、RuntimeWarningが有効なケースがあると思ったので、自分も活用していこうと思います。

また、冒頭でも書きましたが、このRuntimeWarningはAppleが明示的に示した方法ではないので、AppStoreに提出するアプリでは使用しないようにした方が良いです。

実際に使いたいときは 下記のように #if DEBUGで囲うことが推奨されています。

#if DEBUG
os_log(
    .fault,
    dso: info.dli_fbase,
    log: OSLog(
        subsystem: "com.apple.runtime-issues",
        category: "ReportRuntimeWarningSample"
    ),
    "RuntimeWarningが表示されました。"
)
#endif

サンプルリポジトリ(最小実装)はこちらに上げたので少しでも参考になれば幸いです。