<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.joshholtz.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.joshholtz.com/" rel="alternate" type="text/html" /><updated>2026-05-27T13:38:10+00:00</updated><id>https://www.joshholtz.com/feed.xml</id><title type="html">Josh Holtz</title><subtitle>Software superstar. Stuttering stud. Lead maintainer of fastlane tools.</subtitle><entry><title type="html">Understanding my newly recognized operating system</title><link href="https://www.joshholtz.com/blog/2026/05/27/understanding-my-newly-recognized-operating-system.html" rel="alternate" type="text/html" title="Understanding my newly recognized operating system" /><published>2026-05-27T14:00:00+00:00</published><updated>2026-05-27T14:00:00+00:00</updated><id>https://www.joshholtz.com/blog/2026/05/27/understanding-my-newly-recognized-operating-system</id><content type="html" xml:base="https://www.joshholtz.com/blog/2026/05/27/understanding-my-newly-recognized-operating-system.html"><![CDATA[<p>I’ve always known the operating system <a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a> runs on was a little different.</p>

<p>Throughout my life, I unconsciously tuned the way I live for the “best” version of Josh. The environments I work in. The tools I obsess over. The routines I keep. The way I recover after social events. The way I hyperfocus. The way I burn out.</p>

<p>But I never fully understood why.</p>

<p>For most of my life, I assumed <a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a> was running the standard neurotypical operating system. And honestly? For 34 years, things mostly held together.</p>

<p>Until they didn’t.</p>

<p>Recently, the system started crashing harder and harder. Reboot loops. Overclocking. Total freezes. Patch after patch that only made things worse. Something underneath everything wasn’t operating the way I thought it was.</p>

<p><a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a> is not running a neurotypical operating system.</p>

<p>And this week, I finally got confirmation of something I think part of me has probably always known.</p>

<blockquote class="pullquote">"I was officially diagnosed with AuDHD (autism + ADHD)."</blockquote>

<p><img src="/images/2026-05-27/header.jpg" alt="Anxiety vs Autism + ADHD" /></p>

<p>Suddenly, decades of seemingly disconnected behaviors, struggles, sensitivities, coping mechanisms, hyperfixations, burnout cycles, and “quirks” started resolving into a system diagram that finally made sense.</p>

<h2 id="discovering-the-interfacearchitecture-mismatch">Discovering the interface/architecture mismatch</h2>

<p>We can’t just go from a semi-broken app to a perfectly stable release overnight though. No no no.</p>

<p>First, we apparently need to break the app even more 😅</p>

<p>And honestly, that was probably the hardest part of this journey.</p>

<p>There were both positive and negative cues that eventually pushed me toward getting a serious evaluation.</p>

<p>One of the biggest ones was noticing a recurring pattern: friends casually asking if I was ADHD or AuDHD. One person asking? Whatever. But eventually I realized there was a pattern I might have been ignoring for a very long time.</p>

<p>Which now feels a little ironic.</p>

<p>At the same time, I started noticing myself becoming increasingly overstimulated by things that never used to affect me this intensely:</p>
<ul>
  <li>sound</li>
  <li>lights</li>
  <li>too many conversations happening at once</li>
  <li>constant notifications</li>
  <li>context switching</li>
  <li>social overload</li>
</ul>

<p>For the longest time, I genuinely didn’t understand what was happening to me.</p>

<p>But life in your 20s and 30s is very different.</p>

<p>There’s more noise.<br />
More responsibilities.<br />
More unpredictability.<br />
More simultaneous processes running all the time.</p>

<p>And it became much harder to maintain the highly regulated systems and routines I had unconsciously built for myself over the years.</p>

<p>Systems I didn’t realize I <em>deeply</em> depended on to keep <a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a> stable.</p>

<h2 id="the-breaking-change">The breaking change</h2>

<p>Around September-ish of last year (2025), I decided it was time for a major version update of myself.</p>

<p>I ACTUALLY SCHEDULED AN APPOINTMENT WITH A THERAPIST.</p>

<p>FWIW, that felt like the hardest part at the time.</p>

<p>I was very, very wrong.</p>

<p>I went into that first appointment mostly looking to explore an ADHD diagnosis. I explained where my head was at, how overwhelmed I had been feeling, and how I was struggling with overstimulation that I just couldn’t “shake off” anymore.</p>

<p>I answered a lot of questions about my past, which made sense.</p>

<p>But something felt off.</p>

<p>I kept trying to explain that present-day Josh felt VERY different than past Josh. That was mostly brushed aside, which now feels like the first major red flag that I might have been heading down the wrong diagnostic path.</p>

<p>At that appointment, I was told that what I was experiencing could very well be anxiety.</p>

<p>Which honestly confused me even more.</p>

<p>I worry very little.<br />
I roll with the punches most of the time.<br />
Maybe even <em>too</em> much.</p>

<p>I’m obviously not the medical expert though, so I continued down that path and tried to stay open-minded.</p>

<p>Even while the conversation kept leaning toward anxiety, I still pushed for an ADHD assessment.</p>

<p>I was told there were two options:</p>
<ul>
  <li>a quicker computerized assessment</li>
  <li>a longer multi-session in-person evaluation</li>
</ul>

<p>The quick (and cheaper) route seemed reasonable to start with.</p>

<p>So I did it.</p>

<p>The results came back as essentially:
“traits of ADHD, but not enough to fully diagnose.”</p>

<p>Honestly, I was disappointed.</p>

<p>It felt like I had mostly been grouped into an “anxiety” bucket instead.</p>

<p>But at that point, I was desperate for relief from the overstimulation and internal chaos I had been experiencing.</p>

<p>So… I started the meds.</p>

<p>I didn’t really know what to expect. But I was willing to try almost anything if it meant stabilizing the system.</p>

<p>And technically… they worked.</p>

<p>The overstimulation from things like lights and sounds became quieter.</p>

<p>But so did everything else.</p>

<p>The meds didn’t just mute external noise.<br />
They muted <em>me</em>.</p>

<blockquote class="pullquote">"The meds didn't just mute external noise. They muted me."</blockquote>

<p>I became way too relaxed.<br />
Way too sleepy.<br />
Way too emotionally flat.</p>

<p>And maybe the weirdest part:</p>

<p>I lost my ability to enjoy building things.</p>

<p>FWIW, I’ve never actually loved “coding itself.” Coding has always been a means to an end for me. I love making things. Building systems. Creating weird ideas and watching them come alive.</p>

<p>But suddenly even <em>that</em> feeling was gone.</p>

<h3 id="the-burn-and-crash">The burn and crash</h3>

<p>Fast forward to February, and things got really bad.</p>

<p>I’ve learned that if I don’t intentionally schedule long stretches off work way in advance, I simply won’t take them.</p>

<p>Historically, those breaks still involved sitting at a computer doing computer things because… well… I’m a computer person 😅</p>

<p>But this time was different.</p>

<p>I spent almost an entire week unable to leave the couch.</p>

<p>No motivation.<br />
No energy.<br />
No desire to do anything.</p>

<p>Not even the things I normally <em>love</em> doing.</p>

<p>The second week was slightly better, but still deeply not me.</p>

<p>Eventually I needed to take a third week off work.</p>

<p>THIS IS VERY NOT <a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a>.</p>

<p>Honestly, writing this part now still kind of scares me.</p>

<p>I can’t imagine what it felt like for my teammates and manager hearing that something was this wrong.</p>

<p>And I never want to go back there again.</p>

<p>That was the moment I realized this “major version improvement” I had been chasing was actually moving me further away from myself instead of closer to understanding myself.</p>

<p>I had over-engineered in the completely wrong direction.</p>

<p>This wasn’t the right future for <a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a>.</p>

<p>My next therapy appointment needed to go very differently.</p>

<h2 id="revert-research-and-rebuild">Revert, research, and rebuild</h2>

<p>I’m <em>very</em> bad at explaining how I feel.</p>

<p>(Again, something that makes so much more sense now.)</p>

<p>I spent a solid week rubber ducking with ChatGPT before my next therapy appointment. I gave it context from previous appointments, explained how I was physically feeling, and tried to describe patterns about myself that I liked and didn’t like.</p>

<p>Eventually, we got to the point where it helped me put together a script to request a neuropsychological evaluation.</p>

<p>This is something I genuinely do not think I could have easily done on my own.</p>

<p>Having a robot help organize all of my scattered thoughts, patterns, and experiences into something structured and explainable was incredibly helpful.</p>

<p>The appointment finally came around, and for once things actually felt… easy.</p>

<p>I got exactly what I wanted from that session:</p>
<ul>
  <li>I wanted off the meds that were changing me</li>
  <li>I wanted further evaluation and answers</li>
</ul>

<p>Easy peasy!</p>

<p>For now…</p>

<h3 id="hardest-revert-ever">Hardest revert ever</h3>

<p>I’ve reverted <em>a lot</em> of commits and deployments in my life.</p>

<p>(I’m extremely good at making mistakes and then realizing I made them 😅)</p>

<p>But this revert was different.</p>

<p>I had to taper off the meds slowly, and I was absolutely not prepared for what that process was going to feel like.</p>

<p>This wasn’t a quick rollback.</p>

<p>It was a carefully managed 6-week process filled with physical and mental side effects.</p>

<p>The biggest ones for me were:</p>
<ul>
  <li>exhaustion</li>
  <li>low motivation</li>
  <li>emotional flatness</li>
  <li>and “brain zaps”</li>
</ul>

<p>If you’ve never experienced brain zaps before, they’re incredibly weird. The best way I can describe them is:</p>
<ul>
  <li>a physical zap sensation in your brain</li>
  <li>combined with the feeling of a skipped audio track</li>
  <li>or dropped frames in real life</li>
</ul>

<p>It’s kind of dizziness.<br />
But also… not really.</p>

<p>Anyway, those 6 weeks obviously affected everything around me:</p>
<ul>
  <li>personal life</li>
  <li>work</li>
  <li>relationships</li>
  <li>productivity</li>
  <li>energy</li>
</ul>

<p>I wanted out of the loop badly, but getting out required pushing through one of the hardest transitions I’ve gone through in a long time.</p>

<p>At the exact same time, I was also transitioning at work from Engineering Manager back into an Individual Contributor role.</p>

<p>And honestly? It was kind of my dream role.</p>

<p>I was moving into a much more flexible “free agent” style Developer Experience position where I could build weird things, help developers, experiment more, and operate in ways that fit my brain significantly better.</p>

<p>The timing could not have been worse 😅</p>

<p>I was in the middle of:</p>
<ul>
  <li>low productivity</li>
  <li>low motivation</li>
  <li>physical side effects</li>
  <li>emotional instability</li>
  <li>burnout recovery</li>
</ul>

<p>I <em>knew</em> I could do amazing things in this new role.</p>

<p>But I just… couldn’t.</p>

<p>And that disconnect was brutal.</p>

<h3 id="researching-how-my-brain-operates">Researching how my brain operates</h3>

<p>At the same time, I was also going through the neuropsychological evaluation process.</p>

<p>There was:</p>
<ol>
  <li>a long remote intake session</li>
  <li>a giant in-person testing day</li>
  <li>a final results appointment</li>
</ol>

<p>The first appointment was actually kind of… fun?</p>

<p>For the first time in this entire process, I felt like I was talking to someone who genuinely listened and understood me.</p>

<p>She asked questions nobody had asked me before.</p>

<p>And somehow, in a single hour, it felt like we had made more progress than the previous 6 months combined.</p>

<p>The second appointment was… SOMETHING.</p>

<p>It was around 4-ish hours of puzzles, memory tests, attention challenges, pattern recognition, and various forms of “how does this brain actually operate?”</p>

<p>Some parts were really fun.</p>

<p>Others, especially around memory and attention, were honestly pretty defeating.</p>

<p>By the end of it though, I felt strangely optimistic.</p>

<p>Exhausted.<br />
Completely brain-fried.<br />
But optimistic.</p>

<p>For the first time in a long time, it felt like I was actually heading toward answers.</p>

<p>I didn’t know what those answers were going to be yet.</p>

<p>But ANSWERS.</p>

<h3 id="rebuilding-from-the-results">Rebuilding from the results</h3>

<p>Today, literally like 5 hours ago as I’m writing this, I got the results.</p>

<p>Waiting for them was brutal.</p>

<p>For two weeks, every possible scenario went through my head.</p>

<p>At the start of the appointment, we talked about how I felt the testing process went. Then we started going through the actual results:</p>
<ul>
  <li>areas where my brain excelled</li>
  <li>areas where it struggled</li>
  <li>areas where things were inconsistent or all over the place</li>
</ul>

<p>The overall picture ended up being pretty interesting.</p>

<p>Apparently I’m intelligent (obviously 😉).</p>

<p>I scored very high in:</p>
<ul>
  <li>visual thinking</li>
  <li>spatial reasoning</li>
  <li>pattern recognition</li>
</ul>

<p>My working memory was decent.</p>

<p>But my verbal listening and verbal memory scores were… not great.</p>

<p>Which honestly surprised absolutely nobody, including me.</p>

<p>But the bigger conversations ended up being around:</p>
<ul>
  <li>sensory regulation</li>
  <li>social processing</li>
  <li>overstimulation</li>
  <li>masking</li>
  <li>cognitive fatigue</li>
</ul>

<p>And then came the final conclusions:</p>

<blockquote>
  <ol>
    <li>Primary autism</li>
    <li>Secondary ADHD</li>
  </ol>
</blockquote>

<p>I was so relieved to finally have an answer.</p>

<p>But honestly… I wasn’t expecting that order.</p>

<p>I had spent months mentally preparing for:
“primary ADHD with maybe some autistic traits.”</p>

<p>Not:</p>
<ol>
  <li>Autism</li>
  <li>ADHD</li>
</ol>

<p>That genuinely surprised me at first.</p>

<p>But the more I sat with it, the more the entire system architecture of my life suddenly started making sense.</p>

<p>The ADHD explained some of the chaos:</p>
<ul>
  <li>jumping between ideas</li>
  <li>novelty seeking</li>
  <li>hyperfocus spirals</li>
  <li>difficulty regulating attention</li>
  <li>impulsive project energy</li>
</ul>

<p>But autism explained the <em>structure underneath everything</em>:</p>
<ul>
  <li>the deep need for predictable systems</li>
  <li>why I unconsciously built highly regulated routines</li>
  <li>sensory overload</li>
  <li>why social interaction can feel both energizing and exhausting</li>
  <li>why I obsess over reducing friction in workflows</li>
  <li>why I need recovery after high-output events</li>
  <li>why I communicate better through systems than raw emotion</li>
  <li>why I’ve spent most of my life designing environments that help stabilize <a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a></li>
</ul>

<p>It also explained something I had never fully understood about myself:</p>

<p>I don’t just like systems.</p>

<p>I depend on them.</p>

<blockquote class="pullquote">"I don't just like systems. I depend on them."</blockquote>

<p>And when enough of those systems break at once, the operating system underneath starts overheating fast.</p>

<h2 id="the-beta-of-joshholtzapp-20">The beta of <a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a> 2.0</h2>

<p>So… this is where I’m at today.</p>

<p>I’m fully off the medication now, minus a few lingering brain zappies here and there, and I feel like I’m back to the version of <a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a> that somehow managed to work surprisingly well for 34 years under very specific conditions.</p>

<p>“I’m so back” is the internet phrase, and honestly? Yeah… I do feel that.</p>

<p>But…</p>

<p>That’s not actually good enough for me anymore.</p>

<p>I’m not trying to get back to the old version of myself.</p>

