[iOS] UIApplicationを継承したカスタムなアプリケーションクラスを使用する

はじめに

モバイルアプリサービス部の中安です。

UIApplicationのシングルトンインスタンスに対して処理をお願いする場面は、iOSアプリを開発してる中で出てくると思います。

たとえば

// ホーム画面のアイコンにバッジを付けたい時
UIApplication.shared.applicationIconBadgeNumber = 9

という感じです。

このsharedで受け取れるインスタンスは当然ながらUIApplicationオブジェクトです。 そのインスタンスをカスタムなアプリケーションクラスに差し替えて、処理ごとオーバライドしてやることも可能です。

まぁ、そういうことをする機会こそ少ないとは思うのですが、 実際にはどうやってやればいいのかというのが今回のお題です。

カスタムなアプリケーションクラスを用意

class Application: UIApplication {
}

シンプルにUIApplicationを継承してやります。名前はなんでもいいです。ここではApplicationとしています。 Application.swiftファイルを用意して、プロジェクトに追加してやります。

ただし、これだけでは実は何の役にも立ちません。

main.swift

言語をswiftで設定されたiOSアプリのプロジェクトでは、いわゆる「メイン関数」が書かれたファイルはありません(objective-cでは存在した)。

でも実はmain.swiftというファイルをプロジェクトに追加してやることで、 メイン関数の役割を果たすファイルだと認識され、最初に実行されるようになります。

プロジェクトにmain.swiftファイルを追加して以下のように書いてやります。

import UIKit

let argc = CommandLine.argc
let argv = CommandLine.unsafeArgv
let appDelegate = NSStringFromClass(AppDelegate.self)
let application = NSStringFromClass(UIApplication.self)
let code = UIApplicationMain(argc, argv, application, appDelegate)

exit(code)

ちなみに上記の書き方は swift4.2からの記述で、それ以前についてはargvへの代入の仕方が少し違います。

// swift4.2からはこの書き方はdeprecated
let argv = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(
    to: UnsafeMutablePointer<Int8>.self,
    capacity: Int(CommandLine.argc)
)

さて、このmain.swiftの中でアプリケーションクラスを指定してやる箇所があると思うので、 その箇所を変更してあげましょう。

let application = NSStringFromClass(UIApplication.self)
↓
let application = NSStringFromClass(Application.self) // 名前は適宜書き換えてください

これでカスタムなApplicationクラスがUIApplicationから差し替わったはずですが、 このままビルドすると残念ながらコンパイルエラーになると思います。

@UIApplicationMainアトリビュート

前項で出たコンパイルエラーのメッセージは 'UIApplicationMain' attribute cannot be used in a module that contains top-level code という内容だと思います。

これはどういう意味でしょうか。

その説明は、swiftドキュメントの「アトリビュート」について書かれた箇所にて言及されています。

UIApplicationMain

Apply this attribute to a class to indicate that it’s the application delegate.
アプリケーションデリゲートであることを示すためにこのアトリビュートを適用してください。

Using this attribute is equivalent to calling the UIApplicationMain function and passing this class’s name as the name of the delegate class.
このアトリビュートを使用することは、UIApplicationMain関数を呼び出し、このクラスの名前をデリゲートクラスの名前として渡すことと同じです。

If you don’t use this attribute, supply a main.swift file with code at the top level that calls the UIApplicationMain(_:_:_:_:) function.
このアトリビュートを使用しない場合、トップレベルにおいてUIApplicationMain(_:_:_:_:)関数を呼び出すmain.swiftファイルを配置します。

For example, if your app uses a custom subclass of UIApplication as its principal class, call the UIApplicationMain(_:_:_:_:) function instead of using this attribute.
例えば、アプリにカスタムなUIApplicationのサブクラスを主幹クラスとして使用するならば、このアトリビュートを使用する代わりにUIApplicationMain(_:_:_:_:)関数を呼び出します。

まさに当記事でやろうとしている解説が、@UIApplicationMainアトリビュートの説明に記載されているのですが、 AppDelegateにはすでに@UIApplicationMainがクラスの前に記載されていると思います。 main.swiftを使用してカスタムなアプリケーションクラスを設定してやりたい場合は、このアトリビュートは共存できないとされています。

なので、このアトリビュートを消してやります。

@UIApplicationMain ← ここを削除する
class AppDelegate: UIResponder, UIApplicationDelegate {
    // 中身は省略
}

するとビルドができるようになります。

使ってみる

最初の例にあったバッジ数をオーバライドしてみます。

class Application: UIApplication {
    
    override var applicationIconBadgeNumber: Int {
        get {
            return super.applicationIconBadgeNumber
        }
        set {
            super.applicationIconBadgeNumber = newValue > 0 ? 1 : 0
        }
    }
}

こうすることで、どの場所からバッジ数の変更が呼び出されても ホーム画面上のバッジは10しかつかないようになります。

最後に

方法は書き連ねましたが、この記事でしめしたバッジ数の変更方法はテスタブルなものとはいえず、 また不要なバグを仕込んでしまいそうではあるので避けるべきではあると思います。

アプリケーション全体で使う何かしらのインスタンスをここに格納しておくという手法も浮かびそうですが、 それもまた得策とは言えません。

使いどころは設計時に慎重に考える必要はありそうですが、何かの参考になれば幸いです。