Push Notifications With Go

I had never sent a push notification in my life. I wanted to try it out. Writing my server in Go, I used the sideshow/apns2 library.

Being a Go newb, I struggled with a couple things. I was unsure which things needed to be imported. Turns out to use the token method, and the payload builder, three imports are needed:

import (
    "github.com/sideshow/apns2"
    "github.com/sideshow/apns2/payload"
    "github.com/sideshow/apns2/token"
)

Then you create a token and client:

authKey, err := token.AuthKeyFromFile("AuthKey_KEYID.p8")
    if err != nil {
        log.Fatal("token error:", err)
    }

    token := &token.Token{
        AuthKey: authKey,
        KeyID:   "KEYID",
        TeamID:  "TEAMID",
    }

    client := apns2.NewTokenClient(token).Production() // or .Development()

I used the same key file that I use for WeatherKit. After getting the device tokens to send to, you can create a notification and send it:

...
notification := &apns2.Notification{}
notification.DeviceToken = deviceToken
notification.Topic = "com.your.bundle.id"
notification.Priority = apns2.PriorityHigh
notification.Payload = payload.NewPayload().Alert("I'm a friendly push notification!")
res, err := client.Push(notification)
...

You can check the result to make sure it was successful or not. I haven’t implemented that because I’m lazy and want to punish myself in the future for it.

With that, I was able to send some push notifications to my iOS app running the the simulator!

Using WeatherKit in Go

I haven’t had a chance to use WeatherKit yet, and I’ve barely used Go. So for some reason I decided to give both a go at the same time.

WeatherKit has a pretty straightforward REST API available. The trickiest part of it is making the authorization token.

Making the Jwt

I initially followed the steps in this blog about a Node.js implementation. You have to create a key and identifier in your developer account. After you download the key, write down some IDs, you’re good to go.

I used the golang-jwt/jwt library. Took me a bit to figure out how to add things to the header of the JWT, but I got there. Please don’t judge my Go code too harshly—I have barely used it and have no clue what I’m doing.

func signWeatherKitToken() (string, error) {
    privateKeyBytes, err := os.ReadFile("AuthKey_YOUR_KEYID.p8")
    if err != nil {
        log.Fatal(err)
        return "", err
    }
    privateKey, err := jwt.ParseECPrivateKeyFromPEM(privateKeyBytes)
    if err != nil {
        log.Fatal(err)
        return "", err
    }
    claims := &jwt.RegisteredClaims{
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
        IssuedAt:  jwt.NewNumericDate(time.Now()),
        Issuer:    "YOUR_TEAMID",
        Subject:   "com.your.bundle.id",
    }
    token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
    token.Header["kid"] = "YOUR_KEYID"
    token.Header["id"] = "YOUR_TEAMID.com.your.bundle.id"
    tokenString, err := token.SignedString(privateKey)
    if err != nil {
        log.Fatal(err)
        return "", err
    }
    return tokenString, nil
}

Using the Api

Once you have the token, it’s fairly easy to use. Add the token in the Authorization header of the request.

One thing that caught me up was latitude and longitude. I grabbed my coordinates from Apple Maps and plugged them in the url. After a lot of head scratching—because I accidentally was sending my GET request with a body—I got a result. But the result was odd. The forecast said it was currently -8°F. I’m in South Carolina. It is not that cold here. When I was working on it, it was 60°F. Turns out, I needed to put a minus sign in front of the longitude since I am West of the Prime Meridian1. I would’ve figured this out sooner if I read the docs more carefully.

You also need to make sure you add the dataSets query param. Otherwise you get an empty object back, which is not that useful.

All things considered, It wasn’t that bad of a process to get things working. Took me a bit to figure out I needed to use jwt.ParseECPrivateKeyFromPEM to turn the bytes of the key file into something useful. But now I can get some weather data!


  1. Maybe should not admit this, but I’m not sure I could’ve pulled Prime Meridian” out of my brain if you asked me what that line is called before all this.↩︎

Devlog: January 16, 2023

Today I implemented history. Building off of my recent selections work, I created a new little model and store and save each comparison that is run. The idea is that you can look back and use those to initiate a new comparison. This feature was pretty fun to make. With Boutique and reusing some SwiftUI views, it was fast to implement.

A screen recording showing navigation to a history view, displaying several recent comparisons, and tapping one, which populates both search items to initiate a new comparisonA screen recording showing navigation to a history view, displaying several recent comparisons, and tapping one, which populates both search items to initiate a new comparison

So far, I’m fairly happy with this flow. I added a new menu button to hold some other views, and made the reset button match the style. I like that a lot more than it was before. I might adjust the padding/alignment around the search buttons to make it feel more balanced.

I’m planning on adding deletion to history items (as well as a way to delete all.) I’m also considering if I should remove duplicate history items and only show the most recent one. Right now, I’m not showing the timestamp of the history item, but I do have that available.

