I have an app on the App Store, and I want to make some changes that will not effect users that previously downloaded my app.
Is there a way to determine if the user has previously downloaded my app?
Incase anyone is still wondering, a great solution to this problem (assuming you don't already have it) is using the Keychain, which persists through app installation/uninstalls. This library allows you to access the Keychain using NSDictionary-like syntax.
https://github.com/nicklockwood/FXKeychain
So you could implement a function like this:
-(BOOL)alreadyInstalled
{
NSString *installDate = [[FXKeychain defaultKeychain] objectForKey:#"InstallDate"];
if (!installDate)
{
NSString *newInstallDate = [NSString stringWithFormat:#"%i", [[NSDate date] timeIntervalSince1970]];
[[FXKeychain defaultKeychain] setObject:newInstallDate forKey:#"InstallDate"]
return NO;
}
return YES;
}
I don't know a great way to do this but there are some tricks you can do, e.g.:
Look for some data that your application generates. If the data already exists then it's not an update (or an update that completed previously);
Prepare yourself for this, even if this means issuing an intermediate update to your application, then go back to #1. See: How to tell if an iOS application has been newly installed or updated?
Related
I am creating a new feature in one of my apps to show a "What's New" page. I have this all working as expected and it compares the last version (stored) to the current version, then updates the current version once the feature list is shown.
This all works great and as expected. The issue I have now is determining if a user already has the app installed.
Case 1) If the user is a completely new user and it is the first time they have downloaded the app, then the new feature list should not be shown.
Case 2) If they already had the app installed and are updating their app to a new version, then we need to show the new feature list.
The part I need help with is Case 2. How can you determine if the app is already installed by the user, without having to release a minor update just to set a currentInstalledVersion variable beforehand?
if(currentInstalledVersion)
{
if(currentInstalledVersion < actualCurrentVersion)
{
// user upgraded
}
}
else
{
if(someOtherDataSavedOnDiskExists)
{
// user had the app with a version before you implemented currentInstalledVersion and is updating
}
else
{
// fresh install
}
}
Update based on below comment
Okay in that case, there is no built-in mechanism to help you with this. Until you implement a way yourself (with this flag), you can't check. However, if you have any kind of other flags or data you've written to NSUserDefaults for instance, you can check that immediately on startup to see if those values are set which would tell you it is an existing user that just installed the app (only check the other flags if currentInstalledVersion is not set). If you have not written any data to disk or to NSUserDefaults then you are out of luck for your initial release of this feature.
I updated the code above to reflect this.
You could use NSUserDefault and set a bool to check if it's a new user.
ex:
if(![[NSUserDefaults standardUserDefaults] boolForKey:#"user"]){
// it's a new user
[[NSUserDefaults standardUserDefaults] setBool:YES ForKey:#"user"];
}
The boolForKey: method would return NO if the default was not set.
I'm building a cordova app (primarily for IOS & Android) in which the user can take an image, retake (, etc.) it and save it locally.
I'm currently struggling with the cordova cameraPlugin. So, here a short description of the problem.
When the user takes an image, it's saved locally in the apps temp folder and the user is able to view in in the UIWebView. On retaking, the image will be deleted from the temp folder and should not be available any longer (in RAM and local FS).
It works as long as the user doesn't retakes the image 2 or more times, if he does instead of the last image the first image will be referenced/rendered in WebView. After reopening the app, the image is displayed correctly.
An Example:
The user takes the first image. ==> cdv_photo_001.png
The second. ==> cdv_photo_002.png and the first one will be deleted (which seems to work correctly)
And the third. ==> cdv_photo_001.png and the second image will be deleted.
The third image will look the same as the deleted first one. This happens with every image after the third one.
It works fine after restarting the app
I've already tried to disable the App-Cache, delete the app cache before updating the image, refreshing the page and looking for answers online.
I'm getting an error when opening the camera UI, but I could not find a solution for it either.
Snapshotting a view that has not been rendered results in an empty snapshot. Ensure your view has been rendered at least once before
snapshotting or snapshot after screen updates.
The code for the camera Call:
function getPhoto() {
navigator.camera.getPicture(getPhotoOnSuccess, getPhotoOnFail, {
quality: 25,
destinationType: Camera.DestinationType.FILE_URL,
correctOrientation: true,
encodingType: Camera.EncodingType.PNG
});
}
In getPhotoOnSuccess I'm basically saving the image path to a db and appending it with jQuery to the view.
And the code to delete the image: (sidenote I`m new to Objective C)
- (void) deleteImageByPath:(NSString *)imagePath withSelector:(SEL)selector{
NSError *error = nil;
NSFileManager *mgr = [NSFileManager defaultManager];
NSString *tempFolder = NSTemporaryDirectory();
if([mgr removeItemAtPath: imagePath error:&error] == NO) {
NSLog(#"File deleted");
}
//The files can be edited as well, so there can be two files in different directories
if(error != nil){
NSString *imgEl = tempFolder;
imgEl = [imgEl stringByAppendingPathComponent:imagePath.lastPathComponent];
if(![mgr removeItemAtPath:imgEl error:&error]){
NSLog(#"Old element couln't be deleted.");
}
}
[self performSelector:selector withObject:error];
}
The file is not in the directory anymore after deleting it, so I guess it works.
An important detail could be, that I wrote my own IOS cordova plugin, because the method for the file system access provided by cordova sucks.
So thats it.
The specific question is: Why and how is this happening and is there a chance to change this behavior? If yes, how should I proceed?
By the way, I`m using cordova 3.1.0 and the build target is IOS 7.
Thanks in advance.
Ok folks, I finally got it.
The whole problem was not related to my code or any of the cordova code.
So why did it happen? ==> I don't exactly know that, for it seems that this bug or whatever you might call it, has occurred to many people.
And they all tried to delete or deactivate the cache as I did, some of their problems are very close to my own but most aren't, so it took a while til I found a solution.
I read this thread and tried to append a timestamp to the image path and it worked!
My conclusion to this it, that there might be a problem with the UIWebView and the cache management.
Or it might proof as a general WebView problem, I will be able to check that in a few days on an Adroid device.
I am localizing my iOS app, and in the Simulator it runs correctly in my chosen language every time.
When testing on my iPhone 5, it only detects the language properly the first time the app runs. Every other time I recompile and run my app on the device, it detects "en" as the language, even though I am testing with Español ("es") selected.
I detect the language using:
[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0]
I've also used:
[[NSLocale preferredLanguages] objectAtIndex:0]
Same result.
If I kill the app after the first run, and restart it on the device, it continues to detect the language properly.
But if I kill the app and then recompile/restart via Xcode after the initial run, it will load with "en" (English) detected instead.
After that, killing and re-starting the app continuously detects as English unless I delete the app completely, and recompile/reinstall/run the app via Xcode. The cycle then repeats... subsequent rebuild/restart without first deleting the app from the device results in misdetection.
All other apps on my device display with Spanish language the entire time. The entire UI shows in Spanish.
UPDATE: I've now tested on my iPad (3rd gen) also running iOS 6, and am experiencing the same behavior.
UPDATE 2:
In didFinishLaunchingWithOptions, I have this code to detect language: (language is an NSString*):
language = [[NSLocale preferredLanguages] objectAtIndex:0];
Followed by this debugging statement, to compare the value I'm getting, as well as a slightly different way of detecting it, just for debugging:
NSLog(#"Detected language: %# / %#", language, [[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0]);
The output shows as "Detected language: es / es" when the app works properly in Spanish mode, and then shows as "Detected language: en / en" when it doesn't. Still no idea why it decides to load as English sometimes...
UPDATE 4: I appreciate everybody's answers, and I've tried the various suggestions. Unfortunately I was unable to award the +100 bounty as none of the suggestions seemed to fix the issue. If someone does ultimate find a solution that works for me, I will award another +50 bounty to them at that time.
UPDATE 5: I have updated from Xcode 4.5 to 4.5.2, and experiencing this same issue.
UPDATE 6: I have now created a new test project from scratch, and it works perfectly fine! Obviously something must be wrong in the way my project is laid out, or perhaps in one of the data files. I guess my next journey will be to re-create the project from scratch, copying file data over one by one...
UPDATE 7 (MONTHS LATER): Sadly, I am again facing this issue after temporarily resolving it (seemingly) by painstakingly recreating my project. On first load, the language is correctly rendered, but on subsequent loads, it reverts back to English.
SOLVED See my final solution below. Thanks for the help everyone. I may dole out some of the bounty since it will go to waste anyway.
I have FINALLY solved this problem after many months! Thanks to all for the help (I also had some good back and forth with an Apple developer via the dev channels).
TL;DR: I was accidentally syncing language preferences (among many other unexpected things) between devices, using my app's iCloud key value store (via MKiCloudSync)! Read on...
I am using a third-party class called MKiCloudSync, which helps with syncing [NSUserDefaults standardUserDefaults] to my app's iCloud key value store. My intention when I began using it was to let it handle some user favorites syncing in the background.
However, not understanding how standardUserDefaults works, what I didn't realize is that there are a lot of other things being written into standardUserDefaults other than just my own custom app settings!
So what was happening was this:
Start the app up for the first time. Fresh standardUserDefaults in place, and the internal "AppleLanguages" key that stores the ordered list of language preferences is correct based on the current device choices.
App displays properly in the designated language.
In the background, MKiCloudSync syncs ALL standardUserDefaults to iCloud. Conversely, if you had run this app elsewhere, say with an English set device, that device would have also synced it's language settings up to iCloud. So now this current running app is actually having it's language preferences overwritten.
BOOM ... next time the app is run, no matter what you have selected on the device, it's whatever was pulled down from iCloud that will be used as the default language!
What I plan to do to solve the issue with my next app update:
Use a forked version of MKiCloudSync that allows for syncing only whitelisted key names.
Add code that will do a one-time cleanup, first cleaning out the iCloud keystore for my app, then (based on this SO answer), calling this code to reset the user defaults:
NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier];
[[NSUserDefaults standardUserDefaults] removePersistentDomainForName:appDomain];
In my testing so far, this sort of solves the issue... unfortunately, the user would have to restart the app for the language fix to kick in. However, my guess is most users are not experiencing this issue, as they are unlikely to be using multiple devices with different default languages.
In settings->general->international, there is an option to set the location language etc. if you set that to the language you are trying to test it will work if your code is correct.
I tested your steps on my iPhone 5 without issues. This leads me to think there's something else at play here: most probably there's something interferring with the way in which you're reading the locale value.
The steps I'd recommend you take to help you debug this issue are:
Post the complete code of the method in which you're obtaining the preferred language value. Make sure the method is executed each time the app is run.
Make sure the code you post includes the location of the NSLog directive you're using to test for the language setting.
Are you storing the preferred language somewhere else after the first run?
Try with following code:
LocalizationSystem.h===
#import <Foundation/Foundation.h>
#define AMLocalizedString(key, comment) \
[[LocalizationSystem sharedLocalSystem] localizedStringForKey:(key) value:(comment)]
#define LocalizationSetLanguage(language) \
[[LocalizationSystem sharedLocalSystem] setLanguage:(language)]
#define LocalizationGetLanguage \
[[LocalizationSystem sharedLocalSystem] getLanguage]
#define LocalizationReset \
[[LocalizationSystem sharedLocalSystem] resetLocalization]
#interface LocalizationSystem : NSObject {
NSString *language;
}
+ (LocalizationSystem *)sharedLocalSystem;
//gets the string localized
- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)comment;
//sets the language
- (void) setLanguage:(NSString*) language;
//gets the current language
- (NSString*) getLanguage;
//resets this system.
- (void) resetLocalization;
#end
LocalizationSystem.m===
#import "LocalizationSystem.h"
#implementation LocalizationSystem
//Singleton instance
static LocalizationSystem *_sharedLocalSystem = nil;
//Current application bundle to get the languages.
static NSBundle *bundle = nil;
+ (LocalizationSystem *)sharedLocalSystem{
#synchronized([LocalizationSystem class])
{
if (!_sharedLocalSystem){
[[self alloc] init];
}
return _sharedLocalSystem;
}
// to avoid compiler warning
return nil;
}
+(id)alloc{
#synchronized([LocalizationSystem class])
{
NSAssert(_sharedLocalSystem == nil, #"Attempted to allocate a second instance of a singleton.");
_sharedLocalSystem = [super alloc];
return _sharedLocalSystem;
}
// to avoid compiler warning
return nil;
}
- (id)init{
if ((self = [super init]))
{
//empty.
bundle = [NSBundle mainBundle];
}
return self;
}
// Gets the current localized string as in NSLocalizedString.
- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)comment{
return [bundle localizedStringForKey:key value:comment table:nil];
}
// If this function is not called it will use the default OS language.
// If the language does not exists y returns the default OS language.
- (void) setLanguage:(NSString*) l{
NSLog(#"preferredLang: %#", l);
NSString *path = [[ NSBundle mainBundle ] pathForResource:l ofType:#"lproj" ];
if (path == nil)
//in case the language does not exists
[self resetLocalization];
else
bundle = [[NSBundle bundleWithPath:path] retain];
[[NSUserDefaults standardUserDefaults] setObject: [NSArray arrayWithObjects:l, nil] forKey:#"AppleLanguages"];
}
// Just gets the current setted up language.
// returns "es","fr",...
//
// example call:
// NSString * currentL = LocalizationGetLanguage;
- (NSString*) getLanguage{
NSArray* languages = [[NSUserDefaults standardUserDefaults] objectForKey:#"AppleLanguages"];
NSString *preferredLang = [languages objectAtIndex:0];
return preferredLang;
}
// Resets the localization system, so it uses the OS default language.
//
// example call:
// LocalizationReset;
- (void) resetLocalization{
bundle = [NSBundle mainBundle];
}
#end
This code works perfectly as you mentioned.
It worked for me and that game is live now app store, if you want to check(HueShapes).
Do you by chance use NSUserDefaults to save something language related?
Look into your Simulator App directory -> Library -> Preferences -> <YourAppBundleName>.plist
See: How to force NSLocalizedString to use a specific language for description of the NSUserDefaults method of setting a language.
Perhaps you just save your language and thus detection just returns the saved value.
I am using a UIManagedDocument in iOS 5.0, running the app on the simulator, using XCode 4.2 under OSX 10.6. The code in question looks as follows:
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.photoDatabase.fileURL path]]) {
// does not exist on disk, so create it
[self.photoDatabase saveToURL:self.photoDatabase.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self setupFetchedResultsController];
[self fetchFlickrDataIntoDocument:self.photoDatabase];
}];
} else if (self.photoDatabase.documentState == UIDocumentStateClosed) {
// exists on disk, but we need to open it
// *** the following line generates the message ***
[self.photoDatabase openWithCompletionHandler:^(BOOL success) {
//[self setupFetchedResultsController];
}];
} else if (self.photoDatabase.documentState == UIDocumentStateNormal) {
// already open and ready to use
[self setupFetchedResultsController];
}
Running the marked line creates the following message on the log:
2012-01-10 22:33:17.109 Photomania[5149:4803] NSFileCoordinator: A surprising server error was signaled. Details: Connection invalid
After the message is sent, the UIManagedDocument may or may not work—I have not found the circumstances that determine this, yet.
I am pretty sure that the code is correct, as it's actually one of the code examples in the CS193p course from Stanford. The whole example can be downloaded at their website under
http://www.stanford.edu/class/cs193p/cgi-bin/drupal/
Direct link to the code:
http://www.stanford.edu/class/cs193p/cgi-bin/drupal/system/files/sample_code/Photomania_0.zip
Additionally, the code runs fine on the device itself, without generating the "surprising" message, and running all the code that comes afterwards just fine.
I have not found anything on Google, neither on the Apple Developer pages. Restarting the simulator, or XCode, or reinstalling both of them does not change the behaviour.
Any ideas?
I can only say that I've had this happen to me several times. For me, I'm lazy after I update my dataModel and so far, each time I've gotten this error it was because I had changed my data model. Usually, all I need to do is delete my app from the simulator and re-run it and it has always turned out fine. Hope this helps someone out there.
I think I have found the answer. It looks like the automatic saving for UIManagedDocument kicks in only after a few seconds on the simulator.
So I minimized the app on the simulator, by pressing the home button, and then clicked on the icon to maximize it again. And then I terminated the app in simulator.
When I re-launched the app, the database was loaded. The error still shows up - it comes because the document is in "closed" state (that's normal - that's why CS193P asked to call openWithCompletionHandler), but my data across launches is preserved. Unfortunately I have to do the minimize/maximize routine before terminating the app, or the changes are discarded at next launch.
Can you verify that this is the behavior you are able to recreate? At least for testing purposes this should be a good enough trick to use.
Try upgrading to the latest iOS 5.1. I don't think UIManagedDocument with iCloud works reliably in 5.0. This has been my experience.
I love the Stanford iTunes class. However, I think the sample code for using UIManagedDocument is wrong. In fact, he notes in the demo that he is only doing it that way because he wants to just fetch the information right then. In the code comments, he says not to use the auto-save features because the data will not be saved if the app quits. however, UIManagedDocument will save anything that's necessary before quitting. It has all pertinent handlers for quitting/multitasking/etc to make sure the data is saved.
So, if you are using that code as your example, here's a version that should work, and does not use saveToURL (I don't have a flickr account, so I didn't actually run it - but this is how the class is designed to work). Please let me know if it does not work.
- (void)fetchFlickrDataIntoDocument:(UIManagedDocument *)document
{
NSManagedObjectContext *ctx = [[NSManagedObjectContext alloc] initWithConcurrencyType: NSPrivateQueueConcurrencyType];
ctx.parentContext = document.managedObjectContext;
[ctx performBlock:^{
NSArray *photos = [FlickrFetcher recentGeoreferencedPhotos];
for (NSDictionary *flickrInfo in photos) {
[Photo photoWithFlickrInfo:flickrInfo inManagedObjectContext:ctx];
// Push changes to document MOC
[ctx save:0]; // propagates changes to parent MOC
// and tell the document it is dirty and needs to be saved
// It will be saved when the document decides its time to save
// but it *will* be saved.
[document updateChangeCount:UIDocumentChangeDone]
}
}];
}
Still had errors when the last path component for document file url was #"Database". Adding an extension #"Database.db" seems to have fixed it, everything running fine now. Have also upgraded to Lion though.
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Database.db"];
What will be the best way to check that application is already installed or being installed for the first time.
Bundle version and saving it in user defaults.
EDIT:
There are three things to note here.
Bundle version: This is the version of the your application that you want to release.
Old version: This will indicate previous version of your application. We will store this in user defaults so that we will know what was the old version when updating our application. This will obviously be nil if your bundle version is 1.0.
Target version: This indicates the version the user is targeting. We will discuss this later.
So, condition such as
bundleVersion > oldVersion or
if(isVersionBetter:myBundleVersion thanVersion:oldVersion)
would either mean we want to create our database (in this case bundle version would be 1.0 and old version will be nil) or update our database (in this case bundle version would be something greater than 1.0 and hence old version would not be nil).
Thus, as we can see, creation of database means user is installing app for the first time. Updating database means user has already installed the app and is updating the database.
But, there might also be a case when you want to update your app and want to keep the database as it is. That is, only UI updating.
Here, target version comes into picture.
As mentioned above, target version is the version the user is targeting. All would work same as above if user is targeting the bundle version. But if user is targeting some other version than bundle version, we would skip database updating part, thus allowing only the UI to change.
So, the final statement would be something like this:
if( bundleVersion == targetVersion AND bundleVersion > oldVersion ) {
// Either create or update the database.
}else {
// Do nothing. Skips database updating and allows UI update.
}
Thus, your database function would look something like this
-(void) initWithTargetVersion:(NSString *) targetVersion {
NSString *oldDatabaseVersion = [[NSUserDefaults standardUserDefaults] stringForKey:#"OldDatabaseVersion"];
NSString *bundleDatabaseVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleVersion"];
if([bundleDatabaseVersion isEqualToString:targetVersion] && [self isVersionBetter:oldDatabaseVersion new:targetVersion]) {
// Create or update the database.
}else {
// Do nothing.
}
}
where user would pass the target version as follows:
[[DatabaseManager sharedManager] initWithTargetVersion:#"1.0"];