When you ask if you should make a "snapshot", it sounds to me like you're not using source control - or if you are, you're not taking full advantage of it. I would VERY strongly recommend getting hold of a client for Git or Mercurial (Git has better integration with iOS dev tools, if I remember correctly) and setting up a repository on Github or Bitbucket. You can set it to private, so you wouldn't be giving the world access to your source code, and it keeps your code safe from any mishaps, from accidental deletion to loss or damage to the computer you're working on. It also lets you roll back to previous versions in case of problems, compare to earlier versions to find changes that might have caused some behaviour you're trying to track down, and - crucially for your question - it preserves revision history and allows branching.
A source control branch basically lets you split off a version, work on it, and then decide whether to merge it back into the original version. So, you could create a branch from where you are now, and that branch would preserve the state of your code as it was when you gave it to the testers. Any features you add from now on would be kept separate in one branch, and if the testers find bugs, you can fix them in the other branch. Each time you fix a bug, you check to see if the "features" branch needs the fix as well - if it does, you can just merge the fix into both branches. When the test is over, you can pretty much forget about the "testing" branch because you know that any fixes you need to have in the main version were merged in already.
There are various patterns and techniques to determine when and why to branch, all aimed at solving exactly the problem you're describing. A popular one is the "Git-Flow" pattern, although (in my opinion) that has the flaw that it looks rather more complex than it needs to be (I've used it a fair bit, and it's not as complex as it looks, but it's not as simple as it could be, either).
My advice would be to:
- get Git installed
- get a Bitbucket or Github account
- shove your current version of the code into a repository using the above
- make a branch called "version-1"
- from "version-1", make a branch called "version-2"
- keep new features in "version-2"
- fix bugs that the testers find in "version-1" and rebase "version-2" each time you do
- when the testers stop finding bugs, forget about "version-1", make a new branch from "version-2" called "version-3", give "version-2" to the testers and repeat
You can keep doing that until you run out of new features to add, and then you just get the current version tested until they stop finding bugs, and then when you've fixed the bugs, release the app.
The difference between "merge" and "rebase":
Merge does pretty much what it says - it tries to combine the two versions. If one version has changed an area and the other hasn't, that's easy - it takes the changes. If both versions have made changes to the same area, it'll ask you to pick one or the other. There are a variety of diffmerge tools designed to help you work out what's been changed in each version, compare the two side by side, spot where the conflicts are and work out which you need to take.
Rebase is a little different. When you rebase a branch, it "unwinds" the changes you've made to that branch - basically doing them backwards and remembering them. When the branch is back to how it was when it was branched, it gets the current version of the parent branch. Then, it attempts to replay your changes back on top of the new version. Basically, it's as if you'd made the branch now, not a few days ago or whenever. This can be a really powerful tool, and it's a great way of including changes to parent branches in your code. Imagine you have a version being tested in one branch, and you're working on new features in a second branch. Using the method I described above, the "test" branch would be the parent of the "new features" branch. When your testers find a bug, you jump into the "test" branch and fix it. Now, if you'd waited until after the bugfix before starting to develop new features, then you could have built those features with this fix already in place. Rebase basically tries to act like a time machine so that you can do just that. It undoes what you've done, gets the current version, and then makes your changes to it. Obviously, in some cases there will be changes in your parent branch that affect the changes in the branch you're rebasing, so it won't always work, but in my experience it's generally pretty good. When it doesn't work, you can always just use a regular merge, but when it does work, it's really useful - and like I said, in my experience it tends to work quite well. Sometimes you'll have to go back and tweak after rebasing, but often it Just Works.
This isn't quite Git-Flow, but it takes the principles. One of the main criticisms of Git-Flow is that it can get complex and leave you with loads of branches; this tries to address that a little.
- Master branch. You only put working, tested code in here, and you never actually push to it directly - you just merge it in. Each new commit to this branch is a new release to the public.
- Development branch. This is branched from master. This represents the state of the version you're working on. You only put stuff in here when you're pretty sure it's working, although sometimes bugs will turn up.
- Feature branches. When you want to add a feature, you make a branch off development, build the feature in there, and merge back in. These have names starting with "feature/" to help you organise. Name them something that explains what they're for, like "feature/SearchFunction" or whatever. When the branch is merged in, you can close it. You can leave the branch open if you want, maybe to improve the feature or as somewhere to collect fixes for the feature if it turns out you need any, but remember to rebase the branch before you start work on that. Either way, the next time you merge development into master you should close any feature branches that had already been merged in. If you have a feature that you want to save for a future release, or that isn't finished yet, then you wouldn't merge this into development until it's ready, so it can stay open from one release to the next.
- Bugfix branches. If you find a bug in development and there isn't an open feature branch to fix it in, create a branch whose name starts with "bugfix/" and fix the bug in there. When you've checked that the fix works, merge the branch back into development and close it. If the bug is in a feature you've merged in but whose branch is still open, you can rebase that branch, fix the bug in there, and then merge it in again.
- Hotfix branches. If you find a bug in master, then that means it's in the version your users have. This can't wait for the next version. Make a branch from master whose name starts with "hotfix/" and fix the bug in here. Don't merge it back in until you're sure it works. When you merge it back in, you'll need to merge it into development as well, or else you'll go back to the old buggy code when you next merge development into master. You then release the contents of master as a patch release.
- Release branches. At some point, you have to say "ok, no more new features - we need to get this version bug-free and released". When this happens, you make a branch from development. This branch is given a version number; whichever comes after the last version you released. You can then give this branch to your beta testers. Only bugfixes are allowed in this branch. When it's done and you're ready to release, you merge it into master, and into development to make sure the fixes don't get overwritten. Then you can release the contents of master to your users.
This can lead to a lot of branches, but generally speaking, only a few of them exist at any given point. Master is always there, and you can almost always ignore it. Just remember: when code goes into master, build that code and release it as your next version. Development is also always there, but again, you can kind of ignore it; nobody will be working directly in this branch. If you have any in-house testers (not beta testers), they'll be testing what's in here; it's kind of the "alpha-test" code.
You said you're the only developer; that means that I wouldn't expect you to have more than four branches open for most of the time. Master and develop make two, then the feature you're working on makes three, and then if you get interrupted by a bug you decide you should fix then you'll have a bugfix branch. If you get a report of a bug in master and decide to fix that before finishing any of the rest then it'll go up to five. If you get partway into implementing a feature and get stuck or decide to put in on hold for a bit, then that branch will hang around and add to your total. So, worst-case is six branches open at once, but only three of those are being actively worked on, so it's not too difficult to organise.