1
\$\begingroup\$

I have this method, which first basically shows a Welcome screen to user only once per day. The code I have so far is this

private func showGreetingScreen (tabbar: UITabBarController) {
    let defaults = UserDefaults.standard
    if defaults.object(forKey: "greetingDate") == nil {
        defaults.set(Date(), forKey: "greetingDate")
        presentWelcomeScreen(tabbar: tabbar)
    } else {
        if let greetingDate = defaults.object(forKey: "greetingDate") as? Date {
            let order = Calendar.current.compare(Date(), to: greetingDate, toGranularity: .day)
            if order == .orderedDescending {
                defaults.set(Date(), forKey: "greetingDate")
                presentWelcomeScreen(tabbar: tabbar)
            }
        }
    }
}

Now as you can see first I am checking whether Welcome Screen was shown in past or not and then I am saving current date. Then next time app will load, it will compare current date and the date Welcome Screen was shown and based on that It will show Welcome screen to User. I have feeling that this code can be refactored more.

\$\endgroup\$
5
\$\begingroup\$

There is some code duplication for the cases “greeting date has never been set” and “last greeting was yesterday or earlier.” This can be avoided if you use the nil-coalescing operator ?? to set the last greeting date to a default value in the past:

let defaults = UserDefaults.standard
let lastGreeting = defaults.object(forKey: "greetingDate") as? Date ?? .distantPast
let now = Date()
if Calendar.current.compare(now, to: lastGreeting, toGranularity: .day) == .orderedDescending {
    defaults.set(now, forKey: "greetingDate")
    presentWelcomeScreen(tabbar: tabbar)
}
\$\endgroup\$
3
\$\begingroup\$
  • You could rearrange the conditions this way :

    if let greetingDate = defaults.object(forKey: "greetingDate") as? Date {
        let order = Calendar.current.compare(date, to: greetingDate, toGranularity: .day)
        if order == .orderedDescending {
            defaults.set(date, forKey: "greetingDate")
            presentWelcomeScreen(tabbar: tabbar)
        }
    } else {
        defaults.set(date, forKey: "greetingDate")
        presentWelcomeScreen(tabbar: tabbar)
    }
    
  • You could extract Date() as a local variable in this function just in case you are crossing midnight between checks.

  • The parameter name tabbar in both showGreetingScreen and presentWelcomeScreen doesn't seem necessary, you could use a wild card external name _ to avoid being too verbose:

    private func showGreetingScreen (_ tabbar: UITabBarController) { 
        ... 
        presentWelcomeScreen(tabbar)
    }
    
  • tabbar isn't used in this function. It is only passed to presentWelcomeScreen, and this calls for refactoring the messaging in your code.

  • Instead of having string literals laying around inside your code, I would prefer to have a struct with all the keys to avoid possible errors:

    struct UDKeys {
        static let greetingDateKey = "greetingDate"
        static let otherKey        = "anotherKey"
    }
    

    Or declare these keys in an extension of UserDefaults itself :

    extension UserDefaults {
        static let greetingDateKey = "greetingDate"
        static let otherKey        = "anotherKey"
    }
    

Finally, your code would look like this :

struct UDKeys {
    static let greetingDateKey = "greetingDate"
    static let otherKey        = "anotherKey"
}

private func showGreetingScreen (_ tabbar: UITabBarController) {
    let defaults = UserDefaults.standard
    let date = Date()
    if let greetingDate = defaults.object(forKey: UDKeys.greetingDateKey) as? Date {
        let order = Calendar.current.compare(date, to: greetingDate, toGranularity: .day)
        if order == .orderedDescending {
            defaults.set(date, forKey: UDKeys.greetingDateKey)
            presentWelcomeScreen(tabbar)
        }
    } else {
        defaults.set(date, forKey: UDKeys.greetingDateKey)
        presentWelcomeScreen(tabbar)
    }
}
\$\endgroup\$
1
\$\begingroup\$

just some other refactoring hints:

  • encapsulate the check in a own UserUsageController class
  • then you can test it better
  • maybe the check for midnight is dependend on timezone?

user returning over time zone:

  • show the welcome screen also if the last visit longer then a 8 hours - maybe user comes there at 23:00 and again at 02:00 - then it also not make sense to show the welcome screen

short time user:

  • maybe only set the lastVisitDate if the user has used the app more then 2 minutes (for example if he enter the app because he accidently tap on a push notification)
  • or set the last visit date only when the user exits the welcome screen

sample implementation (thanks to @Martin-R for code deduplication):

import UIKit

class UserUsageController {

    static let greetingDateKey = "greetingDate"

    static func isReturning(now: Date = Date(), minHours: Int = 8) -> Bool {

        let lastGreeting = getLastVisit() ?? .distantPast

        let dayBefore = isDayBefore(now: now, last: lastGreeting)
        let leastTime = isLeastTime(now: now, last: lastGreeting, minHours: minHours)
        return dayBefore && leastTime
    }

    private static func isDayBefore(now: Date, last: Date) -> Bool {
        return Calendar.current.compare(now, to: last, toGranularity: .day) == .orderedDescending
    }

    private static func isLeastTime(now: Date, last: Date, minHours: Int) -> Bool {
        let hours = Calendar.current.dateComponents([.hour], from: last, to: now ).hour ?? 0
        return hours > minHours
    }

    static func setLastVisit(date: Date = Date()){
        UserDefaults.standard.set(date, forKey: greetingDateKey)
    }

    static func getLastVisit() -> Date? {
        return UserDefaults.standard.object(forKey: greetingDateKey) as? Date
    }
}

Tests:

let df = DateFormatter()
df.dateFormat = "yyyy/MM/dd HH:mm"

UserUsageController.setLastVisit(date: df.date(from: "2019/04/20 01:00")! )
assert(false == UserUsageController.isReturning(now: df.date(from: "2019/04/20 09:00")!))
assert(false == UserUsageController.isReturning(now: df.date(from: "2019/04/20 23:00")!))

UserUsageController.setLastVisit(date: df.date(from: "2019/04/20 22:00")! )

assert(false == UserUsageController.isReturning(now: df.date(from: "2019/04/21 01:00")!))
assert(true == UserUsageController.isReturning(now: df.date(from: "2019/04/21 01:00")!,
                                               minHours: 1))
assert(false == UserUsageController.isReturning(now: df.date(from: "2019/04/21 06:59")!))
assert(true == UserUsageController.isReturning(now: df.date(from: "2019/04/21 07:00")!))
assert(true == UserUsageController.isReturning(now: df.date(from: "2019/04/21 08:00")!))
assert(true == UserUsageController.isReturning(now: df.date(from: "2019/04/22 08:00")!))

Usage:


// in calling class

if UserUsageController.isReturning() {
    // OpenWelcomeScreen
}


// onClose at welcomeScreen
UserUsageController.setLastVisit()

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.