New Date.FormatStyle: Anyway to do 24 hour?

I cannot find anyway to format hour in 24 hour with the new Date.FormatStyle

import Foundation

let afternoonHour = try Date("2021-10-23T02:37:12Z", strategy: .iso8601)

// this format to 24 hour
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
let twentyFourHour = dateFormatter.string(from: afternoonHour)
print(twentyFourHour)

// with the new Date.FormatStyle
// how to display hour in 24hr format?
afternoonHour.formatted(.dateTime.month().day().hour(.conversationalTwoDigits(amPM: .wide)).minute().timeZone(.specificName(.short)))   // "Oct 22, 07:37 PM PDT"
afternoonHour.formatted(.dateTime.hour(.conversationalDefaultDigits(amPM: .omitted)))       // "07"
// how to get "19"?

try this:

dateFormatter.locale = Locale(identifier: "en_US_POSIX")

Doesn't work:

var formatStyle = Date.FormatStyle.dateTime
formatStyle.locale = Locale(identifier: "en_US_POSIX")
afternoonHour.formatted(formatStyle.hour())       // "07 PM"

Is 24 hour format not possible, intentionally? No way this is due to mistaken oversight.

Let’s start by digging into your DateFormatter example. Consider this code:

// this format to 24 hour
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
let twentyFourHour = dateFormatter.string(from: afternoonHour)
print(twentyFourHour)

This doesn’t work the way you think it does. Try this:

  1. Set your Mac to a 24-hour by default locale (like the UK).

  2. Put that code into a Xcode playground. It’ll print a 24-hour time:

    20:52:54
    
  3. Go to System Preferences > Language & Region > General and uncheck 24-Hour Time.

  4. Run your playground again; this time you get a 12 hour time:

    8:52:54 pm
    

That’s because you haven’t pinned the locale on the date formatter to en_US_POSIX. Without that, the date formatter uses the user’s settings, and that includes this 12-/24-hour override.

You’ll see worse things when it comes to dates )-: QA1480 NSDateFormatter and Internet Dates has a good example of this.

The issue here is that DateFormatter serves two roles:

  • By default it works with user-visible dates.

  • If you pin the locale to en_US_POSIX you can use it to work with fixed-format dates.

Right now you’re crossing those streams, which doesn’t end well.

AFAIK Foundation’s new date formatting support is intended to support the first role, not the second. If you want to work with fixed-format dates, you have various options:

  • ISO8601DateFormatter

  • DateFormatter with the locale pinned to en_US_POSIX

  • Lower-level constructs

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

6 Likes

I run this on device and it's still in AM/PM format, even though it's in 24 hour setting. I verify with DatePicker which is in 24 hour format.

Text(when, format: .dateTime.month().day().hour().minute().timeZone(.specificName(.short)))
Test
import SwiftUI

struct DateFormatting: View {
    let when = Date(timeIntervalSinceReferenceDate: 656904575.211251)
    var body: some View {
        VStack {
            Text(when, format: .dateTime.month().day().hour().minute().timeZone(.specificName(.short)))
                .font(.system(size: 50))
            Text(when, format: .dateTime.month().day().hour(.conversationalTwoDigits(amPM: .omitted)).minute().timeZone(.specificName(.short)))
                .font(.system(size: 50))
        }
    }
}

struct DateFormatting_Previews: PreviewProvider {
    static var previews: some View {
        DateFormatting()
    }
}

try a fixed locale:

