Run code for existing users only - SwiftUI/iOS/Swift - ios

I have a production-ready app where I need to run some code only on users with the previous versions installed but not on new installations. For instance, if 1.0 is the latest version in the AppStore and 2.0 is the new one that will introduce code that needs to run only on users with version 1.0 but not on new users.
e.g.
if isExistingUser{
// run code
}
What would be the best way to run some code only for existing users? How can one determine whether it's a new or existing user?

Does your app create any data? Maybe files in the Documents directory, or maybe a UserDefaults key? If so, check for the presence of one of those things very early in launch, which will signal to you that this must be an upgrade and you should do your thing.
A lot of people store the app's CFBundleShortVersionString Info.plist key into UserDefaults at launch, which makes it easy to know the last version that was run and let you write the logic of what needs to happen to migrate from that version to the new version. This is something you might want to think about doing in future versions.

I see this often and knowing how to do this can be extremely valuable, especially when doing something like introducing a new feature that changes the experience your previous users had.
There are a few approaches you can take depending on your needs.
Firstly, you could create a boolean variable in your user model class that is set during user registration in the standard user registration flow and indicates that this is a newly created user - and name it something like isNewOnVersionTwo which will indicate this user is a new user on this new version.
Code Example:
class User: Decodable {
var uid: string!
var username: string!
var isNewOnVersionTwo: Bool = false
}
class RegistrationViewController: UIViewController {
var user: User!
var isNewOnVersionTwo: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
user.isNewOnVersionTwo = true
}
}
class HomeViewController: UIViewController {
var user: User!
override func viewDidLoad() {
super.viewDidLoad()
isNewOnVersionTwo == true ? normalInit() : showOldUserAView()
}
func normalInit() {
// Run normal functions for the 'HomeViewController' or do nothing.
}
func showOldUserAView() {
//Do something specific for only old users.
}
}
You can choose whether you want to hold onto this variable permanently or not - it could be useful for tracking the new users you've gained on this version versus the previous versions - and you could send it to your database along with the rest of the data from your user model.
A second and cleaner approach...
Could be to only set the boolean on the very last view controller of the registration flow and pass it to the home view controller when you push the user to the view controller.
Like this:
class ViewControllerOne: UIViewController {
var isNewOnVersionTwo: Bool = false
private func pushToNewViewController() {
let vc = HomeViewController()
vc.isNewOnVersionTwo = true
navigationController?.pushViewController(vc, animated: true)
}
}
class HomeViewController: UIViewController {
var isNewOnVersionTwo: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
isNewOnVersionTwo == true ? normalInit() : showOldUserAView()
}
func normalInit() {
// Run normal functions for the 'HomeViewController' or do nothing.
}
func showOldUserAView() {
//Do something specific for only old users.
}
}
I wouldn't take the approach of using UserDefaults for the few following reasons:
1. Devices occasionally wipe this randomly and it's not as reliable as hardcoding a flag in the source code.
2. UserDefaults isn't available for a few moments on cold app launches / initial startup which can complicate things, varying on when you want to show the older users whatever you want to show them.

Here is what I came up with that I think works for 95-98% of the users. What I'm doing is basically comparing the date when the Documents folder was created and the date when version2 will be released. The only issue I see with this method is for users installing the app between the date you specified as the release date and the actual App Store release date. In other words, if you specify a date of June, 5 as the release date but the app isn't really approved until the 7th, users who installed the app on the 6th will be missed.
Ideally and it's what I will start doing is what #Rudedog suggested, basically saving the versions to keep track of what version a user has.
/// - Returns: Returns true if it's a new user otherwise returns false.
func isExistingUser()->Bool{
var isExistingUser:Bool?
var appInstallationDate:Date?
/// Set the date when the version two is released.
var dateComponents = DateComponents()
dateComponents.year = 2022
dateComponents.month = 06
dateComponents.day = 4
let userCalendar = Calendar.current
let versionsTwoReleaseDate = userCalendar.date(from: dateComponents)
/// Get the date of when the documents folder was created to determine when the app was installed.
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
if let creationDate = (try? documentsDirectory!.resourceValues(forKeys: [.creationDateKey]))?.creationDate{
appInstallationDate = creationDate
}
/// Compare dates
if appInstallationDate! < versionsTwoReleaseDate!{
// print("It's existing user")
isExistingUser = true
}else{
// print("It's NEW user")
isExistingUser = false
}
return isExistingUser!
}

