Scorebook is Back!

TL;DR: Scorebook has returned to the App Store!

Scorebook got removed from sale by Apple in January of last year. I spent most of the year sad about that, and late in the year I asked myself a big question: Had the time come to let it go?

Screw that noise. Let’s get to work.

I went about updating it to require iOS 12. There were new devices that it needed to support, safe areas to work with, and an entire web API to replace. I’ve done a lot of work calling web services over the last few years at my day jobs. But I’ve never built one before.

After a lot of head down time over the last month I’m so happy to report that Scorebook 1.5 was released this morning. I’ve got a lot of work I want to do on it including finishing up the CloudKit sync I built out, updating the UI to be less 2014 and more 2019, and figuring out a different pricing strategy (hello in-app purchase!).

The most important thing is that Scorebook is back. I hope you check it out, play some games with your family and friends, and make memories over your games.

Check out Scorebook from Taphouse Software


In the second grade my dad took me to my first Mariners game. It was in the Kingdome against the Blue Jays and I really had no interest in going. We got there after the game started and got to our seats to the right of the press box. At some point during the game a foul ball came up to us (hit by backup catcher Matt Sinatro). A guy near us caught it and handed me the ball. I was hooked.

That was my entry into sports. I loved the Mariners and couldn't get enough of them. Fast forward to today. Last season was a huge disappointment; blowing another lead to some historic A's team is old hat. Thankfully we are going on a plan to tear things down and rebuild in a way that might actually get us competitive in a few years (I'm all aboard this plan FWIW – it's the only way we're going to ever compete for a World Series).

But last week a story came out that is just gross. I feel gross in wanting to cheer them on to win it all.

For about a year, the Mariners employed Dr. Lorena Martin. She came to a new role with the team as a "high performance trainer". She was let go from the team last month and is now saying that GM Jerry Dipoto, Manager Scott Servais, and Farm Director Andy McKay demonstrated clear racist tendencies, especially against Latin players. I don't know if her claims are true or not; the Mariners have fervently denied all the claims.

A lot of the stories she recounts seem to be personal meetings without third parties to back them up. So we seem left in a he-said, she-said position with no real insight of what to think. If it's true, Dipoto, Servais, and McKay need to go. Full stop. I don't want them leading the Mariners, and I don't want to cheer for a team they lead. But if it's not true then Dr. Martin should say so immediately.

As a fan I don't know what to think. I just want the truth to come out. And to root on my Mariners. One day they will win the World Series and I want to feel good about cheering them on to it.

The Ultimate Guide to iOS Notification Lifecycles

I work on the notifications team at Lyft for my day job. One of the things that comes up frequently is the question of how can we trigger our code from a push notification (or even, “what is a push notification”). I’ve had these chats enough that it seemed relevant to put together what I hope to be the definitive answer. I hope it’s useful to you.

Here we go.

What is a user notification?

Let’s start small. A user notification is a way for app developers to let their user know that something in the app warrants their attention. Users can be notified in any combination of three different ways: an alert (most commonly a banner that drops down from the top of the screen), a sound, and/or a badge.

Apps must ask users for permission to show these notifications before anything can be displayed. Oftentimes apps will show their own custom view asking for permission before displaying the system dialog. This is because an app can only prompt their users once for display of notifications. After that, the user must go to the app’s preference screen in the system

How can user notifications be triggered?

Notifications can be scheduled as a local notification, or delivered via push. Both types of notifications can do the same things, appear exactly the same to the user, and as of iOS 10, Apple unified the notification system under the UNUserNotificationCenter suite of APIs. This makes handling of notifications the same.

What is a push notification?

A push notification is a notification that comes in remotely. It may or may not display any content (ones that do not display content are referred to as silent pushes). It is important to note that apps do not have to ask the user’s permission to deliver push notifications. Silent pushes do not require user permission.

You must register your app with the system in order to send a push. This is done with UIApplication.shared.registerForRemoteNotifications(). You’ll be called back with the result of this by implementing application(UIApplication, didRegisterForRemoteNotificationsWithDeviceToken: Data) for success, and application(UIApplication, didFailToRegisterForRemoteNotificationsWithError: Error) for failure. When the registration succeeds, you’ll get a device token that needs to be registered with your server that you’ll use to send a push.

When does my code run via push?

This is going to get complicated. The short answer is: it depends. But you didn’t come here for short answers, so let’s dig in. We’ll start by looking at an actual notification payload. I’ll put in the stuff we need, but you can find the full payload content reference here.

  "aps": {
	"alert": {
	  "title": "Our fancy notification",
	  "body": "It is for teaching purposes only."
	"badge": 13,
	"sound": "fancy-ding",
	"content-available": 1,
	"category": "CUSTOM_MESSAGE",
	"thread-id": "demo_chat",
	"mutable-content": 1

If you were to send this payload to a device, the user would see an alert with the title being the aps.alert.title value, the app icon would badge with the aps.badge value, and the sound would be the aps.sound value (assuming all the permissions were granted by the user).

You might want to send a notification that wakes up your app to fetch some content in the background, and a silent push is perfect for that task. This can be accomplished by setting the aps. content-available field like the example. When the system receives this notification it may wake up your app. Wait, may wake up? Why won’t it always wake up the app?

This brings us to the uncomfortable reality: the payload contents as well as the app state determine when your code is activated. Let’s look at a chart:

App State Payload Result
Backgrounded aps.content-available = 1 application(_:didReceiveRemoteNotification:fetchCompletionHandler:)
Running aps.content-available = 1 application(_:didReceiveRemoteNotification:fetchCompletionHandler:)
Killed aps.content-available = 1 Nothing
Running aps.content-available = 0 Nothing
Backgrounded aps.content-available = 0 Nothing

So, if your app is running or has been backgrounded, you’ll be called. But if the user has killed your app, nothing happens. Bummer! The next question you may ask is if there is a way for your code to run every time a notification comes in. Indeed there is.

Apple gives us two different extension points that let us run code when notifications arrive: UNNotificationServiceExtension and UNNotificationContentExtension.

Notification Service Extension

With the service extension, the intention is that you are going to mutate the content of the extension before it is presented to the user. The typical use cases are for things like decrypting an end-to-end encrypted message, or attaching a photo to a notification.

There are 2 main requirements that your notification must meet in order for this extension to fire: Your payload must contain both aps.alert.title and aps.alert.body values, and aps.mutable-content must be set to 1. The payload we have above satisfies both of those requirements and would trigger our app’s service extension if it had one.

It’s important to note that you cannot trigger a service extension with a silent push. The intention is for the extension to mutate a notification’s content, and silent pushes have no content.

Payload Result
aps.mutable-content = 1 && aps.title != nil && aps.body != nil Extension Fires
Anything else Nothing

Notification Content Extension

The other extension type allows us to paint a custom view on top of a notification, instead of relying on the system to render the notification. Users get to see this custom view when they force press or long press a notification (or in Notification Center, they can swipe left on a notification and tap “View”).

When you include one of these with your app, the extension’s Info.plist file declares an array called UNNotificationExtensionCategory. Each of these categories maps to a notification’s aps.category value. To go back to our sample notification above, our app would have to register a content extension for the CUSTOM_MESSAGE category.

Payload Result
Extension’s UNNotificationExtensionCategory contains aps.category Extension Fires
Anything else Nothing

One more thing

There’s one last bit that I want to cover: The UNUserNotificationCenterDelegate protocol. A conformer will get called in two main cases: when your app is in the foreground and will present a notification, and when a notification is tapped on.

When the user taps on one of your notifications, the app will fire up and come to the foreground. It’s a good idea to register the notification center delegate at startup so you’ll get the userNotificationCenter(_:didReceive:withCompletionHandler:) call. This lets you properly handle the notification, otherwise your app will just come to the foreground and that’s it.


We’ve come a long ways, looking over the kinds of notifications we can have (interactive and silent), the different transport mechanisms for them (local and push), and how all these ways can interact with our code.

Securing an nginx Container with letsencrypt

One of the things I wanted to automate with my new website was deployment, including fetching SSL certificates and installing them in nginx. I came into this project knowing nothing about shell scripts and Docker. Now I know a _little_about each. With the web going increasingly https I knew I had to jump this hurdle to be production ready.

Thankfully I found a blog post that helped me get started. All I had to do was make some adaptations to my setup. While that post is very much talking about his environment I wanted this post to generalize for others (be it a static site or one on a separate container like mine).

My sample project uses a Vapor web server, but you can sub out anything you wish for that. Here’s the sample on GitHub

We start with a Dockerfile that will build up our nginx container:

# build from the official Nginx image
FROM nginx

# install essential Linux packages
RUN apt-get update -qq && apt-get -y install apache2-utils curl

# sets the environment domain variable for use in other files
ARG build_domain
ENV DOMAIN $build_domain

# where we store everything SSL-related
ENV SSL_ROOT /var/www/ssl
# where Nginx looks for SSL files
# copy over the script that is run by the container
COPY ./nginx/ /tmp/
RUN ["chmod", "+x", "/tmp/"]

# establish where Nginx should look for files
ENV WEB_ROOT /home/hello-world/Public/

# Set our working directory inside the image

# copy over static assets
COPY Public /home/hello-world/Public

# copy our Nginx config template
COPY ./nginx/server.conf /tmp/docker_example.nginx

# substitute variable references in the Nginx config template for real values from the environment
# put the final config in its place
RUN envsubst '$DOMAIN:$WEB_ROOT:$SSL_ROOT:$SSL_CERT_HOME' < /tmp/docker_example.nginx > /etc/nginx/conf.d/default.conf

# Define the script we want run once the container boots
# Use the "exec" form of CMD so Nginx shuts down gracefully on SIGTERM (i.e. `docker stop`)
CMD [ "/tmp/" ]

This file had to be modified a bit from the referenced blog post above. The script he used was updated to a new name and support current letsencrypt standards. I also wanted to inject the domain in from the outside. So the build_domain argument comes in to this script and creates an environment variable out of it. Pretty handy!

The last line here calls a script which preps everything for the SSL procedure:

#!/usr/bin/env bash

# initialize the dehydrated environment
setup_letsencrypt() {

  # create the directory that will serve ACME challenges
  mkdir -p .well-known/acme-challenge
  chmod -R 755 .well-known

  # See
  echo "$DOMAIN www.$DOMAIN" > domains.txt

  # See
  echo "WELLKNOWN=\"$WEB_ROOT.well-known/acme-challenge\"" >> config

  # fetch stable version of dehydrated
  curl "" > dehydrated
  chmod 755 dehydrated

# creates self-signed SSL files
# these files are used in development and get production up and running so dehydrated can do its work
create_pems() {
  openssl req \
      -x509 \
      -nodes \
      -newkey rsa:1024 \
      -keyout privkey.pem \
      -out $SSL_CERT_HOME/fullchain.pem \
      -days 3650 \
      -sha256 \
      -config <(cat <<EOF
[ req ]
prompt = no
distinguished_name = subject
x509_extensions    = x509_ext
[ subject ]
commonName = $DOMAIN
[ x509_ext ]
subjectAltName = @alternate_names
[ alternate_names ]
DNS.1 = localhost
IP.1 =
  openssl dhparam -out dhparam.pem 2048
  chmod 600 *.pem

# if we have not already done so initialize Docker volume to hold SSL files
if [ ! -d "$SSL_CERT_HOME" ]; then
  mkdir -p $SSL_CERT_HOME # _HOME
  chmod 755 $SSL_ROOT
  chmod -R 700 $SSL_ROOT/certs
  cd $SSL_ROOT

# if we are configured to run SSL with a real certificate authority run dehydrated to retrieve/renew SSL certs
if [ "$CA_SSL" = "true" ]; then

  # Nginx must be running for challenges to proceed
  # run in daemon mode so our script can continue

  # accept the terms of letsencrypt/dehydrated
  ./dehydrated --register --accept-terms

  # retrieve/renew SSL certs
  ./dehydrated --cron

  # copy the fresh certs to where Nginx expects to find them
  cp $SSL_ROOT/certs/$DOMAIN/fullchain.pem $SSL_ROOT/certs/$DOMAIN/privkey.pem $SSL_CERT_HOME

  # pull Nginx out of daemon mode
  nginx -s stop

# start Nginx in foreground so Docker container doesn't exit
nginx -g "daemon off;"

The gist of what’s going on here is that we are setting up SSL. This script can be invoked in either a dev environment or a production one (that variable is configured in the docker-compose file below). This is handy for local development where a self-signed cert will do. For production it will go through the whole process of phoning the letsencrypt service and getting real certificates.

We then assemble our server config file that gets copied into the container. Here’s that template:

server {
	server_name $DOMAIN;
	listen 443 ssl;

	root $WEB_ROOT;

	# configure SSL
	ssl_certificate $SSL_CERT_HOME/fullchain.pem;
	ssl_certificate_key $SSL_CERT_HOME/privkey.pem;
    ssl_dhparam $SSL_CERT_HOME/dhparam.pem;

	try_files $uri @proxy;

	location @proxy {
		proxy_pass http://web:8080;
		proxy_pass_header Server;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_pass_header Server;
		proxy_connect_timeout 3s;
		proxy_read_timeout 10s;

server {
  # many clients will send unencrypted requests
  listen 80;

  # accept unencrypted ACME challenge requests
  location ^~ /.well-known/acme-challenge {
    alias $SSL_ROOT/.well-known/acme-challenge/;

  # force insecure requests through SSL
  location / {
    return 301 https://$host$request_uri;

I’m no nginx expert and largely this is copied from the other blog post. You’ll hopefully notice the environment variables throughout the file (like $DOMAIN). They get substituted out in our Dockerfile.

Another easily overlooked part of this file is the line proxy_pass http://web:8080. This host is used by Docker as a token. The web host will be setup as a service in our Docker compose file.

Speaking of the compose file, here it is:

version: "3.3"
    image: jsorge/hello-world
      - ./Public:/app/Public
      context: .
      dockerfile: ./nginx/Dockerfile-nginx
      CA_SSL: "true"
      - 80:80
      - 443:443
      - ./Public:/home/hello-world/Public
      - ./nginx/ssl:/var/www/ssl
      - ./nginx/logs:/var/log/nginx
      - web

The webservice sets up our web server, and then the nginx service configures our container by running the Dockerfile. We expose the ports needed by nginx and also install the volumes.

If you wanted to simplify and use a static site, you could pass the whole directory of your site as a volume and put it in the nginx server’s root directory. I’ve not done that myself, but it seems logical for static sites. In that case you wouldn’t need the web service at all, and in the server config file you’d probably use localhost  for the proxy pass value.

I hope this has been helpful, and taken some of the pain out of getting SSL up and running on your website. I’m happy to help with any questions you might have, via email or Twitter.

Nerd Snipe - 2 Apps, 1 Extension

I’m working on a project for Lyft that will bring in a new app extension to both our passenger and driver apps. The functionality between both apps is going to be identical so sharing code should be maximized. I am trying to figure out how to get it working to make just a single extension that can be used on both apps. This is more difficult than you might imagine, because extensions can be fiddly. They need to share the same version and build strings, and building in Xcode revealed this hidden gem:

Embedded binary's bundle identifier is not prefixed with the parent app's bundle identifier.

Ugh. This begged the question of how I could possibly get it to work. Off to chase a snipe. Let’s start with a sample project, shall we?

I’m no shell script wizard, but it seemed that a run script build phase should do the trick. It needs to run in the host app prior to anything else happening. This means it’s the second build phase (dependencies always need to be built first). Here’s what I came up with:

#! /bin/sh


if [ "$IS_EMPLOYEE" = YES ] ; then

plutil -replace CFBundleIdentifier -string "$bundleID" $INFO_PLIST
plutil -replace CFBundleShortVersionString -string "$shortVersion" $INFO_PLIST
plutil -replace CFBundleVersion -string "$version" $INFO_PLIST

I’ve extracted out the short version and build values as build settings in each main app target and those get funneled into the script. In this sample I’ve hard-coded each app’s bundle ID to prefix the extension, but at Lyft these are build settings as well. (Pro-tip: make good use of xcconfig files because editing this stuff in Xcode is a huge pain, not to mention the possible merge conflicts that could arise in your project file.)

I’ve got a build setting in one of my apps indicating what kind of app it is (in this case that is the IS_EMPLOYEE setting). Checking that will tell me the environment the script is running in. I setup 3 variables for each app and replace their values in the extension’s already built Info.plist file. This is important, because it’s too late to change the file in my source directory.

So now I’ve got an Info.plist file with all the proper values. Each app will build and run in the simulator. 🎉!

But we don’t write apps for the simulator. We write them for phones. Let’s give that a shot.

0x16b257000 +MICodeSigningVerifier _validateSignatureAndCopyInfoForURL:withOptions:error:: 147: Failed to verify code signature of /private/var/installd/Library/Caches/ : 0xe8008001 (An unknown error has occurred.)

Dang. Looking at the build steps for my extension reveals that there’s a code signature step at the very end and changing the Info.plist afterwards seems to break that signature. So how do we get this done?

Well, I don’t know. My best guess is that I might need to figure out how to re-sign the extension after manipulating its Info.plist. Is this possible? Reasonable to do?

What I’ll probably wind up doing is have individual extensions (one for each app) and a shared framework that backs each. I think it will be easier to get going and more resilient to changes in tooling in the future.

Unless you’ve got a way for me to get this done… 🙂