-
-
Save robertmryan/98669fcb526c37e449551521e90f91d5 to your computer and use it in GitHub Desktop.
class ViewController: NSViewController { | |
let calendar = Calendar.autoupdatingCurrent | |
var previousIdentifier: String = Calendar.current.timeZone.identifier | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
// poll to see if the identifier changed | |
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in | |
guard let self else { | |
timer.invalidate() | |
return | |
} | |
let identifier = calendar.timeZone.identifier | |
if previousIdentifier != identifier { | |
print("Time zone changed from", previousIdentifier, "to", identifier) | |
previousIdentifier = identifier | |
} | |
} | |
// better, let the OS tell us when the time zone changes | |
NotificationCenter.default.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: .main) { [weak self] _ in | |
guard let self else { | |
return | |
} | |
print("Time zone changed to", calendar.timeZone.identifier) | |
} | |
} | |
} |
Note, spinning on the main thread will not work:
@main
struct MyApp {
static func main() {
print("Spin, watching time zone!")
let calendar = Calendar.autoupdatingCurrent
var previousIdentifier: String = Calendar.current.timeZone.identifier
// poll to see if the identifier changed
while !Task.isCancelled {
let identifier = calendar.timeZone.identifier
if previousIdentifier != identifier {
print("Time zone changed from", previousIdentifier, "to", identifier)
previousIdentifier = identifier
}
Thread.sleep(forTimeInterval: 1)
}
}
}
But if I use, for example, Swift concurrency, to avoid blocking the main thread, it does work:
@main
struct MyApp {
static func main() async {
print("Spin, watching time zone!")
let calendar = Calendar.autoupdatingCurrent
var previousIdentifier: String = Calendar.current.timeZone.identifier
// poll to see if the identifier changed
while !Task.isCancelled {
let identifier = calendar.timeZone.identifier
if previousIdentifier != identifier {
print("Time zone changed from", previousIdentifier, "to", identifier)
previousIdentifier = identifier
}
try? await Task.sleep(for: .seconds(1))
}
}
}
If you call dispatch_main()
that allows dispatch events to run, and calendars are updated as expected.
package main
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#include <Foundation/Foundation.h>
char *currentTimezone()
{
const char *tz = [[[[NSCalendar currentCalendar] timeZone] name] UTF8String];
if (tz != NULL) {
return strdup(tz);
}
return NULL;
}
*/
import "C"
import (
"errors"
"fmt"
"time"
"unsafe"
)
func main() {
go func() {
for {
time.Sleep(time.Second)
fmt.Println(Current())
}
}()
C.dispatch_main()
}
// Current returns the current system local timezone. If Current fails it
// returns time.UTC and a non-nil error.
func Current() (*time.Location, error) {
tz := C.currentTimezone()
if tz == nil {
return time.UTC, errors.New("could not get current timezone")
}
loc, err := time.LoadLocation(C.GoString(tz))
C.free(unsafe.Pointer(tz))
if err != nil {
return time.UTC, err
}
return loc, nil
}
Unfortunately this requires that dispatch_main
runs in the main thread. I can't see a way to make this work with the existing application's need to control it's own termination. I think maybe it can be done, but it will be ugly.
@kortschak – I’m not a Go programmer, but I would have thought that you could os.Exit(…)
.
E.g., this exits after 30 seconds:
package main
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#include <Foundation/Foundation.h>
const char *currentTimezone()
{
static NSCalendar *calendar;
// the static is set only once
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
calendar = [NSCalendar autoupdatingCurrentCalendar];
});
return calendar.timeZone.name.UTF8String;
}
*/
import "C"
import (
"fmt"
"time"
"os"
)
func main() {
// poll for location updates
go func() {
for {
time.Sleep(time.Second)
fmt.Println(CurrentTimeZone())
}
}()
// exit after 30 seconds
go func() {
time.Sleep(time.Second * 30)
os.Exit(0)
}()
// process GCD events
C.dispatch_main()
}
// Current returns the current system local timezone. If CurrentTimeZone fails it
// returns time.UTC and a non-nil error.
func CurrentTimeZone() (*time.Location, error) {
tz := C.GoString(C.currentTimezone())
loc, err := time.LoadLocation(tz)
if err != nil {
return time.UTC, err
}
return loc, nil
}
Also, FWIW, this illustrates the use of autoupdatingCurrentCalendar
with static
local variable. Again, I see no need to contort ourselves like this from Go, but just as a proof of concept that autoupdatingCurrentCalendar
does what one expects.
Yeah, that is how I would have to do it. The reason it's ugly is that it brings in this pattern which is not required on the other platforms that the application targets and complicates the logic for termination control.
I agree that you've shown that it does what's expected and thank you for digging into what is necessary to get it to work in this context.
When I changed my computer’s time zone from LA to NYC and back while the above was running, I saw the following on my console: