Como obtenho uma data ISO 8601 no iOS?

115

É fácil obter a string de data ISO 8601 (por exemplo, 2004-02-12T15:19:21+00:00) em PHP via date('c'), mas como obtê-la em Objective-C (iPhone)? Existe uma maneira igualmente curta de fazer isso?

Este é o longo caminho que encontrei para fazer isso:

NSDateFormatter* dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZ";

NSDate *now = [NSDate date];
NSString *formattedDateString = [dateFormatter stringFromDate:now];
NSLog(@"ISO-8601 date: %@", formattedDateString);

// Output: ISO-8601 date: 2013-04-27T13:27:50-0700

Parece um monte de bobagens para algo tão central.

JohnK
fonte
1
Acho que o short está nos olhos de quem vê. Três linhas de código são muito curtas, não? Há uma subclasse C NSDate que você pode adicionar que fará isso com uma linha, eu apenas tropecei nela: github.com/soffes/SAMCategories/tree/master/SAMCategories/… . Dito isso, realmente, você obteve respostas muito boas (IMHO) e deveria dar a resposta a Etienne ou a rmaddy. É apenas uma boa prática e recompensa as pessoas que fornecem informações realmente úteis (isso me ajudou, eu precisava dessa informação hoje). Etienne poderia usar os pontos mais do que simplesmente. Como um sinal de boa medida, vou até votar na sua pergunta!
David H

Respostas:

212

Use NSDateFormatter:

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];   
NSLocale *enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
[dateFormatter setLocale:enUSPOSIXLocale];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"];
[dateFormatter setCalendar:[NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]];

NSDate *now = [NSDate date];
NSString *iso8601String = [dateFormatter stringFromDate:now];

E em Swift:

let dateFormatter = DateFormatter()
let enUSPosixLocale = Locale(identifier: "en_US_POSIX")
dateFormatter.locale = enUSPosixLocale
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
dateFormatter.calendar = Calendar(identifier: .gregorian)

let iso8601String = dateFormatter.string(from: Date())
rmaddy
fonte
1
Obrigado, Maddy. Acabei de ver sua resposta agora, depois de postar meu código. É o setLocaleimportante?
JohnK
2
@rmaddy Você pode apenas editar seu exemplo para usar ZZZZZ para que as pessoas copiando e colando não se queimem.
Jesse Rusak
4
@jlmendezbonini Não. É muito raro você querer YYYY. Você geralmente quer yyyy.
rmaddy
7
Fonte de por que o local deve ser definido como en_US_POSIX: developer.apple.com/library/mac/qa/qa1480/_index.html
dia
11
@maddy - Queria tornar mais fácil para outras pessoas descobrirem isso através do Google e gostaria de lhe dar crédito por realmente descobrir o formato correto, localidade etc. Mas feliz em postar como uma resposta separada se você quiser
Robin
60

O iOS 10 apresenta uma nova NSISO8601DateFormatterclasse para lidar exatamente com isso. Se você estiver usando o Swift 3, seu código seria algo assim:

let formatter = ISO8601DateFormatter()
let date = formatter.date(from: "2016-08-26T12:39:00Z")
let string = formatter.string(from: Date())
TwoStraws
fonte
e que formato dá? Também seria bom saber se ele pode lidar com de: 2016-08-26T12: 39: 00-0000 e 2016-08-26T12: 39: 00-00: 00 e 2016-08-26T12: 39: 00.374-0000
malhal,
1
Depois que as três linhas da minha resposta forem executadas, stringcontém "2016-09-03T21: 47: 27Z".
TwoStraws
3
você NÃO PODE lidar com milissegundos com este formato.
Enrique
3
@Enrique: você deve adicionar o NSISO8601DateFormatWithFractionalSeconds às opções do formatador para poder trabalhar com milissegundos, verifique a documentação.
PakitoV
1
NSISO8601DateFormatWithFractionalSeconds funciona apenas em 11.2+. Caso contrário, você terá um acidente!
hash3r
27

Como complemento à resposta de maddy, o formato de fuso horário deve ser "ZZZZZ" (5 vezes Z) para ISO 8601 em vez de um único "Z" (que é para formato RFC 822). Pelo menos no iOS 6.