Related

iOS: execute code AFTER a particular date

I have some code I want to run after a particular date/time has passed. For example, if I want the code to run 7 days from now and the user opens the app at any time on day 7 or after the code will run but if they open the app before the beginning of day 7 nothing happens. Timers in the main runloop work but only if the app is still running in the background. I need a method that will work even if the user kills the app.
Your best option is to store it as local data Even though you only want the code to run once, the overhead is so low, the "check" will not impact the speed or feel of the application. Also this will allow you to run additional checks .. If someone deletes the app, for instance, and leaves the local storage behind. If they re-install you could theoretically "remember" that the application has been installed, and said code has already run (until the user clears application data)
Something like:
//Globally set key
struct defaultsKeys {
static let keyDate = "dateKey"
}
// Set the date in local storage
let defaults = UserDefaults.standard
defaults.set("Your Date String", forKey: defaultsKeys.dateKey)
// Get the date from local storage
let defaults = UserDefaults.standard
if let stringDate = defaults.string(forKey: defaultsKeys.dateKey) {
print(stringDate)
// Do your date comparison here
}
Very few lines of code, and even though the check happens every time the application starts .. The overhead is negligible.
You can either set the date you want your app to "remember" on your local storage or web service. Then, when the user opens your app, compare that date to current device time to determine if you should execute your code.
First, save the current time when you want. You can set the key name however you want.
UserDefaults.standard.setValue(Date(), forKey: "rememberTime")
And every time I open the app, You compare the current time with the saved time.
To do so, I created a function that compares time.
extension Date {
func timeAgoSince() -> Bool {
let calendar = Calendar.current
let unitFlags: NSCalendar.Unit = [.day]
let components = (calendar as NSCalendar).components(unitFlags, from: self, to: Date(), options: [])
if let day = components.day, day >= 7 {
// Returns true if more than 7 days have passed.
return true
}
return false
}
}
Recall the previously saved time and use the time comparison function.
let beforeTime: Date = (UserDefaults.standard.object(forKey: "rememberTime") as? Date)!
if beforeTime.timeAgoSince() {
// more than seven days later
...
} else {
...
}
If you have a problem, please leave a comment !
You can use the below sample code:
override func viewDidLoad() {
super.viewDidLoad()
let nextCodeRunDate = Date() + (7 * 24 * 60 * 60) // 7 Days
if let savedDate = UserDefaults.standard.value(forKey: "NEXT_DATE") as? Date {
if Date() > savedDate {
UserDefaults.standard.setValue(nextCodeRunDate, forKey: "NEXT_DATE")
runYourCode()
}
}else {
// First time
UserDefaults.standard.setValue(nextCodeRunDate, forKey: "NEXT_DATE")
runYourCode()
}
}
func runYourCode() {
// Your code
}

iOS swift how to know if any contact is updated even when app is killed

