<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Verb Noun Enter</title>
    <link>https://verbnounenter.net/</link>
    <description></description>
    <pubDate>Mon, 20 Apr 2026 00:53:11 +0000</pubDate>
    <image>
      <url>https://i.snap.as/c9O1F1DH.png</url>
      <title>Verb Noun Enter</title>
      <link>https://verbnounenter.net/</link>
    </image>
    <item>
      <title>Simple yet powerful music browser: SongOwl</title>
      <link>https://verbnounenter.net/simple-yet-powerful-music-browser-songowl?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[  Video&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<blockquote><p><a href="https://www.youtube.com/watch?v=vWJG1wAE0kM">Video</a></p></blockquote>

<p><img src="https://i.snap.as/xsGZdUSD.png" alt=""/></p>
]]></content:encoded>
      <guid>https://verbnounenter.net/simple-yet-powerful-music-browser-songowl</guid>
      <pubDate>Sun, 09 Mar 2025 06:57:58 +0000</pubDate>
    </item>
    <item>
      <title>SongPocket 4</title>
      <link>https://verbnounenter.net/songpocket-4?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[SongPocket is an immersive player for your Apple Music library.&#xA;&#xA;Gallery view showcases one album’s artwork at a time.&#xA;Manual reordering lets you put your favorites on top.&#xA;&#xA;Kinda like a crate of records!&#xA;&#xA;---&#xA;&#xA;I started SongPocket in 2020.&#xA;&#xA;Five years changes a person. Past Me would hate some aspects of modern SongPocket. But I still want giant artwork with no distractions.&#xA;&#xA;Now, bittersweet news: future updates are paused, because I’m becoming a pirate.&#xA;&#xA;I’ll still be working on my apps, just not publicly. SongPocket remains available and open-source. (Likewise with Font Booklet.)&#xA;&#xA;And you’ll still see ideas from me, just not here.&#xA;&#xA;I fell in love with computers when I saw iMovie in school, because I learned that technology can make people creative in ways they otherwise never could be.&#xA;&#xA;Ever since, I’ve tried to pay forward that experience. Here’s to magic carpets for the mind.]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://apps.apple.com/us/app/songpocket/id1538037231">SongPocket</a> is an immersive player for your Apple Music library.</p>

<p><img src="https://i.snap.as/2E49d02z.heic" alt=""/></p>
<ul><li><strong>Gallery view</strong> showcases one album’s artwork at a time.</li>
<li><strong>Manual reordering</strong> lets you put your favorites on top.</li></ul>

<p><img src="https://i.snap.as/7erNMdiA.png" alt=""/></p>

<p>Kinda like a crate of records!</p>

<hr/>

<p>I started SongPocket in 2020.</p>

<p>Five years <a href="https://www.cgpgrey.com/blog/i-have-died-many-times">changes a person</a>. Past Me would hate some aspects of modern SongPocket. But I still want giant artwork with no distractions.</p>

<p>Now, bittersweet news: future updates are paused, because I’m <a href="https://www.folklore.org/The_Macintosh_Spirit.html">becoming a pirate</a>.</p>

<p>I’ll still be working on my apps, just not publicly. SongPocket remains <a href="https://apps.apple.com/us/app/songpocket/id1538037231">available</a> and <a href="https://github.com/LoudSoundDreams/SongPocket">open-source</a>. (Likewise with <a href="https://apps.apple.com/us/app/font-booklet/id6451394358">Font Booklet</a>.)</p>

<p>And you’ll still see ideas from me, just not here.</p>

<p>I fell in love with computers when I saw iMovie in school, because I learned that technology can make people creative in ways they otherwise never could be.</p>