(consulte http://www.unicode.org/reports/tr35/tr35-25.html#Date_Format_Patterns )

Etienne
fonte
Grande captura! Quem estiver interessado nisto, acesse o link, procure por 8601 e verá.
David H
Quando o fuso horário na data é UTC, há uma maneira de fazer com que ele mostre 00:00 em vez de Z (que são equivalentes)?
shim
Muito obrigado;) estava batendo minha cabeça na parede com um formatador de data retornando data nula !!! :)
polo987
19

Um problema frequentemente esquecido é que as strings no formato ISO 8601 podem ter milissegundos ou não.

Em outras palavras, ambos "2016-12-31T23: 59: 59.9999999" e "2016-12-01T00: 00: 00" são legítimos, mas se você estiver usando um formatador de data de tipo estático, um deles não será analisado .

A partir do iOS 10, você deve usar o ISO8601DateFormatterque lida com todas as variações de strings de data ISO 8601. Veja o exemplo abaixo:

let date = Date()
var string: String

let formatter = ISO8601DateFormatter()
string = formatter.string(from: date)

let GMT = TimeZone(abbreviation: "GMT")
let options: ISO8601DateFormatOptions = [.withInternetDateTime, .withDashSeparatorInDate, .withColonSeparatorInTime, .withTimeZone]
string = ISO8601DateFormatter.string(from: date, timeZone: GMT, formatOptions: options)

Para iOS 9 e versões anteriores, use a seguinte abordagem com vários formatadores de dados.

Não encontrei uma resposta que abranja os dois casos e abstraia essa diferença sutil. Aqui está a solução para isso:

extension DateFormatter {

    static let iso8601DateFormatter: DateFormatter = {
        let enUSPOSIXLocale = Locale(identifier: "en_US_POSIX")
        let iso8601DateFormatter = DateFormatter()
        iso8601DateFormatter.locale = enUSPOSIXLocale
        iso8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
        iso8601DateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
        return iso8601DateFormatter
    }()

    static let iso8601WithoutMillisecondsDateFormatter: DateFormatter = {
        let enUSPOSIXLocale = Locale(identifier: "en_US_POSIX")
        let iso8601DateFormatter = DateFormatter()
        iso8601DateFormatter.locale = enUSPOSIXLocale
        iso8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
        iso8601DateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
        return iso8601DateFormatter
    }()

    static func date(fromISO8601String string: String) -> Date? {
        if let dateWithMilliseconds = iso8601DateFormatter.date(from: string) {
            return dateWithMilliseconds
        }

        if let dateWithoutMilliseconds = iso8601WithoutMillisecondsDateFormatter.date(from: string) {
            return dateWithoutMilliseconds
        }

        return nil
    }
}

Uso:

let dateToString = "2016-12-31T23:59:59.9999999"
let dateTo = DateFormatter.date(fromISO8601String: dateToString)
// dateTo: 2016-12-31 23:59:59 +0000

let dateFromString = "2016-12-01T00:00:00"
let dateFrom = DateFormatter.date(fromISO8601String: dateFromString)
// dateFrom: 2016-12-01 00:00:00 +0000

Também recomendo verificar o artigo da Apple sobre formatadores de data.

Vadim Bulavin
fonte
Você poderia também indicar como obter NSISO8601DateFormtter em Obj-C? (no iOS 10 e superior, é claro)
Motti Shneor
8

Portanto, use a categoria de Sam Soffee no NSDate encontrada aqui . Com esse código adicionado ao seu projeto, você pode, a partir de então, usar um único método no NSDate:

- (NSString *)sam_ISO8601String

Não é apenas uma linha, é muito mais rápido do que a abordagem NSDateFormatter, uma vez que é escrito em C. puro.

David H
fonte
1
A localização atual da categoria é aqui
Perry,
1
Eu desaconselho isso, ele tem um problema de exceção de memória que acontece muito aleatoriamente
M_Waly
1
@M_Waly você relatou isso a ele? Eu construí seu código sem avisos e ele passou nas verificações de análise muito bem.
David H
6

Do IS8601 , os problemas são a representação e o fuso horário

  • ISO 8601 = year-month-day time timezone
  • Para data e hora, existem formatos básico (AAAAMMDD, hhmmss, ...) e estendido (AAAA-MM-DD, hh: mm: ss, ...)
  • O fuso horário pode ser Zulu, deslocamento ou GMT
  • O separador de data e hora pode ser um espaço ou T
  • Existem formatos de semana para a data, mas raramente são usados
  • O fuso horário pode ter muitos espaços depois
  • O segundo é opcional

