skip to Main Content

I develop an iOS App called Swordy Quest:
https://apps.apple.com/us/app/swordy-quest-an-rpg-adventure/id1446641513

It contains Game Center integration for Leaderboards, Achievements, Player vs Player (PVP) matchmaking and Clans.

I have a local test version that I use when developing (with a test bundleID). I also have a production version of my game that I use to play the game and progress as if I was a customer. However, in order to upgrade/implement the Game Center functionality above, I need to use my production bundleID for testing. This then overwrites my ‘customer game’ with all my test data (ruining my ‘natural’ progress).

So I am wondering, is it possible to have a ‘clean’ production version of an app and still have a separate test version that allows me to test Game Center functionality. Or is there some way to restore a previous app state in Xcode so I could save my production clean version before polluting it with test data? I know in Mac Apps you can change the custom working directory, but I don’t think you can in iOS?

I have looked into backing up my Production version of the app before working on Game Center upgrades, but it looks like this is probably not possible? Has anyone come up with a clever way around this?

Please note I have stored both CoreData and UserDefaults in the app.

3

Answers


  1. Targets is designed to do just that. You set pre-processor macros values to get the compiler to compile specific code based on target / macros values.

    In your case, you change path to the customer game / test data file based on selected the target / macro combination.

    You can also set a different bundleID for each target.

    Once this is all setup you simply just switch between target and compile. The whole thing should just work seamlessly.

    Make a backup of your project and then follow this tutorial which covers exactly how to do this:
    https://www.appcoda.com/using-xcode-targets/

    If the link above is broken in future, just search "Xcode target tutorials"

    Login or Signup to reply.
  2. Custom working directory is something only command-line tool projects. ChangeCurrentDirectoryPath option is no longer available at this place as the screenshot below in XCode 4.6.1. Sounds crazy but you can try downgrade to Xcode 4 and make it happen.

    enter image description here


    Or you will need load files using Cocoa’s NSBundle class or Core Foundation’s CFBundle functions. So make duplicate target for your Swordy Quest test. It will not affect your clean copy.

    Manage schemes:

    ss

    Finally click the little gear button create a clean copy to avoid touch your production code.

    enter image description here

    After you set up your keys both product and test where

    Build Settings > Packaging ( write to filter Packaging )

    enter image description here

    Implement as a code below to your logic function ( for example implement in it to a function which trigger a GameHomeVC from LoginPlayerVC )

        var key: String?
        #if TARGET_PROD || TARGET_STORE
        key = @"prodKey";
        #else
        key = @"testKey";
    
    Login or Signup to reply.
  3. as a precursor, i’m not familiar with Game Center, so there may be concerns there that i haven’t accounted for. so, with that, my instinct in solving this starts out with launch arguments. there is a great article on how to do this here: https://www.swiftbysundell.com/articles/launch-arguments-in-swift/.

    Now that you’re able to start changing behavior based off of launch arguments from different schemes, you can start to look at how to segment your test / prod data.

    As I’m not a CoreData expert, i can’t say with 100% confidence that this is possible (or easy), but i would investigate how to setup separate persistent stores based off of a launch argument. using this article as a reference, it seems like you could roughly do something like the below after creating a -testGameCenter launch argument to a new TestGameCenter scheme to create an in-memory data store when testing Game Center

    lazy var persistentContainer: NSPersistentContainer = {
      let container = NSPersistentContainer(name: "YourDataStore")
    
      if CommandLine.arguments.contains("-testGameCenter") {
        let description = NSPersistentStoreDescription()
        description.url = URL(fileURLWithPath: "/dev/null")
        container.persistentStoreDescriptions = [description]  
      }
    
      container.loadPersistentStores(completionHandler: { _, error in
        if let error = error as NSError? {
          fatalError("Failed to load stores: (error), (error.userInfo)")
        }
      })
    
      return container
    }()
    

    if you’re able to solve the CoreData problem above, it’s time to start looking at how to segment your UserDefaults data. this gross but easy solution that immediately comes to mind is prefixing your UserDefault keys with test when running from your test scheme. below is an example of how could structure a wrapper around UserDefaults to manage this

    struct UserDefaultsWrapper {
        let userDefaults: UserDefaults
        let keyPrefix: String
    
        init(userDefaults: UserDefaults, keyPrefix: String) {
            self.userDefaults = userDefaults
            self.keyPrefix = keyPrefix
        }
    
        func setValue(_ value: Any?, forKey key: String) {
            self.userDefaults.setValue(value, forKey: prefixedKey(forKey: key))
        }
    
        func value(forKey key: String) -> Any? {
            self.userDefaults.value(forKey: prefixedKey(forKey: key))
        }
    
        func prefixedKey(forKey key: String) -> String {
            return "(keyPrefix)(key)}"
        }
    }
    

    where you could make use of the wrapper like so

        let userDefaultsPrefix = CommandLine.arguments.contains("-testGameCenter") ? "testGameCenter_" : ""
    
        let userDefaultsWrapper = UserDefaultsWrapper(userDefaults: .standard, keyPrefix: userDefaultsPrefix)
    

    to get something more elegant, you could look a little more into UserDefaults to see if you could apply a solution similar to the one for CoreData where there are two entirely separate stores. from a quick glance at this initializer, maybe you could do something as simple as this with your wrapper instead

    struct UserDefaultsWrapper {
        let userDefaults: UserDefaults
    
        init(userDefaults: UserDefaults) {
            self.userDefaults = userDefaults
        }
    
        func setValue(_ value: Any?, forKey key: String) {
            self.userDefaults.setValue(value, forKey: key)
        }
    
        func value(forKey key: String) -> Any? {
            self.userDefaults.value(forKey: key)
        }
    }
    

    where you construct it like so

        let userDefaultsSuiteName: String? = CommandLine.arguments.contains("-testGameCenter") ? myTestingGameCenterSuiteName : nil
    
        let userDefaults = UserDefaults(suiteName: userDefaultsSuiteName)
        let userDefaultsWrapper = UserDefaultsWrapper(userDefaults: userDefaults)
    

    lastly, from a comment you made on another reply, it sounds like you are also concerned with fresh install scenarios. that said, the approaches i’ve outlined will not help (at least i don’t think) with persisting data across deletes/installs. but, what i think you should think about is if it’s necessary to test those delete/install concerns from your production bundle id. could you instead either manually test those concerns from your test bundle id and/or write unit tests around the components that involve those concerns? when you are approaching your testing strategy, it’s important to make sure that you’re testing the right things at the right layers; testing the wrong things at the wrong layers makes each testing layer much, much harder to execute

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search