I’ve started dabbling with Github Actions this week. Even though I’m a team of one — when it comes to code anyway — I know myself well enough to know I shouldn’t be trusted. I decided I should set up a server of some sort to build Peek‑a‑View when I commit new code to ensure I didn’t accidentally break anything.
Tangentially, this week I also split out some code that’s shared between
Vignette and Peek‑a‑View into its own library. This new common library
that Vignette and Peek‑a‑View will share lives as a private repository on
Github. Since it’s mostly extensions
and other small objects, I’m
working on getting decent unit test coverage on it. Since this
project lives on Github, like all my projects do, I thougth I’d use
a Github Action to build and test this shared project every
time I add code to it.
As it turns out, for Swift packages, this is extremely easy to do using Github actions. The default action works out of the box. To add an action:
- Go to your repo on Github on the web
- Click the
Actions
tab - Click the
New Workflow
button - Find the
Swift
workflow and clickSet up this workflow
- Customize it if required, and then click
Start commit
to commit this new.github/workflows/swift.yml
file.
Easy peasy, and now I will get an email if I ever break my own build.
Github Actions
The obvious next step was to try to get Peek‑a‑View building using Github actions. Even though there aren’t any unit tests there yet, it would still be nice to have independent verification that my builds are working.
Unfortunately, thanks to the use of a couple of private Github repositories, that’s far easier said than done. I found some really hacky ways of doing it — in short, using a Github Personal access token — but that would require me to expose what is effectively a password in my own repo. Yes, a private repo, but still; that path seems undesireable.
That got me to thinking: what if i didn’t rely on Github for this? Especially since running builds on Github’s servers is not free if you do too many of them. If only there was a way to do these builds locally, so they’re free, and I can take some more shortcuts with regard to authentication.
Xcode Bots
Introduced in 2013, Xcode added a new feature: Bots. Xcode Bots are basically ways of performing continuous integration locally, on a server of your own control. Thankfully, I have a Mac mini for exactly these sorts of reasons.
Like most Apple documentation these days, the documentation leaves a lot to be desired. As it turns out though, it’s not hard to install. On the machine you want to serve as your server:
- Open Xcode
- Open Xcode’s preferences
- Select the
Server & Bots
tab - Unlock using the 🔒 at the bottom-left
- Flip the switch in the upper-right and follow the prompts
Then, on the machine you use to develop:
- Open your project/workspace
Product
menu →Create Bot...
- Follow the prompts; you should find that your server is auto-discovered using Bonjour
This worked really well and quickly for Peek‑a‑View. So far so good.
Figuring it can’t hurt to have a little redundancy in my life, I decided to try to repeat the process for my shared library. And then I immediately hit a wall.
Bots and Swift Packages
The shared library was created as a SPM package using Xcode 11. Through some
sort of magic, when I open the folder the package is in using Xcode, it seems
to create a sort of anonymous project/workspace for me to use to build and test
the package. There is no xcodeproj
on the filesystem — at least, not
one that I’ve seen.
So I opened up this phantom project, and tried to add a Bot for it the same way that I did for Peek‑a‑View. When the Bot attempted to build it, I kept getting errors about how it couldn’t find a project or workspace.
After some fumbling about, it occurred to me that there isn’t a project nor workspace checked into Github, and the first thing the Bot does is pull down the source from Github. Annoying as it was, the error was correct: there wasn’t a project nor workspace. Unfortunately, the Bot isn’t capable of the same magic incantation Xcode is for Swift Packages; it needs a file on the filesystem to load.
Hm.
SPM packages don’t generally have projects/workspaces, so I wasn’t sure what
to do. Then I had an apostrophy epiphany.
On the command line, I could have SPM create a project for me. When I am in the root of my package:
swift package generate-xcodeproj
This drops a file on the file system. My shared library is called Macma
and
thus the above will drop Macma.xcodeproj
right where I’d expect it.
I then closed the copy of the phantom Macma
project, and opened the one
that I just created. Using this project — the one created by the
swift package
command — I created a Bot. That was an improvement,
but I wasn’t out of the woods yet.
The good news is that the Bot knew which project to look for, but the bad news is that it still isn’t there, because it’s not checked into Github. Now what?
Easy mode would be to just check in that new Magma.xcodeproj
file into Github,
but that felt redundant and wasteful. Perhaps there was another approach?
Bot Triggers
I quickly realized that I needed to have the Bot generate its own Macma.xcodeproj
every time it did a run (an Integration
in Bot parlance). That’s easy enough: I
just needed to add a trigger.
Back in Xcode, in the Macma.xcodeproj
project, I edited my bot. The final tab
in that dialog is Triggers
. There, I added a new Pre-Intergration Script
, which
I called Prepare Project
. The contents of that trigger are as follows:
#!/bin/sh
cd ./Macma
swift package generate-xcodeproj --enable-code-coverage
Now, every time the trigger is run, before the build/test process begins, Swift
Package Manager will re-create the xcodeproj
dynamically. By the time the
build/test starts, it’s there and waiting.
I certainly could check this into Github and make things easier, but I rather like having it dynamically created every time, ensuring the repo remains “pure”.
Now I have Xcode bots running for both Peek‑a‑View and Macma. If I want to, I could even set up an iPad with this snazzy Xcode Bots status page:

In a perfect world, I’d prefer to have a Github action for this, to further independently verify all is kosher, but I’m really pleased to have this new tool in my [local] arsenal.
Today I joined Zac Hall of 9to5Mac on his Watch Time podcast. On this episode, we discussed, well, a whole bunch, actually; primarily my use of my Apple Watch while I exercise. I’ve become a semi-regular casual runner, and I love running with just my Apple Watch and AirPods.
We also discuss a few other things, including, surprisingly, the right way to wash a car.
This was a really fun one to do; I love when I can have a meandering conversation like this and feel like it had a good flow to it.
As far as I’m concerned, it’s impossible to spend too much time talking about
Disney World. You may not agree, and you’re wrong that’s okay.
This week, I joined the Average Dis Nerd on his eponymously named podcast. (His podcast is kinda sorta The Talk Show, but with a Disney/Universal theme). On this episode, we discussed a variety of topics, including Disney+, technology at Disney parks, Disney’s iOS offerings for parkgoers, cameras at Disney World, and more.
I had a blast on this one; I hope you like it too.
Curiously, it all started with a trip to Disney World.
We went for my son’s fifth birthday. He hadn’t been to Disney since he was barely more than an infant. Our daughter, Mikaela, had never been.
Ever-so-independent, even just shy of two years old, Mikaela had strong
opinions about everything when she was willing to ride in the stroller.
After a few days at Disney, my wife and I discovered what would calm
her down: browsing the Photos app on our phones, looking at the pictures
we had taken on the trip so far.
This was utterly terrifying.
Photos isn’t really designed for this, and makes it possible — if not easy — to delete or edit photos. Given that these were the only copies of photos we had taken on the trip, I was scared that Mikaela would accidentally delete one or more of them.
Once we got home from our trip, I knew which app I wanted to write.
Peek-a-View