<p>I finally understand the operating system I’m running on now.</p>

<p><a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a> 2.0 is being rebuilt for neurodivergent architecture from the start.</p>

<p>And honestly, that changes everything.</p>

<p>YOLO monkey-patching random meds onto the system is probably not the strategy 😅</p>

<p>I need to:</p>
<ul>
  <li>actually read the docs</li>
  <li>understand the hardware</li>
  <li>talk to people running similar systems</li>
  <li>build healthier defaults</li>
  <li>stop overclocking the operating system until it crashes</li>
</ul>

<p>This part is going to take time.</p>

<p>But for the first time in a very long time, it actually feels like I’m solving the <em>right</em> problem.</p>

<p>I don’t need to force <a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a> to run like a neurotypical operating system.</p>

<p>I need to build an environment where this operating system can run efficiently, sustainably, and without constantly overheating itself trying to look “normal.”</p>

<h2 id="stay-tuned-for-official-release">Stay tuned for official release</h2>

<p>I don’t have an official release date for <a href="https://joshholtz.app" target="_blank">JoshHoltz.app</a> 2.0.</p>

<p>And honestly, trying to force one probably defeats the point.</p>

<p>Instead, I’m going to do what I do best:</p>
<ul>
  <li>find patterns</li>
  <li>solve real problems</li>
  <li>build weird things</li>
  <li>accidentally create new problems</li>
  <li>iterate</li>
  <li>have fun doing it</li>
</ul>

<p>But this time, hopefully in a way that isn’t actively toxic to my mental and physical health.</p>

<p>And genuinely, thank you to my friends, family, coworkers, and the people around me who experienced this whole thing with me.</p>

<p>I know it wasn’t easy.</p>

<p>I disappeared.<br />
I went dark.<br />
I changed.</p>

<p>But looking back now… that version of me was running on a system that was completely overwhelmed and trying desperately to compensate.</p>

<p>I think I finally understand why now.</p>

<p>And honestly?</p>

<p>That feels really good.</p>

<hr />

