アプリ開発をしていると、実装完了までに時間がかかる大きな機能を作ることがあります。そういう時にはいくつかの戦略がありますが、僕はフィーチャーフラグでユーザーには見せない状態で少しずつ実装して、mainブランチにマージしていく方法が好みです。
OpenFeatureを入れたりしてもいいですが、もっと気軽に入れたいと思っていて色々考えていました。僕のユースケースでは、TestFlightとローカルビルドかどうかを判断できると良さそうです。TestFlightの時だけtrueになるフラグを準備できれば、まだ実装中の機能の動作確認が行えます。
調べてみると、appStoreReceiptURLを使う方法が出てきます。これを参考に実装しても良いのですが、iOS 18でDeprecatedになってしまいました。
代わりに、AppTransactionが使えるようです。AppTransaction.sharedで、アプリ購入に関する情報を取得できます。例えば、originalAppVersionにはユーザーが初めてアプリを購入したバージョンが取得できます。この辺りはWWDC 22のWhat's new with in-app purchaseに詳しいです。
そして、ユーザーがApp Storeからインストールしているか、TestFlightからインストールしているかを判定するためにはenvironmentが使えます。このフィールドは、App Storeの場合は productionが、TestFlightからインストールした場合はsandboxが格納されています。このフィールドを使うと、アプリがTestFlightからインストールされているかどうかを判定できます。
この値を利用して、TestFlightの時だけフラグがtrueになるようにしておけば、開発中の機能をユーザーに見せずにどんどんmainブランチにマージしていけますし、リリースも行えます。僕はこれを利用するために、DistributionChannelを作って暮らしています。
import Foundation import StoreKit @MainActor enum DistributionChannel: String { case appStore case sandbox case debug case unknown static var isDeveloper: Bool { switch currentChannel { case .sandbox, .debug: true case .appStore, .unknown: false } } private static var cachedCurrentChannel: DistributionChannel? static var currentChannel: DistributionChannel { #if DEBUG return .debug #else if let cachedCurrentChannel { return cachedCurrentChannel } else { load() return .unknown } #endif } static func load() { #if DEBUG return #else Task { do { let result = try await AppTransaction.shared switch result { case .unverified(_, _): cachedCurrentChannel = .unknown case .verified(let transaction): if transaction.environment == .production { cachedCurrentChannel = .appStore } else if transaction.environment == .sandbox { cachedCurrentChannel = .sandbox } else { cachedCurrentChannel = .unknown } } } catch { cachedCurrentChannel = .unknown } } #endif } }
DEBUGの時はローカルでビルドしているはずなので、AppTransactionを使うまでもなくフラグは有効にして良いはず。そうじゃない時は、AppTransactionの値を元に値を保存しておきます。DistributionChannel.isDeveloperがデバッグ起動とTestFlightの時はtrueになるようにしているので、UIレイヤーでこのフラグを見て実装中の機能を出し分ければ目的達成です。
AppTransaction.sharedはasyncのプロパティですが、そのままだと扱いにくいので、同期的にキャッシュを返すようにしています。僕の使い方だとMainActorにするので困っていないのですが、MainActorだと困りそうならなんとかしても良いのかも。
最初の読み込み時や、unverifiedの時は.unknownにしていて、この場合はisDeveloperはfalseになります。今回の用途だとDeveloeprだと確定した時に追加で開発中のUIを適用したいのでそのようになっています。