4

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.

zeytin
  • 5,545
  • 4
  • 14
  • 38
Charlie S
  • 4,366
  • 6
  • 59
  • 97
  • 3
    Could you buy an iPod touch or an used iPhone and use it only for the clean version? – EmilioPelaez Dec 28 '20 at 20:23
  • Would rather just use 1 iphone if I can (more of a 'wash n go' style person) ;-) – Charlie S Dec 29 '20 at 11:27
  • @CharlieSeligman did you try go back to old Xcode version on create a custom directory already ? Maybe it would be possible create some location to use with same id. Otherwise only option you need separate two targets and need to implement to your project. I check out the my answer again and it seems the best scenario up to now. Maybe you can share a mockup project in a Github repo and i can try manipulate your bundle ids. Also be aware of that please. Apple is little sensitive about this bundle ids. They can reject your app if you make tricky ways on bundle ids. It happened to me once. – zeytin Jan 01 '21 at 10:54
  • Didnt try old version of xcode. That came with so many other issues (versions of swift, etc). 2 separate targets does not get over the bundleID issue Im afraid. – Charlie S Jan 11 '21 at 09:49

3 Answers3

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";
zeytin
  • 5,545
  • 4
  • 14
  • 38
  • Also useful link: http://nilsou.com/blog/2013/07/29/how-to-have-two-versions-of-the-same-app-on-your-device/ – zeytin Dec 28 '20 at 19:33
  • Thanks for the comprehensive answer! As part of the my testing I often need to do fresh installs. If I delete the app icon from my phone with my prod/test bundleID (which will be the same) will it delete both lots of data? Or will it only delete prod data if I have prod version active and test data if test version is active (both under the same bundleID)? – Charlie S Dec 28 '20 at 22:19
  • Trying to test this now but cant find a build setting called 'Packaging Configuration'? – Charlie S Dec 28 '20 at 22:25
  • Also thanks for the link you attached above, but it requires separate bundleIDs so I don't think it would work with testing game center – Charlie S Dec 28 '20 at 22:32
  • 1
    You're welcome, i edited packaging section. Sorry for too many blurs but i had to. You can create a separate config target for manipulating this kind of configuration and of course it will be different bundle id so you need to make little adjustment in your code as above. Okay maybe someone has a better idea. If i think different i will let you know. Good app btw. – zeytin Dec 28 '20 at 22:45
0

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"

zeytin
  • 5,545
  • 4
  • 14
  • 38
John
  • 3,716
  • 2
  • 19
  • 21
  • But if I have to use my production bundleID for testing Game Center etc, then I will have to overwrite my production app with my test app (for Game Center purposes). So my Test-GameCenter target would need the same bundleID as my Production target of the app? – Charlie S Dec 26 '20 at 21:37
  • Regarding to the BundleID, you can have different IDs for each target – L33MUR Dec 27 '20 at 13:03
  • Yeah - you can have different BundleIDs for each target, but I need to be able to test game center and for that you need to test with your production BundleID. That means my Test-GameCenter target and Production target both need the same production BundleID – Charlie S Dec 27 '20 at 13:33
  • Is there a reason why you can't save your progress data to a different file when using Test-GameCenter as to not mess up the production version progress data? Are you using some 3rd party API or database? – John Dec 27 '20 at 17:41
  • 1
    @John that is what Im after doing. At the moment my Test-GameCenter app saves data to the same default location as my Production version. How do I change where the data is saved by apps by default? – Charlie S Dec 28 '20 at 12:18
  • 1
    You can change the current working directory in IOS by using changeCurrentDirectoryPath but I've never used it myself to know if this will solve your problem. You could give that a try. Failing that, then I would have a string you prepend to the file name so when running production this string is "" and when running the Test-GameCenter this string is something like "TestGC-" and use the pre-processor macros/flags to set this string. – John Dec 28 '20 at 16:13
0

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

zeytin
  • 5,545
  • 4
  • 14
  • 38
jakerl
  • 78
  • 2