<p><em>This is just my experience. Yours might look completely different — and that’s okay.</em></p>]]></content><author><name>joshdholtz</name></author><category term="blog" /><category term="life" /><category term="adhd" /><category term="autism" /><category term="audhd" /><category term="mentalhealth" /><summary type="html"><![CDATA[For 34 years, JoshHoltz.app ran on an operating system I didn't fully understand. This week, I finally got a diagnosis that made the whole system architecture make sense.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joshholtz.com/images/2026-05-27/og.jpg" /><media:content medium="image" url="https://www.joshholtz.com/images/2026-05-27/og.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Deep Dish Swift - CFP Speaker Selection</title><link href="https://www.joshholtz.com/blog/2025/02/09/deep-dish-swift-cfp-speaker-selection.html" rel="alternate" type="text/html" title="Deep Dish Swift - CFP Speaker Selection" /><published>2025-02-09T06:00:00+00:00</published><updated>2025-02-09T06:00:00+00:00</updated><id>https://www.joshholtz.com/blog/2025/02/09/deep-dish-swift-cfp-speaker-selection</id><content type="html" xml:base="https://www.joshholtz.com/blog/2025/02/09/deep-dish-swift-cfp-speaker-selection.html"><![CDATA[<p>I’m so thankful to have been a part of so many rewarding projects and experiences in my life. But starting <a href="https://deepdishswift.com">Deep Dish Swift</a> has to be one of the best.</p>

<p>I’ve had a stutter since I was about six years old. I hated talking. I hated being the center of attention. I couldn’t speak in front of people. But in 2019, I found the courage to change that. Since then, I’ve spoken at iOS and Swift conferences all over the world. I can’t get enough of this community and sharing my passions with you all.</p>

<p>But beyond that, I have the honor of organizing Deep Dish Swift with my wife, Kari. In 2022, I told her that I may have accidentally started a conference. Without hesitation, she responded, “How can I help?” From that moment, I knew this was going to be something great. She’s also a conference speaker and an engineer, so we aligned on what needed to be done. But she’s the organized one who balances out my chaos. This conference would be nothing without her.</p>

<p>With her help, we’ve had two incredible years of Deep Dish Swift, with a third on the way. There are a lot of challenges in running a conference, but the joy we get from seeing it all come together makes it worth it.</p>

<p>However, there’s one part of organizing a conference that always feels the worst—the CFP (Call for Proposals) process.</p>

<p>The first year of Deep Dish Swift, I did a poor job communicating with speakers whose talks we didn’t accept. I didn’t know exactly what I was doing, so I just used the default response in the CFP system. It wasn’t sincere. It wasn’t me. But I also didn’t know how to do it better. The second year, I wrote something more personal and sincere, but I still don’t think it fully conveyed why and how we select talks.</p>

<p>This year was different. I think I finally understand how we pick talks. This is the email I sent to the speakers whose talks we couldn’t accept this year 👇</p>

<blockquote>
  <p>Dear Deep Dish CFP Submitter,</p>

  <p>This is always the hardest email for me to send, but it’s something I have to do… I regret to inform you that your talk submission(s) for Deep Dish Swift 2025 has not been accepted this year. 😔</p>

  <p>As a conference speaker myself, I know how heartbreaking this can be. I really would love to have all of you on the Deep Dish stage. We pour so much time, energy, and hope into crafting a talk submission.</p>

  <p>I’ve been thinking about how best to explain how we select talks, and I think I finally have it.</p>

  <p>Putting together a speaker lineup is a lot like making a deep dish pizza. We need solid foundational topics and speakers–the crust that holds everything together. We need the classic topics that most conferences have, acting like the cheese and sauce. And then, we need the toppings–those unique, standout ideas that set the conference apart, whether it’s a bold technical deep dive, an unexpected perspective, or something experimental.</p>

  <p>There are so many great toppings I’d love to include, but a good pizza isn’t <em>just</em> toppings. A good pizza is balanced, a little unique, and has something for everyone.</p>

  <p>So the question isn’t, <em>“How can my talk be better?”</em>–because in most cases, it’s not about quality. Every ingredient in a pizza is great on its own. The better question is, <em>“How can my talk fit into future conferences?”</em></p>

  <p>Some ingredients work well in most pizzas. Some don’t–but when used right, they become the star of the dish. Figure out what kind of ingredient you are and showcase it as soon as possible. Have a couple of different options in mind to improve your chances. You can even reach out to the “chefs” (organizers) before they start planning the next pizza.</p>

  <p>I hope this helps clarify why some talks are selected over others. It’s not just about how good a talk is–it’s about how all the talks come together to form something special.</p>

  <p>I truly appreciate everyone who submitted talks this year and in previous years. Please don’t stop! There’s always a chance, and always ways to increase that chance. If you’re passionate about speaking, keep showing it. Your special ingredient will find its place.</p>

  <p>Hope to see you all on a future Deep Dish stage–or another conference stage–very soon!</p>

  <p>Love,<br />
Josh and Kari Holtz</p>
</blockquote>

<p>That is all.</p>

<p>Love,
<br />Josh</p>

<p>🍕 Also, please check if <a href="https://deepdishswift.com/">Deep Dish Swift</a> if you want to attend a super fun, useful, and welcoming Swift and iOS conference! It’s being help <strong>April 27th to April 29th</strong> this year in <strong>Chicago, IL</strong>. It’s our third year and it’s going to be a good one!</p>]]></content><author><name>joshdholtz</name></author><category term="blog" /><category term="deepdishswift" /><category term="conference" /><summary type="html"><![CDATA[The CFP process for Deep Dish Swift is heartbreaking. I want everybody to be on the stage but its just not possible. Here is how I communicated it to the the talks that we could not accept.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joshholtz.com/images/2025-02-09/header.png" /><media:content medium="image" url="https://www.joshholtz.com/images/2025-02-09/header.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">SwiftUI - Navigation View If Needed</title><link href="https://www.joshholtz.com/blog/2025/02/08/swiftui-navigation-view-if-needed.html" rel="alternate" type="text/html" title="SwiftUI - Navigation View If Needed" /><published>2025-02-08T06:00:00+00:00</published><updated>2025-02-08T06:00:00+00:00</updated><id>https://www.joshholtz.com/blog/2025/02/08/swiftui-navigation-view-if-needed</id><content type="html" xml:base="https://www.joshholtz.com/blog/2025/02/08/swiftui-navigation-view-if-needed.html"><![CDATA[<p><em>Taps microphone</em></p>

<p>Is this thing on?</p>

<p>Okay wow, it’s been nearly 3 years since I last wrote a blog post. I would normaly say here that I’ve been meaning to write more blog posts but that would be a lie. I already get myself into too much trouble working on other things 😅</p>

<p>Are you new here? Wondering what those things are?</p>

<p>The projects I’ll be working on (but not limited to) are:</p>

<ul>
  <li>Co-founder of <a href="https://deepdishswift.com/">Deep Dish Swift</a></li>
  <li>Lead maintainer of <a href="https://fastlane.tools/">fastlane</a></li>
  <li>Engineer Manager of Monetization/Paywalls at <a href="https://www.revenuecat.com/">RevenueCat</a></li>
  <li>Apps I’ve shipped
    <ul>
      <li><a href="https://anotterrss.com/">An Otter RSS</a></li>
      <li><a href="https://connectkit.app/">ConnectKit</a></li>
      <li><a href="https://apps.apple.com/app/id1608719208">What’s My Age Again</a></li>
      <li><a href="https://apps.apple.com/app/id1608719208">Oh Crop!</a></li>
      <li><a href="https://twitter.com/playpenapp">Playpen</a></li>
    </ul>
  </li>
  <li>Other Stuff
    <ul>
      <li><a href="https://github.com/joshdholtz/DeckUI">DeckUI</a></li>
    </ul>
  </li>
</ul>

<p>But besides <em>that</em>, I also have a family that I love to spend time with that takes priority over all of this.</p>

<p>You aren’t here for that though… you are here because you want to know the madness behind my <strong>“Navigation If Needed” SwiftUI view</strong>.</p>

<p>👉 BTW, <a href="#the-solution-that-i-ended-up-with">click here</a> to jump to the solution way way way way way way at the bottom.</p>

<h2 id="the-problem">The Problem</h2>

<p>Okay, here is the problem I was running into.</p>

<p>I have a view that has a requirement to show a toolbar with some buttons on it. (See example below)</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">MyViewWithToolbar</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span> <span class="p">{</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"This is not a Xib or Storyboard view"</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="n">toolbar</span> <span class="p">{</span>
            <span class="kt">ToolbarItem</span><span class="p">(</span><span class="nv">placement</span><span class="p">:</span> <span class="o">.</span><span class="n">navigationBarTrailing</span><span class="p">)</span> <span class="p">{</span>
                <span class="kt">Button</span><span class="p">(</span><span class="s">"Restore"</span><span class="p">)</span> <span class="p">{</span>
                    <span class="c1">// Restore the user's content</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Looking at this view, it <em>seems</em> like the toolbar should be visible. However, a toolbar in SwiftUI will only show if the view is in a <code class="language-plaintext highlighter-rouge">NavigationView</code> or <code class="language-plaintext highlighter-rouge">NavigationStack</code> (which makes sense).</p>

<p>If I was making this view for some of my own apps, I’m 100% in control of the presentation logic so I would know whether I needed to wrap this view in a <code class="language-plaintext highlighter-rouge">NavigationView</code> or not.</p>

<p>However, I am making this view for a SDK and I don’t want the user to have to worry about this. I need <code class="language-plaintext highlighter-rouge">MyViewWithToolbar</code> to know if it should wrap itself in a <code class="language-plaintext highlighter-rouge">NavigationView</code> or not.</p>

<h2 id="the-solution-that-i-wanted">The Solution (that I wanted)</h2>

<p>I wanted a nice, clean built-in solution to this problem. I was looking if SwiftUI had any way to detect if a view was in a navigation stack or not. Long story short, it doesn’t.</p>

<p>I scoured the web and asked all the AI chat friends that I’ve made and I couldn’t find any “nice” solutions to this. Some suggested to drop down into UIKit and inspect the view hierarchy. I didn’t want to do that. I was looking for a cleaner and pure SwiftUI solution.</p>

<h2 id="the-discovery">The Discovery</h2>

<p>After doing <del>hours</del> minutes of research, I concluded that the only pure SwiftUI implementation that could would know anything if a SwiftUI View was in a navigation controller was the <code class="language-plaintext highlighter-rouge">.toolbar</code> modifier… but not in the way I wanted. The <code class="language-plaintext highlighter-rouge">.toolbar</code> modifier adds the toolbar to a view <strong>BUT ONLY</strong> if its in a <code class="language-plaintext highlighter-rouge">NavigationView</code> or <code class="language-plaintext highlighter-rouge">NavigationStack</code> (as I mentioned before).</p>

<p>I started to wonder if I could use the <code class="language-plaintext highlighter-rouge">.toolbar</code> modifier as a detector of sorts. The toolbar content can take any view and views have a way to see if they appear so I thought I could use that to my advantage.</p>

<h3 id="attempt-1-using-onappear">Attempt 1: Using onAppear</h3>

<p>My first attempt was simple. I wanted to see if using <code class="language-plaintext highlighter-rouge">.onAppear</code> would work. I knew that the <code class="language-plaintext highlighter-rouge">.toolbar</code> would get conditionally added based on if the view is in a <code class="language-plaintext highlighter-rouge">NavigationView</code> or <code class="language-plaintext highlighter-rouge">NavigationStack</code> so I added a <code class="language-plaintext highlighter-rouge">.onAppear</code> to some content in the toolbar.</p>

<p>Summary… it didn’t work as I hoped. It turned out that the <code class="language-plaintext highlighter-rouge">.toolbar</code> would always call <code class="language-plaintext highlighter-rouge">.onAppear</code> for the content of the header no matter if it was in a <code class="language-plaintext highlighter-rouge">NavigationView</code> or not.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Attempt1View</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"Something"</span><span class="p">)</span>
            <span class="o">.</span><span class="n">toolbar</span> <span class="p">{</span>
                <span class="kt">ToolbarItem</span><span class="p">(</span><span class="nv">placement</span><span class="p">:</span> <span class="o">.</span><span class="n">navigationBarTrailing</span><span class="p">)</span> <span class="p">{</span>
                    <span class="kt">Text</span><span class="p">(</span><span class="s">""</span><span class="p">)</span>
                    <span class="o">.</span><span class="n">onAppear</span> <span class="p">{</span>
                        <span class="c1">// I want this to only appear if the view is in a navigation controller</span>
                        <span class="nf">print</span><span class="p">(</span><span class="s">"Please work"</span><span class="p">)</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Goal: I wanted "Please work" to print</span>
<span class="c1">// Result: It did print (yay)</span>
<span class="kd">struct</span> <span class="kt">InNavigationView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">NavigationView</span> <span class="p">{</span>
            <span class="kt">Attempt1View</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Goal: I wanted "Please work" NOT to print</span>
<span class="c1">// Result: It DID print (not yay)</span>
<span class="kd">struct</span> <span class="kt">NoInNavigationView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Attempt1View</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="attempt-2-using-environmentispresented">Attempt 2: Using @Environment(.isPresented)</h3>

<p><code class="language-plaintext highlighter-rouge">.onAppear</code> was a total lie. It actually isn’t when the view visually appears. I guess its when the view is in some sort of UI hierarchy (not a SwiftUI expert here so take this for what it’s worth).</p>

<p>I did some more <del>Googling</del> <del>StackOverflowing</del> AI-ing and I found I could detect if a view appeared by using the <code class="language-plaintext highlighter-rouge">@Environment(\.isPresented)</code> environment variable.</p>

<p>It turns out… this was exactly what I wanted. I ended up creating a navigation detector! 🥳 But like… one that would only tell me if it it detected a navigation view through a print statement 😛 But hey, I could build upon this.</p>

<p>First, here is the <code class="language-plaintext highlighter-rouge">IShouldGoToBedView</code> that could do the navigation detection.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">IShouldGoToBedView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@Environment</span><span class="p">(\</span><span class="o">.</span><span class="n">isPresented</span><span class="p">)</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isPresented</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">""</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">onChange</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="n">isPresented</span><span class="p">)</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">isPresented</span> <span class="k">in</span>
            <span class="k">if</span> <span class="n">isPresented</span> <span class="p">{</span>
                <span class="nf">print</span><span class="p">(</span><span class="s">"Please work"</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">Attempt2View</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"Something"</span><span class="p">)</span>
            <span class="o">.</span><span class="n">toolbar</span> <span class="p">{</span>
                <span class="kt">ToolbarItem</span><span class="p">(</span><span class="nv">placement</span><span class="p">:</span> <span class="o">.</span><span class="n">navigationBarTrailing</span><span class="p">)</span> <span class="p">{</span>
                    <span class="kt">IShouldGoToBedView</span><span class="p">()</span>
                <span class="p">}</span>
            <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Goal: I wanted "Please work" to print</span>
<span class="c1">// Result: It did print (yay)</span>
<span class="kd">struct</span> <span class="kt">InNavigationView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">NavigationView</span> <span class="p">{</span>
            <span class="kt">Attempt2View</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Goal: I wanted "Please work" NOT to print</span>
<span class="c1">// Result: It did NOT print (yay)</span>
<span class="kd">struct</span> <span class="kt">NoInNavigationView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Attempt2View</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-solution-that-i-ended-up-with">The Solution (that I ended up with)</h2>

<p>:warning: <strong>Warning:</strong> This solution is a bit of a hack and I’m <strong>so proud</strong> of it. I’m sharing it because I didn’t see this solution anywhere else (probably for good reasons) but I think it’s a fun solution and could <em>maybe</em> be helpful to someone else.</p>

<h3 id="usage">Usage</h3>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="c1">// This will conditionally add a NavigationView around MyViewWithToolbar if needed</span>
        <span class="kt">NavigationViewIfNeeded</span> <span class="p">{</span>
            <span class="kt">MyViewWithToolbar</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="video-proof">Video Proof</h3>

<video width="100%" poster="/images/2025-02-08/navigation-if-needed-demo.png" controls="">
  <source src="/images/2025-02-08/navigation-if-needed-demo.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>

<h3 id="implementation">Implementation</h3>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// This minimal view does a check if the view is visible rendered</span>
<span class="kd">struct</span> <span class="kt">ZeroFrameDetectionView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@Environment</span><span class="p">(\</span><span class="o">.</span><span class="n">isPresented</span><span class="p">)</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isPresented</span>
    <span class="k">let</span> <span class="nv">didDetect</span><span class="p">:</span> <span class="p">(</span><span class="kt">Bool</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span>

    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">hasReported</span> <span class="o">=</span> <span class="kc">false</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Rectangle</span><span class="p">()</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">onChange</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="n">isPresented</span><span class="p">)</span> <span class="p">{</span> <span class="n">newValue</span> <span class="k">in</span>
                <span class="k">if</span> <span class="n">newValue</span> <span class="p">{</span>
                    <span class="k">self</span><span class="o">.</span><span class="nf">report</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="n">onAppear</span> <span class="p">{</span>
                <span class="c1">// Dispatch once after SwiftUI lay out</span>
                <span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="nf">asyncAfter</span><span class="p">(</span><span class="nv">deadline</span><span class="p">:</span> <span class="o">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="mf">0.05</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">self</span><span class="o">.</span><span class="nf">report</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">report</span><span class="p">(</span><span class="n">_</span> <span class="nv">value</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="o">!</span><span class="k">self</span><span class="o">.</span><span class="n">hasReported</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
        <span class="k">self</span><span class="o">.</span><span class="n">hasReported</span> <span class="o">=</span> <span class="kc">true</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">didDetect</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">NavigationViewIfNeeded</span><span class="o">&lt;</span><span class="kt">Content</span><span class="p">:</span> <span class="kt">View</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">enum</span> <span class="kt">Status</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">unknown</span>
        <span class="k">case</span> <span class="n">inNav</span>
        <span class="k">case</span> <span class="n">notInNav</span>
    <span class="p">}</span>

    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">status</span><span class="p">:</span> <span class="kt">Status</span> <span class="o">=</span> <span class="o">.</span><span class="n">unknown</span>

    <span class="k">let</span> <span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span>

    <span class="nf">init</span><span class="p">(</span><span class="kd">@ViewBuilder</span> <span class="nv">content</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Content</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">content</span> <span class="o">=</span> <span class="nf">content</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">switch</span> <span class="n">status</span> <span class="p">{</span>
        <span class="k">case</span> <span class="o">.</span><span class="nv">unknown</span><span class="p">:</span>
            <span class="c1">// This is where we wait for a (hopefully quick) response</span>
            <span class="c1">// if the view is in a navigation view or not</span>
            <span class="kt">Rectangle</span><span class="p">()</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
                <span class="o">.</span><span class="n">toolbar</span> <span class="p">{</span>
                    <span class="c1">// Zero-sized detection view:</span>
                    <span class="kt">ZeroFrameDetectionView</span> <span class="p">{</span> <span class="n">isInNav</span> <span class="k">in</span>
                        <span class="c1">// The first time we know the answer, store it</span>
                        <span class="k">if</span> <span class="n">status</span> <span class="o">==</span> <span class="o">.</span><span class="n">unknown</span> <span class="p">{</span>
                            <span class="n">status</span> <span class="o">=</span> <span class="n">isInNav</span> <span class="p">?</span> <span class="o">.</span><span class="nv">inNav</span> <span class="p">:</span> <span class="o">.</span><span class="n">notInNav</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                <span class="p">}</span>
        <span class="c1">// If the view is in a navigation view, show the content directly</span>
        <span class="k">case</span> <span class="o">.</span><span class="nv">inNav</span><span class="p">:</span>
            <span class="n">content</span>
                <span class="o">.</span><span class="n">onAppear</span> <span class="p">{</span>
                    <span class="nf">print</span><span class="p">(</span><span class="s">"✅ IN Navigation"</span><span class="p">)</span>
                <span class="p">}</span>
        <span class="c1">// If the view is not in a navigation view, wrap it in a navigation</span>
        <span class="k">case</span> <span class="o">.</span><span class="nv">notInNav</span><span class="p">:</span>
            <span class="k">if</span> <span class="kd">#available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)</span> <span class="p">{</span>
                <span class="c1">// Using NavigationStack is best generic solution if only need</span>
                <span class="c1">// to show a toolbar</span>
                <span class="c1">// NavigatonStack toolbars combine nicely in parent NavigationView</span>
                <span class="kt">NavigationStack</span> <span class="p">{</span>
                    <span class="n">content</span>
                        <span class="o">.</span><span class="n">onAppear</span> <span class="p">{</span>
                            <span class="nf">print</span><span class="p">(</span><span class="s">"❌ NOT IN Navigation"</span><span class="p">)</span>
                        <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="kt">NavigationView</span> <span class="p">{</span>
                    <span class="n">content</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="full-test-suite-lol">Full Test Suite (lol)</h3>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">NavigationView</span> <span class="p">{</span>
            <span class="kt">VStack</span> <span class="p">{</span>
                <span class="kt">NavigationViewIfNeeded</span> <span class="p">{</span>
                    <span class="kt">Text</span><span class="p">(</span><span class="s">"ewfawefaw"</span><span class="p">)</span>
                        <span class="o">.</span><span class="n">toolbar</span> <span class="p">{</span> <span class="c1">// wont show without above ^^</span>
                            <span class="kt">Button</span><span class="p">(</span><span class="nv">action</span><span class="p">:</span> <span class="p">{</span>

                            <span class="p">})</span> <span class="p">{</span>
                                <span class="kt">Text</span><span class="p">(</span><span class="s">"Hey"</span><span class="p">)</span>
                            <span class="p">}</span>
                        <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="n">toolbar</span> <span class="p">{</span>
                <span class="kt">Button</span><span class="p">(</span><span class="nv">action</span><span class="p">:</span> <span class="p">{</span>

                <span class="p">})</span> <span class="p">{</span>
                    <span class="kt">Text</span><span class="p">(</span><span class="s">"And another"</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-end">The End</h2>

<p>This is what it is. Code does what it does. There are guaranteed edge cases in this. The implementation of <code class="language-plaintext highlighter-rouge">.toolbar</code> and <code class="language-plaintext highlighter-rouge">@Environment(\.isPresented)</code> could change from underneith me.</p>

<p>But I enjoyed problem solving this and I’m don’t hate the solution. I’ve written worse code 🙃</p>

<p>Please let me know your thoughts on this. I’m curious if this is a good solution or if there is a better way to do this. But also PLEASE be nice 😊 As I’ve mentioned, I did this for fun, shared it for knowledge, and warned this is hacky.</p>

<p>That is all.</p>

<p>Love,
<br />Josh</p>

<p>🍕 Also, please check if <a href="https://deepdishswift.com/">Deep Dish Swift</a> if you want to attend a super fun, useful, and welcoming Swift and iOS conference! It’s being help <strong>April 27th to April 29th</strong> this year in <strong>Chicago, IL</strong>. It’s our third year and it’s going to be a good one!</p>]]></content><author><name>joshdholtz</name></author><category term="blog" /><category term="swiftui" /><category term="navigation" /><category term="hacks" /><category term="fun" /><summary type="html"><![CDATA[I needed a way to add a toolbar in a view in SwiftUI without knowing if that view came from a navigation stack or not.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joshholtz.com/images/2025-02-08/header.png" /><media:content medium="image" url="https://www.joshholtz.com/images/2025-02-08/header.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Hudson’s First Blog Post</title><link href="https://www.joshholtz.com/blog/2022/04/08/hudson-first-blog-post.html" rel="alternate" type="text/html" title="Hudson’s First Blog Post" /><published>2022-04-08T07:30:00+00:00</published><updated>2022-04-08T07:30:00+00:00</updated><id>https://www.joshholtz.com/blog/2022/04/08/hudson-first-blog-post</id><content type="html" xml:base="https://www.joshholtz.com/blog/2022/04/08/hudson-first-blog-post.html"><![CDATA[<blockquote>
  <p>I gave Hudson my computer to type up his first blog post 🤷‍♂️</p>
</blockquote>

<h2 id="hudsons-first-blog-post">Hudson’s First Blog Post</h2>

<p>Zzx•¨∫˜˜∫ˆ•≥ …,azaz˘≥………………..//////////////<code class="language-plaintext highlighter-rouge">/</code>````./`./////////././//////]]]]]/</p>

<p>sxxdsxx.     ˜n</p>

<p>zr zcfgty5ttgfcdx cdvfgryhe5dvzsthy6htgfvcxxdr56r
3</p>

<p>azszxc vjnb ygvc cvbm/,’k;lkjhbvcvbhjij
≈<strong>‌gfxdxzzxçgchcgbvxc cfgchvjbknnbhjnhbvcx vgysghfsgvc xcvy78ygfcxddxsz</strong>
wqwssswssssiiiioiioilz</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dxzxdf````````````````zzz n ham   vcxx/.v bnm,a	
</code></pre></div></div>

<p>xxcx cxztfdxzzdsxzfdxΩxçcv ezxcxvc≈</p>

<p><img src="/images/2022-04-08/hudson2.jpeg" width="300" /></p>]]></content><author><name>joshdholtz</name></author><category term="blog" /><summary type="html"><![CDATA[I gave Hudson my computer to type up his first blog post]]></summary></entry><entry><title type="html">Launch Announcement: What’s My Age Again for iOS</title><link href="https://www.joshholtz.com/blog/2022/02/15/launching-whats-my-age-again.html" rel="alternate" type="text/html" title="Launch Announcement: What’s My Age Again for iOS" /><published>2022-02-15T07:30:00+00:00</published><updated>2022-02-15T07:30:00+00:00</updated><id>https://www.joshholtz.com/blog/2022/02/15/launching-whats-my-age-again</id><content type="html" xml:base="https://www.joshholtz.com/blog/2022/02/15/launching-whats-my-age-again.html"><![CDATA[<p>Oops, I did it again… I accidently created and launched another app.</p>

<p><strong>Happy launch day, What’s My Age Again!</strong> 🥳</p>

<p><a href="https://apps.apple.com/ro/app/whats-my-age-again/id1608719208" target="_blank">
  <img src="/images/Download_on_App_Store.svg" />
</a></p>

<hr />

<h2 id="what-is-whats-my-age-again"><em>What is What’s My Age Again?</em></h2>

<p><strong>What’s My Age Again</strong> is my solution to me not being able to remember my age. I’m always asking my wife and she doesn’t need that kind of pressure on her. I threw together a quick widget only app in an hour or two 👇</p>

<p><img src="/images/2022-02-15/screenshot1.png" width="300" />
<br />
<img src="/images/2022-02-15/screenshot2.png" width="300" /></p>

<p>And as I do often, I shared my weird experiment on Twitter 👇 It seemed like it resonated with a handful of people so I decided to polish it and ship it!</p>

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">I’m constantly asking my wife how old I am so… I made a thing 🙈<br /><br />“What’s My Age Again” 👇<br /><br />✨ Configure birthdays in widget (app does nothing)<br /><br />😅 Also configure family members age! Great for kids when they go from days to weeks to months to years<br /><br />Maybe launch soon? 🤷‍♂️ <a href="https://t.co/WVFsm4Ymfn">pic.twitter.com/WVFsm4Ymfn</a></p>&mdash; Josh “so many typos” Holtz 💪🚀 (@joshdholtz) <a href="https://twitter.com/joshdholtz/status/1490060893383798789?ref_src=twsrc%5Etfw">February 5, 2022</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<h2 id="why-only-widgets"><em>Why only widgets?</em></h2>

<p>I already debatably stretch myself to thin with <a href="https://fastlane.tools">fastlane</a>, <a href="https://indiedevmonday.com">Indie Dev Monday</a>, other indie apps, and my full-time job at <a href="https://www.revenuecat.com">RevenueCat</a> where I really didn’t want to make and maintain another app 😛 Widgets are a simple enough concept to make and maintain and that is all I really wanted to do. They have very little interaction and a simple user interface that doesn’t overwhelm me if and when I want to make changes.</p>

<h2 id="here-are-we"><em>Here are we</em></h2>

<p>This app (if you can call it that) isn’t meant to be some ground breaking huge thing. I wanted this for me but I wanted others to use it (if they wanted to).</p>

<p>Thanks for reading this very short story about <strong>What’s My Age Again</strong> and I hope that some of you enjoy using this app!</p>

<p>Feel free to tweet me (<a href="https://twitter.com/joshdholtz">@joshdholtz</a>) with any feedback, issues, or feature request🥳</p>

<p><a href="https://apps.apple.com/ro/app/whats-my-age-again/id1608719208" target="_blank">
  <img src="/images/Download_on_App_Store.svg" />
</a></p>]]></content><author><name>joshdholtz</name></author><category term="blog" /><category term="indie" /><category term="whats-my-age-again" /><category term="annoucement" /><summary type="html"><![CDATA[iOS app for remembering your age (and others) with widgets]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joshholtz.com/images/2022-02-15/marketing.png" /><media:content medium="image" url="https://www.joshholtz.com/images/2022-02-15/marketing.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Josh’s M1 Mac Development Environment - homebrew, zsh, Ruby and python version managers</title><link href="https://www.joshholtz.com/blog/2021/10/27/joshs-m1-development-environemnt.html" rel="alternate" type="text/html" title="Josh’s M1 Mac Development Environment - homebrew, zsh, Ruby and python version managers" /><published>2021-10-27T07:30:00+00:00</published><updated>2021-10-27T07:30:00+00:00</updated><id>https://www.joshholtz.com/blog/2021/10/27/joshs-m1-development-environemnt</id><content type="html" xml:base="https://www.joshholtz.com/blog/2021/10/27/joshs-m1-development-environemnt.html"><![CDATA[<p>Hello, it’s me! Josh Holtz. On my <a href="https://joshholtz.com">joshholtz.com</a> blog. I don’t know who else it would be 😅</p>

<p>This post will briefly show how I have my M1 Mac setup to handle homebrew, zsh, Ruby and Python version managers and how they all interact with Rosetta 💪</p>

<p>TL;DR - The problem was not homebrew or any other tools. I was my weird setup and misunderstanding of how M1 and Rosetta worked 🤷‍♂️</p>

<h2 id="the-youtube-format">The YouTube Format</h2>

<p>There is a YouTube format of this blog post on my <a href="https://youtube.com/joshdholtz">YouTube channel</a> over at <a href="https://youtu.be/EG-K5n20_HQ">https://youtu.be/EG-K5n20_HQ</a> 👀</p>

<h2 id="the-problem">The Problem</h2>

<p>I was having mad issues with my develoment environment. Ruby was having issues installing native extensions. Homebrew was having more and more issues installing dependencies for unknown reasons (to me).</p>

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Me, M1, and homebrew are just not getting along… again 😭</p>&mdash; Josh “so many typos” Holtz 💪🚀 (@joshdholtz) <a href="https://twitter.com/joshdholtz/status/1450495076644368391?ref_src=twsrc%5Etfw">October 19, 2021</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<p>After a day or two of constantly uninstalling and reinstalling homebrew and my other tools, I figured out my issue and my development environment that I wanted to use going forward.</p>

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">OMG, I finally figured out how to get my M1, homebrew, and Rubies playing nicely 😅<br /><br />Would anybody like a blog post or video tutorial on how to do this? 🙃</p>&mdash; Josh “so many typos” Holtz 💪🚀 (@joshdholtz) <a href="https://twitter.com/joshdholtz/status/1450501347908984838?ref_src=twsrc%5Etfw">October 19, 2021</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<p>It turns out… the root cause of my problem was I was using a build of <a href="https://github.com/alacritty/alacritty">Alacritty</a> (a terminal ) that was built for Intel so all of my stuff was running in Rosetta without me knowing 🤦‍♂️</p>

<hr />

<h2 id="being-cognizant-of-architecture">Being cognizant of architecture</h2>

<p>I would have solved my issues a lot sooner if I knew what architecture my terminal was running in. The options being <code class="language-plaintext highlighter-rouge">arm64</code> or <code class="language-plaintext highlighter-rouge">i386</code>.</p>

<h3 id="zsh-prompt">Zsh prompt</h3>

<p>So the first thing I did was add the architecture my terminal was running in into my Zsh prompt. The prompt is the thing the shows before your cursor when you are in a terminal. My prompt already shows the directory I’m in and the git branch of the directory (if I’m in a git repo). I thought it would be cool to also prefix that with the architecture!</p>

<p>This can be done with a custom theme. I was using the <code class="language-plaintext highlighter-rouge">robbyrussel</code> theme so I based the <code class="language-plaintext highlighter-rouge">joshdholtz</code> theme off of that 💪</p>

<p>Create <code class="language-plaintext highlighter-rouge">~/.oh-my-zsh/themes/joshdholtz.zsh-theme</code> 👇</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">PROMPT</span><span class="o">=</span><span class="s2">"%(?:%{</span><span class="nv">$fg_bold</span><span class="s2">[green]%}➜ </span><span class="si">$(</span><span class="nb">arch</span><span class="si">)</span><span class="s2"> :%{</span><span class="nv">$fg_bold</span><span class="s2">[red]%}➜ )"</span>
PROMPT+<span class="o">=</span><span class="s1">' %{$fg[cyan]%}%c%{$reset_color%} $(git_prompt_info)'</span>

<span class="nv">ZSH_THEME_GIT_PROMPT_PREFIX</span><span class="o">=</span><span class="s2">"%{</span><span class="nv">$fg_bold</span><span class="s2">[blue]%}git:(%{</span><span class="nv">$fg</span><span class="s2">[red]%}"</span>
<span class="nv">ZSH_THEME_GIT_PROMPT_SUFFIX</span><span class="o">=</span><span class="s2">"%{</span><span class="nv">$reset_color</span><span class="s2">%} "</span>
<span class="nv">ZSH_THEME_GIT_PROMPT_DIRTY</span><span class="o">=</span><span class="s2">"%{</span><span class="nv">$fg</span><span class="s2">[blue]%}) %{</span><span class="nv">$fg</span><span class="s2">[yellow]%}✗"</span>
<span class="nv">ZSH_THEME_GIT_PROMPT_CLEAN</span><span class="o">=</span><span class="s2">"%{</span><span class="nv">$fg</span><span class="s2">[blue]%})"</span>
</code></pre></div></div>

<p>I modified my <code class="language-plaintext highlighter-rouge">~/.zshrc</code> with <code class="language-plaintext highlighter-rouge">ZSH_THEME=joshdholtz</code> 👇</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">ZSH_DISABLE_COMPFIX</span><span class="o">=</span><span class="nb">true
export </span><span class="nv">ZSH</span><span class="o">=</span><span class="s2">"/Users/joshholtz/.oh-my-zsh"</span>

<span class="c"># joshdholtz theme shows arch type in the prompt</span>
<span class="nv">ZSH_THEME</span><span class="o">=</span><span class="s2">"joshdholtz"</span>
<span class="nv">plugins</span><span class="o">=(</span>git<span class="o">)</span>
<span class="nb">source</span> <span class="nv">$ZSH</span>/oh-my-zsh.sh
</code></pre></div></div>

<h3 id="switching-between-m1-arm64-and-rosetta-i386">Switching between M1 (arm64) and Rosetta (i386)</h3>

<p>It’s important to have a terminal that can run in Rosetta (i386) so that you can build/compile/install things that aren’t able to run on M1 yet. A lot of the suggestions I’ve gotten were to copy <code class="language-plaintext highlighter-rouge">Terminal.app</code> and rename it to <code class="language-plaintext highlighter-rouge">Rosetta-Terminal.app</code>, click the “Get Info”, and check the <code class="language-plaintext highlighter-rouge">Run in Rosetta</code> box.</p>

<p>I don’t like this approach (for me) because I don’t want to have multiple Terminal open that look the exact same. And I don’t expect myself to <em>always</em> be in Rosetta mode. Only when I need it. So I found a way switch my current terminal session using <code class="language-plaintext highlighter-rouge">arch --arm64 zsh</code> and <code class="language-plaintext highlighter-rouge">arch --x86_64 zsh</code>. These command essentially replace my current Zsh session with one running in the architecture of my chooseing. I used these often enough where I aliased them <code class="language-plaintext highlighter-rouge">mzsh</code> (M1/arm64) and <code class="language-plaintext highlighter-rouge">ishz</code> (Rosetta/i386). You can see my <code class="language-plaintext highlighter-rouge">.zshrc</code> below where I alias these commands.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">mzsh</span><span class="o">=</span><span class="s2">"arch -arm64 zsh"</span>
<span class="nb">alias </span><span class="nv">izsh</span><span class="o">=</span><span class="s2">"arch -x86_64 zsh"</span>
</code></pre></div></div>

<h3 id="separate-homebrews">Separate homebrews</h3>

<p>The next thing was to have Homebrew installed in M1 and in Rosetta. Homebrew works a little bit differently because M1 Homebrew is installed at in the <code class="language-plaintext highlighter-rouge">/opt</code> directory where Rosetta Homebrew is installed in the <code class="language-plaintext highlighter-rouge">/usr/local</code> directory.</p>

<p>To handle this for M1, the ran the Homebrew install script in my <code class="language-plaintext highlighter-rouge">mzsh</code> session.
To handle this for Rosetta, I ran the Homebrew install script in my <code class="language-plaintext highlighter-rouge">izsh</code> session.</p>

<p>The tricky part is to re-alias the <code class="language-plaintext highlighter-rouge">brew</code> command to point at the correct Homebrew install depending on which architecture you are running in. There is a switch for this <strong>also</strong> in my <code class="language-plaintext highlighter-rouge">.zshrc</code></p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">uname</span> <span class="nt">-p</span><span class="si">)</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"i386"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"Running in i386 mode (Rosetta)"</span>
  <span class="nb">eval</span> <span class="s2">"</span><span class="si">$(</span>/usr/local/homebrew/bin/brew shellenv<span class="si">)</span><span class="s2">"</span>
  <span class="nb">alias </span><span class="nv">brew</span><span class="o">=</span><span class="s1">'/usr/local/homebrew/bin/brew'</span>
<span class="k">else
  </span><span class="nb">echo</span> <span class="s2">"Running in ARM mode (M1)"</span>
  <span class="nb">eval</span> <span class="s2">"</span><span class="si">$(</span>/opt/homebrew/bin/brew shellenv<span class="si">)</span><span class="s2">"</span>
  <span class="nb">alias </span><span class="nv">brew</span><span class="o">=</span><span class="s1">'/opt/homebrew/bin/brew'</span>
<span class="k">fi</span>
</code></pre></div></div>

<h3 id="installing-ruby-and-other-version-managers">Installing Ruby (and other) version managers</h3>

<p>Knowing that you are in the correct architecture is key for installing other tools (like version managers).</p>

<h4 id="ruby-install-and-chruby">ruby-install and chruby</h4>

<p>My <em>personal</em> favorite Ruby version manager is <code class="language-plaintext highlighter-rouge">ruby-install</code> and <code class="language-plaintext highlighter-rouge">chruby</code>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>ruby-install
brew <span class="nb">install </span>chruby
<span class="c"># put stuff in path in ~/.zshrc that chruby instructions tell you to</span>
ruby-install 3.0
<span class="nb">source</span> ~/.zshrc
chruby 3.0
</code></pre></div></div>

<h4 id="asdf">asdf</h4>

<p>My next suggestion would be to use <code class="language-plaintext highlighter-rouge">asdf</code>. <code class="language-plaintext highlighter-rouge">asdf</code> is a complete version manager for multiple runtimes. I use it for Ruby, Python, and NodeJS but it has more.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>asdf
<span class="c"># put some stuff in path in ~/.zshrc that asdf instructions tell you to</span>
asdf plugin add ruby
asdf <span class="nb">install </span>ruby latest
asdf global ruby latest
</code></pre></div></div>

<h3 id="but-why-separate-homebrew-installs">But why separate Homebrew installs?</h3>

<p>Great question! Most things (that I use) through Homebrew will work perfectly fine on M1 (arm64). However, there are some tools that you need to compile or install that won’t work. We can test this out with Python 2.7.18. Running <code class="language-plaintext highlighter-rouge">asdf install python 2.7.18</code> will fail due to architecture issues on M1. But if you change to Rosetta (using <code class="language-plaintext highlighter-rouge">izsh</code>) and run <code class="language-plaintext highlighter-rouge">asdf install python 2.7.18</code>, it will succeed 🥳 And now that Python 2.7.18 is compiled and installed from Rosetta, you can actually use it when you are back in M1 (arm64). Just switch back over with <code class="language-plaintext highlighter-rouge">mzsh</code> and run <code class="language-plaintext highlighter-rouge">python</code> you will see! (But don’t forget to run <code class="language-plaintext highlighter-rouge">asdf global python 2.7.18</code> or <code class="language-plaintext highlighter-rouge">asdf local python 2.7.18</code> first).</p>

<p>Why does this work on M1 (arm64) even though you needed Rosetta for it? Well… here is my understanding of it 😇 Jumping into Rosetta to compile and install it essentially makes an M1/arm64 installation of it <em>after</em> it compiles in the i386 architecture. The M1 Mac don’t have an Intel processor so everything needs to get to an M1 format somehow. So with this above example, we only need Rosetta for the compile and install phase. Now Python 2.7.18 will work anywhere for us!</p>

<h2 id="the-end">The End</h2>

<p>So yeah, that’s it I think for this post! I just wanted to share my Zsh setup that makes it easy for me to use my M1 and have confidence in building and installing different tools between M1 (arm64) and Rosetta (i386) 😁 I may have some things explained wrong since I wrote this SUPER QUICKLY but hopefully this helps some of you out 🙏</p>

<p>Feel free to tweet me (<a href="https://twitter.com/joshdholtz">@joshdholtz</a>) or email me if you have any feedback or questions about this. Thanks for reading and happy Shortcutting!</p>]]></content><author><name>joshdholtz</name></author><category term="blog" /><category term="shortcuts" /><summary type="html"><![CDATA[This is the setup I'm using on my M1 Mac (and Rosetta) to handle homebrew, zsh, Ruby and python version managers]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joshholtz.com/images/2021-10-27/header.png" /><media:content medium="image" url="https://www.joshholtz.com/images/2021-10-27/header.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Automating iOS Shortcuts - The Cron Job Way</title><link href="https://www.joshholtz.com/blog/2021/06/23/automating-ios-shortcuts-the-cron-job-way.html" rel="alternate" type="text/html" title="Automating iOS Shortcuts - The Cron Job Way" /><published>2021-06-23T07:30:00+00:00</published><updated>2021-06-23T07:30:00+00:00</updated><id>https://www.joshholtz.com/blog/2021/06/23/automating-ios-shortcuts-the-cron-job-way</id><content type="html" xml:base="https://www.joshholtz.com/blog/2021/06/23/automating-ios-shortcuts-the-cron-job-way.html"><![CDATA[<p>Hello, it’s me! Josh Holtz. And I do a lot of things that are fun but unnecessary. Today’s unnecessary and over-engineered thing is automating iOS (and macOS) Shortcuts using the same syntax that’s used to schedule cron jobs 😊</p>

<p>This whole blog post is centered around my “Cron” Shortcut which you can <a href="https://www.icloud.com/shortcuts/f75b29e186aa43c59ab0a9ce981c4f6a">download here</a>. It takes text formatted like 👇</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0 10 * * * Daily Morning Shortcut
0 9 * * 1 Weekly Monday Shortcut
0,30 * * * * Shortcut Every Half Hour
</code></pre></div></div>

<p>But I recommend you continue reading on 😉</p>

<h2 id="the-problem">The Problem</h2>

<p>Today’s thing was driven by some iOS Shortcuts that I needed to have automated. The three Shortcuts were:</p>

<ol>
  <li>Fetch and <a href="https://twitter.com/joshdholtz/status/1400935805116428298?s=20">alert me of the App Store Connect API version</a> every hour (I’m waiting for a big and important update)</li>
  <li>Tweet the latest <a href="https://indiedevmonday.com">Indie Dev Monday</a> article every Monday at 9 am</li>
  <li>Tweet the number of days since I <a href="https://twitter.com/joshxcodeproj">last started a new Xcode project</a> every day at 11 am</li>
</ol>

<hr />

<h2 id="doesnt-shortcuts-already-have-automations">Doesn’t Shortcuts Already Have Automations?</h2>

<p>Yes. Yes, it does.</p>

<p>Automating iOS Shortcuts (and soon-to-be macOS Shortcuts in Monterey) to run at a certain time is easy to do. You can configure a Shortcut to run daily, on a specific day of the week, or a specific day of the month. See screenshot below 👇</p>

<p><img src="/images/2021-06-23/1_schedule_automation.png" alt="" data-lity="" /></p>

<p>After setting the schedule, you then need to build your Shortcut but chaining actions together (similar to how you build non-automated Shortcuts). See screenshot below 👇</p>

<p><img src="/images/2021-06-23/2_build_automation.png" alt="" data-lity="" /></p>

<p>But it gets tedious if you want to run the same Shortcut multiple times throughout the day. You can’t just reuse the same configuration. You, instead, need to create additional automations.</p>

<p>To get around building the same Shortcut over and over again in each automation, I start off building all my automated Shortcuts as normal, non-automated Shortcuts. This has two benefits.</p>

<ol>
  <li>I can test the Shortcut at any time (not just at the scheduled time)</li>
  <li>I can run and reuse this Shortcut in automations with the “Run Shortcut” action</li>
</ol>

<p>This approach made it easier to manage and test the Shortcut’s behavior but my automations section started getting out of hand. And once you have a lot of automations, it gets very difficult to see them all and find the correct automation to view or update. There were so many “Run Shortcut” actions but I didn’t know what was being run. This was a side effect of my reuse method from earlier. See screenshot below 👇</p>

<p><img src="/images/2021-06-23/3_list_of_automations.png" alt="" data-lity="" /></p>

<p>I now needed a solution to make both viewing and scheduling new jobs easier and more flexible…</p>

<p>And all the signs pointed to something similar to cron jobs 🤷‍♂️</p>

<hr />

<h2 id="how-do-cron-jobs-get-scheduled">How Do Cron Jobs Get Scheduled</h2>

<p>I’ve been using Unix-like operating systems for the past 17 years for both personal and work. I would often want to automate scripts to run at a certain time (but I don’t remember what specifically for anymore). To do so, I would schedule cron jobs. These jobs are driven by a crontab (cron table). See example below 👇</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 0 * * * printf "" &gt; /var/log/apache/error_log
45 23 * * 6 /home/oracle/scripts/export_dump.sh
</code></pre></div></div>

<p>The format of this is defined by 👇</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday;
# │ │ │ │ │                                   7 is also Sunday on some systems)
# │ │ │ │ │
# │ │ │ │ │
# * * * * * &lt;command to execute&gt;
</code></pre></div></div>

<p>This is the perfect user interface for automating jobs. It’s been used since the beginning of time in Unix and I wanted to use it for Shortcuts. I’d easily be able to see all of my scheduled Shortcuts on one screen. It would also be very painless to edit the schedule times or add and remove Shortcuts from the list. Pretty user interfaces are nice and what I would expect from Apple but that’s not what I needed. Most people probably don’t want to use their mobile device as a driver for many time-based automations. I, however, do 🙃</p>

<p>So even though Apple doesn’t give me crontab capabilities for scheduling natively in Shortcuts, I decided I would try to build it myself 💪</p>

<hr />

<h2 id="architecting-the-cron-job-shortcuts">Architecting the Cron Job Shortcuts</h2>

<p>Instead of using crontab to run a script (like it would on a Unix-like system), I decided to use the crontab syntax to run a Shortcut by name. This is all made possible by the “Run Shortcut” action I talked about earlier. There are two things needed around the crontab to make Shortcuts behave like cron jobs.</p>

<ol>
  <li>A system to parse <code class="language-plaintext highlighter-rouge">crontab</code> and execute Shortcuts by name</li>
  <li>A system to regularly run the parsing and execution</li>
</ol>

<p>The overall flow looks like this 👇</p>

<p><img src="/images/2021-06-23/9_flow.jpeg" alt="" data-lity="" /></p>

<h3 id="creating-the-crontab-shortcut">Creating the <code class="language-plaintext highlighter-rouge">crontab</code> Shortcut</h3>

<p>The system to regularly run the parsing and execution of Shortcuts is going to be driven by Shortcut automations. Previously, I have been defining the specific Shortcut I wanted to run at each time in automations. Instead of doing that, this cron job approach will run the same Shortcut every hour and half hour. The Shortcut will run a new Shortcut named <code class="language-plaintext highlighter-rouge">crontab</code>. See screenshot below 👇</p>

<p><img src="/images/2021-06-23/4_crontab.png" alt="" data-lity="" /></p>

<p>Even though real Unix-like cron jobs can be run at any minute, running the <code class="language-plaintext highlighter-rouge">crontab</code> Shortcut every hour and half hour gave me enough power and flexibility to cover all of my needs. The <code class="language-plaintext highlighter-rouge">crontab</code> Shortcut itself is simple. There is a text action that holds the crontab syntax (as mentioned earlier). This text is passed to another action called <code class="language-plaintext highlighter-rouge">cron</code>. See screenshot below 👇</p>

<p><img src="/images/2021-06-23/7_cron_combined.png" alt="" data-lity="" /></p>

<p>☝️ This is the exact Shortcut automation configuration that I have scheduled on every hour and every half hour. It’s as simple as it can get and doesn’t ever need to be changed.</p>

<p>In this example, <strong>“Fetch App Store Connect Version”</strong> is run every single hour. <strong>“Jobs - New Xcode Project”</strong> is run every day at 11 am. <strong>“Indie Dev Monday - Tweet”</strong> is run only Monday at 10 am.</p>

<p>And that’s it 😉 I mean, that’s not it because the <code class="language-plaintext highlighter-rouge">cron</code> Shortcut is complex. I’ll explain that next 😊</p>

<p>ANYWAY… Adjusting the time I want my Shortcuts to run is now really easy! Let’s say I rename <strong>“Indie Dev Monday”</strong> to <strong>“Indie Dev Monday, Wednesday, and Friday”</strong>. And let’s say I wanted to change “Fetch App Store Connect Version” to run from every hour to every half hour.  All I have to do is change my crontab from…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0 * * * * Fetch App Store Connect Version
0 11 * * * Job - New Xcode Project
* 10 * * 1 Indie Dev Monday - Tweet
</code></pre></div></div>

<p>to…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0,30 * * * * Fetch App Store Connect Version
0 11 * * * Job - New Xcode Project
* 10 * * 1,3,5 Indie Dev Monday - Tweet
</code></pre></div></div>

<p>This took all of about 10 seconds 🙌 Adding new automations to run every half would would have required me to make 24 brand new Shortcut automations without the cron job approach 😝</p>

<h3 id="creating-the-cron-shortcut">Creating the <code class="language-plaintext highlighter-rouge">cron</code> Shortcut</h3>

<p>This is a beast of a Shortcut. It has almost 200 actions that do text parsing, date math, validation 🤯  There is a screen recording of the actions in the Shortcut below but I will list out the high level steps below since that’s easier to process 😉</p>

<p>Download “Cron” Shortcut 👉 <a href="https://www.icloud.com/shortcuts/f75b29e186aa43c59ab0a9ce981c4f6a">Click here</a></p>

<ol>
  <li>Accepts text input in crontab syntax</li>
  <li>Reads crontab line by line and maps to list of dictionaries (easier to look up in further processing)
    <ol>
      <li>Splits line by spaces</li>
      <li>Maps specific index to a variable (0 is the minute, 1 is the hour, etc)</li>
      <li>Verifies Shortcut name exists and errors if it doesn’t</li>
      <li>Takes these variables and saves a dictionary</li>
      <li>Appends dictionary to list</li>
    </ol>
  </li>
  <li>Iterate over crontab list of dictionaries to determine which need to be run
    <ol>
      <li>Checks if the day of the week passes
        <ol>
          <li>Passes if an exact match or wildcard (*)</li>
        </ol>
      </li>
      <li>Checks if a month passes
        <ol>
          <li>Passes if an exact match or wildcard (*)</li>
        </ol>
      </li>
      <li>Checks if the day of the month passes
        <ol>
          <li>Passes if an exact match or wildcard (*)</li>
        </ol>
      </li>
      <li>Checks if an hour passes
        <ol>
          <li>Passes if an exact match or wildcard (*)</li>
        </ol>
      </li>
      <li>Checks if a minute passes
        <ol>
          <li>Passes if an exact match or wildcard (*)</li>
        </ol>
      </li>
      <li>If all passed, appends to list of Shortcuts to run</li>
    </ol>
  </li>
  <li>Iterate over Shortcuts to run list and run Shortcuts
    <ol>
      <li>Call “Run Shortcut” with the name of Shortcut</li>
    </ol>
  </li>
</ol>

<p>And that’s it! That’s the <code class="language-plaintext highlighter-rouge">cron</code> Shortcut that does all the heavy lifting 😊 As seen above, it’s broken down into three main parts. The first is mapping the crontab text to a list of dictionaries. The second is taking the full list of Shortcuts and filtering it down to only the Shortcuts that needed to be run at this exact time. The third is executing the Shortcuts.</p>

<p>This was split up this way for a few reasons:</p>
<ol>
  <li>Easier to develop and debug the separate parts if something goes wrong</li>
  <li>Shortcuts can sometimes take minutes to run which would ruin the date math 🤷‍♂️ Moving all the parsing and checking upfront allows the Shortcuts to take however long they need to</li>
</ol>

<p>I lied, here is a video of the Shortcut if you want to watch it 😉</p>

<video width="100%" poster="/images/2021-06-23/8_cron_shortcut.png" controls="">
  <source src="/images/2021-06-23/8_cron_shortcut.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>

<h3 id="the-negatives">The Negatives</h3>

<p>There are sooooooooo many negatives with this approach 😅</p>

<h4 id="1-large-initial-setup-of-automations-running-every-hour-and-every-half-hour">1. Large initial setup of automations running every hour and every half hour</h4>

<p>That is 48 automations for my setup! But yours may not need this. Maybe you only care about every hour or only the hours you are awake. You don’t need to set up all the automations for every hour if you aren’t going to use them. I just did because I could</p>

<h4 id="2-notifications-on-every-run-of-the-crontab-automation">2. Notifications on every run of the <code class="language-plaintext highlighter-rouge">crontab</code> automation</h4>

<p>Automated Shortcuts will always send you a notification when they are going to be run. It’s nice to know that your automation is going to run but this means I get 48 notifications that say something like “At 8:00 pm, daily - Running your automation” 😛 It’s not useful and some people may find it annoying but I’m fine with it!</p>

<h4 id="3-shortcuts-run-sequentially-and-will-stop-if-one-fails">3. Shortcuts run sequentially and will stop if one fails</h4>

<p>There are some times during the day or week that I will have multiple Shortcuts being run at the same hour. There is no way to kickoff multiple Shortcuts at one time so the Shortcuts need to run sequentially. Sequential is not necessarily bad. It does mean it will take a longer time for all the Shortcuts to finish because they can only run one at a time. The bad comes in if one of them fails. If a Shortcut errors while being executed from “Run Shortcuts”, the whole Shortcut stops. This means any other Shortcut that was supposed to run won’t get run 😔</p>

<p>This isn’t a problem for me since I’ve designed my automated Shortcut in a way where this shouldn’t happen. But it still might 🤷‍♂️</p>

<h4 id="4-approving-shortcuts-to-run-shortcuts">4. Approving Shortcuts to run Shortcuts</h4>

<p>I don’t remember if this was added in iOS 14.6 or iOS 15 (Beta) but there are a lot of permission prompts that you will need to accept for your automations to be fully autonomous (that sounds weird 😬). The first time a Shortcut runs a Shortcut with the “Run Shortcut” action, Shortcuts will prompt you for your approval. This is a safety measure of some sort (which I think I approve of) but it does require some user interaction from you the first time.</p>

<h2 id="the-end">The End</h2>

<p>I do use this every day but this isn’t meant to be the most stable or efficient Shortcut. This is mainly just a fun experiment for me 😇 I wanted to make it easier for me to view and edit my automated Shortcuts while also testing my Shortcut making abilities. I’d say that I succeeded in both of those things ✅</p>

<p>I also wanted another thing to blog about 😉</p>

<p>Feel free to tweet me (<a href="https://twitter.com/joshdholtz">@joshdholtz</a>) or email me if you have any feedback or questions about this. Thanks for reading and happy Shortcutting!</p>]]></content><author><name>joshdholtz</name></author><category term="blog" /><category term="shortcuts" /><summary type="html"><![CDATA[iOS app for cropping photos to the shape of emojis]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joshholtz.com/images/2021-06-23/header.png" /><media:content medium="image" url="https://www.joshholtz.com/images/2021-06-23/header.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Launch Announcement: Oh Crop for iOS</title><link href="https://www.joshholtz.com/blog/2021/06/01/launching-oh-crop.html" rel="alternate" type="text/html" title="Launch Announcement: Oh Crop for iOS" /><published>2021-06-01T07:30:00+00:00</published><updated>2021-06-01T07:30:00+00:00</updated><id>https://www.joshholtz.com/blog/2021/06/01/launching-oh-crop</id><content type="html" xml:base="https://www.joshholtz.com/blog/2021/06/01/launching-oh-crop.html"><![CDATA[<p>I’ve teased this app for about a month now. I even made <a href="https://twitter.com/joshdholtz/status/1388148055124463617">a teaser for a teaser</a> which I was slightly embarrassed by it seemed like something fun to try 🙈 But today is the day…</p>

<p><strong>Happy launch day, Oh Crop!</strong> 🥳</p>

<p><a href="https://apps.apple.com/us/app/oh-crop-emoji-but-as-photos/id1563845967" target="_blank">
  <img src="/images/Download_on_App_Store.svg" />
</a></p>

<hr />

<h2 id="what-is-oh-crop"><em>What is Oh Crop?</em></h2>

<p><strong>Oh Crop</strong> is my latest iOS app that will crop a photo to look like an emoji either in-app or through a Shortcut action 👇</p>

<p><img src="/images/2021-06-01/josh_heart.png" width="200" />
<img src="/images/2021-06-01/josh_wave.png" width="200" />
<img src="/images/2021-06-01/josh_crab.png" width="200" />
<img src="/images/2021-06-01/josh_snow.png" width="200" /></p>

<h2 id="why-did-you-make-oh-crop"><em>Why did you make Oh Crop?</em></h2>

<p>It was an accident 🤷‍♂️ It was initially meant to be nothing more than an empty app that had an iOS Shortcut for cropping an image to a circle. I needed this for automating my processes around preparing, writing, and releasing new <a href="https://indiedevmonday.com">Indie Dev Monday</a> issues.</p>

<p>Indie Dev Monday is my newsletter that spotlights one or two indie developers every week. The issue contains a profile picture of the indie developers – which I usually pull from Twitter – that gets displayed in both the web version and the email version of the newsletter. The easiest way to have a consistent look between the two versions was to create a circle version of the photo. The first few issues were cropped manually through some tool but I eventually wrote a small Ruby script to download and crop the photo to a circle.</p>

<p>This process was <em>technically</em> working fine but it required that I always had access to my Mac for preparing and releasing issues. This was okay until I became a parent in December of 2020 🥰 Since then, a majority of my issues have been prepped, written, and released from my iPhone – as it was easier to use a phone while walking or rocking a baby than awkwardly using a computer 😛  Even though I was doing this all through my phone, I still had to remote into my Mac to run my automation scripts. I didn’t like having to do that so I decided I want to move everything to iOS Shortcuts.</p>

<p>I had no trouble converting my scripts to Shortcuts and the majority of the Shortcuts used built-in actions provided by iOS. The one action I was struggling to find was cropping an image into a circle. I didn’t have any existing apps installed that could do it and I couldn’t easily find an on the App Store that could do it…</p>

<p><strong>So I decided to make my own 😜</strong></p>

<p>I like to take odd approaches when solving problems. I get a thrill from solving problems (even if the problems are self-inflicted). Cropping an image to a circle is pretty trivial when writing an in iOS… But I wanted to have some fun 😈</p>

<h2 id="what-was-the-solution"><em>What was the solution</em>?</h2>

<p>Instead of using code to crop the circle, I decided it would “cool” to use the circle emoji (🔴) to crop the circle.</p>

<p>The steps to do this ended up being:</p>
<ol>
  <li>Find the font size needed for the emoji to fill the photo</li>
  <li>Convert emoji from text into an image</li>
  <li>Use the emoji image as a mask against an all-white image to create an inverted emoji mask</li>
  <li>Use the inverted emoji mask against the user-provided photo</li>
</ol>

<p>The result of this is a photo that looks like the emoji used 😁 In this case… it was just an over-engineered circle.</p>

<p>From there, I decided to try out some other emoji shapes that people might like or use  🟥, 🔷, or ❤️. But then I started to wonder what using other emojis would look like…</p>

<p>The 🐩, 🚽, or 🗑 were some of the first that I used! But I wanted to try them all. My wife and I spent unknown amounts of time trying out all of the emojis 😛 It was hard to pick a favorite because different kinds of photos look better (and worse) with different emojis.</p>

<p>At this point there no functional app and no plans to release the app. It could crop photos to any emoji but I using it only with iOS Shortcuts to crop a photo to a circle with the 🔴 emoji for my Indie Dev Monday issues.</p>

<h2 id="why-did-you-want-to-release-this-app"><em>Why did you want to release this app?</em></h2>

<p>Straight up… I wasn’t planning on releasing this at all. I made this app to solve my needs and that is all I wanted from it.</p>

<p>The only reason that pushed me over the edge was how much of a pain it to keep a non App Store version of an app on my phone 😛 It requires the app to be installed as a development build (installed directly through Xcode to my phone) or as a beta app through TestFlight.</p>

<p>The problem with the development build is that it requires valid certificates and provisioning profiles through my Apple developer account. This would be fine if I wasn’t mean to my account 🙃 Because of the nature of my day job (maintaining <a href="https://fastlane.tools">fastlane</a>), I will often revoke all of my certificates and delete my provisioning profiles. This is fine (for me) unless I have a development build of an app on my devices. When the certificate is revoked or the profile is deleted, the app will no longer run. The only way to get it to run again is to recreate the certificate and profile and manually install a new build. I’m too lazy for that…</p>

<p>Distributing the app as a beta app through TestFlight won’t have any side effects from revoking and deleting certificate and provisioning profile issues but the app version submitted to TestFlight <em>will</em> expire after 90 days. I didn’t want to have to keep uploading new builds just to keep the app alive.</p>

<p><strong>So the only logical thing to do is launch the app on the App Store 😅</strong></p>

<p>However, I have a rule where I won’t launch an app unless it has a good name (which should be a pun) and good looking app icon. On top of this, I also want to use app launches (even for small apps like this one) as a practice ground for leveling up my marketing skills (websites, press kits, graphics, videos, etc).</p>

<h2 id="how-did-this-become-oh-crop"><em>How did this become Oh Crop?</em></h2>

<p>I needed a name, app icon, and a concept to make this interesting enough to release on the App Store. And the way I got this name is slightly embarrassing. The name “Oh Crop” hit me while I was walking into a bathroom 🤦‍♂️</p>

<ol>
  <li>The app was meant for cropping circles so the “Oh” sounds like “O” which looks like a circle</li>
  <li>“Oh Crop” sounds like the phrase “Oh Crap” which made me giggle ☺️</li>
  <li>Because of the “Oh Crap” reference, I could make the app icon a really cute version of the 💩 emoji</li>
</ol>

<p>With the app name and icon figured out, the next steps were to bring “Oh Crop” and “Oh Crap” into the app itself. I wanted to add a level of fun to the app but also something that wasn’t <em>too</em> much work for either me as the developer or the user.</p>

<p><strong>What if… the user could randomly get a 💩 emoji instead of the one this asked for?</strong></p>

<p>I started to hard fall in love with this idea. It was easy to implement and played really well into the “Oh Crop” and “Oh Crap” 😁 Ideas for this started coming fast. This was no longer just an iOS Shortcut app.</p>

<p>I first added a label that showed what odds of success the user had when picking an emoji for their photo. The success rate is based on a random number generator seeded from the day so that it’s the same for every user each day. From here I create a new screen where the user can see a 7-day forecast of the success odds. Users can use this to find the day they had the best chances to successfully crop to emojis.</p>

<p>This decision to have random 💩 emojis backfired when I tried to release a new issue of Indie Dev Monday a few weeks ago. I almost released an issue with a 💩 shaped profile picture of one of my indie devs 😂 After that I decided to add an in-app purchase to unlock the ability to disable the 💩.</p>

<p>After that there was one night where I couldn’t sleep… so I decided to add widgets. I first added a single widget to view the 💩 chance percent. That wasn’t enough so I also added a 7-day forecast widget to go with it. At this point, the app itself still didn’t feel like it had enough substance to it. I wasn’t going to crop photos every day but I wanted to see some of the fun croppings that I created. I added a “widget album” feature. The user can create albums of emojis and photos. A widget can be configured to show a certain album and would rotate through random emoji and photo combinations.</p>

<p>This app was only meant to be an iOS Shortcut that could crop photos to circles. And now that plus this emoji cropper and emoji photo widget thingy. I think after that is where I decided I needed to stop and release this 🤷‍♂️</p>

<h2 id="here-are-we"><em>Here are we</em></h2>

<p>This app isn’t meant to be a serious app. This isn’t meant to be an app that I can make a living off of. This isn’t an app that everyone will enjoy. This app wasn’t ever meant to be used by anybody else.</p>

<p>The result of this accidental app creation was the most inefficient and over-engineered way of cropping an image to a circle. But I don’t care because I had fun making and release this app! Being able to create things like this is why I’m a developer ❤️</p>

<p>Thanks for reading my story about <strong>Oh Crop</strong> and I hope that some of you enjoy using this app!</p>

<p>Feel free to tweet me (<a href="https://twitter.com/joshdholtz">@joshdholtz</a>) or <a href="https://twitter.com/OhCropTheApp">@OhCropTheApp</a> with any feedback, issues, or feature request🥳</p>

<p><a href="https://apps.apple.com/us/app/oh-crop-emoji-but-as-photos/id1563845967" target="_blank">
  <img src="/images/Download_on_App_Store.svg" />
</a></p>]]></content><author><name>joshdholtz</name></author><category term="blog" /><category term="indie" /><category term="oh-crop" /><category term="annoucement" /><summary type="html"><![CDATA[iOS app for cropping photos to the shape of emojis]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joshholtz.com/images/2021-06-01/marketing-1.jpeg" /><media:content medium="image" url="https://www.joshholtz.com/images/2021-06-01/marketing-1.jpeg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Preview: Gargmel - A non-opinionated GitHub monitoring dashboard powered by YAML</title><link href="https://www.joshholtz.com/blog/2021/03/15/preview-gargamel-non-opinionated-github-monitoring-dashboard.html" rel="alternate" type="text/html" title="Preview: Gargmel - A non-opinionated GitHub monitoring dashboard powered by YAML" /><published>2021-03-15T07:30:00+00:00</published><updated>2021-03-15T07:30:00+00:00</updated><id>https://www.joshholtz.com/blog/2021/03/15/preview-gargamel-non-opinionated-github-monitoring-dashboard</id><content type="html" xml:base="https://www.joshholtz.com/blog/2021/03/15/preview-gargamel-non-opinionated-github-monitoring-dashboard.html"><![CDATA[<p>I told myself I wasn’t going to work on other side projects.. I did not want to stretch myself to thin while being a new dad, lead maintainer of <em><a href="https://fastlane.tools">fastlane</a></em>, and just recently release <a href="https://anotterrss.com">An Otter RSS</a> and <a href="https://connectkit.app">ConnectKit</a>. But, while focusing on my day-to-day <em>fastlane</em> work, I shortly realized there was another tool that I needed that didn’t exist.</p>

<p><em>fastlane</em> has one main repository under the   <a href="https://github.com/fastlane/fastlane">https://github.com/fastlane/fastlane</a> organization… but that is not the only one. There are about four other repositories that I need to monitor regularly. There is also the <a href="https://github.com/fastlane-community">https://github.com/fastlane-community</a> organization which contains <em>fastlane</em> plugins and dependencies that <em>fastlane</em> users rely on that needed a new home 😇 There are a total of 19 repositories in there that need some monitoring. Most of these repositories aren’t <em>super</em> active but are still active.</p>

<p>I have GitHub notifications enabled for repositories but it’s very, <em>very</em> easy to let important notifications slip by. I needed a tool that helps me monitor the health of these repositories while also giving my GitHub notifications a supercharged filter to show me notifications that I might have missed.</p>

<p>And that tool is “Gargamel” 😈</p>

<hr />

<h2 id="gargamel">Gargamel</h2>

<p>Gargamel is a non-opinionated monitoring dashboard powered by a YAML file. It was meant to work only with the GitHub API but actually can work with any API.</p>

<p>That probably doesn’t mean much so keep reading about the requirements, the development decisions, and the product 😉</p>

<h3 id="the-requirements">The Requirements</h3>

<p>Gargamel is 100% intended to an app for 100% me 😊 Going into this I did not take anybody’s needs except myself into this. I needed a tool that fit my very specific needs. With that in mind, the first thing I did was put together some quick notes on what I needed to make. But the important part wasn’t what I needed to make… it was also what I <strong>didn’t need</strong> to make. I wanted to make sure I boxed my expectations into something that didn’t get out of hand 😇 I didn’t want to start polishing the app and making it super performant from the beginning. I wanted something that took me a few partial days of hacking that “just worked”.</p>

<hr />

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gu">## Description</span>
Dashboard to monitor GitHub repositories and associated notifications. Also allows a custom calculated health or warning system when attention is needed.

<span class="gu">## MVP</span>
<span class="p">-</span> YAML configuration file
<span class="p">-</span> Repo view
<span class="p">    -</span> Org/name
<span class="p">    -</span> Custom field and value
<span class="p">        -</span> https://stackoverflow.com/a/21233623
<span class="p">-</span> Notification view
<span class="p">    -</span> Group notifications by repo
<span class="p">    -</span> Filter notifications (better than GitHub)
<span class="p">        -</span> Allow batch clearing of notifications
<span class="p">        -</span> Eample
<span class="p">            -</span> Things fastlane bot closed
<span class="p">            -</span> PR status changes
<span class="p">            -</span> PR notifications
<span class="p">            -</span> Issue notifications
<span class="p">            -</span> Discussion notifications

<span class="gu">## Things to not care about</span>
<span class="p">-</span> Speed and caching
<span class="p">    -</span> This will not be looked at often (maybe only at start, middle, and end of day)
<span class="p">-</span> Background refresh
<span class="p">    -</span> Might be a long running job because of number of repos and configurations
<span class="p">    -</span> Could run in background but not necessary (especially on iOS)
<span class="p">-</span> Don't make pretty
<span class="p">    -</span> Just work
</code></pre></div></div>

<hr />

<h3 id="the-development-decisions">The Development Decisions</h3>

<p>Based upon these requirements I set for myself, I decided Gargamel would:</p>

<ul>
  <li>Be a SwiftUI app (for macOS and iOS)
    <ul>
      <li>Rapid development</li>
    </ul>
  </li>
  <li>Don’t use persistent storage (Core Data)
    <ul>
      <li>Mapping responses is too much effort for what I wanted</li>
      <li>Cache will almost always be invalidated
        <ul>
          <li>Data changes frequently</li>
          <li>I will only refresh maybe a few times a day</li>
        </ul>
      </li>
      <li>Use <code class="language-plaintext highlighter-rouge">URLCache</code>  with <code class="language-plaintext highlighter-rouge">URLSession</code>
        <ul>
          <li>GitHub API supports ETags</li>
          <li>Will prevent unnecessary data transfer (quicker response times) if no changes</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Use YAML for configuration
    <ul>
      <li>No UI needed besides SwiftUI’s TextEditor</li>
      <li>Very flexible if I run into roadblocks and need to pivot</li>
      <li>YAML has “anchors” which can be used to reuse basic auth and code definitions</li>
    </ul>
  </li>
  <li>Run data filtering and view configuration in JavaScriptCore
    <ul>
      <li>I did not want to hardcode any logic in Swift</li>
      <li>Each type of view was going to be configured very differently</li>
      <li>API responses will get fed into a function fun in JavaScript Core
        <ul>
          <li>Can optionally filter data</li>
          <li>Return a JavaScript object that configures the few</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h4 id="yaml-configuration">YAML Configuration</h4>

<ol>
  <li>YAML drives the views (either a grid or list type)</li>
  <li>Views have API endpoints that will get requested</li>
  <li>Views get defined with code sections that will be executed using JavaScriptCore</li>
  <li>API responses get forwarded and executed in JavaScriptCore</li>
  <li>SwiftUI renders data return from JavaScriptCore</li>
</ol>

<p>Here is a simple sample that displays <code class="language-plaintext highlighter-rouge">fastlane/fastlane</code> and <code class="language-plaintext highlighter-rouge">fastlane/docs</code> in a grid view:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">aliases</span><span class="pi">:</span>
  <span class="na">personal_token</span><span class="pi">:</span> <span class="nl">&amp;personal_token</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s2">"</span><span class="s">YOUR_GITHUB_USERNAME"</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s2">"</span><span class="s">YOUR_GITHUB_PERSONAL_ACCESS_TOKEN"</span>
  <span class="na">repo_health</span><span class="pi">:</span> <span class="nl">&amp;repo_health</span> <span class="pi">|</span>
      <span class="s">function(repo, issues, pulls) {</span>
        <span class="s">// Need to filter out pull requests</span>
        <span class="s">issues = issues.filter(function(issue) {</span>
          <span class="s">return !issue.pull_request;</span>
        <span class="s">})</span>

        <span class="s">return {</span>
          <span class="s">“fields”: [</span>
            <span class="s">{</span>
              <span class="s">“name”: “Open Issues”,</span>
              <span class="s">“name_weight”: “bold”,</span>
              <span class="s">“value”: issues.length</span>
            <span class="s">},</span>
            <span class="s">{</span>
              <span class="s">“name”: “Open Pulls”,</span>
              <span class="s">“name_weight”: “bold”,</span>
              <span class="s">“value”: pulls.length</span>
            <span class="s">}</span>
          <span class="s">]</span>
        <span class="s">};</span>
      <span class="s">}</span>
<span class="na">views</span><span class="pi">:</span>
  <span class="pi">-</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">👀 fastlane</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">grid</span>
    <span class="na">items</span><span class="pi">:</span>
      <span class="pi">-</span> 
        <span class="na">title</span><span class="pi">:</span> <span class="s">fastlane/fastlane</span>
        <span class="na">subtitle</span><span class="pi">:</span> 
        <span class="na">endpoints</span><span class="pi">:</span>
          <span class="pi">-</span>
            <span class="na">endpoint</span><span class="pi">:</span> <span class="s">https://api.github.com/repos/fastlane/fastlane</span>
            <span class="na">basic_auth</span><span class="pi">:</span> <span class="nv">*personal_token</span>
          <span class="pi">-</span>
            <span class="na">endpoint</span><span class="pi">:</span> <span class="s">https://api.github.com/repos/fastlane/fastlane/issues?state=open&amp;per_page=100</span>
            <span class="na">basic_auth</span><span class="pi">:</span> <span class="nv">*personal_token</span>
            <span class="na">all</span><span class="pi">:</span> <span class="no">true</span>
          <span class="pi">-</span>
            <span class="na">endpoint</span><span class="pi">:</span> <span class="s">https://api.github.com/repos/fastlane/fastlane/pulls?state=open&amp;per_page=100</span>
            <span class="na">basic_auth</span><span class="pi">:</span> <span class="nv">*personal_token</span>
            <span class="na">all</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">code</span><span class="pi">:</span> <span class="nv">*repo_health</span>
      <span class="pi">-</span> 
        <span class="na">title</span><span class="pi">:</span> <span class="s">fastlane/docs</span>
        <span class="na">subtitle</span><span class="pi">:</span> 
        <span class="na">endpoints</span><span class="pi">:</span>
          <span class="pi">-</span>
            <span class="na">endpoint</span><span class="pi">:</span> <span class="s">https://api.github.com/repos/fastlane/docs</span>
            <span class="na">basic_auth</span><span class="pi">:</span> <span class="nv">*personal_token</span>
          <span class="pi">-</span>
            <span class="na">endpoint</span><span class="pi">:</span> <span class="s">https://api.github.com/repos/fastlane/docs/issues?state=open&amp;per_page=100</span>
            <span class="na">basic_auth</span><span class="pi">:</span> <span class="nv">*personal_token</span>
            <span class="na">all</span><span class="pi">:</span> <span class="no">true</span>
          <span class="pi">-</span>
            <span class="na">endpoint</span><span class="pi">:</span> <span class="s">https://api.github.com/repos/fastlane/docs/pulls?state=open&amp;per_page=100</span>
            <span class="na">basic_auth</span><span class="pi">:</span> <span class="nv">*personal_token</span>
            <span class="na">all</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">code</span><span class="pi">:</span> <span class="nv">*repo_health</span>
</code></pre></div></div>

<hr />

<h3 id="the-product">The Product</h3>

<p>I’m very happy with what I’ve thrown together with fairly minimal work 😊</p>

<p>I have…</p>

<ul>
  <li>2 grid views for the <code class="language-plaintext highlighter-rouge">fastlane</code> and <code class="language-plaintext highlighter-rouge">Fastlane-community</code> organizations
    <ul>
      <li>Every repo monitors
        <ul>
          <li>Number of open issues</li>
          <li>Number of open PRs</li>
          <li>Last release</li>
        </ul>
      </li>
      <li>Main <code class="language-plaintext highlighter-rouge">fastlane/fastlane</code> repository also monitors
        <ul>
          <li>Number of issues since last release</li>
          <li>Number of open regressions</li>
          <li>Number of open regressions since last release</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>5 list views
    <ul>
      <li>List <code class="language-plaintext highlighter-rouge">fastlane/fastlane</code> regression issues</li>
      <li>List <code class="language-plaintext highlighter-rouge">fastlane/fastlane</code> issues with a lot of reactions</li>
      <li>List <code class="language-plaintext highlighter-rouge">fastlane/fastlane</code> notifications that are only PRs</li>
      <li>List <code class="language-plaintext highlighter-rouge">fastlane/fastlane</code> notifications that are only issues</li>
      <li>List <code class="language-plaintext highlighter-rouge">fastlane/fastlane</code> notifications that are only discussions</li>
    </ul>
  </li>
</ul>

<p>The grid views are what I was really after when making Gargamel.  It’s not super pretty but it allows me to easily see the health of all of <em>fastlane</em>’s parts 😎</p>

<p>The notifications lists aren’t very interactive at the moment and I’m not sure if I will implement anything for them in the future. This is mainly to get my attention to attend to these issues somewhere else. 🤷‍♂️</p>

<p><em>Side note: I’m currently testing out <a href="https://getlotus.app">Lotus</a> for managing GitHub notifications</em></p>

<h4 id="my-yaml-configuration">My YAML Configuration</h4>

<p>There is <strong>a lot</strong> to this YAML file 😬 A lot of it is copy-paste and I may work on finding ways to trim this down in the future but… I don’t expect this to change too often. This file can easily be edited in a text editor (which I don’t want to recreate).</p>

<style type="text/css">
  .gist {width:500px !important;}
  .gist-file
  .gist-data {max-height: 500px}
</style>

<script src="https://gist.github.com/joshdholtz/c0dbfd1510ee17bf221b0a3704ff05ff.js"></script>

<h4 id="screenshots">Screenshots</h4>

<p>These are screenshots that match the YAML configuration above 👆</p>

<h5 id="fastlane">fastlane</h5>
<p><img src="/images/2021-03-15/1-fastlane.png" alt="" data-lity="" /></p>

<h5 id="fastlane-community">fastlane-community</h5>
<p><img src="/images/2021-03-15/2-fastlane-community.png" alt="" data-lity="" /></p>

<h5 id="fastlane-regressions">fastlane regressions</h5>
<p><img src="/images/2021-03-15/3-fastlane-regressions.png" alt="" data-lity="" /></p>

<h5 id="fastlane-big-reactions">fastlane big reactions</h5>
<p><img src="/images/2021-03-15/4-fastlane-big-reactions.png" alt="" data-lity="" /></p>

<h5 id="notifications---fastlane-pull-requests">Notifications - fastlane pull requests</h5>
<p><img src="/images/2021-03-15/5-notification-prs.png" alt="" data-lity="" /></p>

<h5 id="notificatinos---fastlane-issues">Notificatinos - fastlane issues</h5>
<p><img src="/images/2021-03-15/6-notifications-issues.png" alt="" data-lity="" /></p>

<h5 id="notifications---fastlane-discussions">Notifications - fastlane discussions</h5>
<p><img src="/images/2021-03-15/7-notifications-discussions.png" alt="" data-lity="" /></p>

<h3 id="the-future">The Future</h3>

<p>I’m not <em>exactly</em> sure where I’m going to take Gargamel in the future. I made this for me but I also wanted to share it because I love it 😊 I don’t <em>really</em> want to productize it. That’s a lot of added pressure and I’m not sure I could make everybody happy. I’m thinking of open sourcing it if other find that this would be useful. I don’t want to recruit anybody to help because that is added pressure but I’d be happy if others found excitement in this.</p>

<p>If you see any issues in this article, have any feedback, or would have interest in contributing to Gargamel, please feel free to tweet me at <a href="https://twitter.com/joshdholtz">@joshdholtz</a> or email me.</p>

<p>Happy side projecting, everyone! 😜</p>]]></content><author><name>joshdholtz</name></author><category term="blog" /><summary type="html"><![CDATA[I told myself I wasn’t going to work on other side projects.. I did not want to stretch myself to thin while being a new dad, lead maintainer of fastlane, and just recently release An Otter RSS and ConnectKit. But, while focusing on my day-to-day fastlane work, I shortly realized there was another tool that I needed that didn’t exist.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joshholtz.com/images/2021-03-15/1-fastlane.png" /><media:content medium="image" url="https://www.joshholtz.com/images/2021-03-15/1-fastlane.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How Apple’s Upcoming Two-Step/Two-Factor Enforcement Could Affect Your fastlane Experience</title><link href="https://www.joshholtz.com/blog/2021/02/17/apples-2fa-with-fastlane.html" rel="alternate" type="text/html" title="How Apple’s Upcoming Two-Step/Two-Factor Enforcement Could Affect Your fastlane Experience" /><published>2021-02-17T07:30:00+00:00</published><updated>2021-02-17T07:30:00+00:00</updated><id>https://www.joshholtz.com/blog/2021/02/17/apples-2fa-with-fastlane</id><content type="html" xml:base="https://www.joshholtz.com/blog/2021/02/17/apples-2fa-with-fastlane.html"><![CDATA[<h2 id="preface">Preface</h2>

<p>This blog post is 100% my thoughts and opinions. It does not reflect <em>fastlane</em>’s or Google’s thoughts and opinions.</p>

<p>Also, note that this blog post is not critiquing or complaining about the enforcement of two-step/two-factor. This blog post is only here to help users understand how they could be affected and the options they have.</p>

<p>We good? Let’s get started.</p>

<ul>
  <li><a href="#introdoctuion">Introduction</a></li>
  <li><a href="#comparing-two-step-verification-vs-two-factor-authentication">Comparing “two-step verification” vs “two-factor authentication”?</a></li>
  <li><a href="#so-will-this-affect-you">So… Will this affect you?</a>
    <ul>
      <li><a href="#do-you-already-have-an-apple-id-account-with-2sv2fa-enabled">Do you already have an Apple ID account with 2SV/2FA enabled?</a></li>
      <li><a href="#do-you-use-xcode-and-the-app-store-connect-website">Do you use Xcode and the App Store Connect website?</a></li>
      <li><a href="#do-you-run-fastlane-locally">Do you run <em>fastlane</em> locally?</a></li>
      <li><a href="#do-you-run-fastlane-on-a-ci">Do you run <em>fastlane</em> on a CI?</a></li>
    </ul>
  </li>
  <li><a href="#other-changes-you-might-need-to-make">Other Changes You Might Need To Make</a></li>
  <li><a href="#if-you-really-need-two-step-authentication-on-ci-there-are-ways-">If you REALLY need Two-Step Authentication on CI… there are ways 😈</a></li>
</ul>

<hr />

<h2 id="introduction">Introduction</h2>

<p>Apple is requiring that all Apple ID accounts that sign in to App Store Connect turn on two-step verification or two-factor authentication this month. This is a good move as it does increase the security for all Apple IDs and apps on the App Store. It will however have some negative effects on accounts that are used for automation.</p>

<p>This email was sent out by Apple on November 20th, 2020.</p>

<blockquote>
  <p>Starting February 2021, additional authentication will be required for all users to sign in to App Store Connect. This extra layer of security for your Apple ID helps ensure that you’re the only person who can access your account. You can enable two-step verification or two-factor authentication now in the Security section of your Apple ID account or the Apple ID section of Settings on your device.</p>
</blockquote>

<p>Most users won’t be affected by this. Some users will be mildly annoyed by this. But this change will be crippling to a handful of users.</p>

<p>But <em>which</em> group do you fall in?</p>

<p>I’ve had this question come my way numerous times now so let’s answer it once and for all 😊</p>

<hr />

<h2 id="comparing-two-step-verification-vs-two-factor-authentication">Comparing “two-step verification” vs “two-factor authentication”?</h2>

<p>We could go into a lot of detail about the differences between two-step verification (2SV) and two-factor authentication (2FA). There are plenty of blog posts that get more technical but we are going to keep it simple today. Below are how most Apple ID accounts will see two-step verification and two-factor authentication.</p>

<h3 id="two-step-verification-2sv">Two-Step Verification (2SV)</h3>

<p>Two-step verification is powered by a phone number. After signing in to your Apple ID account, 6 digit verification code is sent via SMS or phone call. This code will need to be entered into the form into the device you are logging into.</p>

<p>You can also have multiple phone numbers attached to your Apple ID account. This is great if you have a team Apple ID with multiple people that need to sign into it. These phone numbers can be set up on <a href="appleid.apple.com">appleid.apple.com</a>. This was the first additional security factor added to the Apple ID account.</p>

<h3 id="two-factor-authentication-2fa">Two-Factor Authentication (2FA)</h3>

<p>Two-factor authentication might be the form most people are familiar with. After signing in to your Apple ID account, this security method shows a popup on your Apple devices (iPhone, iPad, Apple Watch, Mac) that says something like “Apple ID Sign In Requested” with a location and map of where it’s being signed in from. After you click “Allow”, a 6 digit code appears. This 6 digit code is what you enter on the device your logging in from.</p>

<p>Apple will always attempt the 2FA method first if it’s possible. This security step works great for individual accounts but it less great for Apple IDs that are shared by a team.</p>

<p>Luckily, you can always fall back to 2SV! You will see a button/link that says “Didn’t get a verification code?” that will take you two an option to use a phone number for either SMS or phone call verification.</p>

<hr />

<h2 id="so-will-this-affect-you">So… Will this affect you?</h2>

<p>I can’t answer this for you but I can help you answer it 😇  I made a flowchart that should lead you to your answer. This flowchart does not cover every use case but it should hopefully be a good compliment to the questions that are about to follow.</p>

<p><img src="/images/2021-02-15/flowchart.jpeg" alt="Flowchart for seeing how Apple ID affects you" data-lity="" /></p>

<h3 id="do-you-already-have-an-apple-id-account-with-2sv2fa-enabled">Do you already have an Apple ID account with 2SV/2FA enabled?</h3>

<p>This new requirement Apple is enforcing this month (February 2021) is for Apple IDs that don’t already have 2SV/2FA enabled. If you already have it then the following reading will just be for your enjoyment 😉</p>

<p>If you don’t have 2SV/2FA enabled, your life will be changing a little bit. Keep reading to figure out how 👇</p>

<p>I have a few Apple IDs that I manage. My Apple ID has had 2SV/2FA enabled for a long time now. But I’ve also managed several Apple IDs that <strong>did not</strong> have 2SV/2FA enabled. These accounts were used specifically for <em>fastlane</em> automation (locally and on CIs). The passwords were either stored in my local Keychain Access or secure environment variables on the CI. I never had to worry about my automation stalling because these accounts only needed email and a password.</p>

<h3 id="do-you-use-xcode-and-the-app-store-connect-website">Do you use Xcode and the App Store Connect website?</h3>

<p>You will probably only be affected a little bit when using Xcode and the App Store Connect website to upload and release your apps.</p>

<p>Xcode allows you to sign in to your Apple ID through the Preferences window. I don’t remember if signing into your Apple ID through Xcode presents a 2SV/2FA dialog but this might be a place where you see the new security flow.</p>

<p>You will also start seeing the 2SV/2FA prompts when signing into App Store Connect (<a href="appstoreconnect.apple.com">appstoreconnect.apple.com</a>). As mentioned earlier, you will see a new input for 2SV/2FA codes after entering your email and password. I can never pinpoint the exact session length but you should be able to go between 15 and 30 days without needing to use 2SV/2FA again.</p>

<h3 id="do-you-run-fastlane-locally">Do you run <em>fastlane</em> locally?</h3>

<p><em>fastlane</em> has historically only offered Apple ID as the only login method when interacting with Apple’s services. There was a recent addition of authenticating with the <a href="https://docs.fastlane.tools/app-store-connect-api/">App Store Connect API Key</a> but the majority of users that are probably questioning 2SV/2FA have not moved over to that yet.</p>

<p>But… if you are still on Apple ID and only run <em>fastlane</em> locally, you will experience a very minor change. The change won’t even be in your day-to-day work. It will just be a few times a month or so 😊</p>

<p><em>fastlane</em>’s Apple ID login works the same as the web login to App Store Connect website mentioned above. Every 15 to 30 days, <em>fastlane</em> will prompt you for a 2FA code. This will popup that system dialog on your Apple devices. You will simply need to enter that 6 digit code in your CLI.</p>

<p><em>fastlane</em> also supports 2SV if you don’t want to use 2FA. Instead of entering in the 6 digit code, type in either “sms” or “phone” and <em>fastlane</em> will fallback to using your phone number as the two-step method.</p>

<p>Besides needing to enter this code whenever the sessions expire every few weeks, you really shouldn’t notice any big changes.</p>

<h3 id="do-you-run-fastlane-on-a-ci">Do you run <em>fastlane</em> on a CI?</h3>

<p>This is where a majority of the heartaches and headaches will come from 🙃 <em>fastlane</em> is built to run as easily on a CI as it is on a local machine. The enforcing of 2SV/2FA, however, will end up taking down most CIs that use <em>fastlane</em> that use Apple ID authentication. That sounds pretty grim… but it is not the end of the world! There are two solutions for this.</p>

<p>The first is to keep using the Apple ID authentication but with a pre-generated session. The second is to migrate from using Apple ID authentication to App Store Connect API Key authentication.</p>

<p>Keep reading to learn which is best for you (and your team).</p>

<h4 id="option-1-keep-using-apple-id-but-with-pre-generated-session">Option 1: Keep using Apple ID but with pre-generated session</h4>

<p>Apple requiring 2FA/2SV doesn’t mean that you can’t still use your Apple ID to authenticate on a CI. But it does mean that you will have a little bit more maintenance/upkeep to have a valid Apple ID session on your CI.</p>

<p>You previously may have put your Apple ID email address and password in your CI’s secure environment variables. This won’t work anymore. <em>fastlane</em> will attempt to prompt for a 2FA/2SV code which will either halt or fail your CI process. But how do we get around this? The trick is now to generate a session on your local machine.</p>

<p>Instead of setting your Apple ID email and password which creates a new session on your CI, you will instead set the <code class="language-plaintext highlighter-rouge">FASTLANE_SESSION</code> variable. So what is this <code class="language-plaintext highlighter-rouge">FASTLANE_SESSION</code> value? You can think of it as the cookie that your browser stores when you login into App Store Connect and enter your 2FA/2SV code there. That valid session cookie hangs around for some time (15 to 30 days) and so that you don’t need to log in or enter the 2FA/2SV code again.</p>

<p>We can do something similar with <em>fastlane</em>! All you need to do is run <code class="language-plaintext highlighter-rouge">fastlane spaceauth</code> on your local machine. You will need to enter your Apple ID email, password, and 2FA/2SV code. This command will then output the value that you need to use with the <code class="language-plaintext highlighter-rouge">FASTLANE_SESSION</code> variable.</p>

<p>It’s more work than you are currently doing but it shouldn’t be a daunting amount of work. The worst part is that you may have to restart your CI job when your existing session expires.</p>

<p>💡 Is it too exhausting navigating the user interfaces of your CI? Well.. your CI might have an API you can use for setting environment variables! You <em>could</em> create a <em>fastlane</em> lane (that you run locally) that generates the <code class="language-plaintext highlighter-rouge">FASTLANE_SESSION</code> and programmatically updates it on your CI for you 😉</p>

<ul>
  <li><a href="http://docs.fastlane.tools/best-practices/continuous-integration/#environment-variables-to-set">Docs on FASTLANE_SESSION</a></li>
  <li><a href="http://docs.fastlane.tools/best-practices/continuous-integration/#spaceauth">Docs on pre-generated session</a></li>
</ul>

<h4 id="option-2-migrate-to-app-store-connect-api-key">Option 2: Migrate to App Store Connect API Key</h4>

<p>This is my recommended approach if you can make this work for you as migrating from Apple ID to the App Store Connect API Key <em>should</em> be a minimal change for most users.</p>

<p>Instead of using the <code class="language-plaintext highlighter-rouge">username</code> option or <code class="language-plaintext highlighter-rouge">FASTLANE_USER</code>/<code class="language-plaintext highlighter-rouge">FASTLANE_PASSWORD</code> environment variables, you would use the <code class="language-plaintext highlighter-rouge">api_key</code> or <code class="language-plaintext highlighter-rouge">api_key_path</code> options or the <code class="language-plaintext highlighter-rouge">APP_STORE_CONNECT_API_KEY</code> or <code class="language-plaintext highlighter-rouge">APP_STORE_CONNECT_API_KEY_PATH</code> variables 😊</p>

<p>A majority of the tools/actions have support for these new options/environment variables:</p>
<ul>
  <li>Tools
    <ul>
      <li>cert</li>
      <li>deliver</li>
      <li>match</li>
      <li>pilot</li>
      <li>precheck</li>
      <li>sigh</li>
    </ul>
  </li>
  <li>Actions
    <ul>
      <li>app_store_build_number</li>
      <li>latest_testflight_build_number</li>
      <li>register_device</li>
      <li>register_devices</li>
      <li>set_changelog</li>
    </ul>
  </li>
</ul>

<p>It <em>should</em> be a small code change to make the change over for these tools. I recommend going this route as this uses an official API that is supported and documented by Apple. It’s a more stable path for <em>fastlane</em> to use compared to the Apple ID auth APIs.</p>

<ul>
  <li><a href="http://docs.fastlane.tools/app-store-connect-api/">Docs on App Store Connect API</a></li>
</ul>

<p>But… if you use the custom <code class="language-plaintext highlighter-rouge">Spaceship</code> code, <code class="language-plaintext highlighter-rouge">produce</code>, <code class="language-plaintext highlighter-rouge">pem</code>, or <code class="language-plaintext highlighter-rouge">download_dsyms</code> this may not be the best option for you. These tools cannot be fully supported by the App Store Connect API Key yet. The official APIs for these are not released yet. I don’t know an ETA for these yet but I’m hoping it happens soon 😊</p>

<hr />

<h2 id="other-changes-you-might-need-to-make">Other Changes You Might Need To Make</h2>

<p>Keeping your Apple ID auth with <code class="language-plaintext highlighter-rouge">FASTLANE_SESSION</code> or migrating to the App Store Connect API Key might not be enough for you.</p>

<p>Maybe there are some tools you use that aren’t available with App Store Connect API? Maybe you want to move your CI away from using Apple ID and App Store Connect API auth altogether?</p>

<p>Below are some situations you may run into and how to possibly fix them. This isn’t the complete list but I’m hoping it will help you think of other possible solutions 😉</p>

<h3 id="1-rethink-download_dsyms-strategy">#1: Rethink <code class="language-plaintext highlighter-rouge">download_dsyms</code> strategy</h3>

<p>Since the announcement of bitcode in Xcode, the <code class="language-plaintext highlighter-rouge">download_dsysm</code> has been a crucial action to a lot of users. Sending the bitcode to App Store Connect allows Apple to optimize the binaries for a different distribution of the apps. This makes the binaries slightly different and can make crash logs different. Developers need to pull down these new dSYM files from App Store Connect to properly de-symbolicate their crash logs. The <code class="language-plaintext highlighter-rouge">download_dsms</code> is an action that you would run after the build is done processing to download these dSYM files from App Store Connect with your Apple ID.</p>

<p>But now that Apple ID will have 2FA/2SV, you <em>may</em> need to adjust your <em>fastlane</em> setup.</p>

<h4 id="run-locally-or-in-a-separate-job">Run locally or in a separate job</h4>

<p>In <em>most</em> cases, the dSYM doesn’t <em>need</em> to be downloaded right after a build has finished processing which means that you <em>might</em> not need to run <code class="language-plaintext highlighter-rouge">download_dsyms</code> on your normal CI job.</p>

<p>This could instead be run on a separate job that runs once a day or a few times a week. Moving it to a separate job will prevent the job from failing if that <code class="language-plaintext highlighter-rouge">FASTLANE_SESSION</code> expiries.</p>

<p>You could also move this to a job that just gets run locally whenever you start your day 🤷‍♂️</p>

<h4 id="can-you-disable-bitcode-maybe-you-dont-need-download_dsyms">Can you disable bitcode? Maybe you don’t need <code class="language-plaintext highlighter-rouge">download_dsyms</code></h4>

<p>Or maybe… maybe you don’t even need bitcode?! If you disable bitcode, you can use the dSYMs directly from your Xcode build. Now you don’t even need to worry about <code class="language-plaintext highlighter-rouge">download_dsyms</code> action 🤷‍♂️</p>

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Just disable bitcode. It has no measurable benefits. (Also, generated dSYMs are often broken/incomplete)</p>&mdash; Peter Steinberger (@steipete) <a href="https://twitter.com/steipete/status/1360535259905990657?ref_src=twsrc%5Etfw">February 13, 2021</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<h3 id="2-do-you-even-need-apple-id-or-app-store-connect-api-key">#2: Do you even need Apple ID or App Store Connect API Key?</h3>

<p>If you aren’t able to make <code class="language-plaintext highlighter-rouge">FASTLANE_SESSION</code> work for you or if you can’t use the App Store Connect API Key for some reason, you aren’t stuck! You can still set up a CI that signs and uploads binaries with either of those 😏</p>

<p>If you are making use of <code class="language-plaintext highlighter-rouge">match</code> with read-only mode for signing, you don’t need to make use of any auth. <code class="language-plaintext highlighter-rouge">gym</code> also does not need any auth to build and sign your app.</p>

<p>If you are uploading your binary to the App Store or TestFlight, you can use either <code class="language-plaintext highlighter-rouge">deliver</code> or <code class="language-plaintext highlighter-rouge">pilot</code> with <code class="language-plaintext highlighter-rouge">FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD</code>. This does not require Apple ID or the App Store Connect API Key.</p>

<ul>
  <li><a href="http://docs.fastlane.tools/best-practices/continuous-integration/#application-specific-passwords">Docs on Application Specific Passwords</a></li>
  <li><a href="http://docs.fastlane.tools/actions/pilot/#use-an-application-specific-password-to-upload">Docs on pilot with Application Specific Password</a></li>
</ul>

<h3 id="3-produce-and-pem-wont-fully-work-with-app-store-connect-api-key">#3: <code class="language-plaintext highlighter-rouge">produce</code> and <code class="language-plaintext highlighter-rouge">pem</code> won’t fully work with App Store Connect API Key</h3>

<p><code class="language-plaintext highlighter-rouge">produce</code> and <code class="language-plaintext highlighter-rouge">pem</code> are the last two tools that have not had a big migration over to the new App Store Connect API. The reason being there are currently some App Store Connect APIs missing that makes this possible. Those APIs are:</p>

<ul>
  <li>Creating push certificates</li>
  <li>Creating apps on App Store (you can create new app identifiers on Developer Portal)</li>
</ul>

<p>With that being known, <code class="language-plaintext highlighter-rouge">produce</code> and <code class="language-plaintext highlighter-rouge">pem</code> will still need to use the Apple ID auth for the time being. If you need to use these in your flow, you will need to make use of <code class="language-plaintext highlighter-rouge">FASTLANE_SESSION</code> or find a way to split up your jobs.</p>

<p>These tools will be updated as soon as they possibly can when the new APIs become available.</p>

<h3 id="4-do-you-use-a-shared-apple-id-add-multiple-phone-numbers-for-two-step-authentication">#4: Do you use a shared Apple ID? Add multiple phone numbers for Two-Step Authentication</h3>

<p>I’ve mentioned this above a little bit but I just wanted to call this out again. Some users like to run <em>fastlane</em> locally but with a shared Apple ID that is only used for <em>fastlane</em> related things. These Apple IDs will also be subject to the 2FA/2SV requirement.</p>

<p>If you’d like to still use these not like the 2FA/2SV interrupt your workflow, your best bet here is to set multiple phone numbers on this Apple ID which can be used for 2SV. If the user gets hit with needing to use a 2SV, they would need to select their phone number from the list to get the 6 digit code for verification.</p>

<h3 id="5-you-might-need-to-migrate-custom-spaceship-code">#5: You might need to migrate custom Spaceship code</h3>

<p>A lot of users take automation into their own hands and use <code class="language-plaintext highlighter-rouge">spaceship</code> to handle Developer Portal and App Store Connect APIs directly. In the past, this has all been done with the <code class="language-plaintext highlighter-rouge">Spaceship::Portal</code> and <code class="language-plaintext highlighter-rouge">Spaceship::Tunes</code> modules. These modules access the legacy API which uses an Apple ID.</p>

<p>There is another module that interacts with the App Store Connect API. That module is the <code class="language-plaintext highlighter-rouge">Spaceship::ConnectAPI</code> module.   This module takes both either Apple ID auth or App Store Connect API Key auth. The Apple ID auth hits the same private API that the websites use. The API Key auth hits the official API.</p>

<p>If you are running into issues using custom <code class="language-plaintext highlighter-rouge">spaceship</code> with Apple ID and 2FA/2SV, you may need to look into migrating your code over to API Key auth with <code class="language-plaintext highlighter-rouge">Spaceship::ConnectAPI</code></p>

<ul>
  <li><a href="https://github.com/fastlane/fastlane/blob/master/spaceship/docs/AppStoreConnect.md">Docs for Spaceship’s App Store Connect</a></li>
</ul>

<hr />

<h2 id="if-you-really-need-two-step-authentication-on-ci-there-are-ways-">If you REALLY need Two-Step Authentication on CI… there <em>are</em> ways 😈</h2>

<p>If you <em>really</em> need to keep using Apple ID, there <em>are</em> ways to programmatically enter your 2FA code. Below is a monkey patch to overwrite the <code class="language-plaintext highlighter-rouge">ask_for_2fa_code</code> method instead of <code class="language-plaintext highlighter-rouge">spaceship</code>. This method is what would normally prompt for the code via the CLI but you could use this to get the 2FA code from where ever you want.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Spaceship::Client.send(:define_method, :ask_for_2fa_code) do |message|
   some_method_to_get_2fa
end

def some_method_to_get_2fa
  # maybe poll from a message queue
  # maybe poll from a twilio
  # maybe poll from a web form waiting for user input
end
</code></pre></div></div>

<h3 id="send-2sv-to-an-android-phone-number">Send 2SV to an Android phone number</h3>

<p>I don’t have an Android phone with an active number but this an approach I’ve heard of people doing and that I would like to try. Android allows for the apps to programmatically read SMS messages. With this ability, an Apple 2SV code getting sent to an Android phone can get forward into the <em>fastlane</em> job. This could get forwarded into a message queue that the monkey patch polls for.</p>

<p>There are more than enough ways for this to work but this is one suggested way if you need to keep using your Apple ID in an automated way.</p>

<p>⚠️ I do want to call out that you please assess any potential security issues involved when trying this approach.</p>

<h2 id="thats-all-folks">That’s All Folks</h2>

<p>I mean… I don’t think there is ever an end to different <em>fastlane</em> approaches we can talk about when it comes to mitigating effects from the enforcing of 2FA/2SV. But I think that is all that I can fit into this blog post.</p>

<p>As a reminder, this blog post was not for analyzing or criticizing Apple’s enforcement of 2FA/2SV. It is not my job to judge this and nor should it be. This blog post was simply to inform <em>fastlane</em> users what Two-Factor Authentication and Two-Step Verification mean and different approaches to address issues they may run into when the 2FA/2SV enforcement comes into play.</p>

<p>If you see any issues in this article or have any feedback, please feel free to tweet me at <a href="https://twitter.com/joshdholtz">@joshdholtz</a> or email me.</p>

<p>It’s been a pleasure having your attention to the very bottom of this article ❤️ Happy <em>fastlaning</em>!</p>]]></content><author><name>joshdholtz</name></author><category term="blog" /><summary type="html"><![CDATA[Preface]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.joshholtz.com/images/2021-02-15/fastlane_2fa.png" /><media:content medium="image" url="https://www.joshholtz.com/images/2021-02-15/fastlane_2fa.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>