With the help of the internet, I made a little extension to Sequence to allow sorting by the key path of an element. I find this to be incredibly useful and intuitive to use.

extension Sequence {
    func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>, _ comparator: (_ lhs: T, _ rhs: T) -> Bool) -> [Element] {
        return sorted {
            return comparator($0[keyPath: keyPath], $1[keyPath: keyPath])
        }
    }
}

I like this implementation because you can also pass in a comparator function like < or >, or something even more custom since it’s a closure.


Less interestingly—or maybe more interestingly depending on what you’re into—I’m trying to figure out the best way to keep track of tasks/ideas I have for this app. Right now I just keep a list in Tot. It works, but it’s not great. I might give TaskPaper another try—I was using it for a while as part of Setapp, but I got rid of my Setapp subscription. I’ve tried pretty much everything out there (including starting to make my own,) and I get caught up in the details of project planning.” So I want to keep things simple…ish. But I’m getting to the point in this project where it’s small polish details that I can’t always keep in my head. When it’s the broad, big features, my head is fine.

Projects ScreenCred devlog

Devlog: January 12, 2023

Two small features today.

Sorting the credits by number of roles. So you see the person with the most credits at the top and the least at the bottom. This might be customizable in the future. For fun, I implemented Comparable to make this happen. Never done that before! Between that and needing Equatable too, I’ve never seen so much lhs and rhs in my life.

static func == (lhs: CombinedCredit, rhs: CombinedCredit) -> Bool {
    return lhs.id == rhs.id && lhs.name == rhs.name && lhs.roles == rhs.roles
}

static func < (lhs: CombinedCredit, rhs: CombinedCredit) -> Bool {
    let lhsCount = lhs.roles.first.count + lhs.roles.second.count
    let rhsCount = rhs.roles.first.count + rhs.roles.second.count
    if lhsCount == rhsCount {
        return lhs.name > rhs.name
    }
    return lhsCount < rhsCount
}

Those of you with a keen eye will notice that the name comparison is reversed. This is so the names appear alphabetical (when compared against another person with the same number of credits) despite the number of credits decreasing. Otherwise, names would be reverse alphabetical.

No matches found. Up until now, if you searched 2 things and no one worked on both, you’d see a lovely blank screen. No longer! Now you see a single line of small, boring text.

A screenshot of a comparison of the movies “Violent Night” and “Puss in Boots” showing that no one worked on both of these movies.Really? No one??

The design is obviously not great. That’s mostly on purpose. I’m trying something new with the project. I’m trying very hard to get features in and focus on polish later.

The wording is not great and the typography leaves something to be desired1. But I’m getting close to my original set of features. Then I can start polishing it up. After I figure out a name, I might even release it on TestFlight!

Speaking of naming, another thing I can’t quite figure out is what to call Movies or TV Shows.” So far, media” is the best word. But for some reason, I hate that. A lot. So I’m trying to think. Maybe I just say movies or TV shows when I need to refer to both. I’m not sure.


  1. I also really dislike that clear button…↩︎

Projects ScreenCred devlog

Devlog: January 11, 2023

I wrote a unit test.

😱

I wanted to test my algorithm for storing recent selections—make sure it limits to the correct number, reorders duplicates, etc. I am by no means and expert in testing, especially in Xcode and Swift, so I won’t share my code right now—don’t want to lead anyone unintentionally down the wrong road.

So I’ve got lots to learn about best practices of testing iOS projects, writing Swift in a testable way, and things like that. I had to rewrite one of my classes to extract the actual algorithm because I did not know how to mock or setup the rest of the class to be testable.

That’s all I had time for today, but felt like an important step.

Projects ScreenCred devlog

Devlog: January 9, 2023

I was having trouble figuring out Boutique. I had used it in a test project and had no issues getting the store initialized. But this time, I kept getting an error preventing me from even building:

An error in Xcode that says “No exact matches in call to initializer”Ummm, I'm pretty sure you're wrong Xcode?

This took me far too long to figure out. Turns out, the model for a Store has to have a cache key that is a String. Mine were Int. If I had looked at the signature more carefully, I would’ve noticed this. But the docs said Identifiable was fine. But apparently that is only fine if the id is a String. Now I know. But seems like there could’ve been a more helpful error.

But after that, it was really simple to get recent searches working!

A screen recording showing how searches are persisted so you can see your recent searches when starting a new searchMagic!

It’s pretty simple. When a comparison kicks off, I save the two pieces of media involved. I limit the number of recent searches to 6. I also make sure the same piece of media is not added twice.

Probably could’ve implemented this a ton of ways, but I wanted to give Boutique more of a try, and I have some other uses for it in mind too.

Still need a real name for this thing. The working title is Bacon. Like, six degrees of Kevin Bacon…get it? I’m really clever.

Projects ScreenCred devlog

Picture of Sam Warnick

As my daughter says, I'm just a tired dad, with a tired name, Sam Warnick. I'm a software developer in Beaufort, SC.

Some things I do