Skip to content

Instantly share code, notes, and snippets.

@robertmryan
Created May 24, 2024 18:02
Show Gist options
  • Save robertmryan/98669fcb526c37e449551521e90f91d5 to your computer and use it in GitHub Desktop.
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)
}
}
}
@robertmryan
Copy link
Author

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:

Time zone changed to America/New_York
Time zone changed from America/Los_Angeles to America/New_York
Time zone changed to America/Los_Angeles
Time zone changed from America/New_York to America/Los_Angeles

@robertmryan
Copy link
Author

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))
        }
    }
}

@robertmryan
Copy link
Author

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
}

@kortschak
Copy link

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.

@robertmryan
Copy link
Author

robertmryan commented May 25, 2024

@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.

@kortschak
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment