http://cutting.io/ cutting.io 2024-09-02T03:12:00Z Dan Cutting http://cutting.io tag:cutting.io,2024-09-02:/posts/conway-life-and-death-v1_1/ Conway: Life and Death v1.1 2024-09-02T03:12:00Z 2024-09-02T03:12:00Z Dan Cutting http://cutting.io <p>I noticed my game of life app, Conway, had a couple of cosmetic bugs on newer versions of macOS so I spent a couple of hours fixing them up. While I was at it I couldn’t resist the urge to tinker and added an option for cells to leave trails, which I think makes the app very pretty!</p> <video controls="" width="100%"> <source src="https://cutting.io/images/life/life-trails.mp4" type="video/mp4"></source> </video> <p>I originally wrote the app <a href="https://cutting.io/posts/game-of-life-on-metal/">as a tribute</a> to John Conway. It’s no longer available on the Mac App Store but you can <a href="https://cutting.io/apps/Conway1.1.zip">download it as a zipped app</a> instead. (Warning: the binary has not been notarized so you’ll need to proceed <em>at your own risk</em> by following Apple’s instructions for <a href="https://support.apple.com/en-mide/102445">running unnotarized softare</a>.)</p> <p>Adding trails was an interesting technical diversion.</p> <!--MORE--> <p>Each cell in the universe is stored as an unsigned 8-bit integer in a texture, meaning it can have 256 different values. The original version of Conway used <code>0</code> for “dead”, and <code>1</code>, <code>2</code>, or <code>3</code> for “alive”. Each alive value was assigned a different colour, but were equivalent for the purposes of running the simulation.</p> <p>This meant I still had 252 other values I could use for each cell, which just happens to divide nicely by 3, the number of colours each theme supports. I invented a new “zombie” state for each colour which is defined as dead for the purposes of simulation, but acts as a countdown from fully alive to fully dead.</p> <table> <thead> <tr> <th>Cell value</th> <th>Definition</th> </tr> </thead> <tbody> <tr> <td>0</td> <td>dead</td> </tr> <tr> <td>1–84</td> <td><span style="color: red;">zombie</span></td> </tr> <tr> <td>85</td> <td><span style="color: red;"><strong>alive</strong></span></td> </tr> <tr> <td>86–169</td> <td><span style="color: #44ccff;">zombie</span></td> </tr> <tr> <td>170</td> <td><span style="color: #44ccff;"><strong>alive</strong></span></td> </tr> <tr> <td>171–254</td> <td><span style="color: blue;">zombie</span></td> </tr> <tr> <td>255</td> <td><span style="color: blue;"><strong>alive</strong></span></td> </tr> </tbody> </table> <p>When each generation is updated, the zombie cells decrement until they reach fully dead. To show the fading trails, zombie cells are rendered as a blend between the background colour and their assigned alive colour depending upon how dead they are.</p> tag:cutting.io,2024-08-13:/posts/zeds-dev-diary-20240813/ Zeds dev diary 20240813 2024-08-13T12:19:00Z 2024-08-13T12:19:00Z Dan Cutting http://cutting.io <p>Zed’s new modal editor really simplifies a lot because there’s virtually no need for <code>@FocusState</code> any more.</p> <p><img src="https://cutting.io/images/zeds-20240813a.png" class="feature" alt="Zed's modal editor"></p> <p>I also applied extra polish this afternoon in the way of better VoiceOver descriptions and support for other accessibility features like reduced motion and increased contrast. Dynamic Type and dark mode are, of course, well supported.</p> <!--MORE--> <figure class="embed"> <img src="https://cutting.io/images/zeds-20240813b.png" alt="Zed's dark mode footer"> </figure> <p>Zed’s footer is now opaque; avoiding transparency over the scroll view results in fewer animation glitches when showing/hiding the keyboard.</p> <p>In short, I think the UX feels pretty good now!</p> <p>To concentrate on getting this app done, I’ve ditched the iPad-specific feature of showing all three lists on the screen at once. I expect to come back to this in a future version.</p> <p>Next step is to dogfood the app for a few days to find the rough edges.</p> tag:cutting.io,2024-08-12:/posts/zeds-dev-diary-20240812/ Zeds dev diary 20240812 2024-08-12T01:00:00Z 2024-08-12T01:00:00Z Dan Cutting http://cutting.io <p>I’ve clarified the tabs on iPhone and added task separators. The iPad version of Zeds now shows all lists on the screen at once, and supports drag’n’drop both between lists and as a way to export/import text. But drag’n’drop, focus state, text fields and lists are giving me grief!</p> <!--MORE--> <p><img src="https://cutting.io/images/zeds-20240812a.png" class="feature" alt="Zeds on iPad"></p> <h2 id="draggable--dropdestination"> <code>draggable</code> &amp; <code>dropDestination</code> </h2> <p>The new <code>draggable</code> and <code>dropDestination</code> APIs are powerful but limited, as with much of SwiftUI. For instance, there doesn’t appear to be a straightforward way to detect <em>when</em> a drag is in progress unless I fall back to the older <code>onDrag</code> API (which doesn’t play nicely with the new API). This is a problem because my tasks have tap gestures to activate editing which should be ignored during a drag.</p> <figure class="embed"> <img src="https://cutting.io/images/zeds-20240812b.png" alt="Zeds on iPhone with clearer tabs"> </figure> <h2 id="focusstate"><code>@FocusState</code></h2> <p>My current UX has inline editing of multiline text fields in lists. This means editing focus can easily change as the user moves between text fields.</p> <p>Unfortunately, it’s difficult to use a view model as a source of truth for focus because SwiftUI manages this closer to the view level with <code>@FocusState</code> (which can’t be used outside <code>View</code> types). Furthermore, focus state only works for views that are currently on screen so it can’t be used to record which task is currently being edited if that view is scrolled off-screen.</p> <p>Any logic that needs to know whether a task is being edited thus relies on copying state between views and view models as they change with <code>onChange</code>. This is possible but error-prone and ultimately results in no single source of truth.</p> <h2 id="scrollview"><code>ScrollView</code></h2> <p>There is an unavoidable animation glitch if a scroll view or <code>List</code> has a <code>safeAreaInset</code> view at the bottom when the keyboard is dismissed. The workaround I’ve gone with is to not use the <code>safeAreaInset</code> directly, but calculate the amount of space needed with various reader proxies and pad the scroll view content itself.</p> <h2 id="next-steps">Next steps</h2> <p>I have spent hours working around many bugs like these. My views are brittle, with surprising code that tries to balance all the hacks needed to make it fit together. My intention now is to significantly modify the UX to avoid the hacks. A modal editor for tasks rather than inline editing will help me avoid focus state issues and multiline text fields in lists.</p> tag:cutting.io,2024-08-04:/posts/zeds-dev-diary-20240804/ Zeds dev diary 20240804 2024-08-04T05:53:00Z 2024-08-04T05:53:00Z Dan Cutting http://cutting.io <p>I’m learning some new SwiftUI APIs by writing a “do it later” iPhone app called Zeds (ok fine… it’s a to do app, and yeah, that name is meant to sound like snoring but doesn’t work for US readers).</p> <p>The premise is to boil everything down into just three lists: things to do later, things to do now, and things I’ve done. These lists are laid out next to each other in space from left to right.</p> <p><img src="https://cutting.io/images/zeds-20240804a.png" class="feature" alt="The three lists of tasks"></p> <!--MORE--> <p>Only one list is visible on the iPhone screen at a time and tasks are moved between them by swiping left or right. A stylised tab bar at the bottom lets you switch between the lists. Essentially it’s <a href="https://en.wikipedia.org/wiki/Kanban">Kanban</a> with nice gestures. I asked some beta testers to try this out with no help except for some initial built-in tasks acting as a user manual.</p> <figure class="embed"> <img src="https://cutting.io/images/zeds-20240804b.png" alt="The to do list by itself"> </figure> <p>Although it all seemed clear and simple to me, no design ever survives first contact with the user. :)</p> <p>My testers initially found the app confusing:</p> <ul> <li>on launch there appeared to be only one list</li> <li>it wasn’t clear what each tab icon represented, or even what the tab bar did</li> <li>the instructional tasks looked like a large block of help text rather than sample items in a list</li> </ul> <p>This confusion stemmed from some of my design choices:</p> <ul> <li>no labels on the tab icons and no title on each list <ul> <li>the principle was to minimise the amount of text in the UI to focus on the user’s text in the lists</li> </ul> </li> <li>replace the standard iOS tab bar with a minimal tab switch</li> <li>show the to do list without indicating there are other lists either side of it <ul> <li>the tab bar is meant to convey this but doesn’t</li> </ul> </li> <li>remove separators and other visual indicators of the list rows <ul> <li>the list looks “cleaner” but is less obvious for new users</li> </ul> </li> </ul> <p>Next steps to consider:</p> <ul> <li>labels on lists</li> <li>fewer, simpler instructional tasks</li> <li>an initial animation to indicate the lists positions in space</li> <li>a more “direct” way of switching lists than the stylised tab bar</li> <li>separators or otherwise distinguished list rows</li> </ul> tag:cutting.io,2024-07-26:/posts/sierpinski-gaskets-in-swift/ Sierpiński gaskets in Swift 2024-07-26T03:29:00Z 2024-07-26T03:29:00Z Dan Cutting http://cutting.io <p>I played the <a href="https://en.wikipedia.org/wiki/Chaos_game">Chaos Game</a> today to make a <a href="https://en.wikipedia.org/wiki/Sierpi%C5%84ski_triangle">Sierpiński gasket</a> (aka Sierpiński triangle). You can read the code below or <a href="https://cutting.io/samples/SierpinskiGasket.playground.zip">download</a> the Swift Playground to experiment with it yourself.</p> <p><img src="https://cutting.io/images/sierpinski.png" class="plain feature padded" alt="Sierpinski gasket"></p> <p>Just plot some points using this simple algorithm to see a ghostly fractal triangle appear from nothing:</p> <ol> <li>Set your “current” position to any of the three corners of the big triangle</li> <li>Plot a dot at your current position</li> <li>Pick any of the three corners</li> <li>Find the midpoint of your current position and that corner</li> <li>Set your current position to that midpoint</li> <li>Goto 2</li> </ol> <!--MORE--> <p>I’ve been fascinated with fractals since my Dad took me to a lecture given by the legendary <a href="https://en.wikipedia.org/wiki/Benoit_Mandelbrot">Benoit Mandelbrot</a> at <a href="https://www.unsw.edu.au">UNSW</a> (or maybe it was <a href="https://www.sydney.edu.au">USYD</a>?) back when I was a kid in the late 80s. I remember writing BASIC programs for my Apple IIc and leaving them running overnight to make just one fuzzy postcard from some hidden nook of the Mandelbrot set.</p> <p>But for maximal simplicity versus awe, you can’t beat Sierpiński gaskets. So easy!</p> <p>The easiest way I know to plot points these days is on a SwiftUI Canvas. You’ll need to wait for all the points to plot before you can see the image so don’t make <code>numDots</code> too big.</p> <p>Making the canvas redraw as it plots each point is left as an exercise to the reader. ;)</p> <pre><code class="language-swift"><div class="highlight"><pre><span></span><a id="line-1" name="line-1" href="#line-1"></a><span class="kd">import</span> <span class="nc">SwiftUI</span> <a id="line-2" name="line-2" href="#line-2"></a><span class="kd">import</span> <span class="nc">PlaygroundSupport</span> <a id="line-3" name="line-3" href="#line-3"></a> <a id="line-4" name="line-4" href="#line-4"></a><span class="kd">let</span> <span class="nv">numDots</span> <span class="p">=</span> <span class="mi">1000</span> <a id="line-5" name="line-5" href="#line-5"></a><span class="kd">let</span> <span class="nv">dotWidth</span><span class="p">:</span> <span class="n">CGFloat</span> <span class="p">=</span> <span class="mi">1</span> <a id="line-6" name="line-6" href="#line-6"></a><span class="kd">let</span> <span class="nv">dotColour</span><span class="p">:</span> <span class="n">Color</span> <span class="p">=</span> <span class="p">.</span><span class="n">blue</span> <a id="line-7" name="line-7" href="#line-7"></a> <a id="line-8" name="line-8" href="#line-8"></a><span class="kd">let</span> <span class="nv">points</span><span class="p">:</span> <span class="p">[</span><span class="n">CGPoint</span><span class="p">]</span> <span class="p">=</span> <span class="p">[</span> <a id="line-9" name="line-9" href="#line-9"></a> <span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="n">y</span><span class="p">:</span> <span class="mi">1</span><span class="p">),</span> <a id="line-10" name="line-10" href="#line-10"></a> <span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="n">y</span><span class="p">:</span> <span class="mi">1</span><span class="p">),</span> <a id="line-11" name="line-11" href="#line-11"></a> <span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">,</span> <span class="n">y</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> <a id="line-12" name="line-12" href="#line-12"></a><span class="p">]</span> <a id="line-13" name="line-13" href="#line-13"></a> <a id="line-14" name="line-14" href="#line-14"></a><span class="kd">let</span> <span class="nv">canvas</span> <span class="p">=</span> <span class="n">Canvas</span> <span class="p">{</span> <span class="n">context</span><span class="p">,</span> <span class="n">size</span> <span class="k">in</span> <a id="line-15" name="line-15" href="#line-15"></a> <span class="c1">// 1.</span> <a id="line-16" name="line-16" href="#line-16"></a> <span class="kd">var</span> <span class="nv">current</span> <span class="p">=</span> <span class="n">points</span><span class="p">.</span><span class="n">randomElement</span><span class="p">()</span><span class="o">!</span> <a id="line-17" name="line-17" href="#line-17"></a> <a id="line-18" name="line-18" href="#line-18"></a> <span class="k">for</span> <span class="kc">_</span> <span class="k">in</span> <span class="mf">0.</span><span class="p">.&lt;</span><span class="n">numDots</span> <span class="p">{</span> <a id="line-19" name="line-19" href="#line-19"></a> <a id="line-20" name="line-20" href="#line-20"></a> <span class="c1">// 2.</span> <a id="line-21" name="line-21" href="#line-21"></a> <span class="n">context</span><span class="p">.</span><span class="n">fill</span><span class="p">(</span><span class="n">dot</span><span class="p">(</span><span class="k">for</span><span class="p">:</span> <span class="n">current</span><span class="p">,</span> <span class="n">canvasSize</span><span class="p">:</span> <span class="n">size</span><span class="p">),</span> <span class="n">with</span><span class="p">:</span> <span class="p">.</span><span class="n">color</span><span class="p">(</span><span class="n">dotColour</span><span class="p">))</span> <a id="line-22" name="line-22" href="#line-22"></a> <a id="line-23" name="line-23" href="#line-23"></a> <span class="c1">// 3.</span> <a id="line-24" name="line-24" href="#line-24"></a> <span class="kd">let</span> <span class="nv">selection</span> <span class="p">=</span> <span class="n">points</span><span class="p">.</span><span class="n">randomElement</span><span class="p">()</span><span class="o">!</span> <a id="line-25" name="line-25" href="#line-25"></a> <a id="line-26" name="line-26" href="#line-26"></a> <span class="c1">// 4.</span> <a id="line-27" name="line-27" href="#line-27"></a> <span class="kd">let</span> <span class="nv">midpoint</span> <span class="p">=</span> <span class="n">CGPoint</span><span class="p">(</span> <a id="line-28" name="line-28" href="#line-28"></a> <span class="n">x</span><span class="p">:</span> <span class="n">selection</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="p">(</span><span class="n">current</span><span class="p">.</span><span class="n">x</span> <span class="o">-</span> <span class="n">selection</span><span class="p">.</span><span class="n">x</span><span class="p">)</span> <span class="o">/</span> <span class="mf">2.0</span><span class="p">,</span> <a id="line-29" name="line-29" href="#line-29"></a> <span class="n">y</span><span class="p">:</span> <span class="n">selection</span><span class="p">.</span><span class="n">y</span> <span class="o">+</span> <span class="p">(</span><span class="n">current</span><span class="p">.</span><span class="n">y</span> <span class="o">-</span> <span class="n">selection</span><span class="p">.</span><span class="n">y</span><span class="p">)</span> <span class="o">/</span> <span class="mf">2.0</span> <a id="line-30" name="line-30" href="#line-30"></a> <span class="p">)</span> <a id="line-31" name="line-31" href="#line-31"></a> <a id="line-32" name="line-32" href="#line-32"></a> <span class="c1">// 5.</span> <a id="line-33" name="line-33" href="#line-33"></a> <span class="n">current</span> <span class="p">=</span> <span class="n">midpoint</span> <a id="line-34" name="line-34" href="#line-34"></a> <span class="p">}</span> <a id="line-35" name="line-35" href="#line-35"></a><span class="p">}</span> <a id="line-36" name="line-36" href="#line-36"></a><span class="p">.</span><span class="n">frame</span><span class="p">(</span><span class="n">width</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="mi">200</span><span class="p">)</span> <a id="line-37" name="line-37" href="#line-37"></a> <a id="line-38" name="line-38" href="#line-38"></a><span class="kd">func</span> <span class="nf">dot</span><span class="p">(</span><span class="k">for</span> <span class="n">point</span><span class="p">:</span> <span class="n">CGPoint</span><span class="p">,</span> <span class="n">canvasSize</span><span class="p">:</span> <span class="n">CGSize</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">Path</span> <span class="p">{</span> <a id="line-39" name="line-39" href="#line-39"></a> <span class="p">.</span><span class="kd">init</span> <span class="p">{</span> <span class="n">path</span> <span class="k">in</span> <a id="line-40" name="line-40" href="#line-40"></a> <span class="n">path</span><span class="p">.</span><span class="n">addRect</span><span class="p">(</span> <a id="line-41" name="line-41" href="#line-41"></a> <span class="p">.</span><span class="kd">init</span><span class="p">(</span> <a id="line-42" name="line-42" href="#line-42"></a> <span class="n">x</span><span class="p">:</span> <span class="n">point</span><span class="p">.</span><span class="n">x</span> <span class="o">*</span> <span class="p">(</span><span class="n">canvasSize</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">dotWidth</span><span class="p">),</span> <a id="line-43" name="line-43" href="#line-43"></a> <span class="n">y</span><span class="p">:</span> <span class="n">point</span><span class="p">.</span><span class="n">y</span> <span class="o">*</span> <span class="p">(</span><span class="n">canvasSize</span><span class="p">.</span><span class="n">height</span> <span class="o">-</span> <span class="n">dotWidth</span><span class="p">),</span> <a id="line-44" name="line-44" href="#line-44"></a> <span class="n">width</span><span class="p">:</span> <span class="n">dotWidth</span><span class="p">,</span> <a id="line-45" name="line-45" href="#line-45"></a> <span class="n">height</span><span class="p">:</span> <span class="n">dotWidth</span> <a id="line-46" name="line-46" href="#line-46"></a> <span class="p">)</span> <a id="line-47" name="line-47" href="#line-47"></a> <span class="p">)</span> <a id="line-48" name="line-48" href="#line-48"></a> <span class="p">}</span> <a id="line-49" name="line-49" href="#line-49"></a><span class="p">}</span> <a id="line-50" name="line-50" href="#line-50"></a> <a id="line-51" name="line-51" href="#line-51"></a><span class="n">PlaygroundPage</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="n">setLiveView</span><span class="p">(</span><span class="n">canvas</span><span class="p">)</span> </pre></div></code></pre>