I need to set the timezone information of a remote clock to the one on the iOS device.
The remote clock only supports GNU lib C TZ format of:
std offset dst [offset],start[/time],end[/time]
e.g: EST+5EDT,M3.2.0/2,M11.1.0/2
So I need to produce a string similar to above from NSTimeZone.local time zone in Swift. Can't seem to access the current timezone rules as they would be in the IANA TZ database to produce the output.
Can this be done without the horrifying idea of caching a local copy of the TZ database in the app?
Update:
I haven't been able to find anything useful even through other programming languages. The best I was able to find was essentially parsing the tzfile in linux and making my own NSDictionary containing the info.
This was a fun exploration, largely because fitting the data into just the right format is pretty complex. Problem components:
We need the "current" TZ database rule that applies for a given time zone. This is a bit of a loaded concept, because:
Darwin platforms don't actually use the TZ database directly for most applications, but instead use ICU's time zone database, which comes in a different format and is more complex. Even if you produce a string in this format, it's not necessarily descriptive of the actual time behavior on device
While it is possible to read and parse the TZ database on iOS dynamically, the TZ database itself is not guaranteed to store information in the format needed here. rfc8536, the RFC governing the Time Zone Information Format says the following about the format you want:
The TZ string in a version 3 TZif file MAY use the following extensions to POSIX TZ strings. These extensions are described using the terminology of Section 8.3 of the "Base Definitions" volume of [POSIX].
Example: <-03>3<-02>,M3.5.0/-2,M10.5.0/-1
Example: EST5EDT,0/0,J365/25
While spelunking through the iOS TZ database, I found some database entries that do offer a rule at the end of the file in this format, but they appear to be a minority. You could parse these dynamically, but it's likely not worth it
So, we need to use APIs to produce a string in this format.
In order to produce a "rule" that is at least approximately correct on a given date, you need to know information about DST transitions around that date. This is an extremely thorny topic, because DST rules change all the time, and don't always make as much sense as you'd hope. At the very least:
Many time zones in the Northern hemisphere observe DST beginning in the spring and ending in the fall
Many time zones in the Southern hemisphere observe DST beginning in the fall and ending in the spring
Some time zones don't observe DST (are in standard time year-round)
Some time zones don't observe DST and are in daylight time year-round
Because the rules are so complex, the rest of this answer assumes you're okay with producing a "good enough" answer that represents a specific date in time, and is willing to send further strings to your clock some time in the future when corrections are needed. e.g., to describe "now", we will be assuming that producing a rule based off of the last DST transition (if any) and the next DST transition (if any) is "good enough", but this may not work for all situations in many time zones
Foundation provides DST transition information on TimeZone in the form of TimeZone.nextDaylightSavingTimeTransition/TimeZone.nextDaylightSavingTimeTransition(after:). Frustratingly, however, there's no way to get information about previous DST transitions, so we'll need to rectify that:
Foundation's localization support (including calendars and time zones) is based directly on the ICU library, which ships internally on all Apple platforms. ICU does provide a way to get information about previous DST transitions, but Foundation just doesn't offer this as API, so we'll need to expose it ourselves
ICU is a semi-private library on Apple platforms. The library is guaranteed to be present, and Xcode will offer you libicucore.tbd to link against in <Project> > <Target> > Build Phases > Link Binary with Libraries, but the actual headers and symbols are not directly exposed to apps. You can successfully link against libicucore, but you'll need to forward-declare the functionality we need in an Obj-C header imported into Swift
Somewhere in the Swift project, we need to expose the following ICU functionality:
#include <stdint.h>
typedef void * _Nonnull UCalendar;
typedef double UDate;
typedef int8_t UBool;
typedef uint16_t UChar;
typedef enum UTimeZoneTransitionType {
UCAL_TZ_TRANSITION_NEXT,
UCAL_TZ_TRANSITION_NEXT_INCLUSIVE,
UCAL_TZ_TRANSITION_PREVIOUS,
UCAL_TZ_TRANSITION_PREVIOUS_INCLUSIVE,
} UTimeZoneTransitionType;
typedef enum UCalendarType {
UCAL_TRADITIONAL,
UCAL_DEFAULT,
UCAL_GREGORIAN,
} UCalendarType;
typedef enum UErrorCode {
U_ZERO_ERROR = 0,
} UErrorCode;
UCalendar * _Nullable ucal_open(const UChar *zoneID, int32_t len, const char *locale, UCalendarType type, UErrorCode *status);
void ucal_setMillis(const UCalendar * _Nonnull cal, UDate date, UErrorCode * _Nonnull status);
UBool ucal_getTimeZoneTransitionDate(const UCalendar * _Nonnull cal, UTimeZoneTransitionType type, UDate * _Nonnull transition, UErrorCode * _Nonnull status);
These are all forward declarations / constants, so no need to worry about implementation (since we get that by linking against libicucore).
You can see the values in UTimeZoneTransitionType — TimeZone.nextDaylightSavingTimeTransition just calls ucal_getTimeZoneTransitionDate with a value of UCAL_TZ_TRANSITION_NEXT, so we can offer roughly the same functionality by calling the method with UCAL_TZ_TRANSITION_PREVIOUS:
extension TimeZone {
func previousDaylightSavingTimeTransition(before: Date) -> Date? {
// We _must_ pass a status variable for `ucal_open` to write into, but the actual initial
// value doesn't matter.
var status = U_ZERO_ERROR
// `ucal_open` requires the time zone identifier be passed in as UTF-16 code points.
// `String.utf16` doesn't offer a contiguous buffer for us to pass directly into `ucal_open`
// so we have to create our own by copying the values into an `Array`, then
let timeZoneIdentifier = Array(identifier.utf16)
guard let calendar = Locale.current.identifier.withCString({ localeIdentifier in
ucal_open(timeZoneIdentifier, // implicit conversion of Array to a pointer, but convenient!
Int32(timeZoneIdentifier.count),
localeIdentifier,
UCAL_GREGORIAN,
&status)
}) else {
// Figure out some error handling here -- we failed to find a "calendar" for this time
// zone; i.e., there's no time zone date for this time zone.
//
// With more enum cases copied from `UErrorCode` you may find a good way to report an
// error here if needed. `u_errorName` turns a `UErrorCode` into a string.
return nil
}
// `UCalendar` functions operate on the calendar's current timestamp, so we have to apply
// `date` to it. `UDate`s are the number of milliseconds which have passed since January 1,
// 1970, while `Date` offers its time interval in seconds.
ucal_setMillis(calendar, before.timeIntervalSince1970 * 1000.0, &status)
var result: UDate = 0
guard ucal_getTimeZoneTransitionDate(calendar, UCAL_TZ_TRANSITION_PREVIOUS, &result, &status) != 0 else {
// Figure out some error handling here -- same as above (check status).
return nil
}
// Same transition but in reverse.
return Date(timeIntervalSince1970: result / 1000.0)
}
}
So, with all of this in place, we can fill out a crude method to produce a string in the format you need:
extension TimeZone {
struct Transition {
let abbreviation: String
let offsetFromGMT: Int
let date: Date
let components: DateComponents
init(for timeZone: TimeZone, on date: Date, using referenceCalendar: Calendar) {
abbreviation = timeZone.abbreviation(for: date) ?? ""
offsetFromGMT = timeZone.secondsFromGMT(for: date)
self.date = date
components = referenceCalendar.dateComponents([.month, .weekOfMonth, .weekdayOrdinal, .hour, .minute, .second], from: date)
}
}
func approximateTZEntryRule(on date: Date = Date(), using calendar: Calendar? = nil) -> String? {
var referenceCalendar = calendar ?? Calendar(identifier: .gregorian)
referenceCalendar.timeZone = self
guard let year = referenceCalendar.dateInterval(of: .year, for: date) else {
return nil
}
// If no prior DST transition has ever occurred, we're likely in a time zone which is either
// standard or daylight year-round. We'll cap the definition here to the very start of the
// year.
let previousDSTTransition = Transition(for: self, on: previousDaylightSavingTimeTransition(before: date) ?? year.start, using: referenceCalendar)
// Same with the following DST transition -- if no following DST transition will ever come,
// we'll cap it to the end of the year.
let nextDSTTransition = Transition(for: self, on: nextDaylightSavingTimeTransition(after: date) ?? year.end, using: referenceCalendar)
let standardToDaylightTransition: Transition
let daylightToStandardTransition: Transition
if isDaylightSavingTime(for: date) {
standardToDaylightTransition = previousDSTTransition
daylightToStandardTransition = nextDSTTransition
} else {
standardToDaylightTransition = nextDSTTransition
daylightToStandardTransition = previousDSTTransition
}
let standardAbbreviation = daylightToStandardTransition.abbreviation
let standardOffset = formatOffset(daylightToStandardTransition.offsetFromGMT)
let daylightAbbreviation = standardToDaylightTransition.abbreviation
let startDate = formatDate(components: standardToDaylightTransition.components)
let endDate = formatDate(components: daylightToStandardTransition.components)
return "\(standardAbbreviation)\(standardOffset)\(daylightAbbreviation),\(startDate),\(endDate)"
}
/* These formatting functions can be way better. You'll also want to actually cache the
DateComponentsFormatter somewhere.
*/
func formatOffset(_ dateComponents: DateComponents) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute, .second]
formatter.zeroFormattingBehavior = .dropTrailing
return formatter.string(from: dateComponents) ?? ""
}
func formatOffset(_ seconds: Int) -> String {
return formatOffset(DateComponents(second: seconds))
}
func formatDate(components: DateComponents) -> String {
let month = components.month ?? 0
let week = components.weekOfMonth ?? 0
let day = components.weekdayOrdinal ?? 0
let offset = formatOffset(DateComponents(hour: components.hour, minute: components.minute, second: components.second))
return "M\(month).\(week).\(day)/\(offset)"
}
}
Note that there's lots to improve here, especially in clarity and performance. (Formatters are notoriously expensive, so you'll definitely want to cache them.) This also currently only produces dates in the expanded form "Mm.w.d" and not Julian days, but that can be bolted on. The code also assumes that it's "good enough" to restrict unbounded rules to the current calendar year, since this is what the GNU C library docs seem to imply about e.g. time zones which are always in standard/daylight time. (This also doesn't recognize well-known time zones like GMT/UTC, which might be sufficient to just write out as "GMT".)
I have not extensively tested this code for various time zones, and the above code should be considered a basis for additional iteration. For my time zone of America/New_York, this produces "EST-5EDT,M3.3.2/3,M11.2.1/1", which appears correct to me at first glance, but many other edge cases might be good to explore:
Boundary conditions around the start/end of the year
Giving a date which exactly matches a DST transition (consider TRANSITION_PREVIOUS vs. TRANSITION_PREVIOUS_INCLUSIVE)
Time zones which are always standard/daylight
Non-standard daylight/timezone offsets
There's a lot more to this, and in general, I'd recommend trying to find an alternative method of setting a time on this device (preferably using named time zones), but this might hopefully at least get you started.
I need to identify if the device is rebooted.
Currently saving time in the database, and periodically check time interval from last boot using the following code as suggested in Apple forums:
func bootTime() -> Date? {
var tv = timeval()
var tvSize = MemoryLayout<timeval>.size
let err = sysctlbyname("kern.boottime", &tv, &tvSize, nil, 0);
guard err == 0, tvSize == MemoryLayout<timeval>.size else {
return nil
}
return Date(timeIntervalSince1970: Double(tv.tv_sec) + Double(tv.tv_usec) / 1_000_000.0)
}
But the problem with this is even without a reboot, tv.tv_sec value differs around 30 (it varies from 0 seconds to 30 secs).
Anybody have any idea about this variation? or any other better way to identify device reboot without using sysctl or other reliable sysctl.
https://developer.apple.com/forums/thread/101874?answerId=309633022#309633022
Any pointer is highly appreciated.
I searched over SO, all the answer points to the solution mentioned here. Which have the issue as I mentioned. Please don't mark duplicate.
i have this code from here(Using MIDIPacketList in swift) but i can't pm the user or comment on that question, so i will ask my question.
#ephemer dude if you see my question, i love your code on midi list and it work so well, but when i change the time stamp, nothing happens, it must create some delay but it will be the same as 0 time stamp.
do anyone know how to fix this?
and how i can have the time stamp out of this extension to have that in midi event, i want to be able to change time stamp for every midi event,
to have it here:
var packets = MIDIPacketList(midiEvents: [[0x90, 60, 100]])
public typealias MidiEvent = [UInt8]
extension MIDIPacketList {
init(midiEvents: [MidiEvent]) {
let timestamp = MIDITimeStamp(0) // do it now
let totalBytesInAllEvents = midiEvents.reduce(0) { total, event in
return total + event.count
}
// Without this, we'd run out of space for the last few MidiEvents
let listSize = MemoryLayout<MIDIPacketList>.size + totalBytesInAllEvents
// CoreMIDI supports up to 65536 bytes, but in practical tests it seems
// certain devices accept much less than that at a time. Unless you're
// turning on / off ALL notes at once, 256 bytes should be plenty.
assert(totalBytesInAllEvents < 256,
"The packet list was too long! Split your data into multiple lists.")
// Allocate space for a certain number of bytes
let byteBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: listSize)
// Use that space for our MIDIPacketList
self = byteBuffer.withMemoryRebound(to: MIDIPacketList.self, capacity: 1) { packetList -> MIDIPacketList in
var packet = MIDIPacketListInit(packetList)
midiEvents.forEach { event in
packet = MIDIPacketListAdd(packetList, listSize, packet, timestamp, event.count, event)
}
return packetList.pointee
}
byteBuffer.deallocate() // release the manually managed memory
}
}
// to send use this
var packets = MIDIPacketList(midiEvents: [[0x90, 60, 100]])
MIDISend(clientOutputPort, destination, &packetList)
i figure it out, how it should work, but not quite right.
according to apple documentation, it must use mach_absolute_time() .
so i used that but i don't know how to work properly with mach_absolute_time().if anyone knows please tell me too.
if i use var timestamp = mach_absolute_time()+1000000000 it will delay like 1 min or so.if i change the 1000000000 with any number lower than this ,will not make a delay.
do anyone know how to work with mach_absolute_time()?
i saw some code on mach_absolute_time() but they use that as timer, it is acutely a timer that give time from the fist boot ,but how to give a time as a mach_absolute_time() to work with midi timestamp.
I found the answer.
set the timestamp to :
var delay = 0.2
var timestamp: MIDITimeStamp = mach_absolute_time() + MIDITimeStamp(delay * 1_000_000_000)
the delay is the time you want the midi message age to have delay.
I write a test app with complications support
For some reason clock faces presenting only 1-2 backward events, but I can see in logs 10-15 events before current date.
And when I return an empty array for forward events all my backward events start showing in clock face.
Here is my function
func getTimelineEntriesForComplication(complication: CLKComplication, beforeDate date: NSDate, limit: Int, withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
var entries: [CLKComplicationTimelineEntry] = []
let events = self.events.filter { (event: CEEvent) -> Bool in
return date.compare(event.startDate) == .OrderedDescending
}
var lastDate = date.midnightDate
for event in events {
let entry = CLKComplicationTimelineEntry(date: lastDate, complicationTemplate: event.getComplicationTemplate(complication.family))
if let endDate = event.endDate {
lastDate = endDate
} else {
lastDate = event.startDate
}
entries.append(entry)
if entries.count >= limit {
break
}
}
handler(entries)
}
P.S. I know about 'limit' parameter and it's always greater than my array's count
P.P.S. Sorry about my English :)
I've seen that identical behavior for watchOS 2.0.1 where time travel backwards initially only shows two earlier entries, even though the datasource was asked for and returned 100 entries.
About 15 minutes after launch, more entries started appearing for backwards time travel. About 30 minutes after launch, all 100 prior entries were present.
This was not due to any update I scheduled, as my complication's update interval is 24 hours.
It appears that the complication server prioritizes adding the forward entries, but defers populating the cache with all the backward time travel entries. You'd have to ask Apple whether it's an optimization or a bug.
I don't know if this is a coincidence, but my timeline entries are spaced 15 minutes apart. Perhaps when the complication server updates the complication to show the new timeline entry, it also adds more of the batched earlier entries?
I have added to the Bridging file XMPPLastActivity.h file and in Swift file I did the next:
let abc = XMPPLastActivity()
let a = abc.sendLastActivityQueryToJID(user.jid)
print("A is: \(a)")
but it returns me response like
1686F740-C50C-477B-BAE2-02C897826B97
How can I return it in human readable format?
UPDATE
Please, join to chat to help me: https://chat.stackoverflow.com/rooms/90972/ios-errors-fixing
You will get a response via the XMPP delegate. You need to implement:
func xmppLastActivityDidReceiveResponse(sender:XMPPLastActivity, response:XMPPIQ)
And get the time lag to now in seconds with:
let time : NSUInteger = response.lastActivitySeconds
Then it's NSDate all the way.