2

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)
}
behrad
  • 596
  • 1
  • 5
  • 19

3 Answers3

2

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"
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
0

You are looking for the doesRelativeDateFormatting property of a DateFormatter.

matt
  • 515,959
  • 87
  • 875
  • 1,141
0

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)
behrad
  • 596
  • 1
  • 5
  • 19
  • This won't work as expected it will return day 0 for any time interval within 24 hours and day 1 for any time interval between 24 and 48 hours (if there is no daylight savings transition). Again you should use the calendar methods I mentioned above isDateInToday and isDateInYesterday. – Leo Dabus Sep 05 '20 at 00:57
  • Resuming first check if isDateInToday or isDateInYesterday and use the DateFormatter otherwise use the DateComponentsFormatter – Leo Dabus Sep 05 '20 at 01:05
  • this is what I want, any time interval within 24 hours should return Today and any time interval between 24 and 48 hours should return Yesterday. – behrad Sep 05 '20 at 09:41
  • but using isDateInToday or isDateInYesterday is another solution for my problem, and even better than my answer when I want to show another titles for today or yesterday like saying "Now" instead "Today" when isDateInToday returning true. – behrad Sep 05 '20 at 09:47
  • your answer is more better than my :) – behrad Sep 05 '20 at 09:51