I have seen many SO question curious about this case but still I am posting this as many of developers out there may also want to know this another reason is that no solution is working for me .
I have used following code but it only works when My app is in background. but I am not notified when my app is killed and meanwhile user has updated the info of any contact. So in this case I am not sure how to do it.
What I am doing: here is a code snippet what I am trying to do
From iOS 9 you can register your class to observe CNContactStoreDidChangeNotification
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(addressBookDidChange),
name: NSNotification.Name.CNContactStoreDidChange,
object: nil)
And then:
#objc func addressBookDidChange(notification: NSNotification){
//Handle event here...
}
I found this solution over here:
Whats Happening: Through this way I am able to get my app notified once the user has updated his contact while app is in background.
What I want: I just want to know that if the user has updated any contact even though my app was killed then How to get my app notified with updated contacts?
Please let me know if you have solution of this issue in advance.
UPDATE: I have seen Whatsapp doing this. Is there anyone who can tell me how Whatsapp is doing this?
To check if a contact has changed you can use a custom hash function because the native one only checks for the identifier:
extension CNContact {
var customHash : Int {
var hasher = Hasher()
hasher.combine(identifier)
hasher.combine(contactType)
hasher.combine(namePrefix)
hasher.combine(givenName)
hasher.combine(middleName)
hasher.combine(familyName)
hasher.combine(previousFamilyName)
hasher.combine(nameSuffix)
hasher.combine(nickname)
hasher.combine(organizationName)
hasher.combine(departmentName)
hasher.combine(jobTitle)
hasher.combine(phoneticGivenName)
hasher.combine(phoneticMiddleName)
hasher.combine(phoneticFamilyName)
if #available(iOS 10.0, *) {
hasher.combine(phoneticOrganizationName)
}
hasher.combine(note)
hasher.combine(imageData)
hasher.combine(thumbnailImageData)
if #available(iOS 9.0, *) {
hasher.combine(imageDataAvailable)
}
hasher.combine(phoneNumbers)
hasher.combine(emailAddresses)
hasher.combine(postalAddresses)
hasher.combine(urlAddresses)
hasher.combine(contactRelations)
hasher.combine(socialProfiles)
hasher.combine(instantMessageAddresses)
hasher.combine(birthday)
hasher.combine(nonGregorianBirthday)
hasher.combine(dates)
return hasher.finalize()
}
}
(You can remove fields you don't care)
Then you have to keep a dictionary inside your app to store the hash values of all the contacts, to build it just do:
let hashedContacts = [String:Int]()
for contact in allContacts {
hashedContacts[contact.identifier] = contact.customHash
}
You have to store it on the file system.
Whenever a contact is updated, you update it:
hashedContacts[updatedContact.identifier] = updatedContact.customHash
Then at every launch, you load the saved dictionary, and you check for differences:
for contact in allContacts {
if contact.customHash != savedHashedValues[contact.identifier] {
// This contact has changed since last launch
...
}
}
And voilĂ !
EDIT:
How to save the hash map on disk...
var hashedContacts = ...
guard let name = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("hashedContacts")
else { return }
try? (hashedContacts as NSDictionary).write(to: name)
How to load the hash map from disk...
guard
let name = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("hashedContacts"),
let loadedContacts = (try? NSDictionary(contentsOf: name, error: ())) as? [String:Int]
else { return }
// Do whatever you want with loaded contacts...
Whenever you open your app you need to get all the contacts from the contact list and can compare to previous one which is saved inside of your app. After that you can push your contact list to server.
What you can do is send an update notification to your application on launch screen. This might have an illusion to your user that you have done the changes while in background.

Saving data (numbers) in an iOS app?

I'm learning application development working on a quiz game. I'd like to add statistics to the game. For example, the average score since the app has been downloaded. How can I store the scores on the device in order to reuse them after the app has been closed?
You should take a look at UserDefault. It's basically a dictionary that persists until the user uninstalls your app. I like to write a wrapper around it to get strong typing and ease of reference:
struct Preferences {
static func registerDefaults() {
UserDefaults.standard.register(defaults: [kAverageScore: 0])
}
// Define your key as a constant so you don't have to repeat a string literal everywhere
private static let kAverageScore = "averageScore"
static var averageScore: Double {
get { return UserDefaults.standard.double(forKey: kAverageScore) }
set { UserDefaults.standard.set(newValue, forKey: kAverageScore) }
}
}
Here's how to use it: before you call it for the first time in your app, you must register the defaults. These are the values that your app ships with. On iOS, it only really matters for the very first time the user launches your app. On OS X, do this every time your app starts because the user can delete the app's preferences from ~/Library/Application Support.
// You usually do this in viewDidLoad
Preferences.registerDefaults()
From then on, getting and setting the property is easy:
let averageScore = Preferences.averageScore
Preferences.averageScore = 5.5
You should take a look at UserDefaults
Example
let defaults = UserDefaults.standard
defaults.set(25, forKey: "Age")
defaults.set(true, forKey: "UseTouchID")
defaults.set(Double.pi, forKey: "Pi")
To read values back
let age = defaults.integer(forKey: "Age")
let useTouchID = defaults.bool(forKey: "UseTouchID")
let pi = defaults.double(forKey: "Pi")
UserDefaults

iOS App Update and User Defaults

I've developed a game and I released it to the App Store about 2 months ago (Supernatural Slayer). The game uses user defaults to store player data such as level, xp, gold, etc.
I want to update the game to include review requests which I programmed based on help from hacking with swift. (I also had to switch ad networks since chart boost suspended my account indefinitely for no reason...)
My question is that I seem to remember during development about 4-6 months ago that every time I added another variable to save and load from user defaults it would cause an existing game to crash and I would have to start from scratch by deleting and reloading the game onto my phone to erase user defaults. But now that I'm adding the reviewRequest variable it isn't causing the same error, I'm not sure if I'm not testing correctly and if once I update all of my players will lose their progress...
My code for the save and load functions is below, will this cause an error for existing users that update my app if I add the reviewRequest variable and the save and load lines for it? I've updated both my Mac OS and Xcode since it used to crash for me, so maybe this is why it is not an issue anymore?
let defaults = UserDefaults.standard
var reviewRequest = 0
func save() {
defaults.set(reviewRequest, forKey: "Review Request")
defaults.set(player.name, forKey: "PlayerName")
}
func load() {
player.name = defaults.object(forKey: "PlayerName") as! String
reviewRequest = defaults.integer(forKey: "Review Request")
}
You should try adding a nil check to avoid trying to retrieve an empty key.
func load() {
if defaults.object(forKey: "PlayerName") != nil {
player.name = defaults.object(forKey: "PlayerName") as! String
} else {
//Default name or something similar
}
if defaults.object(forKey: "ReviewRequest") != nil {
reviewRequest = defaults.integer(forKey: "Review Request")
} else {
reviewRequest = 0
}
}

Accessing and storing viewController variables in appDelegate

Here's my problem: I have several variables in my viewController, for example:
var money = 0.0
I want to store this value on the device as soon as the app is going to terminate. To do this, I need to be able to acces the variable in the appDelegate and use it in 'applicationWillTerminate'. I then want it to be stored on the device and when the app restarts I want to be able to acces the stored value again. How do I do this in (swift) Xcode 8.2?
"applicationWillTerminate" as Apple docs say, is no more called in modern app unless You set a specific flag in plist.
The correct approach is to store values in NSUserDefault:
For swift 3.0:
1) let's create a class to manage money, and saving them:
class MoneyManager: NSObject {
private let MONEY_KEY = "money"
static let sharedInstance = MoneyManager()
var money: Float? {
get {
let returnValue = UserDefaults.standard.object(forKey: MONEY_KEY) as? Float
return returnValue
}
set(newValue){
if newValue == nil {
UserDefaults.standard.removeObject(forKey: MONEY_KEY)
} else {
UserDefaults.standard.set(newValue!, forKey: MONEY_KEY)
}
UserDefaults.standard.synchronize()
}
}
}
2) to use it in every controller you need:
let money = MoneyManager.sharedInstance.money
if money == nil{
MoneyManager.sharedInstance.money = 1000
}
As every time we call UserDefaults.standard.synchronize(), we are safe everything is saved.

Resources