如何在运行时判断iOS应用程序是否通过TestFlight Beta安装运行

183

在运行时,是否可能检测到应用程序是通过TestFlight Beta(通过iTunes Connect提交)还是通过App Store安装的?您可以提交一个单独的应用程序包,并使其通过两者都可用。是否有API可以检测它是哪种方式安装的?或者收据是否包含允许确定此信息的信息?


4
您是在谈论通过iTunes Connect进行的新TestFlight测试,还是在谈论您直接上传到TestFlight时的情况?请确认一下。 - keji
新的TestFlight beta版本将会澄清。 - combinatorial
1
看起来-[NSString containsString:]是ios8的新增功能。 如果App Store自动测试尝试在ios7上运行它,将无法运行。 ([receiptURLString rangeOfString @“sandboxReceipt”] .location!= NSNotFound)应该解决问题。 - rgeorge
@rgeorge 谢谢,那是一个愚蠢的错误! - combinatorial
2
我本来想询问如何在没有appStoreReceiptURL的iOS 6上进行检测,但似乎TestFlight应用只支持iOS 8;所以-[NSString containsString]可能仍然可以使用。因为这个问题,我已经暂停了应用商店的beta测试,但我猜有些人可能正在使用混合测试策略,通过Ad-Hoc进行传统测试和AppStore beta进行公共测试,所以rangeOfString仍然是最好的选择。 - Gordon Dove
如果你正在寻找macOS应用的解决方案,你可以参考这个链接:https://gist.github.com/lukaskubanek/cbfcab29c0c93e0e9e0a16ab09586996。 即使你在谷歌搜索macOS,这个Stack Overflow的帖子也会排在首位,所以我把它放在这里。 - undefined
5个回答

166

通过 TestFlight Beta 安装的应用程序,收据文件名为 StoreKit/sandboxReceipt,而不是通常的 StoreKit/receipt。使用 [NSBundle appStoreReceiptURL],您可以在 URL 的末尾查找 sandboxReceipt。

NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSString *receiptURLString = [receiptURL path];
BOOL isRunningTestFlightBeta =  ([receiptURLString rangeOfString:@"sandboxReceipt"].location != NSNotFound);

注意,sandboxReceipt也是在本地运行构建和在模拟器中运行构建时的收据文件名称。

Swift版本:

let isTestFlight = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"

7
注意,这适用于设备上的本地测试,但不适用于模拟器。我添加了类似以下的内容:#if TARGET_IPHONE_SIMULATOR isRunningInTestMode = YES; #endif显然,需要导入头文件 #import <TargetConditionals.h>。 - Gordon Dove
13
紧凑版代码:[[[[NSBundle mainBundle] appStoreReceiptURL] lastPathComponent] isEqualToString:@"sandboxReceipt"] (如果运行的是通过TestFlight分发的二进制文件,则为True)通过Supertop/Haddad - Nick
3
由于收据仅适用于宿主捆绑包,因此此方法无法用于扩展捆绑包。 - jeeeyul
4
似乎在使用Ad Hoc分发安装构建时也会返回“YES”。 - Keller
7
2020年能正常运作,六年后依然如此。 - fireant
显示剩余10条评论

129

根据combinatorial的答案,我创建了以下SWIFT辅助类。使用该类,您可以确定它是调试、testflight还是appstore构建。

enum AppConfiguration {
  case Debug
  case TestFlight
  case AppStore
}

struct Config {
  // This is private because the use of 'appConfiguration' is preferred.
  private static let isTestFlight = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
  
  // This can be used to add debug statements.
  static var isDebug: Bool {
    #if DEBUG
      return true
    #else
      return false
    #endif
  }

  static var appConfiguration: AppConfiguration {
    if isDebug {
      return .Debug
    } else if isTestFlight {
      return .TestFlight
    } else {
      return .AppStore
    }
  }
}

我们在项目中使用这些方法为不同的环境提供不同的跟踪ID连接字符串

  func getURL(path: String) -> String {    
    switch (Config.appConfiguration) {
    case .Debug:
      return host + "://" + debugBaseUrl + path
    default:
      return host + "://" + baseUrl + path
    }
  }

