Friday, January 22, 2021

Can we make Cmd+Z undo Cmd+V (only)?

Hey developers! If you're fortunate enough to be doing greenfield development on a new text editing app or component, I have a quick feature request for you!

In all existing text editors and text fields that I'm aware of, if you do the following:

  1. Key in some text.
  2. Append another piece of text via a press of Cmd+V (Ctrl+V on Windows) to paste from the clipboard.
  3. Press Cmd+Z (Ctrl+Z on Windows) to undo.

What happens is that both the pasted text, and some or all of the keyed-in text, get removed.

What I'd like to have happen is for only the pasted text to be removed. The keyed-in text can be removed upon additional Cmd+Z press(es).

I bump into this problem quite frequently when I Cmd+V to append something to some text I'm in the middle of composing. I see that the wrong thing was on the clipboard and got added to the text, reflexively hit Cmd+Z to try and undo that mistake -- and now I have two problems, because some of the text that I had keyed in got removed along with the incorrect pasted-in value.

It would be super fun to have this no longer be an issue in the text editors of the future. Thanks in advance! 👍

Monday, January 18, 2021

Controversy corner: Wired earbuds vs Airpods

This is a "just for fun" post on the pros and cons of traditional wired earbuds vs Apple AirPods for everyday use. 

Hat tip to my brother Jeremy for inspiring me to finally put this post together. He was recently seen on Instagram rocking some sweet wired earbuds, even though he's retired, and has sufficient discretionary funds to buy himself AirPods, if he so chose!

Wired Earbuds

Apple AirPods

Price tag 💸

✅ From around US $10-15 💰

US $159+ 💰💰💰💰💰💰💰💰💰💰

Ease of switching between multiple devices (e.g. iPhone 📱 and MacBook 💻) 

✅ Plug them in

Fiddle with the Bluetooth settings menu

Troubleshooting pairing issues

✅ 100% reliable

Seldom, hopefully...?

Charging ⚡

✅ Never needs charging

Up to 5 hours listening per charge; requires charging case + Apple lightning cable or wireless charging mat ($)

Anxiety when they get lost 😰

✅ Shrug and buy a new pair


Works with Nintendo Switch, the treadmill TV at the gym, the entertainment systems on airplanes

✅ Plug them in

No; can work around with Bluetooth adapter ($)

Audio quality 🎶

✅ Great

✅ Great

Can simultaneously charge device 🔌 and listen

✅ Yes (on devices with a 3.5mm jack, like my iPhone 6S)

✅ Yes

Risk of inadvertently getting yanked or knocked out of ears 👂

Wires can get caught when doing chores

✅ Minimal 

Works with newer iPhones

Dongle needed 🙄

✅ Yes 🍎💰

Looks like AirPods win in a landslide! Let's all throw away our inexpensive, never-needs-charging, high-fidelity earbuds, and buy AirPods! 😜

For clarity, I am indeed perfectly aware that the ship has (for the most part) sailed on this debate. Apple, at least, doesn't seem likely to release a new phone ever again with 3.5mm headphone jack technology, when they could sell the folks buying that phone $160 AirPods instead. I stand by my entitlement to my (unpopular) opinion on this topic regardless. 😁

Friday, January 08, 2021

Configuring git to prevent commits of temporary/debug code

Developers, have you ever embarrassingly pushed a commit that inadvertently included temporary debug log/print statements, or other code that you intended to remove before committing? I have!

I wondered if it was possible to add some keyword to a code file -- for example, DONOTCOMMIT -- which, if present, would cause git to automatically reject the commit. This could be added as a comment alongside any temporary debug code, to make it impossible to forget to remove that code before committing.

It turns out that it is possible to do this -- and have it apply automatically to all of your local git repositories -- using a git pre-commit hook.

Quick instructions

(The specifics in these instructions apply to Unix-like filesystems, including Mac OS.)

1. If it doesn't already exist, create the directory: ~/.git_templates/hooks/

2. If it doesn't already exist, in that hooks directory, create a text file named: pre-commit (no extension).

3. Add the following content to the pre-commit file:


FOUND=$(git diff-index --cached -U0 HEAD -- | grep DONOTCOMMIT | wc -l)

if [[ $FOUND -gt 0 ]]; then
    echo "pre-commit hook: DONOTCOMMIT detected, commit not allowed"
    echo "(enforced from: ~/.git_templates/hooks/pre-commit)"
    exit 1

4. To apply that pre-commit hook to all git commit operations on your local machine, from the terminal, run:

git config --global core.hooksPath ~/.git_templates/hooks


Credit to for the pre-commit hook code.

Credit to for the core.hooksPath global git config command.

Saturday, November 07, 2020

New Free iOS App Now Available: Desktop Journey

 I've just released a new, free app for iPhone: Desktop Journey!

Desktop Journey is a single-page dashboard app for iPhone, with an attractive display of time, current and next calendar appointments, reminders, weather, and micro-break prompts. It gives your phone a purpose while it's in its charging cradle on your desk while you work.

With the exception of an optional weather add-in, Desktop Journey is completely free and ad-free.

Why did I make Desktop Journey?

For a while now, when I'm at work at my desk, I've had my iPhone sitting in a charging cradle on the desk.  I wanted to put the iPhone's screen to good use, so it wasn't just sitting there doing nothing.


This isn't adding value. We can do better!

I tried a few different alternatives to just a blank screen, such as a simple analog clock app, but I couldn't find any "dashboard" apps that took advantage of the phone's screen to display a variety of useful information. 

So, as with Vigil RPG, I decided to build it myself!

What does Desktop Journey do?

Here's an overview:

Annotated image of an iPhone 12 running the Desktop Journey app.

Read on for more details!

Current Time