Text(when, format: .dateTime.locale(.init(identifier: "en_US_POSIX")).month().day().hour(...

No diff, still show "6:29 PM" :

Text(when, format: .dateTime.locale(.current).month().day().hour().minute().timeZone(.specificName(.short)))
Text(when, format: .dateTime.locale(.init(identifier: "en_US_POSIX")).month().day().hour().minute().timeZone(.specificName(.short)))

Maybe it's a bug, looks like it's not respecting user preferences? @eskimo ?

looks buggy. i'd stick to DateFormatter for now which is less buggy.

btw, this gave me 24h result (tested on simulator):

        Text(when, format: .dateTime)
            .environment(\.locale, .init(identifier: "en_UK"))

Ah, I forgot. This is not a bug, it's how Text works: it ignores the locale set in the FormatStyle and use the environment. However, it doesn't seem to do the same for Calendar and TimeZone. It should, though, like the DatePicker:

import SwiftUI

struct ContentView: View {
    static let calendar = Calendar(identifier: .chinese)
    static let locale = Locale(identifier: "zh_Hans_CN")
    static let timeZone = TimeZone(identifier: "Asia/Shanghai")!

    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm:ss.SSSz"
        return formatter
    }()

    @State private var when = Date(timeIntervalSinceReferenceDate: 656904575.211251)

    var body: some View {
        VStack {
            // default
            Text(when, format: .dateTime.month().day().hour().minute().timeZone(.specificName(.long)))
                .padding()

            // New Date.FormatStyle: only locale in environment work
            Text(when, format: .dateTime.month().day().hour().minute().timeZone(.specificName(.long)))
                .padding()
                .environment(\.locale, Self.locale) // works

            // New Date.FormatStyle: timezone and calendar in environment don't work
            Text(when, format: .dateTime.month().day().hour().minute().timeZone(.specificName(.long)))
                .padding()
                .environment(\.locale, Self.locale)     // works
                .environment(\.timeZone, Self.timeZone) // not work, still at system
                .environment(\.calendar, Self.calendar) // not work, still at system

            // old DateFormatter: only timeZone in environment work
            Text(when, formatter: Self.dateFormatter)
                .environment(\.locale, Self.locale)     // not work
                .environment(\.timeZone, Self.timeZone) // works
                .environment(\.calendar, Self.calendar) // not work

            // DatePicker respect all the env values
            DatePicker("When", selection: $when)
                // all work:
                .environment(\.locale, Self.locale)
                .environment(\.timeZone, Self.timeZone)
                .environment(\.calendar, Self.calendar)
        }

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

that's quite non obvious... have a formatter/format parameter which is ... ignored?

struct ContentView: View {
    static let when = Date(timeIntervalSince1970: 15*3600)
    static let locale = Locale(identifier: "en_US_POSIX")
    static let timeZone = TimeZone(identifier: "UTC")!

    var body: some View {
        Text(Self.when, format: Date.FormatStyle.dateTime.locale(Self.locale))
            .environment(\.locale, Self.locale)
            .environment(\.timeZone, Self.timeZone)
    }
    // this surely has to show "15:00", right?
    // nope... "1/1/1970, 4:00 PM"
    // same with en_US. slightly better but still wrong with en_UK (16:00)
}
1 Like

That's how SwiftUI view that deal with Date value work: DatePicker and Text<Subject>(Subject, formatter: Formatter).

The problem is the new Text<F>(F.FormatInput, format: F) only use Locale from environment, but not for TimeZone and Calendar. I think it's a bug I reported FB9217356.

Look at this example, the second Text use the Locale, Timezone and Calendar from the environment, the first Text only use Locale from environment:

    static let when = Date(timeIntervalSince1970: 15*3600)
    static let locale = Locale(identifier: "en_US_POSIX")
    static let timeZone = TimeZone(identifier: "UTC")!

    static let localeChina = Locale(identifier: "zh_Hans_CN")
    static let calendar = Calendar(identifier: .chinese)

    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .long
        formatter.timeStyle = .long
        return formatter
    }()

    var body: some View {
        // this one only use locale from environment, not timezone and calendar. it's a bug
        Text(Self.when, format: Date.FormatStyle.dateTime.locale(Self.locale))
            .environment(\.locale, Self.localeChina)    // it's using this locale, not the one set above
            .environment(\.timeZone, Self.timeZone)     // but it's not using this, still at system default
            .environment(\.calendar, Self.calendar)     // and not this
        // this surely has to show "15:00", right?
        // nope... "1/1/1970, 4:00 PM"
        // same with en_US. slightly better but still wrong with en_UK (16:00)

        // this one use locale, timezone and calendar from environment:
        Text(Self.when, formatter: Self.dateFormatter)
            .environment(\.locale, Self.localeChina)    // it's using this
            .environment(\.timeZone, Self.timeZone)     // and this
            .environment(\.calendar, Self.calendar)     // and this
    }

my example also shows that en_US_POSIX locale set on environment level is ignored.

That not what I see: my example above set a china locale in environment and it's showing in Chinese. But timezone and calendar are not used from environment.

This new Text should work like the DateFormatter version: use locale, timezone and calendar from environment. I reported this many months ago but no fix so far. This prevents me from using the new Date.FormatStyle with Text to display Date value in specific timezone and calendar, different from user's system preference.

i'm not contradicting you here, i can see "non fixed" locales like "en_UK" or "en_US" working... just not the fixed locale "en_US_POSIX".

it would be logical for the format/formatter parameter of Text initialiser to take precedence over environment settings, don't you think?

view initialiser param >
    view environment setting >
       "outer view" environment setting >
          ...
             default (current OS settings)
1 Like

Yes, I think.

What I wish is Apple make it 1) follow user's 24-hour or AM/PM preference, 2) allow setting of locale, timezone and calendar work as you say, or be consisted with the current state: take them all from the environment.

@luca_bernardi ?

until it is fixed, in my experience DateFormatter's "setLocalizedDateFormatFromTemplate" works ok. although i do not know how to ask it for "user preferred 24 or 12 hour symbol", in other words while i can pass it "h" / "hh" / "H" or "HH" i don't know which one to use to match the OS setting.

in my experience DateFormatter's
setLocalizedDateFormatFromTemplate(_:) works ok.

This is definitely your friend.

i do not know how to ask it for "user preferred 24 or 12 hour symbol"

The go-to reference for this stuff is Unicode Technical Standard #35. It describes the j and J elements, that do exactly this.

IMPORTANT This only works in the context of date format templates. That is, do this:

df.setLocalizedDateFormatFromTemplate("j")

not this:

df.dateFormat = "j"

Then again, if you’re working with localised dates then the dateFormat property is nonsense anyway.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes
Terms of Service

Privacy Policy

Cookie Policy