Peek-a-View is, at its core, a read-only photo browser.
It is designed to be safe to hand to anyone, and know that you’re not going to need to worry about the safety of your photos.
When paired with Guided Access — the Apple tool that lets you lock your phone into one app — it is the perfect Mikaela-safe photo browser.
Peek-a-View can be used for other reasons though. Perhaps you’re showing off a series of screenshots to a client. Perhaps you want to share your vacation pictures with a friend, but only your vacation pictures. Peek-a-View also lets you select a particular album to view, thus limiting inquisitive eyes to only the photos you know are safe.
Purchase
Peek-a-View, like Vignette, has a free tier, with a one-time $5 purchase to unlock more functionality.
Peek-a-View is limited to showing only the 20 most recent media items for free; the unlocked version has no such limit. Furthermore, if you do unlock Peek-a-View, you also get a series of fun alternative icons you can choose from.
![]() Business Casual |
![]() Eco Friendly |
![]() Flat and Friendly |
![]() High Contrast |
![]() Trendy |
Thanks
I owe a great debt of gratitude to my friends Jelly and Ste. Jelly (of GIFWrapped fame) gave me a ton of technical help, and some design pointers as well. Ste (more at his website) did the app icon, store screenshots, as well as providing many suggestions for Peek‑a‑View’s design.
I’d be honored if you gave Peek-a-View a try, and even more so, if you decided to throw a few bucks my way for making it.
This week I joined Jonathan Ruiz and Mark Fransen on their tech show, Everyday Robots. On this episode, we discussed my history as a developer, my journey into being an iOS developer, and finally my transition into being an indie.
Though Analog(ue) is probably the canonical podcast series about my history and journey of the last few years, this episode of Everyday Robots is a really terrific summary. If you want a ~75 minute answer to “who the hell is Casey?”, this episode is it.
On a recent ATP, I recapped my family’s recent trip to Disney World. It was brief, but it was fun.
Glenn & Chris of the Starport75 podcast reached out and asked if I would be interested in joining them on their Disney World podcast.
Um, yes.
On this wide-ranging episode, we discussed my history with Disney, the Disney Dining Plan, photo strategies, Galaxy’s Edge, DVC rentals, and the Disney Cruise Line.
It was a ton of fun to join Chris & Glenn; they were gracious hosts and let me blab more than I probably should have.
If you’re a Disney fan at all, you should check it out.
This week I joined Lisa Schmeiser, Dan Moren, and Mikah Sargent on Clockwise.
In this episode, we discussed our [probably questionable] security practices, services we yearn for, Twitter’s experiments with reply limitations, and apps we’ve recently fallen in love with.
Fast and fun without fail, Clockwise is always a blast.
One of my favorite holiday traditions — the Do By Friday holiday party — has happened again. As with last year’s, my fellow ATP co-hosts and I joined the Do By Friday hosts, including Kevin Budnick, to have a holiday party.
To discuss the goings-on of the episode is to take an express train to Spoiler City, so I’ll just say that this one was a fun one.
As with all programming posts, we start with a completely contrived example that makes no sense in the real world.
Say you’re writing ListOfIntegers
, which is, well, a list of integers. In the
real world, you’d absolutely use Array<Int>
or something similar. Just go with
me on this.
We can start this way:
class ListOfIntegers {
typealias Element = Int
private var backingStore = [Int]()
init() { }
init(_ value: [Int]) {
self.backingStore = value
}
}
At a minimum, you probably want your ListOfIntegers
to be a Sequence
, so
you can iterate over your list. Thus, you need to provide a makeIterator()
function. This provides the Iterator
that will allow Swift to perform that
iteration.
Creating an Iterator
seems like a whole lot of work, involving making a whole new
subtype. No thanks. I’m lazy; that’s why I’m a developer.
AnyIterator
This week I discovered a very neat shortcut: AnyIterator<T>
. When I first
saw this struct
, I thought it was simply there for the purposes of type erasure.
Looking through the class documentation, however, I found this gem:
/// Creates an iterator that wraps the given closure in its next() method.
init(_ body: @escaping () -> Element?)
Wait… what?
If you look at IteratorProtocol
, it’s rather simple:
public protocol IteratorProtocol {
/// The type of element traversed by the iterator.
associatedtype Element
/// Advances to the next element and returns it, or
/// `nil` if no next element exists.
mutating func next() -> Self.Element?
}
Suddenly AnyIterator<T>
's init(:)
's comment makes sense:
Creates an iterator that wraps the given closure in its
next()
method.
By providing a closure to the init(:)
, we can provide an implementation for
this Iterator
’s next()
method. Sweet!
Adding an Iterator
to ListOfIntegers
We can leverage this to easily add an Iterator
to our ListOfIntegers
:
class ListOfIntegers: Sequence {
typealias Element = Int
private var backingStore = [Int]()
init() { }
init(_ value: [Int]) {
self.backingStore = value
}
func makeIterator() -> AnyIterator<Int> {
// We establish the index *outside* the
// closure. More below.
var index = self.backingStore.startIndex
// Note the use of AnyIterator.init(:) with
// trailing closure syntax.
return AnyIterator { () -> Int? in
// Is the current index before the end?
if index < self.backingStore.endIndex {
// If so, get the current value
let currentValue = self.backingStore[index]
// Set a new index for the next execution
index = self.backingStore.index(after: index)
// Return the current value
return currentValue
} else {
// We've run off the end of the array, return nil.
return nil
}
}
}
}
A couple things to note here:
- We’re expressly returning
AnyIterator<Int>
instead of the defaultListOfIntegers.Iterator
. The latter would require us to have a secondtypealias
to specify the type of theIterator
; by being explicit, the compiler can inferListOfIntegers.Iterator
to beAnyIterator<Int>
. - When I first wrote this, I made the rookie mistake of creating
var index
within the closure. This meant that every time the iterator was asked to move to the next element, instead it just started over. 🤦🏻♂️ Thus, it’s important to create yourindex
outside the closure, so it doesn’t get reset every time you callnext()
. - Since we’re using an
Array<Int>
as our backing store, a much simpler implementation would be to simply
but that would defeat the purpose of this post.func makeIterator() -> Array<Int>.Iterator { return self.backingStore.makeIterator() }
I don’t often find myself in a situation wherein I need to create a custom
Sequence
; much less, a custom Iterator
for that Sequence
. However, when
I find myself needing a custom Iterator
in the future, I’ll certainly start
with AnyIterator<T>
.
If you’d like to play with this in a Playground, I’ve put the code up in a gist. Just copy/paste that into a new Playground.
It’s the holidays, so the ATP Store is back!
For this season, we have a new retelling of an old story. We’re
continuing to suck what we can out of returning to the //////ATP
logo,
but this time, doing so in six colors and two modes.
All the shirts are available in both men’s and women’s cuts, in tri-blend and 100% cotton.
First we have ATP’s take on the Six Colors theme:

Additionally, we have the same in Dark Mode:

We also have some older merchandise returning:
Everything except the pins is available for pre-order until Sunday, November 17. Don’t miss out! Every year we hear stories of people who procrastinated, and then missed out! Don’t be that person. Order now!