how to use DateComponentsFormatter for showing date as "Today" or "Yesterday"?

Viewed 1061

I'm using swift in my project and I want to show specific date like this: 5 days ago or 5 month ago or ... I am using DateComponentsFormatter and it's doing well, but the problem is I want to show 1 day ago like "Yesterday" or showing 3 second ago like "Today". how can I do this? can I use DateComponentsFormatter for this problem? this is my codes:

func shortDate(_ local: LocaleIdentifier = .fa) -> String {
    
    let now = Date()
    let date = self

    let formatter = DateComponentsFormatter()
    formatter.calendar?.locale = Locale(identifier: local.rawValue)
    formatter.unitsStyle = .full
    formatter.maximumUnitCount = 1
    formatter.allowedUnits = [.year, .month, .day, .hour, .minute, .second]

    guard let timeString = formatter.string(from: date, to: now) else {
         return ""
    }

    return String(format: "Ago".localized, timeString)
}
3 Answers

For iOS 13 or later you can use RelativeDateTimeFormatter

let relativeDateTimeFormatter = RelativeDateTimeFormatter()
relativeDateTimeFormatter.dateTimeStyle = .named
relativeDateTimeFormatter.unitsStyle = .full
let date = Date.init(timeIntervalSinceNow: -60*60*24)
relativeDateTimeFormatter.string(for: date)  // "yesterday"

edit/update:

If you would like to support iOS 11 you would need to implement your own relative date formatter. You can use Calendar method isDateInToday and isDateInYesterday to combine a relative date formatter with date components formatter. Note that there is no need to check the time interval for setting a single unit in your date components formatter you can set the allowed units of your date components formatter, just make sure you set them respecting the desired priority to be displayed:

// This will avoid creating a formatter every time you call relativeDateFormatted property

extension Formatter {
    static let dateComponents: DateComponentsFormatter = {
        let dateComponentsFormatter = DateComponentsFormatter()
        dateComponentsFormatter.allowedUnits = [.day, .month, .year]  // check the order of the units it does matter when allowing only 1 unit to be displayed
        dateComponentsFormatter.maximumUnitCount = 1
        dateComponentsFormatter.unitsStyle = .full
        return dateComponentsFormatter
    }()
    static let relativeDate: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.doesRelativeDateFormatting = true
        dateFormatter.timeStyle = .none
        dateFormatter.dateStyle = .medium
        return dateFormatter
    }()
}

extension Date {
    var relativeDateFormatted: String {
        Calendar.current.isDateInToday(self) || Calendar.current.isDateInYesterday(self) ?
        Formatter.relativeDate.string(from: self) :
        Formatter.dateComponents.string(from: self, to:  Date()) ?? "" 
    }
}

Playground testing:

let date1 = DateComponents(calendar: .current, year: 2020, month: 9, day: 4, hour: 5).date!
let date2 = DateComponents(calendar: .current, year: 2020, month: 9, day: 3, hour: 23, minute: 50).date!
let date3 = DateComponents(calendar: .current, year: 2020, month: 8, day: 25, hour: 10).date!
let date4 = DateComponents(calendar: .current, year: 2020, month: 8, day: 3).date!
let date5 = DateComponents(calendar: .current, year: 2019, month: 8, day: 27).date!

date1.relativeDateFormatted  // "Today"
date2.relativeDateFormatted  // "Yesterday"
date3.relativeDateFormatted  // "10 days"
date4.relativeDateFormatted  // "1 month"
date5.relativeDateFormatted  // "1 year"

You are looking for the doesRelativeDateFormatting property of a DateFormatter.

I think, I must use both DateComponentsFormatter for more than 1 day and DateFormatter for showing Today(day=0) and Yesterday(day=1) for my problem. I found my answer here.

but I make codes a little better, this is my codes:

var calendar = Calendar.current
calendar.locale = Locale(identifier: local.rawValue)
    
let componentFormatter = DateComponentsFormatter()
componentFormatter.calendar = calendar
componentFormatter.unitsStyle = .full
componentFormatter.maximumUnitCount = 1

let interval = Calendar.current.dateComponents([.year, .month, .day], from: self, to: Date())
    
if let year = interval.year, year > 0 {
    componentFormatter.allowedUnits = .year
} else if let month = interval.month, month > 0 {
    componentFormatter.allowedUnits = .month
} else if let day = interval.day, day > 1 {
    componentFormatter.allowedUnits = .day
} else { // if let day = interval.day, day == 0 || day == 1 for Today or Yesterday

    let formatter = DateFormatter()
    formatter.doesRelativeDateFormatting = true
    formatter.calendar.locale = Locale(identifier: local.rawValue)
    formatter.timeStyle = .none
    formatter.dateStyle = .medium

    return formatter.string(from: self)
}

guard let timeString = componentFormatter.string(from: self, to: Date()) else {
        return ""
}
return String(format: "Ago".localized, timeString)
Related