或者:

  static var trackingKey: String {
    switch (Config.appConfiguration) {
    case .Debug:
      return debugKey
    case .TestFlight:
      return testflightKey
    default:
      return appstoreKey
    }
  }

更新于 2016 年 5 月 2 日:使用类似于#if DEBUG的预处理器宏的先决条件是设置一些Swift编译器自定义标志。更多信息请参见此答案:https://dev59.com/xWAf5IYBdhLWcg3w9Wiu#24112024


2
@Urkman 确保您设置了-D DEBUG 标志。更多信息可以在此处找到:https://dev59.com/UGAg5IYBdhLWcg3wE3nQ。 - Caleb
谢谢@Caleb,我在答案中添加了更多关于先决条件的解释。 - LorenzoValentijn
3
谢谢你的回答,我觉得非常有帮助!另外很好知道,使用 #if targetEnvironment(simulator) 可以确定你是否在模拟器中运行。所以我有三个选项:模拟器、TestFlight 和 AppStore(在我的情况下,这比 Debug 更好) :-) - Jeroen

53

现代Swift版本,考虑模拟器(基于接受的答案):

private func isSimulatorOrTestFlight() -> Bool {
    guard let path = Bundle.main.appStoreReceiptURL?.path else {
        return false
    }
    return path.contains("CoreSimulator") || path.contains("sandboxReceipt")
}

很好地包含了模拟器,但您可能需要更改函数名称,因为它不再适用于所有情况。 - dbn
2
哇!它运行了!太棒了!对于相同的构建(在一个方案中使用一个配置文件构建的一个构建),TestFlight返回TRUE,AppStore返回FALSE。完美!谢谢! - Argus
@dbn你能详细说明为什么这不再适用于所有情况吗? - Ethan
3
@Ethan 我发表评论后,这个答案已经被编辑了;原来的方法名是 isTestFlight() - dbn
1
请注意,此方法是一种启发式方法,可能不会100%准确或具有未来性,因为苹果可能会在未来的更新中更改收据文件路径或命名约定。 - Rob

27

我在Swift 5.2中使用扩展Bundle+isProduction:

import Foundation

extension Bundle {
    var isProduction: Bool {
        #if DEBUG
            return false
        #else
            guard let path = self.appStoreReceiptURL?.path else {
                return true
            }
            return !path.contains("sandboxReceipt")
        #endif
    }
}
然后:
if Bundle.main.isProduction {
    // do something
}

-8

我在我的项目中使用一种方法。以下是步骤。

在Xcode中,转到项目设置(项目而非目标),并将“beta”配置添加到列表中:

enter image description here



然后,您需要创建一个新的方案,以在“beta”配置中运行项目。要创建方案,请转到此处:

enter image description here



给这个方案起任何你想要的名字。然后,你应该编辑此方案的设置。为此,请点击这里:

enter image description here



选择存档选项卡,您可以选择构建配置

enter image description here



然后您需要在项目信息属性列表中添加一个键Config,其值为$(CONFIGURATION),如下所示:

enter image description here



然后,关键是你需要在代码中做一些特定于beta版本的事情:

let config = Bundle.main.object(forInfoDictionaryKey: "Config") as! String
if config == "Debug" {
  // app running in debug configuration
}
else if config == "Release" {
  // app running in release configuration
}
else if config == "Beta" {
  // app running in beta configuration
}

8
虽然这是一种有用的技术,但它并没有回答问题。一个单独的二进制文件被提交到应用商店,并且可以通过TestFlight下载后运行,或者待获得批准后从应用商店下载后再运行。问题在于如何检测正在运行哪个版本。 - combinatorial
有没有选项可以一开始就制作两个存档。一个用于TestFlight,另一个用于应用商店。 - Klemen
可以做到,但它们必须拥有不同的构建号。这意味着需要管理两个构建而不是一个。 - combinatorial
好的,在我看来这是值得的。特别是如果你使用持续集成工具。 - Klemen
@KlemenZagar,您的方法是众所周知且不错的,但它并没有回答问题。 - Stanislav Pankevich
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接