The top portion of the screen is devoted to a simple analog clock. Before creating Desktop Journey, I was using a simple iOS analog clock app as a workaround, and I wanted to keep that core experience (while also adding more to it).

A little "desktop calendar page" icon, styled to look like the native iOS Calendar app icon, shows the current weekday, and day of the month.

"Now" and "Next"

These two panels, shown in the middle portion of the app, display calendar events, reminders, and -- with an optional subscription -- weather and temperature.

🗓 Calendar events are taken from the device's calendar (after permission has been granted to read the calendar). Thus, if you've already set up your iPhone's calendar to sync with another source, such as a Google or Microsoft Outlook account, events from that calendar will appear.

The "Now" panel shows the meeting or appointment that is happening currently, if there is one. The "Next" panel shows just the upcoming appointment (not all of the day's remaining appointments) -- plus the single next appointment after that, if there is one immediately following -- which makes for a nice at-a-glance answer to the question "What do I have coming up next?".

Tapping on an event opens up the view of the current day's events in the native iOS Calendar app.

🎗 Reminders likewise are sourced from the native iOS (again, only after you've granted permission).

The "Now" panel shows a reminder that was due earlier today that you haven't completed yet, if there is one. The "Next" panel shows today's next upcoming reminder that has a date and time set, if any.

Tapping a reminder opens it in the iOS Reminders app, so you can mark it as complete, or edit it.

Temperature and weather for the current locale, and today's upcoming high or low temperature, are optionally shown, with the purchase of a subscription. (These cost me money to subscribe to an API that provides worldwide weather information, and while I'm happy to provide Desktop Journey for free, and with no ads, I'd like to not lose money on it!)

Celsius and Fahrenheit are both supported. Which is displayed can be toggled on the Settings page.

The Hero's Journey

A hero character -- who may look familiar if you've played Vigil RPG! -- walks along at the bottom of the screen, on a long journey.

The biome changes each day, providing some visual variety. In other words, if you run the app again tomorrow, the hero will be somewhere else! There are several biomes, including taiga (pictured up above), desert (pictured here), grassland, caves, forest, dungeon, and more.

The sky also changes, based on the real time of day: Night (as shown here), sunrise, daytime, and dusk.

Encounters - Tasks and Exercises

Several times per day -- at semi-random intervals; typically a little bit less frequently than once per hour -- an enemy encounter will take place!

Each encounter prompts you with an action that you can perform yourself, "in real life," to defeat the enemy. Each of these are quick-to-perform actions that are good opportunities to take a very brief break from whatever task you're working on as the app is running. In this pictured example, you'd stand up from your chair and stretch; then, you'd tap the ✅ button.

There's no time pressure; encounters will last indefinitely (as long as the app is running), so you can delay if needed, and complete them at a convenient time. Alternatively, you can always just hit the Skip button to bypass the encounter instead.

There are two types of encounters: Tasks and exercises.

Tasks are like this one: A simple action that you can perform from your desk as a micro-break. Other tasks include "tidy your workspace" and "message a loved one".

Exercises can help you get moving a bit during your work day. An example is "Do 5 push-ups!". Exercise quantities can be adjusted with + and - buttons that appear during the encounter; so you could tell the app you did less or more than 5 push-ups, for example.

The encounters feature can be turned on or off in the Settings ⚙️ menu. Individual tasks and exercises can also be enabled or disabled, if (for example) your circumstances don't permit standing up, or if you don't feel like being prompted to do push-ups. 🙂

When you complete an encounter, you'll get a stats display of how many of that task or exercise you've completed -- both today, and all-time.


Settings can be accessed by tapping the Settings ⚙️ icon at the top of the app's page. In addition to customizing exercises, you can disable screen lock while the app is in the foreground; toggle temperature units between Fahrenheit and Celsius; and view the in-app "About" and "Credits" pages. 

Give it a try!

As mentioned above, Desktop Journey has no ads, and is a free download, so give it a try, and let me know if you like it! Download Desktop Journey on the App Store.

Friday, August 28, 2020

How to wire up a delegate method in Objective-C

This post covers how to wire up a delegate method in Objective-C. I'm always forgetting how to do this, so this post will serve as a reference for myself going forward, and hopefully may be useful to others as well!

A delegate method is useful when a given object needs to call a method on an object to which it does not have a reference. This typically comes up when the calling object is the child in a parent-child relationship in the application's object model. 

A practical example of when I've used this pattern is in Vigil RPG, after a combat has concluded, the combat screen ViewController notifies the parent ViewController (either the world map, or the dungeon level) that the combat has concluded, so that the parent can resume playing its background music.

In the parent class (the CALLEE): 

In the .h file:

Add the delegate as an interface in the class declaration:

@interface ParentClass: NSObject<myDelegate>

(There's no need to declare the callee method in the class's public interface.)


In the .m file:

1. Wire up the delegate

This can be done in the init method, or in any location where a reference to the child object is available which gets called only once:

self.myChild.myDelegate = self;

2. Define the callee method

- (void) myDelegateMethod

 // Implementation of the method that we want to call 

// from the child class goes here.

In the child class (the CALLER):

In the .h file:

1. Define the delegate protocol

(This can alternatively be done in a standalone .h file if the delegate is going to be used from more than a single callee class.)


This can go just before the @interface:

@protocol myDelegate<NSObject>
- (void) myDelegateMethod;

2. Declare the delegate property

(This goes inside the @interface, along with any other @property items)

@property id<myDelegate> myDelegate;

In the .m file:

Add calls to the delegate methods 


These go in whatever are the appropriate place(s) in the class method implementations: 

if (self.myDelegate != nil)
// Calls myDelegateMethod on the parent class
    [self.myDelegate myDelegateMethod];