Aqui estão algumas strings válidas

2016-04-08T10:25:30Z
2016-04-08 11:25:30+0100
2016-04-08 202530GMT+1000
20160408 08:25:30-02:00
2016-04-08 11:25:30     +0100

Soluções

NSDateFormatter

Então, aqui está o formato que estou usando no onmyway133 ISO8601

let formatter = NSDateFormatter()
formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
formatter.dateFormat = "yyyyMMdd HHmmssZ"

Sobre o Zidentificador Tabela de símbolos de campo de data

Z: The ISO8601 basic format with hours, minutes and optional seconds fields. The format is equivalent to RFC 822 zone format (when optional seconds field is absent)

Sobre a locale formatação de dados usando as configurações locais

As localidades representam as opções de formatação para um determinado usuário, não o idioma preferido do usuário. Muitas vezes são iguais, mas podem ser diferentes. Por exemplo, um falante nativo de inglês que mora na Alemanha pode selecionar inglês como idioma e Alemanha como região

Sobre en_US_POSIX perguntas e respostas técnicas QA1480 NSDateFormatter e datas na Internet

Por outro lado, se estiver trabalhando com datas de formato fixo, você deve primeiro definir a localidade do formatador de data para algo apropriado para seu formato fixo. Na maioria dos casos, o melhor local para escolher é "en_US_POSIX", um local que foi projetado especificamente para produzir resultados em inglês dos EUA, independentemente das preferências do usuário e do sistema. "en_US_POSIX" também é invariável no tempo (se os EUA, em algum ponto no futuro, mudarem a forma como formata datas, "en_US" mudará para refletir o novo comportamento, mas "en_US_POSIX" não), e entre máquinas ( "en_US_POSIX" funciona no iOS da mesma forma que no OS X e como em outras plataformas).

Perguntas interessantes relacionadas

onmyway133
fonte
Acabei de tentar com 2016-07-23T07: 24: 23Z, não funciona
Kamen Dobrev
@KamenDobrev o que você quer dizer com "não funciona"? Acabei de adicionar seu exemplo aos testes github.com/onmyway133/ISO8601/blob/master/ISO8601Tests/… e funciona
onmyway133
3

Com base nesta essência: https://github.com/justinmakaila/NSDate-ISO-8601/blob/master/NSDateISO8601.swift , o método a seguir pode ser usado para converter NSDate em string de data ISO 8601 no formato aaaa-MM -dd'T'HH: mm: ss.SSSZ

-(NSString *)getISO8601String
    {
        static NSDateFormatter *formatter = nil;
        if (!formatter)
        {
            formatter = [[NSDateFormatter alloc] init];
            [formatter setLocale: [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
            formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation: @"UTC"];
            [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"];

        }
        NSString *iso8601String = [formatter stringFromDate: self];
        return [iso8601String stringByAppendingString: @"Z"];
    }
Zack Zhu
fonte
O limite Spara sub-segundo foi fundamental aqui
Mark Meyer
Observe que se você precisar de mais de 3 S's, então não será possível usar o formatador de data para analisá-lo manualmente.
malhal
-1

Isso é um pouco mais simples e coloca a data no UTC.

extension NSDate
{
    func iso8601() -> String
    {
        let dateFormatter = NSDateFormatter()
        dateFormatter.timeZone = NSTimeZone(name: "UTC")
        let iso8601String = dateFormatter.stringFromDate(NSDate())
        return iso8601String
    }
}

let date = NSDate().iso8601()
iphaaw
fonte
-1

Usando Swift 3.0 e iOS 9.0

extension Date {
    private static let jsonDateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.timeZone = TimeZone(identifier: "UTC")!
        return formatter
    }()

    var IOS8601String: String {
        get {
            return Date.jsonDateFormatter.string(from: self)
        }
    }

    init?(fromIOS8601 dateString: String) {
        if let d = Date.jsonDateFormatter.date(from: dateString) {
            self.init(timeInterval: 0, since:d)
        } else {
            return nil
        }
    }
}
Denis Krivitski
fonte