What is the best way to track App usage time and time a user spends viewing a Screen (or interacting with a UIView) for use within the App itself? Google Analytics seems to do a wonderful job, but the numbers I want to use inside the App itself to unlock items and areas of the App.
You could probably roll your own solution based on Core Data, or if your data is small you could even think of using NSDefaults.
Here's a good start. It involves having a base view controller which you should inherit from in each view controller you want to measure the time spent:
#interface BaseViewController : UIViewController
- (NSString *)screenKey;
+ (NSInteger)secondsInScreen:(NSString *)screenKey;
#end
The implementation simply measures the seconds between the appearance of the screen until it disappears. It's very important to notice the appDidEnterForeground and appDidEnterBackground notifications. When you send your app to the background or it comes back to the foreground, viewDidAppear and viewDidDisappear are not called.
#import "BaseViewController.h"
#implementation BaseViewController {
NSDate *_startDate;
}
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self startMeasuring];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self stopMeasuring];
}
- (void)appDidEnterBackground:(NSNotification *)not {
[self stopMeasuring];
}
- (void)appDidEnterForeground:(NSNotification *)not {
[self startMeasuring];
}
- (void)startMeasuring {
_startDate = [NSDate date];
}
- (void)stopMeasuring {
NSInteger secondsInScreen = ABS([_startDate timeIntervalSinceNow]);
[self addSecondsToScreen:secondsInScreen];
}
- (NSString *)screenKey {
// Subclasses must override this method
return #"";
}
- (void)addSecondsToScreen:(NSInteger)seconds {
NSString *key = [self screenKey];
if (key.length > 0) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSNumber *s = [defaults objectForKey:key];
NSInteger addedSeconds = s.integerValue + seconds;
[defaults setObject:[NSNumber numberWithInteger:addedSeconds] forKey:[self screenKey]];
[defaults synchronize];
}
}
+ (NSInteger)secondsInScreen:(NSString *)screenKey {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSNumber *s = [defaults objectForKey:screenKey];
return s.integerValue;
}
#end
Your subclasses must override screenKey retuning a unique identifier for each screen you want to measure. There's a static method in BaseViewController that allows you to get the sum of seconds spent in each screen.
This is a simple way of doing it. From a design point of view, it would be better to store this information in Core Data and to separate the view controller logic from the storage of that data (the static method secondsInScreen should probably be in another class). But it's a good, working start.
I think you would like to get app usage time to enable new features in the app or give some gifts for your users right?
To reach this, don't use any SDK to track audience like Google Analytics and Flurry for example. Both are for a different purpose you want to do.
A very very simple approach is to store locally using NSUserDefaults the session time of some user, or you can store more detailed information about this using CoreData or SQLite.
The iOS provide a lot of options to you do that, for example, each time the user start the session or open some screen, you can store some NSDate or mach_abosulte_time to save the time of user started the session/screen and you can get the offset time when the app goes to background or when the user closes the screen.
And if you want to store this remotely (server), you can create a service to send this time while the app is visible.
I hope this first insight can help you.
Related
I want to do something like this in my iOS app. Let's say user open the app now. then I want to show a view. during that 1st hour, no matter how manytimes he open the app I need to show the 1st view through out this hour. when start a new hour I want to show the view 2. again after the 3rd hour I need to show that first view.
Like wise
1hr - view 1
2hr - view 2
3hr - view 1
4hr - view 2
How can I monitor this hours changing from my ios app even its not runing in the background
Thank you
If the app isn't running in the background, you really can't monitor the hour changing. But that doesn't matter since you can't show a view when the app is not running.
When the app is running, just use NSTimer and set it to repeat at the right time to tell your controller that the hour is changing. Let the controller deal with figuring out which view to show.
This is important, because you have to make the decision of which view to show even when the hour isn't changing. For example, when you first open the app.
See NSTimer
AppDelegate.m
- (void)applicationDidEnterBackground:(UIApplication *)application {
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
[standardDefaults setObject:[NSDate date] forKey:#"kTimeInterval"];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationNameForBecameActive object:nil userInfo:#{kUserInfoForBecameActive: self.currentTime}];
}
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(didBecomeActive:) name:kNotificationNameForBecameActive object:nil];
}
- (void)didBecomeActive:(NSNotification *)notification {
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
NSDate *previousDate = [standardDefaults objectForKey:#"kTimeInterval"];
NSTimeInterval secondsPassed = [[NSDate date] timeIntervalSinceDate:previousDate];
if (secondsPassed >= CHECK_SWAP_VIEW) {
// Change your view here.
}
}
Here you can do like save time when application goes in background or quit. Then whenever application will open check for previous time and then swap your views.
I have created an app extension for the notification center which actually works fine except for I am not able to update its UILabel continuously. The reason I need to do this is because my app has a constantly changing data set which I want to show in the extension.
I have tried using NSUserDefaultsDidChangeNotificationfor updating the data in the extension but it's not working. Here is my code:
Registering for change notification (Extension)
- (void)viewDidLoad
{
[super viewDidLoad];
...
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(userDefaultsDidChange:)
name:NSUserDefaultsDidChangeNotification
object:nil];
}
- (void)userDefaultsDidChange:(NSNotification *)notification
{
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.company.app"];
//update label
}
Sending data to extension
- (void)updateData
{
NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.company.app"];
[sharedDefaults setInteger:seconds forKey:#"seconds"];
...
[sharedDefaults synchronize];
}
I have App Groups correctly set up in both the main app and the extension who are both accessing the same group.
Does anybody know why this is not working or if there is another method to do this?
Thanks a lot!
I'm facing the same issue in my WatchKit app and NSUserDefaultsDidChangeNotification does indeed not get triggered in the extension for the shared NSUserDefaults.
The best solution I've found is a library called MMWormhole which uses
CFNotificationCenter Darwin Notifications to achieve this.
I am working on a app with 300 images(no text) and trying to change the language with button click and without restarting.
- (IBAction)changeArab:(id)sender {
NSArray* languages = [NSArray arrayWithObjects:#"en", #"fr", nil];
[[NSUserDefaults standardUserDefaults] setObject:languages forKey:#"AppleLanguages"];
}
I am able to change the language when i am restarting the app with the code above.
I localized all the images with the required language, is there anyway where i can reload the view once the language is change or change the app one the fly.
Yes, you can change it without restarting app.
You have to use NSNotificationCenter.
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(localeChanged:)
name:NSCurrentLocaleDidChangeNotification
object:nil];
}
- (void)localeChanged:(NSNotification *)notif
{
// the user changed the locale
//code to update views or data set.
}
If you have not localized your app in that way. Then Add Custom Observer to Notification Center and when user change language in your custom settings view then it will generate local notification which will be captured by application did receive notification and from that you can load all your view's again.
I'm developing a game with Sprite Kit but I am having a multitasking issue. When I press the home button, during the game execution, my SKScene.paused becomes true, and I make the proper changes to the app in my applicationDidEnterBackground method at AppDelegate.m, such as saving stuff with NSUserDefaults. Anyway, if I opened my app again, it should resume from where it left off, but what happens is that my app terminates and starts again. This only happens in my iPhone (in the iOS Simulator it works fine). Since I am new at creating games with Sprite Kit and creating apps at all, I was hoping for some clue of what could the problem be...
PS: I think the problem is something about the app not being "suspended" correctly, because if I press the home button and immediately reopen the app, it works fine.
Here is my code:
- (void)applicationDidEnterBackground:(UIApplication *)application
{
if(self.scene == nil) return;
[self.scene saveUserDefaults];
[self.scene pausar];
self.scene.paused = true;
}
In MyScene.m, inside the initWithSize method:
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
appDelegate.scene = self;
And in the AppDelegate.h file: #property (weak, nonatomic) MyScene * scene;
Now, the saveUserDefaults method, which is inside the MyScene.m file:
-(void) saveUserDefaults
{
[userDefaults setBool:true forKey:#"active"];
[userDefaults setInteger:highScore forKey:#"highScore"];
[userDefaults setBool:soundOn forKey:#"soundOn"];
[userDefaults setBool:musicOn forKey:#"musicOn"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
Well, without seeing more of your code, it is impossible to know what is causing your issue.
First, read this question. Similarly, you should read up on the UIAppDelegate class since it is something you must know very well when programming on a phone where frequent interruptions like phone calls, etc can happen while someone is using your app.
More than likely, you have a problem with how you are responding to one of the protocol methods in the delegate.
I'm trying to save for the first time. Made my application, and now going to save data and load on startup and close. Looked into NSKeyedArchiver and added the two methods from the NSCoding to all my custom classes:
-(void)encodeWithCoder:(NSCoder *)aCoder{
// Encode Stuff
}
-(id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init])
// Decode Stuff
return self;
}
But now i'm finding a problem, where and how do i need to call my saveFile and loadFile? My data is stored in 2 arrays in my FirstViewController. And i wanted to save the data with the AppDelegate's - (void)applicationWillTerminate:(UIApplication *)application and load data with - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions But how is this possible to reach the FirstViewController class with the Arrays in it to store from the AppDelegate method's?
Is the thinking right, or do i need to do it on an other way?
Kind Regards
Loading data should be "lazy". This means the data should be loaded the first instant that you actually need to read the data. Also, if it's a lot of data, you should be prepared to free it while your app is running so other apps can use the RAM, this means your app is more likely to still be running next time the user launches your app.
So, make a class that provides access to the data, and the first time anything needs data it checks if the internal NSCoding object is nil, and if it is then that is where you should load the data.
As for saving, you should save before terminating but more importantly you should also save within a second or so of any data being modified by the user. Your app should crash at any moment due to a software bug, or it could be terminated for some other reason, or the battery could simply run out.
Lets say your internal data storage is an NSMutableDictionary saved using NSKeyedArchiver. It has a value with the key #"value", with a "getter" and "setter" implemented like this:
- (NSString *)value
{
if (!self.data)
self.data = [NSKeyedUnarchiver unarchiveObjectWithFile:self.dataFile];
return self.data[#"value"];
}
- (void)setValue:(NSString *)value
{
if (!self.data)
self.data = [NSKeyedUnarchiver unarchiveObjectWithFile:self.dataFile];
self.data[#"value"] = value;
self.needsSave = YES;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self save];
});
}
- (void)save
{
if (!self.needsSave)
return;
[NSKeyedArchiver archiveRootObject:self.data toFile:self.dataFile];
self.needsSave = NO;
}
Finally, your class should also register for UIApplicationDidReceiveMemoryWarningNotification, UIApplicationWillResignActiveNotification, UIApplicationWillTerminateNotification, where you want to save to disk and free the RAM so other apps can use it:
- (id)init
{
if (!(self = [super init]))
return nil;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(saveAndFreeMemory:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(saveAndFreeMemory:) name:UIApplicationWillResignActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(saveAndFreeMemory:) name:UIApplicationWillTerminateNotification object:nil];
return self;
}
- (void)saveAndFreeMemory:(NSNotification *)notif
{
[self save];
self.data = nil;
}
When a UIApplication launches in the normal way, you can always call [[UIApplication sharedApplication] delegate] from any object in the app, and you will thus obtain a reference to the application's delegate.
This is how view controller code generally gets ahold of the app delegate, you establish the connection in the view controllers in their respective -awakeFromNib methods -- it's generally less gotcha-prone to have the view controllers make the first contact with the app delegate, than having the app delegate reach out to the view controllers.
Have your app delegate decode the saved data into a model object hierarchy, have your view controllers connect with the app delegate and begin KV observing the model in -awakeFromNib.
Or just use Core Data.