Search code examples
iosswiftdatecountdown

countdown from current date to specific date swift


Users can sell items and add an expiration date that varies from 1-3 weeks out from current date.

This is how I store the expiration date as a Double. Do I need to be explicit about other date components like year, month, etc?

enum Weeks: Int  {
    case one
    case two
    case three
}

extension Weeks {
    var timeInterval: Double? {
        let currentDate = Date()
        let calendar = Calendar.current
        let calendarComponents: Set<Calendar.Component> = Set(arrayLiteral: Calendar.Component.year, Calendar.Component.month, Calendar.Component.day, Calendar.Component.hour, Calendar.Component.minute, Calendar.Component.second)

        var components = calendar.dateComponents(calendarComponents, from: currentDate)

        switch self {
        case .one: components.weekOfMonth = 1
        case .two: components.weekOfMonth = 2
        case .three: components.weekOfMonth = 3
        }

        if let expirationDate = calendar.date(from: components) {
            return expirationDate.timeIntervalSince1970 as Double
        } else {
            return nil
        }
    }
}

This is how I calculate the countdown in Date extension:

func countdown(to date: Date) -> CountdownResponse {
    let difference = Calendar.current.dateComponents([.day, .hour, .minute, .second], from: self, to: date)

    guard let day = difference.day, let hour = difference.hour, let minute = difference.minute, let second = difference.second else {
        return .error(message: "One or more date components are nil.")
    }

    if day <= 0 && hour <= 0 && minute <= 0 && second <= 0 {
        return .isFinished
    }

    let days = displayableText(from: difference.day)
    let hours = displayableText(from: difference.hour)
    let minutes = displayableText(from: difference.minute)
    let seconds = displayableText(from: difference.second)

    let timeRemaining = "\(days) : \(hours) : \(minutes) : \(seconds)"

    return .result(time: timeRemaining)
}

This is how I'd like to get the countdown, and then I check the response enum for error, or result. But this gives me incorrect time.

let expirationDate = Date(timeIntervalSince1970: self.item.expirationDate)

let response = currentDate.countdown(to: expirationDate)

When I create date manually and test like below, it works as expected.

var comp = calendar.dateComponents(calendarComponents, from: Date())

comp.year = 2017
comp.month = 8
comp.day = 24
comp.minute = 50
comp.second = 30

What am I doing wrong? Am I persisting date as time interval incorrectly, or creating date from timeInterval incorrectly?

As I look over the code I realize that I may be calculating the week wrong. If it's the second week of the month and a user chooses for item to expire in 3 weeks, week of month shouldn't be 3, it needs to be 3 weeks from current week of month. Any guidance on how to fix this logic is appreciated.


Solution

  • Why did you make your code so complicated? The Foundation framework provide everything you need, from calendrical calculation (add and subtract dates) to output formatting (x days, y hours, z minutes, etc.).

    Here's an example of how you can shorten it:

    enum Week: Int {
        case one = 7, two = 14, three = 21
    }
    
    enum CountdownResponse {
        case isFinished
        case result(time: String)
    }
    
    struct ProductListing {
        // Customize this if you want to change timeRemaining's format
        // It automatically take care of singular vs. plural, i.e. 1 hr and 2 hrs
        private static let dateComponentFormatter: DateComponentsFormatter = {
            var formatter = DateComponentsFormatter()
            formatter.allowedUnits = [.day, .hour, .minute, .second]
            formatter.unitsStyle = .short
            return formatter
        }()
    
        var listingDate: Date
        var duration: Week
        var expirationDate: Date {
            return Calendar.current.date(byAdding: .day, value: duration.rawValue, to: listingDate)!
        }
    
        var timeRemaining: CountdownResponse {
            let now = Date()
    
            if expirationDate <= now {
                return .isFinished
            } else {
                let timeRemaining = ProductListing.dateComponentFormatter.string(from: now, to: expirationDate)!
                return .result(time: timeRemaining)
            }
        }
    }
    
    // Usage
    let august01 = DateComponents(calendar: .current, year: 2017, month: 8, day: 1).date!
    let august19 = DateComponents(calendar: .current, year: 2017, month: 8, day: 19).date!
    
    let listing1 = ProductListing(listingDate: august01, duration: .three)
    let listing2 = ProductListing(listingDate: august19, duration: .one)
    
    print(listing1.timeRemaining) // .isFinished
    print(listing2.timeRemaining) // .result("2 days, 4 hrs, 9 min, 23 secs")
    

    Note though, the calculation becomes real hairy when it crosses over the clock change days as the time shifts back and forth. I haven't test these edge cases with the code above.