<p>Ever since, I’ve tried to pay forward that experience. Here’s to <a href="https://verbnounenter.net/magic-carpet">magic carpets for the mind</a>.</p>
]]></content:encoded>
      <guid>https://verbnounenter.net/songpocket-4</guid>
      <pubDate>Fri, 07 Mar 2025 17:00:00 +0000</pubDate>
    </item>
    <item>
      <title>SongPocket: replacing the engine</title>
      <link>https://verbnounenter.net/songpocket-migration?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[I’ve always wanted to do this: I rewrote almost all of SongPocket’s internals, but because nothing’s new for users, I only bumped it to v3.5.2. 🎩&#xA;&#xA;Specifically, I rewrote how it positions albums and songs, and saves those positions to storage.&#xA;&#xA;The goal&#xA;&#xA;SongPocket lets you reorder albums and songs, so it stores the position and Apple Music ID for each.&#xA;&#xA;To do that, it used to use Core Data, which provides both runtime structures and persistence. Those are separate under the hood; you use different code to handle RAM and storage, because RAM is fast but temporary, while storage is slow but persistent.&#xA;&#xA;So why replace Core Data? I’m preparing to someday switch from Media Player IDs to MusicKit IDs, which’ll require changing the persistence system anyway. I could migrate to a new Core Data model or to SwiftData, but I chose … 🥁&#xA;!--more--&#xA;A PLAIN-ASS TEXT FILE&#xA;&#xA;Because control. I’ve learned that controlling my software is critical for my happiness. Testing how APIs behave sucks; deciding how to solve problems is fun.&#xA;&#xA;There’s a limit—I haven’t written an OS. But user data is priceless, and it makes me sick to risk destroying it by holding the tool wrong. This documentation is horrifying. Yes, Core Data should exist, but think carefully before locking your jewels in an opaque box.&#xA;&#xA;I can show you. Here’s SongPocket’s Core Data storage:&#xA;&#xA;How confident am I that this contains exactly my data and no more, and that I can control it perfectly for SongPocket’s entire future? Is -10 out of 5 allowed? 😨&#xA;&#xA;Now here’s SongPocket’s plain-text storage:&#xA;&#xA;Lines with only numbers are album IDs, and lines with an `` are song IDs within those albums. I just explained the entire format in one sentence. It’s dumb in the best way: understandable and debuggable.&#xA;&#xA;So how did I make that work?&#xA;&#xA;The rewrite&#xA;&#xA;For persistence, I wrote some code called Disk to write and read the file. It was my first time, but it wasn’t rocket science. Bam.&#xA;&#xA;But the runtime structures. Every part of SongPocket that shows and edits albums and songs was accustomed to Core Data. So I had to rearchitect all that.&#xA;&#xA;Or rather, I got to. The result is so straightforward because it suits my exact needs, and I love it.&#xA;&#xA;For example, the old way to add a song:&#xA;&#xA;if let existingsongs = album.contents as? [Song] {&#xA;  existingsongs.forEach { song in&#xA;    song.index += 1&#xA;  }&#xA;}&#xA;let song = Song(context: context)&#xA;song.id = mpMediaItem.persistentID&#xA;song.container = album&#xA;song.index = 0&#xA;&#xA;Versus the new way:&#xA;&#xA;let id = mpMediaItem.persistentID&#xA;album.songIDs.insert(id, at: 0)&#xA;Librarian.registersongID(id, with: album)&#xA;&#xA;That Librarian thing is a chunk of RAM that lets any part of the app quickly look up album and song info, like which album contains a given song.&#xA;&#xA;There are multiple ways I could’ve enabled that, and it wasn’t obvious at first which was best. I’m actually still unsure what was the best way using Core Data, because I don’t know its internals.&#xA;&#xA;As a guideline, I made my caller code as straightforward as possible.&#xA;&#xA;For example, SongPocket adds songs in 3 scenarios:&#xA;&#xA;Loading from storage&#xA;Merging from Apple Music&#xA;Migrating from Core Data&#xA;&#xA;In all 3, I need to add a song ID at some position in an album. But when migrating, I actually don’t want the rest of the app to be able to look up that song yet.&#xA;&#xA;let id = oldsong.persistentID&#xA;newalbum.songIDs.append(id)&#xA;// Don’t need:&#xA;// Librarian.registersongID(id, with: newalbum)&#xA;&#xA;So I didn’t give Librarian a procedure like insertandregistersongID that I had to use everywhere. Instead, when migrating, I insert the song and just don’t register it. But I do register it in the other 2 cases.&#xA;&#xA;I reckon if I forgot about this code for a year, this would be the easiest thing to rediscover.&#xA;&#xA;So I rewrote all editing and merging code to use Librarian’s runtime structures instead of Core Data’s. Bam.&#xA;&#xA;At this point, if SongPocket were a new app, I would’ve been done. But it has existing data, so I had to migrate that.&#xA;&#xA;The migration&#xA;&#xA;Permanence is scary. Once you save data out there, you want to support it forever. Meanwhile, lousy apps waste storage until you delete and reinstall them, because they don’t clean up after themselves. I wanted to do it right.&#xA;&#xA;I realized that a migration’s goal is to sneak in modern save data as if the old system never existed. None of the code that runs after migration should even know it happened. After SongPocket v3.5.2 starts up, it always has an albums file and an empty Core Data database.&#xA;&#xA;But also, let’s accommodate downgrading_ from v3.5.2 somehow, then re-upgrading. That’s useful mostly for development, but also just in case. What should happen? Let’s try to not break spacetime:&#xA;&#xA;SongPocket v3.5.1 and earlier ignore any albums file and operate with Core Data. They don’t know any better.&#xA;That’s the only scenario where both an albums file and Core Data data exist, and in that case, Core Data is the truth.&#xA;So SongPocket v3.5.2 always checks for Core Data data on startup, and if it finds any, it uses that to replace any albums file.&#xA;Then it destroys the Core Data database.&#xA;&#xA;So it always saves your newest data in the newest form, and leaves nothing behind.&#xA;&#xA;The lesson&#xA;&#xA;Did I need to do all that? No.&#xA;&#xA;But was it the right move? I think hell yes.&#xA;&#xA;A day job probably would’ve made me migrate to another persistence system, not my own. “Don’t reinvent the wheel!”&#xA;&#xA;But listen: everything I learned here is reusable knowledge. You don’t get that by only using other people’s systems. The next time I migrate, I know how to do it and why, regardless what exactly I’m moving from and to.&#xA;&#xA;Just helping prevent the collapse of civilization.&#xA;&#xA;Now I wanna work on the UI. In the meantime, enjoy SongPocket v3.5.2: it has an all-new engine, and zero new features\.&#xA;&#xA;SongPocket - App Store&#xA;&#xA;(Reflecting changes to your Apple Music library is now 1.9× as fast, and first reading it is 51× as fast. Tested with 12,000 songs.)]]&gt;</description>
      <content:encoded><![CDATA[<p>I’ve always wanted to do this: I rewrote almost all of <a href="https://apps.apple.com/us/app/songpocket/id1538037231">SongPocket</a>’s internals, but because nothing’s new for users, I only bumped it to v3.5.2. 🎩</p>

<p><img src="https://i.snap.as/OTaTWlhR.png" alt=""/></p>

<p>Specifically, I rewrote how it <strong>positions</strong> albums and songs, and <strong>saves</strong> those positions to storage.</p>

<h1 id="the-goal" id="the-goal">The goal</h1>

<p>SongPocket lets you reorder albums and songs, so it stores the position and Apple Music ID for each.</p>

<p>To do that, it used to use Core Data, which provides both <strong>runtime structures</strong> and <strong>persistence</strong>. Those are separate under the hood; you use different code to handle RAM and storage, because RAM is fast but temporary, while storage is slow but persistent.</p>

<p><img src="https://i.snap.as/LR4wWVR2.jpg" alt=""/></p>

<p>So why replace Core Data? I’m preparing to someday switch from Media Player IDs to MusicKit IDs, which’ll require changing the persistence system anyway. I could migrate to a new Core Data model or to SwiftData, but I chose … 🥁

A PLAIN-ASS TEXT FILE</p>

<p>Because control. I’ve learned that controlling my software is critical for my happiness. Testing how APIs behave sucks; deciding how to solve problems is fun.</p>

<p>There’s a limit—I haven’t written an OS. But user data is priceless, and it makes me sick to risk destroying it by holding the tool wrong. <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/vmLightweightMigration.html">This documentation</a> is horrifying. Yes, Core Data should exist, but think carefully before locking your jewels in an opaque box.</p>

<p>I can show you. Here’s SongPocket’s Core Data storage:</p>

<p><img src="https://i.snap.as/QCR1qGYh.png" alt=""/></p>

<p>How confident am I that this contains exactly my data and no more, and that I can control it perfectly for SongPocket’s entire future? Is -10 out of 5 allowed? 😨</p>

<p>Now here’s SongPocket’s plain-text storage:</p>

<p><img src="https://i.snap.as/O3SmG2Uh.png" alt=""/></p>

<p>Lines with only numbers are album IDs, and lines with an <code>_</code> are song IDs within those albums. I just explained the entire format in one sentence. It’s dumb in the best way: understandable and debuggable.</p>

<p>So how did I make that work?</p>

<h1 id="the-rewrite" id="the-rewrite">The rewrite</h1>

<p>For <strong>persistence</strong>, I wrote <a href="https://github.com/LoudSoundDreams/SongPocket">some code</a> called <code>Disk</code> to write and read the file. It was my first time, but it wasn’t rocket science. Bam.</p>

<p>But the <strong>runtime structures</strong>. Every part of SongPocket that shows and edits albums and songs was accustomed to Core Data. So I had to rearchitect all that.</p>

<p>Or rather, I <em>got</em> to. The result is so straightforward because it suits my exact needs, and I love it.</p>

<p>For example, the old way to add a song:</p>

<pre><code>if let existing_songs = album.contents as? [Song] {
  existing_songs.forEach { song in
    song.index += 1
  }
}
let song = Song(context: context)
song.id = mpMediaItem.persistentID
song.container = album
song.index = 0
</code></pre>

<p>Versus the new way:</p>

<pre><code>let id = mpMediaItem.persistentID
album.songIDs.insert(id, at: 0)
Librarian.register_songID(id, with: album)
</code></pre>

<p>That <code>Librarian</code> thing is a chunk of RAM that lets any part of the app quickly look up album and song info, like which album contains a given song.</p>

<p>There are multiple ways I could’ve enabled that, and it wasn’t obvious at first which was best. I’m actually still unsure what was the best way using Core Data, because I don’t know its internals.</p>

<p>As a guideline, I made my <em>caller code</em> as straightforward as possible.</p>

<p>For example, SongPocket adds songs in 3 scenarios:</p>
<ol><li>Loading from storage</li>
<li>Merging from Apple Music</li>
<li>Migrating from Core Data</li></ol>

<p>In all 3, I need to add a song ID at some position in an album. But when migrating, I actually don’t want the rest of the app to be able to look up that song yet.</p>

<pre><code>let id = old_song.persistentID
new_album.songIDs.append(id)
// Don’t need:
// Librarian.register_songID(id, with: new_album)
</code></pre>

<p>So I didn’t give <code>Librarian</code> a procedure like <code>insert_and_register_songID</code> that I had to use everywhere. Instead, when migrating, I insert the song and just don’t register it. But I do register it in the other 2 cases.</p>

<p>I reckon if I forgot about this code for a year, this would be the easiest thing to rediscover.</p>

<p>So I rewrote all editing and merging code to use <code>Librarian</code>’s runtime structures instead of Core Data’s. Bam.</p>

<p>At this point, if SongPocket were a new app, I would’ve been done. But it has existing data, so I had to migrate that.</p>

<h1 id="the-migration" id="the-migration">The migration</h1>

<p>Permanence is scary. Once you save data out there, you want to support it forever. Meanwhile, lousy apps waste storage until you delete and reinstall them, because they don’t clean up after themselves. I wanted to do it right.</p>

<p>I realized that a migration’s goal is to <strong>sneak in modern save data</strong> as if the old system never existed. None of the code that runs after migration should even know it happened. After SongPocket v3.5.2 starts up, it always has an <code>albums</code> file and an empty Core Data database.</p>

<p>But also, let’s accommodate <em>downgrading</em> from v3.5.2 somehow, then re-upgrading. That’s useful mostly for development, but also just in case. What should happen? Let’s try to not break spacetime:</p>
<ol><li>SongPocket v3.5.1 and earlier ignore any <code>albums</code> file and operate with Core Data. They don’t know any better.</li>
<li>That’s the only scenario where both an <code>albums</code> file and Core Data data exist, and in that case, Core Data is the truth.</li>
<li>So SongPocket v3.5.2 always checks for Core Data data on startup, and if it finds any, it uses that to replace any <code>albums</code> file.</li>
<li>Then it destroys the Core Data database.</li></ol>

<p>So it always saves your newest data in the newest form, and leaves nothing behind.</p>

<p><img src="https://i.snap.as/I4cBAi7F.png" alt=""/></p>

<h1 id="the-lesson" id="the-lesson">The lesson</h1>

<p>Did I need to do all that? No.</p>

<p>But was it the right move? I think hell yes.</p>

<p>A day job probably would’ve made me migrate to another persistence system, not my own. “Don’t reinvent the wheel!”</p>

<p>But listen: everything I learned here is <strong>reusable knowledge</strong>. You don’t get that by only using other people’s systems. The next time I migrate, I know how to do it and why, regardless what exactly I’m moving from and to.</p>

<p>Just helping <a href="https://www.youtube.com/watch?v=ZSRHeXYDLko&amp;t=2759s">prevent the collapse of civilization</a>.</p>

<p>Now I wanna work on the UI. In the meantime, enjoy SongPocket v3.5.2: it has an all-new engine, and zero new features*.</p>

<p><a href="https://apps.apple.com/us/app/songpocket/id1538037231">SongPocket – App Store</a></p>

<p><img src="https://i.snap.as/EuXAaaej.png" alt=""/></p>

<p>(*Reflecting changes to your Apple Music library is now 1.9× as fast, and first reading it is 51× as fast. Tested with 12,000 songs.)</p>
]]></content:encoded>
      <guid>https://verbnounenter.net/songpocket-migration</guid>
      <pubDate>Tue, 07 Jan 2025 20:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Granny Yuai: video swipy bar</title>
      <link>https://verbnounenter.net/granny-yuai-scrubber?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Today, a post written by my granny. Enjoy.&#xA;&#xA;---&#xA;&#xA;dear internet&#xA;&#xA;i am granny yuai and i am 84 today as far as u know.&#xA;&#xA;i got the ios 18 on my iphone and the video swipy bar in the photos app is no good.&#xA;&#xA;on the ios 17 it had a long swipy bar for long videos but now it’s always the same size.&#xA;&#xA;so now when i watch a 4-hour video of my great great great grandkids i touch it and it goes like 5 minutes not 5 seconds.&#xA;&#xA;i wish they would keep the good things.&#xA;&#xA;love&#xA;&#xA;granny yuai]]&gt;</description>
      <content:encoded><![CDATA[<p>Today, a post written by my granny. Enjoy.</p>

<hr/>

<p>dear internet</p>

<p>i am granny yuai and i am 84 today as far as u know.</p>

<p>i got the ios 18 on my iphone and the video swipy bar in the photos app is no good.</p>

<p><img src="https://i.snap.as/ffM3XGJJ.png" alt=""/></p>

<p>on the ios 17 it had a long swipy bar for long videos but now it’s always the same size.</p>

<p>so now when i watch a 4-hour video of my great great great grandkids i touch it and it goes like 5 minutes not 5 seconds.</p>

<p>i wish they would keep the good things.</p>

<p>love</p>

<p>granny yuai</p>
]]></content:encoded>
      <guid>https://verbnounenter.net/granny-yuai-scrubber</guid>
      <pubDate>Wed, 02 Oct 2024 16:41:00 +0000</pubDate>
    </item>
    <item>
      <title>Touch-friendly range selection UI</title>
      <link>https://verbnounenter.net/range-selection?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[How can you let people easily select multiple items on a touchscreen?&#xA;&#xA;It’d be nice to not need to tap or swipe over each item, and remember, no Shift or Command keys.&#xA;&#xA;I can’t believe we still don’t have a convention for this, seventeen years after the iPhone.&#xA;!--more--&#xA;Band selection is inadequate&#xA;&#xA;Yes, iOS lets you swipe to quickly select in lists and grids, but you still have to smear your finger over each item.&#xA;&#xA;That’s like dragging a box on a computer. It’s better than nothing, but you don’t want to select 1000 items that way.&#xA;&#xA;Let’s call that band selection. I want range selection: “1, 1000, and everything in between”.&#xA;&#xA;And it must be discoverable.&#xA;&#xA;My design&#xA;&#xA;Here’s what I came up with:&#xA;&#xA;If an item is unselected, offer “Select Range Above” and “Select Range Below”. If it’s selected, offer “Deselect Range Above” and “Deselect Range Below”.&#xA;“Select Range Above” selects that item and continues selecting upward, until it reaches the top or an already-selected item.&#xA;Likewise for “Below” except downward.&#xA;Likewise for “Deselect” except deselecting.&#xA;Disable “Above” if there’s no item above, or it has the opposite state from this item. This is important; if “Above” works but doesn’t do anything meaningful, you sit there confused wondering what it’s for.&#xA;&#xA;It’s simple yet powerful.&#xA;&#xA;Please copy it! If you’re a product manager looking for a UI to use in your app, you can stop reading now. My source code is below.&#xA;&#xA;But for enthusiasts, here’s my thinking.&#xA;&#xA;Failed approach&#xA;&#xA;I tried imitating Shift-click, which selects the range from your most-recently selected item.&#xA;&#xA;Hence “Select Range From Previous”, which points up if you last selected an item above, and down otherwise.&#xA;&#xA;So if you select A then G, then try a range from C, the range should point down.&#xA;&#xA;But it’s not so simple. What if you then deselect G? The range from C should point up again, right?&#xA;&#xA;Apparently this design needs to remember the order in which you selected each item, which is a lot of hidden state.&#xA;&#xA;And deselection is confusing. What if you select A–G, then want to deselect A–C? The range from C depends on how you selected A–G.&#xA;&#xA;At this point, I realized I don’t fully understand Shift- and Command-click, and I reckon you don’t either. Quiz time! What happens if you do this?&#xA;&#xA;Click A.&#xA;Shift-click G.&#xA;Command-click C.&#xA;&#xA;Good so far? Now Shift-click A.&#xA;&#xA;Did you expect that? I didn’t.&#xA;&#xA;Elegant approach&#xA;&#xA;My touch-friendly design avoids all these problems.&#xA;&#xA;There’s no hidden state or order effects; you can always see what’ll happen, without ever considering what you did earlier. The resulting clarity is incredible.&#xA;&#xA;Imagine using this UI to select hundreds of images among thousands, or key moments in a long video.&#xA;&#xA;With Shift- and Command-click, you’d give up, because you’re always one misclick away from messing up a range or deselecting everything; it’s a breath-holding operation. So those UIs sometimes give you helper tools like marking, filtering, and splitting.&#xA;&#xA;But with touch-friendly ranges, I could spend hours selecting clips in a movie, and never worry about losing my place.&#xA;&#xA;A shortcoming&#xA;&#xA;One problem is that most of my testers initially thought ranges always reached the top or bottom of the list. They were surprised when they first saw a range stop at the end of a block.&#xA;&#xA;So some people will stick with subpar usage: if they selected T–Z and want to add L–P, they’ll think they need to …&#xA;&#xA;On L, “Select Range Below”, temporarily selecting Q–S.&#xA;On Q, “Deselect Range Below”, losing the original selection.&#xA;On T, “Select Range Below”, restoring the original selection.&#xA;&#xA;Hopefully you realize it’d be better to …&#xA;&#xA;Select L.&#xA;On P, “Select Range Above”.&#xA;&#xA;(… or select P, then on L, “Select Range Below”.)&#xA;&#xA;“Well, if people think the range always reaches the end, why not just make it do that?” That subpar usage is why.&#xA;&#xA;“Select to End” is only a little simpler, yet an order of magnitude less powerful. Intuitiveness is important, but not the top priority.&#xA;&#xA;Maybe a simple wording change like “region” would communicate ranges better, but I haven’t had luck. If anyone can invent a better design, I’d love to know.&#xA;&#xA;Regardless, even with subpar usage, touch-friendly ranges are still more useful than the statuses quo, which are band selection or Shift- and Command-click.&#xA;&#xA;Please copy my design&#xA;&#xA;I once whinged to a senior programmer with good taste that to delete ten songs on my iPhone, I had to swipe ten times. I thought that was stupid when I was a kid using an iPod Touch in 2009. Yet I assumed we’d surely figure out range selection on touchscreens within a few years. But here we are with iOS 18.&#xA;&#xA;“So make something better, and share it with the world.”&#xA;&#xA;There.&#xA;&#xA;Please copy and improve my design if you think it’s a good idea. Any UI that lets you select multiple items should let you select ranges! In fact, this touch-friendly design also works on computers. In five years, when I want to move multiple emails, contacts, or bookmarks, I don’t want to have to tap or swipe over each one.&#xA;&#xA;Just like you deserve games that inspire curiosity, you deserve computers that help you think great ideas. A simple, powerful way to handle multiple items is an important part of a bicycle for the mind.&#xA;&#xA;---&#xA;&#xA;struct RangeList: View {&#xA;  var body: some View {&#xA;    NavigationStack {&#xA;      List(0 ..&lt; Self.rows.count, id: \.self) { index in&#xA;        let isSelected = selected.contains(index)&#xA;        HStack {&#xA;          if isSelected {&#xA;            Image(systemName: &#34;checkmark.circle.fill&#34;)&#xA;              .foregroundStyle(.tint)&#xA;          } else {&#xA;            Image(systemName: &#34;circle&#34;)&#xA;              .foregroundStyle(.secondary)&#xA;          }&#xA;          Text(String(Self.rows[index]))&#xA;          Spacer()&#xA;          Menu {&#xA;            aboveButton(index, isSelected)&#xA;            belowButton(index, isSelected)&#xA;          } label: {&#xA;            Image(systemName: &#34;ellipsis.circle.fill&#34;)&#xA;              .symbolRenderingMode(.hierarchical)&#xA;          }&#xA;        }&#xA;        .contentShape(Rectangle()) // Makes spacer tappable.&#xA;        .onTapGesture {&#xA;          if isSelected {&#xA;            selected.remove(index)&#xA;          } else {&#xA;            selected.insert(index)&#xA;          }&#xA;        }&#xA;      }.toolbar {&#xA;        ToolbarItem(placement: .topBarLeading) {&#xA;          Button(&#34;Clear&#34;) { selected = [] }&#xA;        }&#xA;      }&#xA;    }&#xA;  }&#xA;  @State private var selected: SetInt = []&#xA;  private static let rows: [Character] = &#34;ABCDEFGHIJKLMNOPQRSTUVWXYZ&#34;.map { $0 }&#xA;&#xA;  @ViewBuilder private func aboveButton(&#xA;     index: Int,  isSelected: Bool&#xA;  ) -  some View {&#xA;    Button {&#xA;      var inRange = index&#xA;      if isSelected {&#xA;        while&#xA;          Self.rows.indices.contains(inRange),&#xA;          selected.contains(inRange)&#xA;        {&#xA;          selected.remove(inRange)&#xA;          inRange -= 1&#xA;        }&#xA;      } else {&#xA;        while&#xA;          Self.rows.indices.contains(inRange),&#xA;          !selected.contains(inRange)&#xA;        {&#xA;          selected.insert(inRange)&#xA;          inRange -= 1&#xA;        }&#xA;      }&#xA;    } label: {&#xA;      Label(&#xA;        isSelected ? &#34;Deselect Range Above&#34; : &#34;Select Range Above&#34;,&#xA;        systemImage: &#34;chevron.up.circle&#34;)&#xA;    }.disabled({&#xA;      let above = index - 1&#xA;      guard Self.rows.indices.contains(above) else { return true }&#xA;      return selected.contains(above) != isSelected&#xA;    }())&#xA;  }&#xA;  @ViewBuilder private func belowButton(&#xA;     index: Int,  isSelected: Bool&#xA;  ) -  some View {&#xA;    Button {&#xA;      var inRange = index&#xA;      if isSelected {&#xA;        while&#xA;          Self.rows.indices.contains(inRange),&#xA;          selected.contains(inRange)&#xA;        {&#xA;          selected.remove(inRange)&#xA;          inRange += 1&#xA;        }&#xA;      } else {&#xA;        while&#xA;          Self.rows.indices.contains(inRange),&#xA;          !selected.contains(inRange)&#xA;        {&#xA;          selected.insert(inRange)&#xA;          inRange += 1&#xA;        }&#xA;      }&#xA;    } label: {&#xA;      Label(&#xA;        isSelected ? &#34;Deselect Range Below&#34; : &#34;Select Range Below&#34;,&#xA;        systemImage: &#34;chevron.down.circle&#34;)&#xA;    }.disabled({&#xA;      let below = index + 1&#xA;      guard Self.rows.indices.contains(below) else { return true }&#xA;      return selected.contains(below) != isSelected&#xA;    }())&#xA;  }&#xA;}&#xA;`]]&gt;</description>
      <content:encoded><![CDATA[<p>How can you let people easily select multiple items on a touchscreen?</p>

<p><img src="https://i.snap.as/rcCkv7ef.png" alt=""/></p>

<p>It’d be nice to not need to tap or swipe over each item, and remember, no Shift or Command keys.</p>

<p>I can’t believe we still don’t have a convention for this, seventeen years after the iPhone.
</p>

<h1 id="band-selection-is-inadequate" id="band-selection-is-inadequate">Band selection is inadequate</h1>

<p>Yes, iOS lets you swipe to quickly select in <a href="https://ios.gadgethacks.com/how-to/apples-mail-app-ios-13-has-new-faster-ways-select-multiple-emails-0207952">lists</a> and <a href="https://ios.gadgethacks.com/how-to/fastest-way-select-multiple-photos-your-iphone-0238250">grids</a>, but you still have to smear your finger over each item.</p>

<p><img src="https://i.snap.as/Pa70l3WB.gif" alt=""/></p>

<p>That’s like dragging a box on a computer. It’s better than nothing, but you don’t want to select 1000 items that way.</p>

<p>Let’s call that <em>band</em> selection. I want <em>range</em> selection: “1, 1000, and everything in between”.</p>

<p>And it must be discoverable.</p>

<h1 id="my-design" id="my-design">My design</h1>

<p>Here’s what I came up with:</p>

<p><img src="https://i.snap.as/u6syzZYv.gif" alt=""/></p>
<ol><li>If an item is <em>unselected</em>, offer “Select Range Above” and “Select Range Below”. If it’s <em>selected</em>, offer “Deselect Range Above” and “Deselect Range Below”.</li>
<li>“Select Range Above” selects that item and continues selecting upward, until it reaches the top or an already-selected item.</li>
<li>Likewise for “Below” except downward.</li>
<li>Likewise for “Deselect” except deselecting.</li>
<li>Disable “Above” if there’s no item above, or it has the opposite state from this item. This is important; if “Above” works but doesn’t do anything meaningful, you sit there confused wondering what it’s for.</li></ol>

<p><img src="https://i.snap.as/5k0vs4Nj.png" alt=""/></p>

<p>It’s simple yet powerful.</p>

<p>Please copy it! If you’re a product manager looking for a UI to use in your app, you can stop reading now. My source code is below.</p>

<p>But for enthusiasts, here’s my thinking.</p>

<h1 id="failed-approach" id="failed-approach">Failed approach</h1>

<p>I tried imitating Shift-click, which selects the range from your most-recently selected item.</p>

<p>Hence “Select Range From Previous”, which points up if you last selected an item above, and down otherwise.</p>

<p><img src="https://i.snap.as/QccFRvKD.png" alt=""/></p>

<p>So if you select A then G, then try a range from C, the range should point down.</p>

<p>But it’s not so simple. What if you then deselect G? The range from C should point up again, right?</p>

<p><img src="https://i.snap.as/vzHWIX5y.png" alt=""/></p>

<p>Apparently this design needs to remember the order in which you selected each item, which is a lot of hidden state.</p>

<p>And deselection is confusing. What if you select A–G, then want to deselect A–C? The range from C depends on <em>how</em> you selected A–G.</p>

<p><img src="https://i.snap.as/5r2OJ3WR.png" alt=""/></p>

<p>At this point, I realized I don’t fully understand Shift- and Command-click, and I reckon you don’t either. Quiz time! What happens if you do this?</p>
<ol><li>Click A.</li>
<li>Shift-click G.</li>
<li>Command-click C.</li></ol>

<p><img src="https://i.snap.as/piKAIx4S.png" alt=""/></p>

<p>Good so far? Now Shift-click A.</p>

<p><img src="https://i.snap.as/vjUfM3eL.png" alt=""/></p>

<p>Did you expect that? I didn’t.</p>

<h1 id="elegant-approach" id="elegant-approach">Elegant approach</h1>

<p>My touch-friendly design avoids all these problems.</p>

<p><img src="https://i.snap.as/yowWsQj5.png" alt=""/></p>

<p>There’s no hidden state or order effects; you can always see what’ll happen, without ever considering what you did earlier. The resulting clarity is incredible.</p>

<p>Imagine using this UI to select hundreds of images among thousands, or key moments in a long video.</p>

<p>With Shift- and Command-click, you’d give up, because you’re always one misclick away from messing up a range or deselecting everything; it’s a breath-holding operation. So those UIs sometimes give you helper tools like marking, filtering, and splitting.</p>

<p>But with touch-friendly ranges, I could spend hours selecting clips in a movie, and never worry about losing my place.</p>

<h1 id="a-shortcoming" id="a-shortcoming">A shortcoming</h1>

<p>One problem is that most of my testers initially thought ranges always reached the top or bottom of the list. They were surprised when they first saw a range stop at the end of a block.</p>

<p>So some people will stick with subpar usage: if they selected T–Z and want to add L–P, they’ll think they need to …</p>
<ol><li>On L, “Select Range Below”, temporarily selecting Q–S.</li>
<li>On Q, “Deselect Range Below”, losing the original selection.</li>
<li>On T, “Select Range Below”, restoring the original selection.</li></ol>

<p><img src="https://i.snap.as/Du7Ckny4.png" alt=""/>
<img src="https://i.snap.as/YpnLTOo9.png" alt=""/></p>

<p>Hopefully you realize it’d be better to …</p>
<ol><li>Select L.</li>
<li>On P, “Select Range Above”.</li></ol>

<p><img src="https://i.snap.as/SOa2NhYF.png" alt=""/></p>

<p>(… or select P, then on L, “Select Range Below”.)</p>

<p>“Well, if people think the range always reaches the end, why not just make it do that?” That subpar usage is why.</p>

<p>“Select to End” is only a little simpler, yet an order of magnitude less powerful. Intuitiveness is important, but not the top priority.</p>

<p>Maybe a simple wording change like “region” would communicate ranges better, but I haven’t had luck. If anyone can invent a better design, I’d love to know.</p>

<p>Regardless, even with subpar usage, touch-friendly ranges are still more useful than the statuses quo, which are band selection or Shift- and Command-click.</p>

<h1 id="please-copy-my-design" id="please-copy-my-design">Please copy my design</h1>

<p>I once whinged to <a href="https://inessential.com">a senior programmer with good taste</a> that to delete ten songs on my iPhone, I had to swipe ten times. I thought that was stupid when I was a kid using an iPod Touch in 2009. Yet I assumed we’d surely figure out range selection on touchscreens within a few years. But here we are with iOS 18.</p>

<p>“So make something better, and share it with the world.”</p>

<p>There.</p>

<p>Please copy and improve my design if you think it’s a good idea. Any UI that lets you select multiple items should let you select ranges! In fact, this touch-friendly design also works on computers. In five years, when I want to move multiple emails, contacts, or bookmarks, I don’t want to have to tap or swipe over each one.</p>

<p>Just like <a href="https://www.puzzmo.com/manifesto">you deserve games</a> that inspire curiosity, you deserve computers that help you think great ideas. A simple, powerful way to handle multiple items is an important part of a <a href="https://verbnounenter.net/magic-carpet">bicycle for the mind</a>.</p>

<hr/>

<pre><code>struct RangeList: View {
  var body: some View {
    NavigationStack {
      List(0 ..&lt; Self.rows.count, id: \.self) { index in
        let isSelected = selected.contains(index)
        HStack {
          if isSelected {
            Image(systemName: &#34;checkmark.circle.fill&#34;)
              .foregroundStyle(.tint)
          } else {
            Image(systemName: &#34;circle&#34;)
              .foregroundStyle(.secondary)
          }
          Text(String(Self.rows[index]))
          Spacer()
          Menu {
            aboveButton(index, isSelected)
            belowButton(index, isSelected)
          } label: {
            Image(systemName: &#34;ellipsis.circle.fill&#34;)
              .symbolRenderingMode(.hierarchical)
          }
        }
        .contentShape(Rectangle()) // Makes spacer tappable.
        .onTapGesture {
          if isSelected {
            selected.remove(index)
          } else {
            selected.insert(index)
          }
        }
      }.toolbar {
        ToolbarItem(placement: .topBarLeading) {
          Button(&#34;Clear&#34;) { selected = [] }
        }
      }
    }
  }
  @State private var selected: Set&lt;Int&gt; = []
  private static let rows: [Character] = &#34;ABCDEFGHIJKLMNOPQRSTUVWXYZ&#34;.map { $0 }

  @ViewBuilder private func aboveButton(
    _ index: Int, _ isSelected: Bool
  ) -&gt; some View {
    Button {
      var inRange = index
      if isSelected {
        while
          Self.rows.indices.contains(inRange),
          selected.contains(inRange)
        {
          selected.remove(inRange)
          inRange -= 1
        }
      } else {
        while
          Self.rows.indices.contains(inRange),
          !selected.contains(inRange)
        {
          selected.insert(inRange)
          inRange -= 1
        }
      }
    } label: {
      Label(
        isSelected ? &#34;Deselect Range Above&#34; : &#34;Select Range Above&#34;,
        systemImage: &#34;chevron.up.circle&#34;)
    }.disabled({
      let above = index - 1
      guard Self.rows.indices.contains(above) else { return true }
      return selected.contains(above) != isSelected
    }())
  }
  @ViewBuilder private func belowButton(
    _ index: Int, _ isSelected: Bool
  ) -&gt; some View {
    Button {
      var inRange = index
      if isSelected {
        while
          Self.rows.indices.contains(inRange),
          selected.contains(inRange)
        {
          selected.remove(inRange)
          inRange += 1
        }
      } else {
        while
          Self.rows.indices.contains(inRange),
          !selected.contains(inRange)
        {
          selected.insert(inRange)
          inRange += 1
        }
      }
    } label: {
      Label(
        isSelected ? &#34;Deselect Range Below&#34; : &#34;Select Range Below&#34;,
        systemImage: &#34;chevron.down.circle&#34;)
    }.disabled({
      let below = index + 1
      guard Self.rows.indices.contains(below) else { return true }
      return selected.contains(below) != isSelected
    }())
  }
}
</code></pre>
]]></content:encoded>
      <guid>https://verbnounenter.net/range-selection</guid>
      <pubDate>Sat, 21 Sep 2024 19:00:00 +0000</pubDate>
    </item>
    <item>
      <title>SongPocket 3</title>
      <link>https://verbnounenter.net/songpocket-3?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[SongPocket is a music player inspired by record crates.&#xA;&#xA;It’s got reorderable albums. Group related releases, or put your favorites on top.&#xA;&#xA;And super fun expandable tracklists!&#xA;&#xA;What’s great&#xA;&#xA;SongPocket is the best damn music viewer I know of. I made it for myself.&#xA;&#xA;You can’t unsee reordering. How come you can reorder your to-dos, but not your albums? Your collection feels so personal when you can arrange it just so.&#xA;&#xA;Artwork goes edge-to-edge because phones are tiny compared to vinyls. You don’t even need to play an album to see it at full size.&#xA;&#xA;And the play confirmation prevents accidental interruptions. It makes other apps feel like hot coals.&#xA;&#xA;But most importantly …&#xA;&#xA;!--more--&#xA;&#xA;Spatialness&#xA;&#xA;Each album or song appears in exactly one place. That might seem obvious, but other apps let you find each song by artist, by album, etc.&#xA;&#xA;SongPocket feels different: it’s like flipping through vinyls, not querying a database.&#xA;&#xA;Consider how the App Switcher shows each app as a card. If it also had an alphabetical list of the same apps, the cards wouldn’t seem like physical objects anymore. It’d feel more like Activity Monitor.&#xA;&#xA;That’s actually how iOS 14 ruined the Home Screen. App icons now appear on the Home Screen and in App Library (possibly thrice!), so it no longer feels like they live on the Home Screen. It’s no longer spatial.&#xA;&#xA;In SongPocket, “now playing” isn’t a screen that duplicates the current artwork; it’s a button that navigates to the current album. It doesn’t overlay a new view; it scrolls the album list to that album.&#xA;&#xA;This is a surprising tradeoff because you lose your place, but the spatialness is worth it.&#xA;&#xA;In SongPocket, you don’t browse info about albums; you browse albums themselves.&#xA;&#xA;What could be better&#xA;&#xA;SongPocket’s major design flaws are a lack of scaling and hierarchy.&#xA;&#xA;The scaling problem is the horrible information density: you can see like two album titles, and the list is a kilometer long.&#xA;&#xA;I don’t want a grid because it forces your eyes to zigzag.&#xA;&#xA;I tried making closed albums half-height and expanding them to square when you opened them, but it felt too jumpy.&#xA;&#xA;A solution could be like Cover Flow sideways, where albums shrink as you scroll them away from the center. That would be both scalable and spatial.&#xA;&#xA;The hierarchy problem is that you can’t group albums to view or manipulate them.&#xA;&#xA;I used to have multiple crates, but I hated the UI because it broke spatialness: “what do you mean, the crate list is both the main view and inside this sheet?”&#xA;&#xA;I’ve since changed the main view to the album list, and a solution could be a crate menu, which changes the albums in the list.&#xA;&#xA;To move albums between crates, you’d select them, then switch crates, and those albums would move with you to the top of that crate. That would be spatial.&#xA;&#xA;A third problem is select-mode hell. “Select-albums mode” is separate from “select-songs mode”, which isn’t obvious, and in “select-songs mode”, you can scroll away, which is bananas.&#xA;&#xA;A solution could be to hide other albums when an album is open, but when I reinserted them, the table view kept doing an unnecessary animation I couldn’t prevent.&#xA;&#xA;Fourth, there should be an artwork-only view that hides everything except a single album cover, and allows pinch to zoom. There’s no excuse for a music app being a worse artwork viewer than the Photos app.&#xA;&#xA;---&#xA;&#xA;Anyway, future potential. SongPocket already does things no other music app does, and it’s free, so try it out!&#xA;&#xA;SongPocket - App Store&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://apps.apple.com/us/app/songpocket/id1538037231">SongPocket</a> is a music player inspired by record crates.</p>

<p>It’s got <strong>reorderable albums</strong>. Group related releases, or put your favorites on top.</p>

<p>And super fun <strong>expandable tracklists</strong>!</p>

<p><img src="https://i.snap.as/V7VC6vWL.heic" alt=""/></p>

<h1 id="what-s-great" id="what-s-great">What’s great</h1>

<p>SongPocket is the best damn music viewer I know of. I made it for myself.</p>

<p>You can’t unsee <strong>reordering</strong>. How come you can reorder your to-dos, but not your albums? Your collection feels so personal when you can arrange it just so.</p>

<p><img src="https://i.snap.as/16miTtyH.png" alt=""/></p>

<p>Artwork goes <strong>edge-to-edge</strong> because phones are tiny compared to vinyls. You don’t even need to play an album to see it at full size.</p>

<p>And the <strong>play confirmation</strong> prevents accidental interruptions. It makes other apps feel like hot coals.</p>

<p><img src="https://i.snap.as/V8zxb2O8.png" alt=""/></p>

<p>But most importantly …</p>



<h2 id="spatialness" id="spatialness">Spatialness</h2>

<p>Each album or song appears in exactly one place. That might seem obvious, but other apps let you find each song by artist, by album, etc.</p>

<p>SongPocket <strong>feels different</strong>: it’s like flipping through vinyls, not querying a database.</p>

<p>Consider how the App Switcher shows each app as a card. If it also had an alphabetical list of the same apps, the cards wouldn’t seem like physical objects anymore. It’d feel more like Activity Monitor.</p>

<p><img src="https://i.snap.as/4ea466lb.png" alt=""/></p>

<p>That’s actually how iOS 14 ruined the Home Screen. App icons now appear on the Home Screen <em>and</em> in App Library (possibly thrice!), so it no longer feels like they live on the Home Screen. It’s no longer spatial.</p>

<p><img src="https://i.snap.as/mbISlx6k.png" alt=""/></p>

<p>In SongPocket, <strong>“now playing”</strong> isn’t a <em>screen</em> that duplicates the current artwork; it’s a <em>button</em> that navigates to the current album. It doesn’t overlay a new view; it scrolls the album list to that album.</p>

<p><img src="https://i.snap.as/VFVu1z68.gif" alt=""/></p>

<p>This is a surprising tradeoff because you lose your place, but the spatialness is worth it.</p>

<p>In SongPocket, you don’t browse info <em>about</em> albums; you browse albums <em>themselves</em>.</p>

<h1 id="what-could-be-better" id="what-could-be-better">What could be better</h1>

<p>SongPocket’s major design flaws are a lack of scaling and hierarchy.</p>

<p>The <strong>scaling</strong> problem is the horrible information density: you can see like two album titles, and the list is a kilometer long.</p>

<p>I don’t want a grid because it forces your eyes to zigzag.</p>

<p>I tried making closed albums half-height and expanding them to square when you opened them, but it felt too jumpy.</p>

<p>A solution could be like Cover Flow sideways, where albums shrink as you scroll them away from the center. That would be both scalable and spatial.</p>

<p><img src="https://i.snap.as/ffWICxgi.png" alt=""/></p>

<p>The <strong>hierarchy</strong> problem is that you can’t group albums to view or manipulate them.</p>

<p>I used to have multiple crates, but I hated the UI because it broke spatialness: “what do you mean, the crate list is both the <em>main</em> view and <em>inside</em> this sheet?”</p>

<p><img src="https://i.snap.as/F0S5tXXS.png" alt=""/></p>

<p>I’ve since changed the main view to the album list, and a solution could be a crate menu, which changes the albums in the list.</p>

<p><img src="https://i.snap.as/c4sZqP6V.png" alt=""/></p>

<p>To move albums between crates, you’d select them, then switch crates, and those albums would move with you to the top of that crate. That would be spatial.</p>

<p>A third problem is <strong>select-mode hell</strong>. “Select-albums mode” is separate from “select-songs mode”, which isn’t obvious, and in “select-songs mode”, you can scroll away, which is bananas.</p>

<p><img src="https://i.snap.as/LaqlzVC8.png" alt=""/></p>

<p>A solution could be to hide other albums when an album is open, but when I reinserted them, the table view kept doing an unnecessary animation I couldn’t prevent.</p>

<p>Fourth, there should be an <strong>artwork-only view</strong> that hides everything except a single album cover, and allows pinch to zoom. There’s no excuse for a music app being a worse artwork viewer than the Photos app.</p>

<hr/>

<p>Anyway, future potential. SongPocket already does things no other music app does, and it’s free, so try it out!</p>

<p><a href="https://apps.apple.com/us/app/songpocket/id1538037231">SongPocket – App Store</a></p>

<p><img src="https://i.snap.as/Dmbh4wjO.heic" alt=""/></p>
]]></content:encoded>
      <guid>https://verbnounenter.net/songpocket-3</guid>
      <pubDate>Sun, 25 Aug 2024 04:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Implementing the “one or more” UI component</title>
      <link>https://verbnounenter.net/one-or-more?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[I wanted to try this for years.&#xA;&#xA;In Tog on Interface, Tog designs a picker for selecting one or more options, but not none.&#xA;&#xA;Think languages or time zones: you need one; you can have more; you can’t have none.&#xA;&#xA;We actually don’t have a dedicated component for this, even today. We use checkboxes for multiple selection, and disable continuing when none are selected.&#xA;&#xA;But what if?&#xA;&#xA;!--more--&#xA;&#xA;So Tog gets cracking.&#xA;&#xA;It’s riveting. He makes prototypes that epic-fail usability testing, and ultimately finds this design which makes sense to users:&#xA;&#xA;Click to select or deselect.&#xA;If deselecting would leave none selected, auto-select the next option.&#xA;… but if there’s no next option, auto-select the previous option, and enable “go up” mode.&#xA;Disable “go up” mode after auto-selecting the topmost option, or the user manually selects any option\.&#xA;&#xA;It was inspired a drop of mercury, which pops sideways when you poke it. Otherwise, it’s regular checkboxes.&#xA;&#xA;So we’re all dying to find out how this actually feels to use, right? What better to do this fine day than implement a fabled UI component? Are you not entertained?&#xA;&#xA;Drum roll:&#xA;&#xA;struct MercuryPicker: View {&#xA;  var body: some View {&#xA;    List(0 ..&lt; picker.options.count, id: \.self) { index in&#xA;      HStack {&#xA;        Image(&#xA;          systemName: picker.selected.contains(index)&#xA;          ? &#34;diamond.fill&#34;&#xA;          : &#34;diamond&#34;&#xA;        )&#xA;        Text(picker.options[index])&#xA;        Spacer()&#xA;      }&#xA;      .contentShape(Rectangle())&#xA;      .onTapGesture {&#xA;        if picker.selected.contains(index) {&#xA;          picker.deselect(at: index)&#xA;        } else {&#xA;          picker.select(at: index)&#xA;        }&#xA;      }&#xA;    }&#xA;  }&#xA;  @State private var picker = OneOrMore(&#xA;    [&#34;English&#34;, &#34;French&#34;, &#34;Japanese&#34;, &#34;Klingon&#34;, &#34;Lojban&#34;],&#xA;    selected: [0])!&#xA;}&#xA;&#xA;struct OneOrMore {&#xA;  init?( options: [String], selected: SetInt) {&#xA;    guard !options.isEmpty, !selected.isEmpty else { return nil }&#xA;    self.options = options; self.selected = selected&#xA;  }&#xA;  let options: [String]&#xA;  private(set) var selected: SetInt&#xA;  mutating func select(at index: Int) {&#xA;    selected.insert(index)&#xA;    // squishesUp = false //  Tog’s version includes this, but I find it clearer without it.&#xA;  }&#xA;  mutating func deselect(at index: Int) {&#xA;    selected.remove(index)&#xA;    guard selected.isEmpty else { return }&#xA;    var toSelect: Int = squishesUp ? (index-1) : (index+1)&#xA;    if !options.indices.contains(toSelect) {&#xA;      squishesUp.toggle()&#xA;      toSelect = squishesUp ? (index-1) : (index+1)&#xA;    }&#xA;    selected.insert(toSelect)&#xA;  }&#xA;  private var squishesUp = false&#xA;}&#xA;&#xA;… and??_&#xA;&#xA;It’s fine.&#xA;&#xA;The hidden state of “go up” mode is a little annoying. It also has hidden logic; so does the checkbox version, but that’s simpler.&#xA;&#xA;I tested it on my parents. Both figured out the “one or more” rule, but only one figured out which way the mercury would pop every time.&#xA;&#xA;The checkbox version is better overall because it’s easier to both learn and implement.&#xA;&#xA;But that was fun!&#xA;&#xA;Open questions&#xA;&#xA;What should we call this component? I like “multipicker”; shorter than “one-or-more picker”, and easier to explain than “mercury picker”.&#xA;Can someone make a web version so everyone can play with it? How about you? \Update: [that was quick.\]&#xA;Why doesn’t iOS even distinguish between checkboxes and radio buttons?&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>I wanted to try this for years.</p>

<p><img src="https://i.snap.as/vqdglc0M.heic" alt=""/></p>

<p>In <em>Tog on Interface</em>, Tog designs a picker for selecting <strong>one or more options, but not none</strong>.</p>

<p>Think languages or time zones: you need <em>one</em>; you can have <em>more</em>; you can’t have <em>none</em>.</p>

<p>We actually don’t have a dedicated component for this, even today. We use checkboxes for multiple selection, and disable continuing when none are selected.</p>

<p><img src="https://i.snap.as/JtiqIUOn.gif" alt=""/></p>

<p>But <em>what if?</em></p>



<p>So Tog gets cracking.</p>

<p><img src="https://i.snap.as/XBqBMj7W.heic" alt=""/></p>

<p>It’s riveting. He makes prototypes that epic-fail usability testing, and ultimately finds this design which makes sense to users:</p>
<ol><li>Click to select or deselect.</li>
<li>If deselecting would leave none selected, auto-select the next option.</li>
<li>… but if there’s no next option, auto-select the previous option, and enable “go up” mode.</li>
<li>Disable “go up” mode after auto-selecting the topmost option, or the user manually selects any option*.</li></ol>

<p><img src="https://i.snap.as/ktUL0Ydd.heic" alt=""/></p>

<p>It was inspired a drop of mercury, which pops sideways when you <a href="https://www.youtube.com/watch?v=Noqd5fvY0i0">poke it</a>. Otherwise, it’s regular checkboxes.</p>

<p><img src="https://i.snap.as/UaFSshsd.jpg" alt=""/></p>

<p>So we’re all dying to find out how this actually feels to use, right? What better to do this fine day than implement a fabled UI component? Are you not entertained?</p>

<p>Drum roll:</p>

<pre><code>struct MercuryPicker: View {
  var body: some View {
    List(0 ..&lt; picker.options.count, id: \.self) { index in
      HStack {
        Image(
          systemName: picker.selected.contains(index)
          ? &#34;diamond.fill&#34;
          : &#34;diamond&#34;
        )
        Text(picker.options[index])
        Spacer()
      }
      .contentShape(Rectangle())
      .onTapGesture {
        if picker.selected.contains(index) {
          picker.deselect(at: index)
        } else {
          picker.select(at: index)
        }
      }
    }
  }
  @State private var picker = OneOrMore(
    [&#34;English&#34;, &#34;French&#34;, &#34;Japanese&#34;, &#34;Klingon&#34;, &#34;Lojban&#34;],
    selected: [0])!
}

struct OneOrMore {
  init?(_ options: [String], selected: Set&lt;Int&gt;) {
    guard !options.isEmpty, !selected.isEmpty else { return nil }
    self.options = options; self.selected = selected
  }
  let options: [String]
  private(set) var selected: Set&lt;Int&gt;
  mutating func select(at index: Int) {
    selected.insert(index)
    // squishesUp = false // * Tog’s version includes this, but I find it clearer without it.
  }
  mutating func deselect(at index: Int) {
    selected.remove(index)
    guard selected.isEmpty else { return }
    var toSelect: Int = squishesUp ? (index-1) : (index+1)
    if !options.indices.contains(toSelect) {
      squishesUp.toggle()
      toSelect = squishesUp ? (index-1) : (index+1)
    }
    selected.insert(toSelect)
  }
  private var squishesUp = false
}
</code></pre>

<p><em>… and??</em></p>

<p><img src="https://i.snap.as/JYrcNGaS.gif" alt=""/></p>

<p>It’s fine.</p>

<p>The hidden state of “go up” mode is a little annoying. It also has hidden logic; so does the checkbox version, but that’s simpler.</p>

<p>I tested it on my parents. Both figured out the “one or more” rule, but only one figured out which way the mercury would pop every time.</p>

<p>The checkbox version is better overall because it’s easier to both learn and implement.</p>

<p>But that was fun!</p>

<h1 id="open-questions" id="open-questions">Open questions</h1>
<ol><li>What should we call this component? I like “multipicker”; shorter than “one-or-more picker”, and easier to explain than “mercury picker”.</li>
<li>Can someone make a web version so everyone can play with it? How about you? [Update: <a href="https://github.com/imapersonman/MercuryPicker">that was quick.</a>]</li>
<li>Why doesn’t iOS even distinguish between checkboxes and radio buttons?</li></ol>

<p><img src="https://i.snap.as/Et1Qb3pC.jpg" alt=""/></p>
]]></content:encoded>
      <guid>https://verbnounenter.net/one-or-more</guid>
      <pubDate>Mon, 29 Jul 2024 19:00:00 +0000</pubDate>
    </item>
    <item>
      <title>You, your program, and nothing else</title>
      <link>https://verbnounenter.net/basic?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Wait, you can do that with BASIC line numbers?&#xA;&#xA;  To add a line in between line 10 and 20, I just have to pick a number between 10 and 20, like 15. And then I type my line.&#xA;    —Kurt Leucht&#xA;&#xA;I thought they were just for GOTOs.&#xA;&#xA;There’s more:&#xA;&#xA;  I wasn’t happy with line 15 and I wanted it to be different. I could just type in a new line number 15.&#xA;&#xA;That blew my whippersnapper mind.&#xA;&#xA;The trick is, there’s no files and no compiling. There’s no “editor”!&#xA;&#xA;You simply write into the computer’s memory, and tell it to run, from the moment you turn it on.&#xA;&#xA;Even on my TI-83 calculator, the program editor is a separate mode. But on an Apple II, it’s the default environment.&#xA;&#xA;Defaults matter; they express a product’s purpose.&#xA;&#xA;A Macintosh shows you the Finder. I guess I’ll arrange some files and windows.&#xA;An iPhone shows you the Home Screen. I guess I’ll browse some music and websites.&#xA;A Game Boy? Put in a game, silly.&#xA;And an Apple II? This thing is for programming._&#xA;&#xA;That’s what inspired a generation about what computers should be.&#xA;&#xA;Hence this complaint:&#xA;&#xA;  Every step between turning on the computer and running your program loses 30% of the students.&#xA;    —paraphrasing David Brin&#xA;&#xA;Imagine having to log in to your guitar and create a music document before you could start strumming.]]&gt;</description>
      <content:encoded><![CDATA[<p>Wait, you can do <em>that</em> with BASIC line numbers?</p>

<blockquote><p>To add a line in between line 10 and 20, I just have to pick a number between 10 and 20, like 15. And then I type my line.</p>

<p><a href="https://www.youtube.com/watch?v=1AiUPVIPd0Y&amp;t=792s">—Kurt Leucht</a></p></blockquote>

<p><img src="https://i.snap.as/Lx1jWeKa.jpg" alt=""/></p>

<p>I thought they were just for <code>GOTO</code>s.</p>

<p>There’s more:</p>

<blockquote><p>I wasn’t happy with line 15 and I wanted it to be different. I could just type in a new line number 15.</p></blockquote>

<p><img src="https://i.snap.as/p26JEzWW.jpg" alt=""/></p>

<p>That blew my whippersnapper mind.</p>

<p>The trick is, there’s no files and no compiling. <strong><a href="https://softwareengineering.stackexchange.com/questions/309767/why-did-basic-use-line-numbers/309820#309820">There’s no “editor”!</a></strong></p>

<p>You simply write into the computer’s memory, and tell it to run, <em>from the moment you turn it on</em>.</p>

<p>Even on my TI-83 calculator, the program editor <a href="https://en.wikibooks.org/wiki/How_to_Program_a_TI-83_Plus/Intro">is a separate mode</a>. But on an Apple II, it’s the default environment.</p>

<p>Defaults matter; they express a product’s purpose.</p>
<ul><li>A Macintosh shows you the Finder. <em>I guess I’ll arrange some files and windows.</em></li>
<li>An iPhone shows you the Home Screen. <em>I guess I’ll browse some music and websites.</em></li>
<li>A Game Boy? <em><a href="https://www.youtube.com/watch?v=ji6BhC32c9k&amp;t=9s">Put in a game, silly.</a></em></li>
<li>And an Apple II? <em>This thing is for programming.</em></li></ul>

<p>That’s what inspired a generation about what computers should be.</p>

<p>Hence this complaint:</p>

<blockquote><p>Every step between turning on the computer and running your program loses 30% of the students.</p>

<p><a href="https://time.com/69316/basic">—paraphrasing David Brin</a></p></blockquote>

<p>Imagine having to log in to your guitar and create a music document before you could start strumming.</p>
]]></content:encoded>
      <guid>https://verbnounenter.net/basic</guid>
      <pubDate>Fri, 26 Apr 2024 17:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Good design offers one way to do it</title>
      <link>https://verbnounenter.net/monotony?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[A popular misconception is that good design offers multiple ways to do the same action.&#xA;&#xA;That’s redundancy; the opposite is monotony—having exactly one way to do some action.&#xA;&#xA;Monotony sounds dull, but is actually a profoundly clarifying guideline. I first heard about it from Jef Raskin—yes, the Macintosh guy.&#xA;&#xA;Let’s analyze some examples.&#xA;&#xA;1. Journal entries&#xA;&#xA;iOS’s Journal app has a delightfully short feature set:&#xA;&#xA;Create and delete entries&#xA;Edit and add attachments to entries&#xA;Bookmark and filter entries&#xA;&#xA;Unfortunately, it lets you bookmark, edit, or delete an entry in 4 ways:&#xA;&#xA;!--more--&#xA;&#xA;Redundancy distracts.&#xA;&#xA;Imagine if you had exactly one way to edit an entry. You’d focus more on actually journaling; and spend less time learning to use the app, and deciding which way to use it.&#xA;&#xA;Meanwhile, there’s only one way to create an entry, or attach an image.&#xA;&#xA;Would it be better if you could also create an entry from within the entry editor? Or also attach an image by touching and holding an entry?&#xA;&#xA;Quoting Jef Raskin:&#xA;&#xA;  When you have to choose among methods, your locus of attention is drawn from the task and temporarily becomes the decision itself.&#xA;&#xA;When there’s only one way to do something, you never decide which way to do it; you just do it.&#xA;&#xA;2. Playlists&#xA;&#xA;There are 2 ways to add a song to a playlist: from the song, or from the playlist.&#xA;&#xA;From the song makes sense. You’re already looking at it: “this song would be good for the party!” Plunk.&#xA;&#xA;From the playlist is horrible. You have to browse your entire library inside a sheet, using views with smaller artwork and no sort settings.&#xA;&#xA;And what if you want a song not in your library? You can’t search the catalog. Perhaps they could add a Search tab here…&#xA;&#xA;No, now we’re cloning the entire app inside one of its sheets. Bananas.&#xA;&#xA;The main UI is the best way to choose songs, because that’s what it’s for. It should be the only way to add to playlists.&#xA;&#xA;Those inferior versions of the albums, artists, and songs lists inside the sheet? It took effort to make those, all for an experience that was incomplete and unnecessary in the first place.&#xA;&#xA;Monotony simplifies development too.&#xA;&#xA;3. App folders&#xA;&#xA;iOS offers one way to create an app folder: drag an icon onto another.&#xA;&#xA;You can’t create an empty folder first, then put icons into it. You don’t even have the option, so there’s no decision to make. If you never noticed that, that’s a good thing.&#xA;&#xA;I designed album folders in Songpocket the same way: the only way to create a new folder is while moving albums. It’s simpler for users to use, and for me to maintain.&#xA;&#xA;Clarification&#xA;&#xA;Monotony doesn’t mean only one way to produce some result. That would eliminate the Backspace key, because it lets you put “hello” on your screen in infinite ways.&#xA;&#xA;So there’s room for interpretation in what you call an action.&#xA;&#xA;You could call it redundant for a keyboard shortcut and a clickable button to activate the same command, but that’s generally fine. Accessible controls too.&#xA;&#xA;Rather, it’s redundant for a toolbar button and menu command to do the same thing. When you explore each area, you have to remember which actions are unique, versus which actions are also available elsewhere. That’s a load of cognitive load.&#xA;&#xA;Monotony improves focus&#xA;&#xA;A monotonous design is easier to learn, use, and maintain.&#xA;&#xA;Plus, it’s easier to support, because there’s only one help article to write, and when users contact you, you know they’re all using it the same way.&#xA;&#xA;Quoting Jef Raskin again:&#xA;&#xA;  \[A\]n interface that is both modeless and, insofar as possible, monotonous \[…\] would be extraordinarily pleasant to use. A user would be able to develop an unusually high degree of trust in \[their\] habits. The interface would, from these two properties alone, tend to fade from the user’s consciousness, allowing \[them\] to give \[their\] full attention to the task at hand.&#xA;&#xA;Design means deciding how things should work: to consider all the possibilities, and choose the best one.&#xA;&#xA;It is irresponsible to waste people’s attention with pointless options by being indecisive. Eliminate what doesn’t need people’s attention, and you free people to focus on what actually matters.&#xA;&#xA;---&#xA;&#xA;See also:&#xA;&#xA;The Humane Interface by Jef Raskin&#xA;Zusch Login]]&gt;</description>
      <content:encoded><![CDATA[<p>A popular misconception is that good design offers multiple ways to do the same action.</p>

<p>That’s <strong>redundancy</strong>; the opposite is <strong>monotony</strong>—having exactly one way to do some action.</p>

<p>Monotony sounds dull, but is actually a profoundly clarifying guideline. I first heard about it from Jef Raskin—yes, <a href="https://web.stanford.edu/dept/SUL/sites/mac/primary/docs/bom/anthrophilic.html">the Macintosh guy</a>.</p>

<p>Let’s analyze some examples.</p>

<h1 id="1-journal-entries" id="1-journal-entries">1. Journal entries</h1>

<p>iOS’s Journal app has a delightfully short feature set:</p>
<ol><li>Create and <em>delete</em> entries</li>
<li><em>Edit</em> and add attachments to entries</li>
<li><em>Bookmark</em> and filter entries</li></ol>

<p>Unfortunately, it lets you <em>bookmark</em>, <em>edit</em>, or <em>delete</em> an entry in 4 ways:</p>

<p><img src="https://i.snap.as/SeK7SP41.png" alt=""/></p>



<p>Redundancy distracts.</p>

<p>Imagine if you had exactly one way to edit an entry. You’d focus more on actually journaling; and spend less time learning to use the app, and deciding which way to use it.</p>

<p>Meanwhile, there’s only one way to <em>create</em> an entry, or <em>attach</em> an image.</p>

<p>Would it be better if you could also <em>create</em> an entry from within the entry editor? Or also <em>attach</em> an image by touching and holding an entry?</p>

<p>Quoting Jef Raskin:</p>

<blockquote><p>When you have to choose among methods, your locus of attention is drawn from the task and temporarily becomes the decision itself.</p></blockquote>

<p>When there’s only one way to do something, you never decide which way to do it; you just do it.</p>

<h1 id="2-playlists" id="2-playlists">2. Playlists</h1>

<p>There are 2 ways to add a song to a playlist: from the song, or from the playlist.</p>

<p><strong>From the song</strong> makes sense. You’re already looking at it: “this song would be good for the party!” <em>Plunk.</em></p>

<p><img src="https://i.snap.as/BbFBOuwq.png" alt=""/></p>

<p><strong>From the playlist</strong> is horrible. You have to browse your entire library inside a sheet, using views with smaller artwork and no sort settings.</p>

<p><img src="https://i.snap.as/5Z2MhZ2v.png" alt=""/></p>

<p>And what if you want a song not in your library? You can’t search the catalog. Perhaps they could add a Search tab here…</p>

<p>No, now we’re cloning the entire app inside one of its sheets. Bananas.</p>

<p>The main UI is the best way to choose songs, because <em>that’s what it’s for</em>. It should be the only way to add to playlists.</p>

<p>Those inferior versions of the albums, artists, and songs lists inside the sheet? It took effort to make those, all for an experience that was incomplete and unnecessary in the first place.</p>

<p>Monotony simplifies development too.</p>

<h1 id="3-app-folders" id="3-app-folders">3. App folders</h1>

<p>iOS offers one way to create an app folder: drag an icon onto another.</p>

<p><img src="https://i.snap.as/d1LS7fxg.png" alt=""/></p>

<p>You can’t create an empty folder first, then put icons into it. You don’t even have the option, so there’s no decision to make. If you never noticed that, that’s a good thing.</p>

<p>I designed <a href="https://verbnounenter.net/songpocket-2">album folders</a> in Songpocket the same way: the only way to create a new folder is while moving albums. It’s simpler for users to use, and for me to maintain.</p>

<p><img src="https://i.snap.as/yt5zPUH8.png" alt=""/></p>

<h1 id="clarification" id="clarification">Clarification</h1>

<p>Monotony doesn’t mean only one way to produce some <strong>result</strong>. That would eliminate the Backspace key, because it lets you put “hello” on your screen in infinite ways.</p>

<p>So there’s room for interpretation in what you call an <strong>action</strong>.</p>

<p>You could call it redundant for a keyboard <em>shortcut</em> and a clickable <em>button</em> to activate the same command, but that’s generally fine. <a href="https://www.xbox.com/en-US/accessories/controllers/xbox-adaptive-controller">Accessible controls</a> too.</p>

<p>Rather, it’s redundant for a <em>toolbar</em> button and <em>menu</em> command to do the same thing. When you explore each area, you have to remember which actions are unique, versus which actions are also available elsewhere. That’s a load of cognitive load.</p>

<h1 id="monotony-improves-focus" id="monotony-improves-focus">Monotony improves focus</h1>

<p>A monotonous design is easier to <strong>learn</strong>, <strong>use</strong>, and <strong>maintain</strong>.</p>

<p>Plus, it’s easier to <strong>support</strong>, because there’s only one help article to write, and when users contact you, you know they’re all using it the same way.</p>

<p>Quoting Jef Raskin again:</p>

<blockquote><p>[A]n interface that is both modeless and, insofar as possible, monotonous […] would be extraordinarily pleasant to use. A user would be able to develop an unusually high degree of trust in [their] habits. The interface would, from these two properties alone, tend to fade from the user’s consciousness, allowing [them] to give [their] full attention to the task at hand.</p></blockquote>

<p>Design means deciding how things should work: to consider all the possibilities, and choose the best one.</p>

<p>It is irresponsible to waste people’s attention with pointless options by being indecisive. Eliminate what doesn’t need people’s attention, and you free people to focus on what actually matters.</p>

<hr/>

<p>See also:</p>
<ul><li><a href="https://en.wikipedia.org/wiki/The_Humane_Interface"><em>The Humane Interface</em></a> by Jef Raskin</li>
<li><a href="http://www.zuschlogin.com/?p=27">Zusch Login</a></li></ul>
]]></content:encoded>
      <guid>https://verbnounenter.net/monotony</guid>
      <pubDate>Tue, 16 Apr 2024 13:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Why I call my blog Verb Noun Enter</title>
      <link>https://verbnounenter.net/verb-noun-enter?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[It’s the 1960s. You have no mouse and no keyboard, and nobody has a computer on their desk. And you’re wearing a space suit.&#xA;&#xA;How do you design a computer to be easy to use in this situation?&#xA;&#xA;The Apollo Guidance Computer’s idea: numeric verbs and nouns.&#xA;&#xA;Verb, 16: display. Noun, 65: time. Enter.&#xA;&#xA;Bam. Simple yet powerful.&#xA;&#xA;(Also, y’know, because I enter verbs and nouns here.)&#xA;&#xA;---&#xA;&#xA;Learn more:&#xA;&#xA;Ars Technica&#xA;Apollo Flight Journal&#xA;13 Minutes to the Moon]]&gt;</description>
      <content:encoded><![CDATA[<p>It’s the 1960s. You have no mouse and no keyboard, and nobody has a computer on their desk. And you’re wearing a space suit.</p>

<p>How do you design a computer to be easy to use in this situation?</p>

<p>The Apollo Guidance Computer’s idea: <a href="https://www.youtube.com/watch?v=ndvmFlg1WmE&amp;t=375s">numeric </a><em><a href="https://www.youtube.com/watch?v=ndvmFlg1WmE&amp;t=375s">verbs</a></em><a href="https://www.youtube.com/watch?v=ndvmFlg1WmE&amp;t=375s"> and </a><em><a href="https://www.youtube.com/watch?v=ndvmFlg1WmE&amp;t=375s">nouns</a></em>.</p>

<p><strong>Verb, 16</strong>: <em>display</em>. <strong>Noun, 65</strong>: <em>time</em>. <strong>Enter</strong>.</p>

<p><img src="https://i.snap.as/es5pOz6w.png" alt=""/></p>

<p>Bam. Simple yet powerful.</p>

<p>(Also, y’know, because I enter verbs and nouns here.)</p>

<hr/>

<p>Learn more:</p>
<ul><li><a href="https://arstechnica.com/science/2020/01/a-deep-dive-into-the-apollo-guidance-computer-and-the-hack-that-saved-apollo-14">Ars Technica</a></li>
<li><a href="https://apollojournals.org/afj/compessay.html">Apollo Flight Journal</a></li>
<li><a href="https://www.youtube.com/watch?v=FTiccgAFs-A&amp;t=259s">13 Minutes to the Moon</a></li></ul>
]]></content:encoded>
      <guid>https://verbnounenter.net/verb-noun-enter</guid>
      <pubDate>Mon, 08 Apr 2024 02:00:00 +0000</pubDate>
    </item>
  </channel>
</rss>