<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>No-Wham Dev</title><description>Practical mobile app development guides for iOS and Android teams, covering architecture, testing, release workflows, and developer productivity.</description><link>https://nowham.dev/</link><atom:link href="https://nowham.dev/rss.xml" rel="self" type="application/rss+xml"/><item><title>Rules files and context - teaching the agent your codebase</title><link>https://nowham.dev/posts/rules-files-and-context/</link><guid isPermaLink="true">https://nowham.dev/posts/rules-files-and-context/</guid><description>How to stop the agent from writing code that works but looks nothing like the rest of your project.</description><pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;So, you did your first agentic session. Congrats! The agent wrote code that compiles, runs, and technically does what you asked. But you looked at it and thought... &quot;this is &lt;em&gt;not&lt;/em&gt; how we do things here.&quot;&lt;/p&gt;
&lt;p&gt;The architecture is wrong. The naming is off. It puts files in places that make no sense for your project. It used some library you&apos;ve never heard of when you&apos;ve got a perfectly good utility that does the same thing. The code &lt;em&gt;works&lt;/em&gt;, but it looks like a stranger wrote it. Which, to be fair, it is.&lt;/p&gt;
&lt;p&gt;This is the single most common complaint I hear from people who try agentic coding for the first time: &quot;It writes code that works but doesn&apos;t match my project.&quot; And the fix is simpler than you think.&lt;/p&gt;
&lt;h2&gt;Rules files: your project&apos;s instruction manual&lt;/h2&gt;
&lt;p&gt;Most agent tools support some form of a &quot;rules file&quot; — a file that lives in your repo and tells the agent how your project works. Think of it as onboarding documentation, but for your AI pair programmer.&lt;/p&gt;
&lt;p&gt;Different tools call them different things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://CLAUDE.md&quot;&gt;CLAUDE.md&lt;/a&gt;&lt;/strong&gt; — for Claude Code and tools that use Anthropic&apos;s models&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.cursorrules&lt;/strong&gt; — for Cursor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copilot instructions&lt;/strong&gt; — for GitHub Copilot&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.windsurfrules&lt;/strong&gt; — for Windsurf&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The name and format vary, but the concept is identical: a file in your repo that the agent reads &lt;em&gt;before&lt;/em&gt; it starts working. It&apos;s the &quot;hey, before you write any code, here&apos;s how we do things&quot; brief.&lt;/p&gt;
&lt;p&gt;And no, you don&apos;t need to pick just one. If your team uses different tools, throw a few of these in your repo. They don&apos;t conflict, and they&apos;re just text files. Low cost, high impact.&lt;/p&gt;
&lt;h2&gt;What to put in them&lt;/h2&gt;
&lt;p&gt;Here&apos;s where people either overthink or underthink this. You don&apos;t need to write a 50-page architecture document. But you also can&apos;t just write &quot;make good code&quot; and call it a day.&lt;/p&gt;
&lt;p&gt;Here&apos;s what actually matters:&lt;/p&gt;
&lt;h3&gt;Architecture pattern&lt;/h3&gt;
&lt;p&gt;This is the big one. If your project uses MVVM, say so. If you&apos;re using TCA, say so. If you&apos;ve got your own homebrewed architecture that you swear is better than everything else (we all have that friend), &lt;em&gt;describe it&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Architecture
We use MVVM across the app.
- Views are SwiftUI views that observe a ViewModel
- ViewModels are ObservableObject classes that handle business logic
- Models are plain structs in the Models/ folder
- Views never call services directly, always go through the ViewModel
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Naming conventions&lt;/h3&gt;
&lt;p&gt;You&apos;d be amazed how much this one matters. Agents &lt;em&gt;love&lt;/em&gt; making up their own names, and they&apos;re rarely the ones you&apos;d choose.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Naming
- ViewModels: `{Feature}ViewModel` (e.g., `SettingsViewModel`)
- Views: `{Feature}Screen` for full screens, `{Feature}View` for components
- Files match their primary type name
- Use camelCase for properties, PascalCase for types
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;File structure&lt;/h3&gt;
&lt;p&gt;Where do things go? The agent doesn&apos;t inherently know that your ViewModels go in &lt;code&gt;Sources/Features/{Feature}/ViewModels/&lt;/code&gt; and not just floating around in the root.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## File structure
Each feature has its own folder under Sources/Features/:
- Sources/Features/{Feature}/Views/
- Sources/Features/{Feature}/ViewModels/
- Sources/Features/{Feature}/Models/
Tests mirror the source structure under Tests/Features/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Preferred libraries and patterns&lt;/h3&gt;
&lt;p&gt;If you&apos;ve got a design system, mention it. If you use specific libraries for networking, storage, or whatever, list them. This prevents the agent from importing random third-party dependencies.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Libraries &amp;amp; patterns
- Networking: Use our APIClient (Sources/Networking/APIClient.swift), NOT URLSession directly
- UI components: Use components from our DesignSystem module
- Navigation: Use the Router pattern (see Sources/Navigation/Router.swift)
- Async: Prefer async/await over Combine for new code
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Beyond rules files: context in your prompts&lt;/h2&gt;
&lt;p&gt;The rules file is your baseline. It covers the general stuff that applies to &lt;em&gt;every&lt;/em&gt; task. But for specific tasks, you&apos;ll want to give the agent more context directly in your prompt.&lt;/p&gt;
&lt;p&gt;Here&apos;s the difference:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rules file&lt;/strong&gt; (general): &quot;We use MVVM, files go in Features/ folders, use our DesignSystem module.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompt context&lt;/strong&gt; (specific): &quot;Add a settings screen. Look at how ProfileScreen is built — follow the same pattern. The toggles should use DesignSystem.Toggle, and navigation should go through the Router.&quot;&lt;/p&gt;
&lt;p&gt;See the difference? The rules file sets the stage. The prompt points at specific examples. Together, they give the agent everything it needs to write code that actually fits your project.&lt;/p&gt;
&lt;p&gt;Some tips for giving good context:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reference existing files by name.&lt;/strong&gt; &quot;Follow the pattern in ProfileScreen&quot; is 10x better than &quot;follow our patterns.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be specific about what to reuse.&lt;/strong&gt; &quot;Use the same cell style as in ProfileScreen&apos;s list&quot; is better than &quot;make it look similar.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mention what &lt;em&gt;not&lt;/em&gt; to do.&lt;/strong&gt; &quot;Don&apos;t add new dependencies&quot; or &quot;Don&apos;t use UIKit, stay in SwiftUI&quot; saves you a round trip.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The payoff&lt;/h2&gt;
&lt;p&gt;Let me paint a picture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Without rules or context:&lt;/strong&gt; You ask the agent to add a settings screen. It creates a single massive ViewController with inline layout code, imports a random settings library from GitHub, puts everything in a folder called &quot;Misc&quot;, and names the class &quot;SettingsVC.&quot; It &lt;em&gt;works&lt;/em&gt;, but you want to cry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With rules and context:&lt;/strong&gt; You ask the agent to add a settings screen. It creates a SettingsScreen view that observes a SettingsViewModel, uses your DesignSystem.Toggle for switches, follows your Router for navigation, puts files in Sources/Features/Settings/, and the code looks like it was written by someone who&apos;s been on your team for months.&lt;/p&gt;
&lt;p&gt;Same agent. Same model. Same intelligence. The only difference is &lt;em&gt;you&lt;/em&gt; told it how your project works. That&apos;s it. That&apos;s the whole trick.&lt;/p&gt;
&lt;h2&gt;A starter template&lt;/h2&gt;
&lt;p&gt;Here&apos;s a rules file you can steal, adapt, and throw in your repo today. It&apos;s opinionated — change it to match &lt;em&gt;your&lt;/em&gt; project, not mine.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Project Rules

## Architecture
- We use [MVVM/MVI/TCA/whatever you use]
- [Brief description of the data flow]
- [Any other architectural constraints]

## Naming Conventions
- Views: `{Feature}Screen` for full screens, `{Feature}View` for reusable components
- ViewModels: `{Feature}ViewModel`
- Models: plain descriptive name (e.g., `UserProfile`, `Settings`)
- Files match their primary type name

## File Structure
- Features live in `Sources/Features/{FeatureName}/`
- Each feature has Views/, ViewModels/, and Models/ subfolders
- Tests mirror source structure under Tests/

## Dependencies &amp;amp; Libraries
- Networking: [your networking setup]
- UI: [your design system or UI library]
- Navigation: [your navigation approach]
- Storage: [your persistence approach]

## Code Style
- [Async approach: async/await, Combine, RxSwift, etc.]
- [Error handling approach]
- [Any specific linting rules or formatters]

## Things to Avoid
- Don&apos;t add new third-party dependencies without asking
- Don&apos;t use [deprecated pattern/library] - use [preferred alternative] instead
- Don&apos;t put business logic in Views
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy it, fill in the brackets, drop it in your repo root, and you&apos;re 80% of the way there. You can refine it over time as you notice patterns the agent keeps getting wrong.&lt;/p&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rules files are the single biggest quality improvement you can make&lt;/strong&gt; to your agentic coding workflow. Low effort, massive impact.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They don&apos;t need to be long.&lt;/strong&gt; 20-30 lines covering architecture, naming, file structure, and key libraries is enough to start.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Combine rules files with specific context in your prompts.&lt;/strong&gt; Rules set the baseline, prompts handle the specifics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reference real files in your project.&lt;/strong&gt; &quot;Follow the pattern in ProfileScreen&quot; beats &quot;follow our patterns&quot; every time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iterate on your rules file.&lt;/strong&gt; When the agent gets something wrong repeatedly, add a rule for it. It&apos;s a living document.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next time, we&apos;ll look at managing your context windows, through actual examples and usages!. Stay tuned.&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Your first agentic session - let&apos;s actually do something</title><link>https://nowham.dev/posts/your-first-agentic-session/</link><guid isPermaLink="true">https://nowham.dev/posts/your-first-agentic-session/</guid><description>A hands-on walkthrough of your first real task with an AI agent. No theory, just vibes.</description><pubDate>Mon, 09 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Alright, so you&apos;ve read the &lt;a href=&quot;/blog/agentic-coding-for-mobile-developers-simplified&quot;&gt;intro post&lt;/a&gt;, you have a vague idea of what agentic coding is, you might have even installed a tool or two. Now what?&lt;/p&gt;
&lt;p&gt;Now we actually &lt;em&gt;do&lt;/em&gt; something. Open up your project, pick your weapon of choice (IDE, terminal, standalone app, whatever), and let&apos;s walk through your first real session together. No theory, no fluff, just a real task from start to finish.&lt;/p&gt;
&lt;h2&gt;What are we building?&lt;/h2&gt;
&lt;p&gt;For this walkthrough, I&apos;m going to add a feature that pretty much every app has and every developer has built a hundred times: a &lt;strong&gt;settings screen&lt;/strong&gt;. It&apos;s got a list of options, maybe some toggles, maybe some navigation to sub-screens. It&apos;s not rocket science, and that&apos;s the point.&lt;/p&gt;
&lt;p&gt;You don&apos;t want your first agentic session to be &quot;rewrite my entire networking layer.&quot; You want something that&apos;s:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Familiar&lt;/strong&gt; — you know what the end result should look like&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scoped&lt;/strong&gt; — it&apos;s not going to spiral into a week-long refactor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verifiable&lt;/strong&gt; — you can look at the result and immediately tell if it&apos;s right&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A settings screen ticks all those boxes. Pick whatever feature makes sense for &lt;em&gt;your&lt;/em&gt; project, but if you&apos;re following along, settings screen it is.&lt;/p&gt;
&lt;h2&gt;Setting up&lt;/h2&gt;
&lt;p&gt;Open your project in whatever tool you picked. If you haven&apos;t picked one yet, here&apos;s a quick refresher from the last post: your IDE probably already has agent support built in, there are terminal CLIs from the big providers, and there are standalone apps that wrap those CLIs.&lt;/p&gt;
&lt;p&gt;For this walkthrough, it genuinely does not matter which one you use. The &lt;em&gt;flow&lt;/em&gt; is the same across all of them:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Point the agent at your project&lt;/li&gt;
&lt;li&gt;Give it a task&lt;/li&gt;
&lt;li&gt;Watch it work&lt;/li&gt;
&lt;li&gt;Tell it what to fix&lt;/li&gt;
&lt;li&gt;Repeat until you&apos;re happy&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&apos;s it. That&apos;s the whole workflow. Let&apos;s do it.&lt;/p&gt;
&lt;h2&gt;The first prompt&lt;/h2&gt;
&lt;p&gt;Here&apos;s where most people overthink things. Your first prompt doesn&apos;t need to be a masterpiece. It doesn&apos;t need to be a perfectly engineered 500-word essay about exactly what you want. It&apos;s a conversation, not a contract.&lt;/p&gt;
&lt;p&gt;Here&apos;s roughly what I&apos;d type:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Add a settings screen to the app. It should have a list of options including: dark mode toggle, notification preferences, account info, and an about section. Follow the existing navigation patterns in the project.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That&apos;s it. Nothing fancy. A few things to note:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I told it &lt;em&gt;what&lt;/em&gt; to build (settings screen)&lt;/li&gt;
&lt;li&gt;I gave it &lt;em&gt;specifics&lt;/em&gt; (the list of options)&lt;/li&gt;
&lt;li&gt;I told it to follow existing patterns (this is key — more on this later)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Could I have given it more detail? Sure. Could I have given it less? Also sure. The beauty of agentic coding is that it&apos;s a &lt;em&gt;conversation&lt;/em&gt;. If the agent doesn&apos;t get it right, you tell it. You&apos;re not writing a requirements doc, you&apos;re pair programming with a very fast, very eager junior developer.&lt;/p&gt;
&lt;h2&gt;Watching the agent work&lt;/h2&gt;
&lt;p&gt;This is where it gets fun. And, if I&apos;m being honest, a little bit unsettling the first time.&lt;/p&gt;
&lt;p&gt;The agent is going to start &lt;em&gt;reading your project&lt;/em&gt;. It&apos;s going to look at your file structure, your existing screens, your navigation setup, your models. It might read 10, 20, 30 files before it writes a single line of code. And you&apos;re going to sit there watching it, going &quot;oh shit, it&apos;s reading &lt;em&gt;everything.&lt;/em&gt;&quot;&lt;/p&gt;
&lt;p&gt;This is normal. This is &lt;em&gt;good&lt;/em&gt;. It&apos;s trying to understand your project before it starts making changes. You know, like what a good developer should do.&lt;/p&gt;
&lt;p&gt;Then it&apos;s going to start writing. It&apos;ll create files, modify existing ones, maybe add new dependencies. Depending on the tool you&apos;re using, you&apos;ll see this happening in real time, or you&apos;ll get a summary at the end.&lt;/p&gt;
&lt;p&gt;Here&apos;s the thing that trips people up: &lt;strong&gt;the first output is not the final output.&lt;/strong&gt; Read that again. The agent&apos;s first attempt is a &lt;em&gt;draft&lt;/em&gt;. It&apos;s a starting point. And this is where the &lt;em&gt;actual&lt;/em&gt; skill of agentic coding comes in.&lt;/p&gt;
&lt;h2&gt;Reviewing and course-correcting&lt;/h2&gt;
&lt;p&gt;The agent wrote you a settings screen. Great. Now look at it.&lt;/p&gt;
&lt;p&gt;Does it compile? Probably. Does it &lt;em&gt;work&lt;/em&gt;? Most likely. Does it look like something you&apos;d actually ship? Ehh. Let&apos;s see.&lt;/p&gt;
&lt;p&gt;Here&apos;s some stuff you might notice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It used a different architecture pattern than the rest of your app&lt;/li&gt;
&lt;li&gt;The naming conventions are off&lt;/li&gt;
&lt;li&gt;It added a dependency you don&apos;t want&lt;/li&gt;
&lt;li&gt;The UI is functional but doesn&apos;t match your design system&lt;/li&gt;
&lt;li&gt;It put files in the wrong folders&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of this is &lt;em&gt;fine&lt;/em&gt;. This is expected. Here&apos;s what you do: you tell it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The settings screen looks good, but we use MVVM in this project. Can you restructure it to match the pattern used in ProfileScreen?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The toggles are working but they should use our custom DesignSystem.Toggle component, not the default one.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Move the SettingsViewModel to the ViewModels folder, not the Screens folder.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;See what&apos;s happening? You&apos;re not starting from scratch. You&apos;re &lt;em&gt;refining&lt;/em&gt;. The agent did 80% of the work, and you&apos;re steering the last 20%. That back-and-forth, that conversation, &lt;em&gt;that&lt;/em&gt; is the skill. Not the first prompt.&lt;/p&gt;
&lt;p&gt;Some sessions, you&apos;ll go back and forth 2-3 times and you&apos;re done. Other sessions, you&apos;ll be going back and forth 10 times and want to throw your laptop out the window. Both are normal. The more you do it, the better your first prompts get, and the fewer rounds you need.&lt;/p&gt;
&lt;h2&gt;Know when to start fresh&lt;/h2&gt;
&lt;p&gt;Here&apos;s a thing that a lot of noobies trip on: conversations with agents have a shelf life. The longer a session goes on, the more back-and-forth you have, the worse the agent gets. It starts forgetting things you told it earlier, contradicting itself, or confidently making up stuff that doesn&apos;t exist in your project.&lt;/p&gt;
&lt;p&gt;This isn&apos;t a bug, it&apos;s just how these things work. The agent has a limited memory for the conversation (they call it &quot;context&quot;), and when it fills up, older stuff gets compressed or dropped. Think of it like a whiteboard — eventually you run out of space and start erasing the stuff at the top.&lt;/p&gt;
&lt;p&gt;My rule of thumb: &lt;strong&gt;one task, one conversation.&lt;/strong&gt; If you asked the agent to build a settings screen and it&apos;s done (or close enough), and now you want to add a networking layer — start a new conversation. Don&apos;t keep piling tasks into the same session hoping the agent remembers everything from an hour ago. It won&apos;t.&lt;/p&gt;
&lt;p&gt;Signs that it&apos;s time to start fresh:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The agent starts &quot;forgetting&quot; things you already told it (re-adding that dependency you asked it to remove, ignoring your architecture, etc.)&lt;/li&gt;
&lt;li&gt;It confidently references files or classes that don&apos;t exist&lt;/li&gt;
&lt;li&gt;The responses start feeling generic, like it lost track of your project&lt;/li&gt;
&lt;li&gt;You&apos;ve been going back and forth for &lt;em&gt;ages&lt;/em&gt; and you&apos;re going in circles&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When in doubt, just start a new one. It&apos;s free, it&apos;s fast, and you&apos;ll get better results than trying to salvage a session that&apos;s gone off the rails.&lt;/p&gt;
&lt;h2&gt;The result&lt;/h2&gt;
&lt;p&gt;So, what did we end up with?&lt;/p&gt;
&lt;p&gt;A working settings screen with a list of options, some toggles, navigation to sub-screens, and it mostly follows the existing patterns in the project. I had to manually tweak a few things — some spacing was off, a couple of naming conventions were wrong, and it didn&apos;t quite nail the navigation transition I wanted.&lt;/p&gt;
&lt;p&gt;Was it faster than writing it from scratch? Yes. Meaningfully so. The boilerplate alone — the view, the view model, the navigation registration, the list cells — that would have taken me a solid chunk of time just typing it all out. The agent did that in seconds.&lt;/p&gt;
&lt;p&gt;Was the code &lt;em&gt;good&lt;/em&gt;? It was... fine. It compiled, it worked, it was readable. It wasn&apos;t going to win any architecture awards, and it needed some massaging to fit the project properly. But as a starting point? Pretty damn solid.&lt;/p&gt;
&lt;p&gt;Here&apos;s my honest assessment: the agent is &lt;em&gt;really&lt;/em&gt; good at the boring parts. The boilerplate, the scaffolding, the repetitive setup code. It&apos;s less good at understanding the &lt;em&gt;soul&lt;/em&gt; of your project — the little conventions, the design patterns, the &quot;we do it this way because of that one bug in 2024&quot; stuff. That&apos;s still your job. For now.&lt;/p&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Your first prompt doesn&apos;t need to be perfect.&lt;/strong&gt; It&apos;s a conversation. You&apos;ll refine as you go.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The agent&apos;s first output is a draft, not a final product.&lt;/strong&gt; The skill is in the back-and-forth, not in the initial prompt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start with something familiar.&lt;/strong&gt; Don&apos;t try to learn agentic coding &lt;em&gt;and&lt;/em&gt; a new framework at the same time. Use it on something you already know how to build, so you can focus on learning the &lt;em&gt;workflow&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The agent is great at boilerplate, less great at your project&apos;s conventions.&lt;/strong&gt; You&apos;ll spend most of your time steering it toward &lt;em&gt;your&lt;/em&gt; patterns, not fixing compilation errors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One task, one conversation.&lt;/strong&gt; Long sessions degrade. If the agent starts forgetting things or making stuff up, start fresh — don&apos;t try to rescue a stale session.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the next post, we&apos;re going to tackle that last point head-on. We&apos;ll look at how to &lt;em&gt;teach&lt;/em&gt; the agent your codebase — rules files, context, and all the tricks to make it stop writing code that works but looks nothing like the rest of your project.&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Agentic coding for mobile developers simplified - you&apos;re not as far behind as you think</title><link>https://nowham.dev/posts/agentic-coding-for-mobile-developers-simplified/</link><guid isPermaLink="true">https://nowham.dev/posts/agentic-coding-for-mobile-developers-simplified/</guid><description>A practical, no-hype intro to agentic coding for mobile developers.</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I know, I know, &lt;em&gt;another&lt;/em&gt; blog post about AI and agentic coding to clutter your feed and swarm you with information you don&apos;t feel like you need. You&apos;ve been hearing about AI, you must have, but you either tried it back when GPT-3 was a thing and it would make silly mistakes all the time, or you&apos;re constantly being bombarded with people on Twitter telling you that AI is cooking their brains, or telling you that they&apos;ve created a 10k MRR SaaS B2B app and you&apos;re a sucker who doesn&apos;t use it yet.&lt;/p&gt;
&lt;p&gt;I know, you&apos;re tired of it, but it feels inevitable. So, you want to at least get a &lt;em&gt;little bit&lt;/em&gt; of knowledge about it, but don&apos;t know where to start. Well, that&apos;s what I&apos;m here for. Let&apos;s take a bit of a step back from all the noise and the faff, and distill the situation with AI agentic coding to the basics, stuff that you can use right now, and what you should care about.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Disclaimer: This is a very, &lt;em&gt;very&lt;/em&gt; fast-moving industry, with very dynamic changes. The content of this blog post is accurate at the time of writing, and I will attempt to edit it as things change, but bear that in mind.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What is agentic coding? Is it ChatGPT in my editor?&lt;/h2&gt;
&lt;p&gt;Let&apos;s cover the basics first, to make sure we&apos;re all up to speed on terminology and the general vibe (pun intended). Agentic coding is, spoiler alert, coding with an agent. The main difference between what you might be used to with just ChatGPT is that an agent can have reasoning levels, and can complete a full task end to end, prompting itself, and basically &quot;take control&quot; - write to multiple files, delete files, create new ones, run tests, run terminal commands, etc. It &lt;em&gt;can&lt;/em&gt;, but it doesn&apos;t &lt;em&gt;have to&lt;/em&gt;. &lt;strong&gt;You&lt;/strong&gt; control how much power it has. There&apos;s of course more to it than that, it&apos;s an iceberg, but for our use case, this is the gist that you should care about.&lt;/p&gt;
&lt;h3&gt;Where am I using it?&lt;/h3&gt;
&lt;p&gt;This can be in a few places, but to make things simple, let&apos;s simmer it down to 3 locations:&lt;/p&gt;
&lt;h4&gt;The IDE&lt;/h4&gt;
&lt;p&gt;The current IDE you&apos;re working with has an agent mode connected to it. It&apos;s not a &quot;probably it does&quot;, it absolutely already does. Android Studio has Junie / AI Agent / Claude integration (we&apos;ll discuss it later), and Xcode recently received the ability to have agentic coding in it as well. Hell, there are even tools like Cursor and Antigravity that are entire IDEs dedicated to AI coding. Wherever you already are, there&apos;s agentic coding involved in it.&lt;/p&gt;
&lt;h4&gt;The terminal&lt;/h4&gt;
&lt;p&gt;This is where a lot of these tools started, and where a lot of them still live. Each one of the &quot;big players&quot; has their own CLI too, and there are tools like OpenCode that can be a central point for all of them. However, to keep things simple, we will be talking today about the first-party CLI tools, the ones the companies created for themselves.&lt;/p&gt;
&lt;h4&gt;The standalone apps&lt;/h4&gt;
&lt;p&gt;Again, the big players here have their own standalone apps. These apps are, in reality, a wrapper around their CLIs, but they make installation a lot easier, provide a nice GUI, and make project management easier.&lt;/p&gt;
&lt;h2&gt;Who is &lt;strong&gt;them&lt;/strong&gt;?&lt;/h2&gt;
&lt;p&gt;Right, yeah, time to talk about it. What actual agents are we talking about here?&lt;/p&gt;
&lt;p&gt;Well, there are honestly a bucket load, and it&apos;s partially why this sphere is so daunting for so many people. In reality, there are &lt;strong&gt;only two&lt;/strong&gt; players you should care about: OpenAI and Anthropic.&lt;/p&gt;
&lt;p&gt;Are there other providers? Yes, there are.&lt;/p&gt;
&lt;p&gt;Are they close in performance? Some.&lt;/p&gt;
&lt;p&gt;Do you &lt;em&gt;need&lt;/em&gt; to follow all of them? Absolutely not.&lt;/p&gt;
&lt;p&gt;In reality, these two are the biggest players out there, and for good reason. Their models (Codex 5.3 for OpenAI, and Sonnet and Opus 4.6 for Anthropic) are highly optimized, really accurate, and honestly do the job. There are other good providers, like Google for example, but I&apos;m going to focus on these two because they&apos;re the gold standard.&lt;/p&gt;
&lt;h3&gt;Where &lt;strong&gt;should&lt;/strong&gt; you be using them?&lt;/h3&gt;
&lt;p&gt;It does not matter. Surprising, right?&lt;/p&gt;
&lt;p&gt;Well, each of these platforms has its pluses and minuses, pros and cons, and you might find that you prefer to, for example, stay within your IDE for all of your development. That&apos;s fine. You might find that you like the simplicity of the terminal. That&apos;s also fine. Give them each a whirl, take a day to play around, find the flow that works for &lt;em&gt;you&lt;/em&gt;, and that one is the best. Can you optimize things? Probably. Endlessly. You can spend all day optimizing the flow for best performance and execution and whatever. Or you can, and in my opinion should, fuck around and find out.&lt;/p&gt;
&lt;h3&gt;Which one do I choose?&lt;/h3&gt;
&lt;p&gt;Ready for this? It does not matter.&lt;/p&gt;
&lt;p&gt;I know this might be a hot take for some, I know the die-hards for either platform are fuming right now and are writing me angry emails (or using their provider of choice to write me an email), but in reality, for your day-to-day usage, it really does boil down to which one you like more. Once more, this is a case of fucking around and finding out.&lt;/p&gt;
&lt;p&gt;&quot;But so-and-so on Twitter said that if I don&apos;t use Opus I&apos;m a pleb.&quot; It doesn&apos;t matter.&lt;/p&gt;
&lt;p&gt;&quot;But they told me on LinkedIn (what?) that Opus is slopus!&quot; It doesn&apos;t matter.&lt;/p&gt;
&lt;p&gt;&quot;But this dude said that...&quot; IT DOES NOT MATTER.&lt;/p&gt;
&lt;p&gt;For 99% of your time, you will not feel any difference between them. Try them both, they each come with a trial period, see which one jives with you more, and go with that one.&lt;/p&gt;
&lt;h3&gt;Takeaways&lt;/h3&gt;
&lt;p&gt;I think it&apos;s quite clear: &lt;strong&gt;fuck around and find out.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Install some agents, in your favorite workspace of choice, or in a new one like the terminal, and see how you go.&lt;/p&gt;
&lt;p&gt;In the next few weeks, we&apos;re going to go down the path of agentic coding. I&apos;m going to hold your hand and tell you that you&apos;re doing great, and by the end of it, you&apos;ll have enough knowledge to feel confident being a programmer in today&apos;s day and age. Or enough knowledge to know you prefer to move to a farm in the woods and leave this whole thing behind. That&apos;s up to you.&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Delete Dead Swift Code Safely Using Periphery</title><link>https://nowham.dev/posts/swift-periphery-cleanup/</link><guid isPermaLink="true">https://nowham.dev/posts/swift-periphery-cleanup/</guid><description>Safely delete dead Swift code with one script (and a verification pass)</description><pubDate>Mon, 11 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;If you&apos;ve worked on an iOS app for more than five minutes, you know how quickly codebases collect “just in case” types, old imports, and properties that no one calls anymore. I love shipping features, not dusting shelves. So I wrote a script that uses Periphery to find unused Swift code and then cleans the obvious stuff safely, with a verification scan at the end and a tidy report for you to review.&lt;/p&gt;
&lt;p&gt;This isn’t a magic wand that deletes half your project. It’s a pragmatic helper that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ensures Periphery is installed and configured&lt;/li&gt;
&lt;li&gt;Runs a scan and removes only the low‑risk things (unused imports and single‑line stored properties explicitly marked as unused)&lt;/li&gt;
&lt;li&gt;Suggests full‑file removals conservatively&lt;/li&gt;
&lt;li&gt;Re‑runs Periphery to verify things actually improved&lt;/li&gt;
&lt;li&gt;Writes everything to a neat &lt;code&gt;.periphery_cleanup/&lt;/code&gt; folder that you can review (and it auto‑ignores it in git)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why automate this?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Consistency: the same checks, every time, across projects&lt;/li&gt;
&lt;li&gt;Confidence: a verification pass catches accidental regressions&lt;/li&gt;
&lt;li&gt;Speed: Periphery + a tiny bit of scripting can handle the boring bits for you&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What the script does (high level)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Detects your project root using git if available, or by walking up to find an &lt;code&gt;.xcworkspace&lt;/code&gt;/&lt;code&gt;.xcodeproj&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Installs Periphery via &lt;code&gt;brew&lt;/code&gt; if it’s missing.&lt;/li&gt;
&lt;li&gt;Ensures you have a &lt;code&gt;.periphery.yml&lt;/code&gt;. It can:
&lt;ul&gt;
&lt;li&gt;Launch Periphery’s interactive setup, or&lt;/li&gt;
&lt;li&gt;Generate a non‑interactive config in “auto mode” (see flags below) and pick a scheme/target for you (prefers &lt;code&gt;BrewBuddy&lt;/code&gt; if found, otherwise the first available).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Runs a Periphery scan and saves JSON output.&lt;/li&gt;
&lt;li&gt;Parses the JSON with a small Python helper and:
&lt;ul&gt;
&lt;li&gt;Removes unused imports&lt;/li&gt;
&lt;li&gt;Removes single‑line stored properties that Periphery flags as unused&lt;/li&gt;
&lt;li&gt;Collects conservative candidates for whole‑file deletion (files that contain exactly one unused top‑level type)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Runs a second Periphery scan to verify improvement and summarizes before/after counts.&lt;/li&gt;
&lt;li&gt;Stores a report and artifacts under &lt;code&gt;.periphery_cleanup/&lt;/code&gt; and keeps that directory out of git.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;p&gt;Drop the script into your repo (for example, &lt;code&gt;Scripts/periphery_cleanup.sh&lt;/code&gt;) and make it executable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x Scripts/periphery_cleanup.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it interactively (guided Periphery setup if you don’t have a config):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./Scripts/periphery_cleanup.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or run it in auto mode (no prompts), optionally providing scheme/target/workspace/project overrides:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./Scripts/periphery_cleanup.sh --auto \
  --scheme YourScheme \
  --target YourTarget \
  --workspace YourApp.xcworkspace \
  --project YourApp.xcodeproj
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Short flags are supported too: &lt;code&gt;-a&lt;/code&gt;, &lt;code&gt;-s&lt;/code&gt;, &lt;code&gt;-t&lt;/code&gt;, &lt;code&gt;-w&lt;/code&gt;, &lt;code&gt;-p&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;macOS with &lt;code&gt;brew&lt;/code&gt; (for auto‑install) and Xcode command line tools&lt;/li&gt;
&lt;li&gt;&lt;code&gt;periphery&lt;/code&gt; (the script will install it via &lt;code&gt;brew&lt;/code&gt; if missing)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python3&lt;/code&gt; available on your PATH&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What gets edited (and what doesn’t)&lt;/h2&gt;
&lt;p&gt;This is intentionally conservative:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Removes only:
&lt;ul&gt;
&lt;li&gt;Unused imports&lt;/li&gt;
&lt;li&gt;Single‑line stored properties explicitly flagged as unused by Periphery&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Suggests (does not delete):
&lt;ul&gt;
&lt;li&gt;Whole files that contain exactly one unused top‑level type&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything is logged to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.periphery_cleanup/deleted_symbols.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.periphery_cleanup/removable_files.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.periphery_cleanup/scan_2.json&lt;/code&gt; (verification pass)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pro tip: skim the &lt;code&gt;removable_files.txt&lt;/code&gt; list and delete those files in Xcode first so it updates your project/workspace correctly.&lt;/p&gt;
&lt;h2&gt;How it works (under the hood)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The script locates your project root via git (if present) or by walking up the tree to find an Xcode workspace/project.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;.periphery.yml&lt;/code&gt; is missing:
&lt;ul&gt;
&lt;li&gt;Interactive mode: launches &lt;code&gt;periphery scan --setup&lt;/code&gt; and exits if a config isn’t produced&lt;/li&gt;
&lt;li&gt;Auto mode: resolves workspace/project, lists schemes/targets, prefers &lt;code&gt;BrewBuddy&lt;/code&gt; if present, and writes a minimal &lt;code&gt;.periphery.yml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;First scan: &lt;code&gt;periphery scan --format json&lt;/code&gt; → saves the output&lt;/li&gt;
&lt;li&gt;Python helper:
&lt;ul&gt;
&lt;li&gt;Parses findings by file and applies safe deletions (unused imports and single‑line stored properties)&lt;/li&gt;
&lt;li&gt;Detects “single unused top‑level type” files and lists them as removable candidates&lt;/li&gt;
&lt;li&gt;Emits a small JSON summary consumed by the shell script&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Second scan (verification) confirms improvements and prevents loops&lt;/li&gt;
&lt;li&gt;The script writes a human‑readable summary and clearly points you to the artifacts&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CI? Maybe&lt;/h2&gt;
&lt;p&gt;I keep this as a local tool because it edits files. If you want CI visibility, run Periphery there and surface findings, but I recommend committing code deletions manually after reviewing diffs locally.&lt;/p&gt;
&lt;h2&gt;The script&lt;/h2&gt;
&lt;p&gt;Copy‑paste as is. No edits needed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash

# Periphery Cleanup Script
# Robust, safe, portable helper to detect and neutralize unused Swift code.
# - Installs Periphery if missing (via Homebrew if available)
# - Ensures a .periphery.yml exists (auto-generates one for Xcode projects)
# - Runs scan, annotates unused symbols (@available(*, deprecated, ...))
# - Suggests whole-file removals when safe to do so
# - Re-runs scan once to confirm improvements, avoids loops

set -euo pipefail
IFS=$&apos;\n\t&apos;

SCRIPT_DIR=&quot;$(cd &quot;$(dirname &quot;$0&quot;)&quot; &amp;amp;&amp;amp; pwd)&quot;

# CLI arguments
AUTO_MODE=0
OVERRIDE_SCHEME=&quot;&quot;
OVERRIDE_TARGET=&quot;&quot;
OVERRIDE_WORKSPACE=&quot;&quot;
OVERRIDE_PROJECT=&quot;&quot;

usage() {
  cat &amp;lt;&amp;lt;EOF
Usage: $(basename &quot;$0&quot;) [options]

Options:
  -a, --auto              Generate .periphery.yml non-interactively (no prompts)
  -s, --scheme NAME       Scheme to use when generating config (auto mode)
  -t, --target NAME       Target to use when generating config (auto mode)
  -w, --workspace PATH    Use this .xcworkspace (auto mode)
  -p, --project PATH      Use this .xcodeproj (auto mode)
  -h, --help              Show this help

Defaults:
  - Without --auto, the script launches Periphery&apos;s guided setup (interactive)
  - With --auto, the script detects workspace/project &amp;amp; picks a scheme/target
EOF
}

while [[ $# -gt 0 ]]; do
  case &quot;$1&quot; in
    -a|--auto) AUTO_MODE=1; shift ;;
    -s|--scheme) OVERRIDE_SCHEME=&quot;$2&quot;; shift 2 ;;
    -t|--target) OVERRIDE_TARGET=&quot;$2&quot;; shift 2 ;;
    -w|--workspace) OVERRIDE_WORKSPACE=&quot;$2&quot;; shift 2 ;;
    -p|--project) OVERRIDE_PROJECT=&quot;$2&quot;; shift 2 ;;
    -h|--help) usage; exit 0 ;;
    *) err &quot;Unknown option: $1&quot;; usage; exit 1 ;;
  esac
done

# Determine project root robustly
determine_root() {
  local dir
  # Prefer git root if available
  if command -v git &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
    local git_root
    git_root=$(cd &quot;$SCRIPT_DIR&quot; &amp;amp;&amp;amp; git rev-parse --show-toplevel 2&amp;gt;/dev/null || true)
    if [[ -n &quot;$git_root&quot; &amp;amp;&amp;amp; -d &quot;$git_root&quot; ]]; then
      echo &quot;$git_root&quot;
      return
    fi
  fi
  # Walk up until we find an Xcode project/workspace
  dir=&quot;$SCRIPT_DIR&quot;
  while [[ &quot;$dir&quot; != &quot;/&quot; &amp;amp;&amp;amp; -n &quot;$dir&quot; ]]; do
    if compgen -G &quot;$dir/*.xcworkspace&quot; &amp;gt; /dev/null || compgen -G &quot;$dir/*.xcodeproj&quot; &amp;gt; /dev/null; then
      echo &quot;$dir&quot;
      return
    fi
    dir=&quot;$(dirname &quot;$dir&quot;)&quot;
  done
  # Fallback: parent of tools
  echo &quot;$(cd &quot;$SCRIPT_DIR/..&quot; &amp;amp;&amp;amp; pwd)&quot;
}

ROOT_DIR=&quot;$(determine_root)&quot;
cd &quot;$ROOT_DIR&quot;

ASCII_LOGO=&quot;
============================================================
   _____                 _                 _
  |  __ \               (_)               | |
  | |__) |___ _ __ _ __  _ _ __ _   _  ___| |_ ___ _ __
  |  _  // _ \ &apos;__| &apos;_ \| | &apos;__| | | |/ __| __/ _ \ &apos;__|
  | | \ \  __/ |  | |_) | | |  | |_| | (__| ||  __/ |
  |_|  \_\___|_|  | .__/|_|_|   \__,_|\___|\__\___|_|
                   | |
                   |_|   Cleanup • Periphery Automation
============================================================
&quot;

info()  { printf &quot;\033[1;34m[INFO]\033[0m %s\n&quot;  &quot;$*&quot;; }
warn()  { printf &quot;\033[1;33m[WARN]\033[0m %s\n&quot;  &quot;$*&quot;; }
err()   { printf &quot;\033[1;31m[ERR ]\033[0m %s\n&quot;  &quot;$*&quot;; }
good()  { printf &quot;\033[1;32m[DONE]\033[0m %s\n&quot;  &quot;$*&quot;; }
bold()  { printf &quot;\033[1m%s\033[0m\n&quot; &quot;$*&quot;; }

echo &quot;$ASCII_LOGO&quot;

# 1) Ensure periphery is installed
if ! command -v periphery &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
  warn &quot;Periphery not found. Attempting install via Homebrew.&quot;
  if command -v brew &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
    info &quot;brew install periphery&quot;
    brew install periphery || {
      err &quot;Failed to install Periphery via Homebrew. Please install Periphery manually and re-run.&quot;
      exit 1
    }
    good &quot;Periphery installed.&quot;
  else
    err &quot;Homebrew not found. Please install Homebrew (https://brew.sh) or Periphery manually, then rerun.&quot;
    exit 1
  fi
else
  good &quot;Periphery found: $(command -v periphery)&quot;
fi

detect_targets_from_list() {
  # Reads xcodebuild -list output from stdin, prints Targets as lines
  awk &apos;/Targets:/{flag=1;next} flag &amp;amp;&amp;amp; NF {print $0} /^\s*$/{if(flag) exit}&apos; \
    | sed &apos;s/^ *//;s/ *$//&apos; | sed &apos;/^$/d&apos;
}

append_targets_to_config() {
  local cfg=&quot;$1&quot;; shift
  local targets=(&quot;$@&quot;)
  [[ ${#targets[@]} -eq 0 ]] &amp;amp;&amp;amp; return 0
  if ! grep -q &apos;^targets:&apos; &quot;$cfg&quot; 2&amp;gt;/dev/null; then
    info &quot;Adding targets to $cfg: ${targets[*]}&quot;
    {
      echo &quot;targets:&quot;
      for t in &quot;${targets[@]}&quot;; do echo &quot;  - $t&quot;; done
    } &amp;gt;&amp;gt; &quot;$cfg&quot;
  fi
}

# 2) Ensure .periphery.yml exists
CONFIG_FILE=&quot;$ROOT_DIR/.periphery.yml&quot;
if [[ ! -f &quot;$CONFIG_FILE&quot; ]]; then
  if [[ &quot;$AUTO_MODE&quot; -eq 1 ]]; then
    warn &quot;.periphery.yml not found. Generating a non-interactive configuration (auto mode)...&quot;

    # Resolve workspace/project
    if [[ -n &quot;$OVERRIDE_WORKSPACE&quot; ]]; then
      WORKSPACE_CANDIDATE=&quot;$OVERRIDE_WORKSPACE&quot;
    else
      WORKSPACE_CANDIDATE=$(find &quot;$ROOT_DIR&quot; -maxdepth 1 -name &quot;*.xcworkspace&quot; -print -quit || true)
    fi
    if [[ -n &quot;$OVERRIDE_PROJECT&quot; ]]; then
      PROJECT_CANDIDATE=&quot;$OVERRIDE_PROJECT&quot;
    else
      PROJECT_CANDIDATE=$(find &quot;$ROOT_DIR&quot; -maxdepth 1 -name &quot;*.xcodeproj&quot; -print -quit || true)
    fi

    if [[ -n &quot;$WORKSPACE_CANDIDATE&quot; ]]; then
      info &quot;Using workspace: $(basename &quot;$WORKSPACE_CANDIDATE&quot;)&quot;
      LIST_OUTPUT=$(xcodebuild -list -workspace &quot;$WORKSPACE_CANDIDATE&quot; 2&amp;gt;/dev/null || true)
    elif [[ -n &quot;$PROJECT_CANDIDATE&quot; ]]; then
      info &quot;Using project: $(basename &quot;$PROJECT_CANDIDATE&quot;)&quot;
      LIST_OUTPUT=$(xcodebuild -list -project &quot;$PROJECT_CANDIDATE&quot; 2&amp;gt;/dev/null || true)
    else
      err &quot;No .xcworkspace or .xcodeproj found at $ROOT_DIR. Cannot configure Periphery.&quot;
      exit 1
    fi

    # Extract schemes and targets (compatible with bash 3.2 on macOS)
    SCHEMES=()
    TARGETS=()
    while IFS= read -r line; do
      [[ -n &quot;$line&quot; ]] &amp;amp;&amp;amp; SCHEMES+=(&quot;$line&quot;)
    done &amp;lt;&amp;lt;EOF
$(printf &quot;%s\n&quot; &quot;$LIST_OUTPUT&quot; | awk &apos;/Schemes:/{flag=1;next} flag &amp;amp;&amp;amp; NF {print $0} /^\s*$/{if(flag) exit}&apos; | sed &apos;s/^ *//;s/ *$//&apos;)
EOF
    while IFS= read -r line; do
      [[ -n &quot;$line&quot; ]] &amp;amp;&amp;amp; TARGETS+=(&quot;$line&quot;)
    done &amp;lt;&amp;lt;EOF
$(printf &quot;%s\n&quot; &quot;$LIST_OUTPUT&quot; | awk &apos;/Targets:/{flag=1;next} flag &amp;amp;&amp;amp; NF {print $0} /^\s*$/{if(flag) exit}&apos; | sed &apos;s/^ *//;s/ *$//&apos;)
EOF

    DEFAULT_SCHEME=&quot;${OVERRIDE_SCHEME:-${SCHEMES[0]:-}}&quot;
    DEFAULT_TARGET=&quot;${OVERRIDE_TARGET:-${TARGETS[0]:-}}&quot;

    # Prefer a scheme/target named BrewBuddy if present and not overridden
    if [[ -z &quot;$OVERRIDE_SCHEME&quot; ]]; then
      for s in &quot;${SCHEMES[@]}&quot;; do
        if [[ &quot;$s&quot; == &quot;BrewBuddy&quot; ]]; then DEFAULT_SCHEME=&quot;$s&quot;; break; fi
      done
    fi
    if [[ -z &quot;$OVERRIDE_TARGET&quot; ]]; then
      for t in &quot;${TARGETS[@]}&quot;; do
        if [[ &quot;$t&quot; == &quot;BrewBuddy&quot; ]]; then DEFAULT_TARGET=&quot;$t&quot;; break; fi
      done
    fi

    if [[ -z &quot;$DEFAULT_SCHEME&quot; || -z &quot;$DEFAULT_TARGET&quot; ]]; then
      err &quot;Failed to detect a scheme/target from xcodebuild. Use --scheme/--target or ensure the project builds.&quot;
      exit 1
    fi

    {
      if [[ -n &quot;$WORKSPACE_CANDIDATE&quot; ]]; then
        echo &quot;workspace: $(basename &quot;$WORKSPACE_CANDIDATE&quot;)&quot;
      else
        echo &quot;project: $(basename &quot;$PROJECT_CANDIDATE&quot;)&quot;
      fi
      echo &quot;schemes:&quot;
      echo &quot;  - $DEFAULT_SCHEME&quot;
      echo &quot;targets:&quot;
      echo &quot;  - $DEFAULT_TARGET&quot;
      echo &quot;retain_public: false&quot;
      echo &quot;retain_objc_accessible: false&quot;
      echo &quot;clean_build: false&quot;
    } &amp;gt; &quot;$CONFIG_FILE&quot;

    good &quot;Created $CONFIG_FILE (scheme=$DEFAULT_SCHEME, target=$DEFAULT_TARGET)&quot;
  else
    warn &quot;.periphery.yml not found. Launching interactive Periphery setup...&quot;
    bold &quot;(You will be prompted by Periphery. Choose workspace/project, scheme, and targets as usual.)&quot;
    periphery scan --setup || { err &quot;Periphery setup failed.&quot;; exit 1; }
    if [[ ! -f &quot;$CONFIG_FILE&quot; ]]; then
      err &quot;Setup finished but .periphery.yml was not created. Aborting.&quot;
      exit 1
    fi
    good &quot;Created $CONFIG_FILE via interactive setup&quot;
  fi
else
  good &quot;Found existing .periphery.yml&quot;
fi

WORK_DIR=&quot;$ROOT_DIR/.periphery_cleanup&quot;
mkdir -p &quot;$WORK_DIR&quot;
SCAN_JSON_1=&quot;$WORK_DIR/scan_1.json&quot;
SCAN_JSON_2=&quot;$WORK_DIR/scan_2.json&quot;
REPORT_TXT=&quot;$WORK_DIR/report.txt&quot;
REMOVABLE_FILES_TXT=&quot;$WORK_DIR/removable_files.txt&quot;
DELETED_SYMBOLS_TXT=&quot;$WORK_DIR/deleted_symbols.txt&quot;
: &amp;gt; &quot;$REPORT_TXT&quot;; : &amp;gt; &quot;$REMOVABLE_FILES_TXT&quot;; : &amp;gt; &quot;$DELETED_SYMBOLS_TXT&quot;

# Keep artifacts out of source control by ensuring .gitignore contains the work directory
GITIGNORE_FILE=&quot;$ROOT_DIR/.gitignore&quot;
if [[ -f &quot;$GITIGNORE_FILE&quot; ]]; then
  if ! grep -qE &apos;^\.periphery_cleanup/?$&apos; &quot;$GITIGNORE_FILE&quot; 2&amp;gt;/dev/null; then
    echo &quot;# Periphery cleanup artifacts&quot; &amp;gt;&amp;gt; &quot;$GITIGNORE_FILE&quot;
    echo &quot;.periphery_cleanup/&quot; &amp;gt;&amp;gt; &quot;$GITIGNORE_FILE&quot;
    good &quot;Updated .gitignore to ignore .periphery_cleanup/&quot;
  fi
else
  {
    echo &quot;# Periphery cleanup artifacts&quot;
    echo &quot;.periphery_cleanup/&quot;
  } &amp;gt; &quot;$GITIGNORE_FILE&quot;
  good &quot;Created .gitignore to ignore .periphery_cleanup/&quot;
fi

bold &quot;\nRunning Periphery scan (pass 1)...&quot;
periphery scan --config &quot;$CONFIG_FILE&quot; --format json --disable-update-check &amp;gt; &quot;$SCAN_JSON_1&quot; || {
  err &quot;Periphery scan failed.&quot;
  exit 1
}
good &quot;Scan completed. Output: $SCAN_JSON_1&quot;

# 3) Parse JSON and annotate unused declarations safely with @available(*, deprecated, ...)
#    Use Python for robust JSON parsing and in-place edits.
PYTHON3_BIN=&quot;python3&quot;
if ! command -v &quot;$PYTHON3_BIN&quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
  err &quot;python3 not found. Please install Python 3.&quot;
  exit 1
fi

cat &amp;gt; &quot;$WORK_DIR/annotate.py&quot; &amp;lt;&amp;lt;&apos;PY&apos;
import json, os, sys, re

scan_path = sys.argv[1]
deleted_log_path = sys.argv[2]

with open(scan_path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
    data = json.load(f)

# Group entries by file
entries_by_file = {}
for e in data:
    loc = e.get(&apos;location&apos;, &apos;&apos;)
    # Periphery sometimes splits lines with wrapping; normalize
    # Expect: /abs/path/File.swift:LINE:COL
    m = re.search(r&quot;(.+\.swift):(\d+):(\d+)$&quot;, loc)
    if not m:
        continue
    path, line, col = m.group(1), int(m.group(2)), int(m.group(3))
    entries_by_file.setdefault(path, []).append({
        &apos;line&apos;: line,
        &apos;col&apos;: col,
        &apos;name&apos;: e.get(&apos;name&apos;,&apos;&apos;),
        &apos;kind&apos;: e.get(&apos;kind&apos;,&apos;&apos;),
        &apos;hints&apos;: e.get(&apos;hints&apos;, []),
        &apos;accessibility&apos;: e.get(&apos;accessibility&apos;,&apos;&apos;)
    })

deleted = []

def is_stored_property_line(line: str) -&amp;gt; bool:
    # Single-line stored property like: `public var foo: Type = ...` or `let name = ...`
    # Avoid computed properties `{` and protocol requirements `get set` patterns
    s = line.strip()
    if not re.search(r&quot;\b(var|let)\b&quot;, s):
        return False
    if &apos;{&apos; in s:
        return False
    if re.search(r&quot;\b(get|set)\b&quot;, s):
        return False
    return &apos;=&apos; in s

applied = []

for path, entries in entries_by_file.items():
    if not os.path.isfile(path):
        continue
    try:
        with open(path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
            lines = f.readlines()
    except Exception:
        continue

    # For @Observable files, we DO allow deletions of unused stored properties.

    # Sort entries descending by line to keep indices stable when inserting
    entries.sort(key=lambda x: x[&apos;line&apos;], reverse=True)

    changed = False
    for e in entries:
        line_idx = e[&apos;line&apos;] - 1
        if line_idx &amp;lt; 0 or line_idx &amp;gt;= len(lines):
            continue

        decl_line = lines[line_idx]
        hints = set(h.lower() for h in e.get(&apos;hints&apos;, []))

        # 1) Remove unused imports (only when explicitly marked unused)
        if decl_line.lstrip().startswith(&apos;import &apos;) and &apos;unused&apos; in hints:
            del lines[line_idx]
            changed = True
            deleted.append(f&quot;{path}:{e[&apos;line&apos;]} import {e.get(&apos;name&apos;,&apos;&apos;)}&quot;)
            continue

        # 2) Remove single-line stored properties only if explicitly unused
        if is_stored_property_line(decl_line) and &apos;unused&apos; in hints:
            del lines[line_idx]
            changed = True
            deleted.append(f&quot;{path}:{e[&apos;line&apos;]} var/let {e.get(&apos;name&apos;,&apos;&apos;)}&quot;)
            continue

    if changed:
        with open(path, &apos;w&apos;, encoding=&apos;utf-8&apos;) as f:
            f.writelines(lines)

with open(deleted_log_path, &apos;w&apos;, encoding=&apos;utf-8&apos;) as f:
    for a in deleted:
        f.write(a + &quot;\n&quot;)

# Compute removable file candidates conservatively:
# If a file contains exactly one top-level type declaration (struct/class/enum/protocol),
# and that same declaration appears in the unused report, suggest it.
def top_level_decl_count(path):
    try:
        with open(path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
            src = f.read()
    except Exception:
        return 0
    # Remove comments to avoid false positives
    src = re.sub(r&quot;//.*&quot;, &quot;&quot;, src)
    src = re.sub(r&quot;/\*[\s\S]*?\*/&quot;, &quot;&quot;, src)
    m = re.findall(r&quot;^\s*(struct|class|enum|protocol)\s+\w+&quot;, src, flags=re.MULTILINE)
    return len(m)

removable_candidates = []
for path, entries in entries_by_file.items():
    # Only consider if file still exists and exactly one top-level decl
    if not os.path.isfile(path):
        continue
    if top_level_decl_count(path) != 1:
        continue
    # If every entry in this file is an unused type (struct/class/enum/protocol)
    all_types_unused = all(any(k in e[&apos;kind&apos;] for k in [&apos;struct&apos;, &apos;class&apos;, &apos;enum&apos;, &apos;protocol&apos;]) for e in entries)
    if all_types_unused:
        removable_candidates.append(path)

print(&quot;\n\n===REPORT-BEGIN===&quot;)
print(json.dumps({
    &apos;deleted_count&apos;: sum(1 for _ in open(deleted_log_path, &apos;r&apos;, encoding=&apos;utf-8&apos;)) if os.path.isfile(deleted_log_path) else 0,
    &apos;removable_candidates&apos;: removable_candidates
}, indent=2))
print(&quot;===REPORT-END===\n\n&quot;)
PY

if [[ -s &quot;$SCAN_JSON_1&quot; ]] &amp;amp;&amp;amp; grep -q &apos;&quot;kind&quot;&apos; &quot;$SCAN_JSON_1&quot;; then
  python3 &quot;$WORK_DIR/annotate.py&quot; &quot;$SCAN_JSON_1&quot; &quot;$DELETED_SYMBOLS_TXT&quot; | tee -a &quot;$REPORT_TXT&quot; &amp;gt;/dev/null
else
  warn &quot;No unused items found by Periphery (empty JSON). Skipping annotation.&quot;
  echo &quot;\n===REPORT-BEGIN===&quot; &amp;gt;&amp;gt; &quot;$REPORT_TXT&quot;
  echo &apos;{&quot;deleted_count&quot;:0,&quot;removable_candidates&quot;:[]}&apos; &amp;gt;&amp;gt; &quot;$REPORT_TXT&quot;
  echo &quot;===REPORT-END===\n&quot; &amp;gt;&amp;gt; &quot;$REPORT_TXT&quot;
fi

REMOVABLE_JSON=$(sed -n &apos;/===REPORT-BEGIN===/,/===REPORT-END===/p&apos; &quot;$REPORT_TXT&quot; | sed &apos;1d;$d&apos;)
echo &quot;$REMOVABLE_JSON&quot; &amp;gt; &quot;$WORK_DIR/removable.json&quot;
&quot;$PYTHON3_BIN&quot; - &quot;$WORK_DIR/removable.json&quot; &amp;lt;&amp;lt;&apos;PY&apos; &amp;gt; &quot;$WORK_DIR/removable_extracted.txt&quot;
import json,sys
with open(sys.argv[1], &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
    d = json.load(f)
print(&quot;Deleted symbols:&quot;, d.get(&apos;deleted_count&apos;,0))
print(&quot;\nPotentially removable files (manual delete, safe heuristic):&quot;)
for p in d.get(&apos;removable_candidates&apos;,[]):
    print(p)
PY

cat &quot;$WORK_DIR/removable_extracted.txt&quot; | tee &quot;$REMOVABLE_FILES_TXT&quot;

# Remove transient helper files
rm -f &quot;$WORK_DIR/removable_extracted.txt&quot; &quot;$WORK_DIR/removable.json&quot; &quot;$WORK_DIR/annotate.py&quot;

bold &quot;\nRunning Periphery scan (pass 2, verification)...&quot;
periphery scan --config &quot;$CONFIG_FILE&quot; --format json --disable-update-check &amp;gt; &quot;$SCAN_JSON_2&quot; || {
  err &quot;Periphery verification scan failed.&quot;
  exit 1
}
good &quot;Verification scan completed. Output: $SCAN_JSON_2&quot;

# Basic loop avoidance: only two scans are performed.
# Summarize counts before/after

COUNT1=$(grep -c &apos;&quot;kind&quot;&apos; &quot;$SCAN_JSON_1&quot; || true)
COUNT2=$(grep -c &apos;&quot;kind&quot;&apos; &quot;$SCAN_JSON_2&quot; || true)

echo &quot;&quot; | tee -a &quot;$REPORT_TXT&quot;
bold &quot;Summary&quot;
echo &quot;- Unused reported (pass 1): $COUNT1&quot; | tee -a &quot;$REPORT_TXT&quot;
echo &quot;- Unused reported (pass 2): $COUNT2&quot; | tee -a &quot;$REPORT_TXT&quot;
DELETED_COUNT=$(wc -l &amp;lt; &quot;$DELETED_SYMBOLS_TXT&quot; | tr -d &apos; &apos;)
echo &quot;- Deleted symbols: $DELETED_COUNT&quot; | tee -a &quot;$REPORT_TXT&quot;
echo &quot;- Removable file candidates listed in: $REMOVABLE_FILES_TXT&quot; | tee -a &quot;$REPORT_TXT&quot;

cat &amp;lt;&amp;lt;&apos;EOT&apos;

============================================================
 Result
============================================================
  • Deleted clearly unused imports and single-line stored properties.
  • Listed conservative whole-file removals where the file contains a
    single unused top-level type (manual review recommended).
  • To review:
      - .periphery_cleanup/deleted_symbols.txt
      - .periphery_cleanup/removable_files.txt
      - .periphery_cleanup/scan_2.json

 Tips
  - Re-run this script after code changes.
  - For whole-file deletion, confirm in Xcode first, then delete.

============================================================
EOT

good &quot;Periphery cleanup complete.&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;Dead code adds weight and confusion. This script gives you a safe, repeatable way to trim the obvious stuff, confirm improvements, and make a short list of files that are probably ready to go. Run it, read the report, commit the wins.&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>GitHub Actions Caching for iOS CI: Strategies That Work</title><link>https://nowham.dev/posts/github-actions-ios-caching/</link><guid isPermaLink="true">https://nowham.dev/posts/github-actions-ios-caching/</guid><description>Stop waiting around for builds and make your CI actually fast</description><pubDate>Thu, 31 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Alright, let&apos;s be honest here. You&apos;ve set up your fancy GitHub Actions workflow from our &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/&quot;&gt;previous posts&lt;/a&gt;, you&apos;re feeling like a CI/CD wizard, and then... you sit there for 15 minutes watching your build crawl along while Xcode downloads the same dependencies for the 47th time this week.&lt;/p&gt;
&lt;p&gt;Sound familiar? Yeah, I thought so.&lt;/p&gt;
&lt;p&gt;Well, my friend, today we&apos;re going to fix that. We&apos;re going to take your glacially slow builds and turn them into something that&apos;ll make you look like you&apos;ve got superpowers. How? &lt;strong&gt;Caching&lt;/strong&gt;. The magical art of not doing the same work twice.&lt;/p&gt;
&lt;p&gt;After implementing the caching strategies in this post, your 15-minute builds will drop to 3-4 minutes, your teammates will think you&apos;ve sold your soul to the DevOps gods, and you&apos;ll finally have time to grab that coffee instead of watching progress bars.&lt;/p&gt;
&lt;h2&gt;Why caching matters (and why you should care)&lt;/h2&gt;
&lt;h3&gt;The cold, hard numbers&lt;/h3&gt;
&lt;p&gt;Without caching, here&apos;s what happens every single time your workflow runs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ruby gems&lt;/strong&gt;: Downloaded and installed from scratch (1-2 minutes)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swift Package Manager&lt;/strong&gt;: Downloads and builds all dependencies (3-5 minutes)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Xcode derived data&lt;/strong&gt;: Builds everything from zero (5-10 minutes)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CocoaPods&lt;/strong&gt; (if you&apos;re still using them): Downloads and installs everything (2-3 minutes)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&apos;s potentially &lt;strong&gt;15+ minutes&lt;/strong&gt; of pure waiting. For work that&apos;s been done hundreds of times before.&lt;/p&gt;
&lt;p&gt;With proper caching:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ruby gems&lt;/strong&gt;: Restored in 10-15 seconds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swift Package Manager&lt;/strong&gt;: Restored in 30 seconds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Xcode derived data&lt;/strong&gt;: Restored in 1-2 minutes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CocoaPods&lt;/strong&gt;: Restored in 30 seconds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Total time saved: &lt;strong&gt;10-12 minutes per build&lt;/strong&gt;. That&apos;s enough time to contemplate life, check Twitter, or actually be productive. Your choice! 😄&lt;/p&gt;
&lt;h3&gt;GitHub Actions pricing impact&lt;/h3&gt;
&lt;p&gt;Here&apos;s the kicker - GitHub Actions charges by the minute for private repos. Those extra 10 minutes per build? That&apos;s 10x more expensive than it needs to be. Caching literally pays for itself by reducing your compute costs.&lt;/p&gt;
&lt;h2&gt;Understanding GitHub Actions cache fundamentals&lt;/h2&gt;
&lt;p&gt;Before we dive into iOS-specific strategies, let&apos;s understand how GitHub Actions caching works:&lt;/p&gt;
&lt;h3&gt;Cache keys and restore keys&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;- uses: actions/cache@v4
  with:
    path: path/to/cache
    key: my-cache-${{ hashFiles(&apos;**/lockfile&apos;) }}
    restore-keys: |
      my-cache-
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;key&lt;/code&gt;&lt;/strong&gt;: The exact cache identifier. If this matches, you get a perfect cache hit&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;restore-keys&lt;/code&gt;&lt;/strong&gt;: Fallback patterns. If the exact key doesn&apos;t match, it finds the closest match&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;path&lt;/code&gt;&lt;/strong&gt;: What directories/files to cache&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cache invalidation strategy&lt;/h3&gt;
&lt;p&gt;The beauty is in the &lt;code&gt;hashFiles()&lt;/code&gt; function. It creates a hash of your dependency files (like &lt;code&gt;Package.resolved&lt;/code&gt; or &lt;code&gt;Gemfile.lock&lt;/code&gt;). When dependencies change, the hash changes, and you automatically get a fresh cache. When they don&apos;t change, you get blazing fast restores.&lt;/p&gt;
&lt;h2&gt;The iOS caching trinity&lt;/h2&gt;
&lt;p&gt;Let&apos;s set up caching for the three main bottlenecks in iOS builds:&lt;/p&gt;
&lt;h3&gt;1. Xcode derived data caching&lt;/h3&gt;
&lt;p&gt;This is the big one. Xcode&apos;s derived data contains all the intermediate build artifacts, and it&apos;s usually the slowest part of your build.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Cache Xcode derived data
  uses: actions/cache@v4
  with:
    path: ~/Library/Developer/Xcode/DerivedData
    key: ${{ runner.os }}-xcode-deriveddata-${{ hashFiles(&apos;**/*.xcodeproj/project.pbxproj&apos;, &apos;**/*.xcworkspace/contents.xcworkspacedata&apos;) }}
    restore-keys: |
      ${{ runner.os }}-xcode-deriveddata-
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Caches the entire derived data directory&lt;/li&gt;
&lt;li&gt;Invalidates when your project structure changes&lt;/li&gt;
&lt;li&gt;Falls back to any previous derived data if project changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Pro tip&lt;/strong&gt;: The derived data cache can get huge (several GB). GitHub Actions has a 10GB limit per repository, so you might want to clean it periodically.&lt;/p&gt;
&lt;h3&gt;2. Swift Package Manager caching&lt;/h3&gt;
&lt;p&gt;SPM dependencies can take forever to download and build. Let&apos;s cache them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Cache Swift Package Manager
  uses: actions/cache@v4
  with:
    path: |
      .build
      ~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex
      ~/Library/Caches/org.swift.swiftpm
      ~/Library/org.swift.swiftpm
    key: ${{ runner.os }}-spm-${{ hashFiles(&apos;**/Package.resolved&apos;) }}
    restore-keys: |
      ${{ runner.os }}-spm-
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What this caches:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.build&lt;/code&gt;: Your local SPM build artifacts&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ModuleCache.noindex&lt;/code&gt;: Compiled Swift modules&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/Library/Caches/org.swift.swiftpm&lt;/code&gt;: SPM&apos;s download cache&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/Library/org.swift.swiftpm&lt;/code&gt;: SPM&apos;s configuration and metadata&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Ruby gems caching (for Fastlane)&lt;/h3&gt;
&lt;p&gt;Since you&apos;re using Fastlane (you are, right?), let&apos;s cache those Ruby gems:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Set up Ruby
  uses: ruby/setup-ruby@v1
  with:
    ruby-version: 3.1
    bundler-cache: true  # This automatically handles gem caching!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wait, that&apos;s it? Yep! The &lt;code&gt;ruby/setup-ruby&lt;/code&gt; action has built-in caching when you set &lt;code&gt;bundler-cache: true&lt;/code&gt;. It&apos;s so good that you don&apos;t need to do it manually.&lt;/p&gt;
&lt;h3&gt;4. CocoaPods caching (if you&apos;re still using them)&lt;/h3&gt;
&lt;p&gt;For those who haven&apos;t made the SPM transition yet:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Cache CocoaPods
  uses: actions/cache@v4
  with:
    path: |
      Pods
      ~/Library/Caches/CocoaPods
    key: ${{ runner.os }}-pods-${{ hashFiles(&apos;**/Podfile.lock&apos;) }}
    restore-keys: |
      ${{ runner.os }}-pods-
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The complete caching workflow&lt;/h2&gt;
&lt;p&gt;Here&apos;s how to put it all together in a workflow that&apos;ll make your builds fly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Build and Test with Caching

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-test:
    runs-on: macos-latest

    steps:
    - uses: actions/checkout@v4

    # Set up Xcode (this doesn&apos;t need caching)
    - name: Set up Xcode
      uses: maxim-lobanov/setup-xcode@v1
      with:
        xcode-version: &quot;15.4.0&quot;

    # Cache Ruby gems (built into setup-ruby)
    - name: Set up Ruby with caching
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 3.1
        bundler-cache: true

    # Cache Xcode derived data
    - name: Cache Xcode derived data
      uses: actions/cache@v4
      with:
        path: ~/Library/Developer/Xcode/DerivedData
        key: ${{ runner.os }}-xcode-deriveddata-${{ hashFiles(&apos;**/*.xcodeproj/project.pbxproj&apos;, &apos;**/*.xcworkspace/contents.xcworkspacedata&apos;) }}
        restore-keys: |
          ${{ runner.os }}-xcode-deriveddata-

    # Cache Swift Package Manager
    - name: Cache Swift Package Manager
      uses: actions/cache@v4
      with:
        path: |
          .build
          ~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex
          ~/Library/Caches/org.swift.swiftpm
          ~/Library/org.swift.swiftpm
        key: ${{ runner.os }}-spm-${{ hashFiles(&apos;**/Package.resolved&apos;) }}
        restore-keys: |
          ${{ runner.os }}-spm-

    # Optional: Cache CocoaPods (if you&apos;re using them)
    - name: Cache CocoaPods
      if: hashFiles(&apos;**/Podfile.lock&apos;) != &apos;&apos;
      uses: actions/cache@v4
      with:
        path: |
          Pods
          ~/Library/Caches/CocoaPods
        key: ${{ runner.os }}-pods-${{ hashFiles(&apos;**/Podfile.lock&apos;) }}
        restore-keys: |
          ${{ runner.os }}-pods-

    # Set up code signing (from our previous posts)
    - name: Set up SSH key for Match
      run: |
        mkdir -p ~/.ssh
        echo &quot;${{ secrets.MATCH_GIT_PRIVATE_KEY }}&quot; &amp;gt; ~/.ssh/id_rsa
        chmod 600 ~/.ssh/id_rsa
        ssh-keyscan github.com &amp;gt;&amp;gt; ~/.ssh/known_hosts

    - name: Set up code signing
      run: bundle exec fastlane match appstore --readonly --app_identifier com.yourcompany.yourapp
      env:
        MATCH_REPOSITORY: ${{ secrets.MATCH_REPOSITORY }}
        MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}

    # Run tests (now much faster!)
    - name: Run tests
      run: bundle exec fastlane test

    # Build app (also much faster!)
    - name: Build app
      run: bundle exec fastlane build
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Advanced caching strategies&lt;/h2&gt;
&lt;p&gt;Once you&apos;ve got the basics down, here are some pro-level techniques:&lt;/p&gt;
&lt;h3&gt;Cache warming&lt;/h3&gt;
&lt;p&gt;Pre-populate caches in a dedicated job that runs on a schedule:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Cache Warming

on:
  schedule:
    - cron: &apos;0 2 * * *&apos;  # Run daily at 2 AM

jobs:
  warm-cache:
    runs-on: macos-latest
    steps:
    - uses: actions/checkout@v4

    # ... set up steps ...

    - name: Warm SPM cache
      run: swift package resolve

    - name: Warm derived data cache
      run: xcodebuild build -scheme YourScheme -destination &apos;platform=iOS Simulator,name=iPhone 15 Pro&apos; -quiet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures your main workflows always have warm caches to work with.&lt;/p&gt;
&lt;h3&gt;Matrix caching&lt;/h3&gt;
&lt;p&gt;If you&apos;re building for multiple configurations, share caches between them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;strategy:
  matrix:
    scheme: [MyApp, MyAppTests]

steps:
- name: Cache Xcode derived data
  uses: actions/cache@v4
  with:
    path: ~/Library/Developer/Xcode/DerivedData
    key: ${{ runner.os }}-xcode-${{ matrix.scheme }}-${{ hashFiles(&apos;**/*.xcodeproj/project.pbxproj&apos;) }}
    restore-keys: |
      ${{ runner.os }}-xcode-${{ matrix.scheme }}-
      ${{ runner.os }}-xcode-  # Fallback to any scheme
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conditional caching&lt;/h3&gt;
&lt;p&gt;Only cache CocoaPods if you actually use them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Cache CocoaPods
  if: hashFiles(&apos;**/Podfile.lock&apos;) != &apos;&apos;  # Only run if Podfile.lock exists
  uses: actions/cache@v4
  with:
    path: Pods
    key: ${{ runner.os }}-pods-${{ hashFiles(&apos;**/Podfile.lock&apos;) }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cache size optimization&lt;/h3&gt;
&lt;p&gt;Clean up your derived data before caching to keep it manageable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Clean derived data before caching
  run: |
    # Keep only the most recent build products
    find ~/Library/Developer/Xcode/DerivedData -name &quot;Build&quot; -type d -exec rm -rf {}/Intermediates.noindex \;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Monitoring cache performance&lt;/h2&gt;
&lt;p&gt;Add some logging to see how much time you&apos;re saving:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Cache Swift Package Manager
  id: spm-cache
  uses: actions/cache@v4
  with:
    path: .build
    key: ${{ runner.os }}-spm-${{ hashFiles(&apos;**/Package.resolved&apos;) }}

- name: Report cache status
  run: |
    if [ &quot;${{ steps.spm-cache.outputs.cache-hit }}&quot; == &quot;true&quot; ]; then
      echo &quot;✅ SPM cache hit! Saved time on dependency resolution.&quot;
    else
      echo &quot;❌ SPM cache miss. Dependencies will be downloaded.&quot;
    fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also track cache performance over time by checking the Actions logs and seeing how often you get cache hits vs misses.&lt;/p&gt;
&lt;h2&gt;Troubleshooting common caching issues&lt;/h2&gt;
&lt;h3&gt;&quot;Cache size exceeded 10GB limit&quot;&lt;/h3&gt;
&lt;p&gt;GitHub Actions has a 10GB cache limit per repository. If you hit this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Clean your derived data&lt;/strong&gt;: Remove build intermediates before caching&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use more specific cache keys&lt;/strong&gt;: Separate caches for different branches/schemes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up cache cleanup&lt;/strong&gt;: Use &lt;code&gt;actions/cache/restore&lt;/code&gt; and &lt;code&gt;actions/cache/save&lt;/code&gt; to have more control&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;- name: Clean cache before saving
  run: |
    # Remove large intermediate files
    rm -rf ~/Library/Developer/Xcode/DerivedData/*/Build/Intermediates.noindex
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&quot;Cache is slower than rebuilding&quot;&lt;/h3&gt;
&lt;p&gt;This usually happens when your cache keys are too broad. If you&apos;re invalidating cache too often:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Make keys more specific&lt;/strong&gt;: Use more granular file hashes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use restore keys effectively&lt;/strong&gt;: Have good fallback strategies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Profile your cache sizes&lt;/strong&gt;: Large caches can be slower to restore than rebuilding&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&quot;Dependencies not found after cache restore&quot;&lt;/h3&gt;
&lt;p&gt;This often happens with SPM when the cache doesn&apos;t include everything needed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Cache Swift Package Manager
  uses: actions/cache@v4
  with:
    path: |
      .build
      ~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex
      ~/Library/Caches/org.swift.swiftpm
      ~/Library/org.swift.swiftpm  # Don&apos;t forget this!
    key: ${{ runner.os }}-spm-${{ hashFiles(&apos;**/Package.resolved&apos;) }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&quot;CocoaPods cache not working&quot;&lt;/h3&gt;
&lt;p&gt;Make sure you&apos;re not skipping the cache in your pod install:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Install pods
  run: bundle exec pod install
  # Note: Don&apos;t use COCOAPODS_SKIP_CACHE=TRUE if you want caching!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cache key collisions&lt;/h3&gt;
&lt;p&gt;If you&apos;re seeing unexpected cache behavior, you might have key collisions. Make your keys more unique:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;key: ${{ runner.os }}-${{ github.workflow }}-xcode-${{ hashFiles(&apos;**/*.xcodeproj/project.pbxproj&apos;) }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best practices and gotchas&lt;/h2&gt;
&lt;h3&gt;Do&apos;s&lt;/h3&gt;
&lt;p&gt;✅ &lt;strong&gt;Start with Ruby gem caching&lt;/strong&gt; - It&apos;s built into &lt;code&gt;setup-ruby&lt;/code&gt;, easy to set up, and gives immediate benefits&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Use specific file patterns for cache keys&lt;/strong&gt; - &lt;code&gt;Package.resolved&lt;/code&gt;, &lt;code&gt;Podfile.lock&lt;/code&gt;, etc. are better than generic patterns&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Set up restore keys&lt;/strong&gt; - Always have a fallback strategy&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Monitor cache hit rates&lt;/strong&gt; - Add logging to see how well your caching is working&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Clean before caching&lt;/strong&gt; - Remove temporary files to keep cache sizes manageable&lt;/p&gt;
&lt;h3&gt;Don&apos;ts&lt;/h3&gt;
&lt;p&gt;❌ &lt;strong&gt;Don&apos;t cache node_modules equivalent directories without good reason&lt;/strong&gt; - They can be huge and change frequently&lt;/p&gt;
&lt;p&gt;❌ &lt;strong&gt;Don&apos;t use overly broad cache keys&lt;/strong&gt; - &lt;code&gt;${{ runner.os }}-cache&lt;/code&gt; will almost never hit&lt;/p&gt;
&lt;p&gt;❌ &lt;strong&gt;Don&apos;t cache secrets or sensitive data&lt;/strong&gt; - Caches are not encrypted&lt;/p&gt;
&lt;p&gt;❌ &lt;strong&gt;Don&apos;t forget about cache limits&lt;/strong&gt; - 10GB per repo, plan accordingly&lt;/p&gt;
&lt;p&gt;❌ &lt;strong&gt;Don&apos;t cache absolute paths&lt;/strong&gt; - Use relative paths when possible&lt;/p&gt;
&lt;h2&gt;Real-world impact&lt;/h2&gt;
&lt;p&gt;Let me share some real numbers from implementing this in production:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before caching:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Average build time: 14 minutes&lt;/li&gt;
&lt;li&gt;Cache hit rate: 0% (obviously)&lt;/li&gt;
&lt;li&gt;GitHub Actions cost: ~$50/month for a team of 5&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;After implementing smart caching:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Average build time: 4 minutes&lt;/li&gt;
&lt;li&gt;Cache hit rate: 85% (meaning 85% of builds use cached dependencies)&lt;/li&gt;
&lt;li&gt;GitHub Actions cost: ~$15/month&lt;/li&gt;
&lt;li&gt;Developer happiness: Significantly improved 😄&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&apos;s a &lt;strong&gt;70% reduction in build time&lt;/strong&gt; and &lt;strong&gt;70% reduction in costs&lt;/strong&gt;. Not bad for a few hours of YAML writing!&lt;/p&gt;
&lt;h2&gt;What&apos;s next?&lt;/h2&gt;
&lt;p&gt;Once you&apos;ve got caching humming along nicely, you can explore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Self-hosted runners&lt;/strong&gt; with persistent caches (for even faster builds)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker layer caching&lt;/strong&gt; (if you&apos;re containerizing your builds)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Artifact caching&lt;/strong&gt; between different workflows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build result caching&lt;/strong&gt; (only rebuild what changed)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But honestly? The strategies in this post will get you 90% of the benefit with 10% of the complexity. Perfect is the enemy of good, and good caching is already pretty amazing.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Look, I get it. YAML configuration isn&apos;t the sexiest part of iOS development. But trust me on this one - spending an hour setting up proper caching will save you &lt;strong&gt;hours every week&lt;/strong&gt; waiting for builds.&lt;/p&gt;
&lt;p&gt;Your future self will thank you when you push a small change and your CI finishes before you can even switch to Twitter. Your teammates will think you&apos;re a wizard. Your manager will love the reduced CI costs. And you&apos;ll finally have time to do what we all really want to do: write more Swift code instead of watching progress bars.&lt;/p&gt;
&lt;p&gt;The strategies in this post have saved my team literally hundreds of hours over the past year. That&apos;s time we could spend on features, bug fixes, or just not being frustrated by slow builds.&lt;/p&gt;
&lt;p&gt;So go forth, cache all the things, and may your builds be swift and your coffee always hot! ☕️&lt;/p&gt;
&lt;p&gt;Got questions about caching? Hit me up! I love talking about developer productivity, especially when it involves making computers do less work. That&apos;s basically the entire point of programming, right? 😄&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;P.S. - If you found this helpful, you might also enjoy our previous posts on &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/&quot;&gt;GitHub Actions setup&lt;/a&gt; and &lt;a href=&quot;https://nowham.dev/posts/github-actions-testflight-deploy/&quot;&gt;TestFlight automation&lt;/a&gt;. Together, they form the holy trinity of iOS CI/CD automation.&lt;/em&gt;&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Learning to Learn with AI (for Developers)</title><link>https://nowham.dev/posts/learn-to-learn-with-ai/</link><guid isPermaLink="true">https://nowham.dev/posts/learn-to-learn-with-ai/</guid><description>How to use AI to make you a better learner instead of replacing you.</description><pubDate>Wed, 16 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;I know I know, &lt;em&gt;another&lt;/em&gt; blog post about AI. But this one is different, I swear!&lt;br /&gt;
In a sea of AI agents, tools, IDEs and everything in between, it&apos;s really easy to get lost in the &quot;let this thing do it for me&quot; mindset. And I have to admit, I&apos;ve been there.&lt;/p&gt;
&lt;p&gt;I love using AI to take the boring away from me. That be boilerplate, revision of PRs, things that I can&apos;t be bothered to do.&lt;br /&gt;
But I&apos;ve also been guilty of using AI to do things that I should be doing myself, not because I know how to do them, but because I &lt;em&gt;don&apos;t&lt;/em&gt;. let&apos;s talk about it.&lt;/p&gt;
&lt;h2&gt;The problem&lt;/h2&gt;
&lt;p&gt;AI is great right? you can build shit that you wouldn&apos;t normally be able to build, and without much effort. Are you a web-dev that wants an app? no problem, just ask Claude to build it for you. Are you an iOS dev that wants a backend? Sick, Cursor has go you sorted. We&apos;ve all been there, and it&apos;s great.&lt;/p&gt;
&lt;p&gt;But what happens when you don&apos;t know what you don&apos;t know? when AI inevitable hits the infinite feedback loop of &quot;a ha! now i see the problem&quot; and you&apos;re left with a bug or unexpected behavior?&lt;/p&gt;
&lt;p&gt;Or, and perhaps more importantly, what happens when you don&apos;t know what you&apos;re &lt;em&gt;supposed&lt;/em&gt; to know? That can be the codebase that you&apos;re working on in your job, that can be a feature or a product you&apos;re delivering to a client, or that can even be something that you&apos;re trying to learn?&lt;/p&gt;
&lt;p&gt;What happens when someone asks you, live, face to face or in a meeting, &quot;how is this thing done?&quot;, &quot;how long will it take to do this?&quot;, &quot;is this thing feasible?&quot;? if you don&apos;t know what you&apos;re doing, you&apos;ve only been a pass-thorough to some agent, you be stuck in a very embarrassing situation.&lt;/p&gt;
&lt;p&gt;And lastly, you&apos;re going to change jobs eventually. might not be today, might not be next week, but it&apos;s going to happen. and you&apos;re going to be left stuck in the same place, with whatever knowledge you&apos;ve had the last time you were actually coding, stagnant. the world moves fast, and if you&apos;re not continuously learning, you&apos;re left behind.&lt;/p&gt;
&lt;h2&gt;My approach&lt;/h2&gt;
&lt;p&gt;You&apos;ll notice that i&apos;m not calling this section &quot;the solution&quot;, because I don&apos;t claim to have the answer. or at least not the answer that will work for everyone. i have the answer that has worked for &lt;em&gt;me&lt;/em&gt; and I&apos;m going to share it with you.&lt;/p&gt;
&lt;p&gt;I&apos;ve recently started a new job at a tremendous company, and the code base is a bit of a beast. firstly, we use KMP, a framework i have never touched before, bar the tiniest of projects. And secondly, it has a lot more emphasis on Kotlin and Android than i had in the recent years, being an iOS developer. I haven&apos;t lied during my interview process, the company knew i&apos;m an iOS developer who&apos;s happy to learn new things, but i still felt like i have that self-inflected stress over my head of &quot;you should be able to do this&quot;.&lt;/p&gt;
&lt;p&gt;Not only that, i&apos;ve also had some client work that has involved some MacOS development, which i&apos;ve never done before. goes without saying, that has stressed me out as well.&lt;/p&gt;
&lt;p&gt;So, while i &lt;em&gt;could&lt;/em&gt; use AI to do tickets for me ( or at least attempt to), i&apos;ve been using it for a different purpose. I&apos;ve been using it to teach me about the project, about KMP, and about everything else that i&apos;m not familiar with.&lt;/p&gt;
&lt;p&gt;The obvious way i do this is by asking AI how things are done in Kotlin, or on MacOS, or whatever that might be. while yes, i can just ask it to do it for me, by asking it &lt;em&gt;how&lt;/em&gt; things are done, and then doing them myself, i get the repetition and learning process.&lt;/p&gt;
&lt;p&gt;Another way i&apos;ve been using it, is by asking it to onboard me on certain parts of the codebase. So if i&apos;m working on a new feature in an existing module / part of the code, i will ask it to explain to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What are the main components of the module?&lt;/li&gt;
&lt;li&gt;What is the architecture used?&lt;/li&gt;
&lt;li&gt;What are patterns or conventions i should adhere to ?&lt;/li&gt;
&lt;li&gt;What are the main concepts that i should be aware of?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These questions help me greatly understand what am i looking at. this is a &lt;strong&gt;very&lt;/strong&gt; important step in learning any new code base, and i think it&apos;s crucial to understand those pointers so you can become comfortable and confident in the codebase, and then you can start to build your own knowledge of the codebase.&lt;/p&gt;
&lt;h3&gt;Tools&lt;/h3&gt;
&lt;p&gt;So while doing this, i&apos;ve found certain tools seem to be more helpful than other, but like everything else in programming (and life), pick the right tool for the job.&lt;/p&gt;
&lt;h4&gt;Cursor&lt;/h4&gt;
&lt;p&gt;I&apos;ve been using cursor for context-specific code questions that require knowledge of the codebase.&lt;br /&gt;
The main ways i&apos;ve used this before is &quot;Where is this thing&quot;, or &quot;Why is this error happening&quot;, or my personal favorite, &quot;am i doing this like the rest of the codebase?&quot;&lt;/p&gt;
&lt;p&gt;The reason for the last question is that we&apos;ve all been doing things our way, and that&apos;s great. but new codebases have their own ways of doing things, their own conventions and patterns. so while you might be used to having view models and presenters, the new codebase might have a different approach. so it&apos;s good to ask either before you start, or just after you&apos;ve started, &quot;is this following the codebase&apos;s conventions?&quot;&lt;/p&gt;
&lt;h4&gt;Claude&lt;/h4&gt;
&lt;p&gt;Claude has been my new google for a while now. i mainly use it for &quot;how do you do this in Kotlin?&quot; or &quot;how do you do this in Android?&quot; or &quot;how do you do this in KMP?&quot;. Think of it like that, you know that the thing you&apos;re building needs a switch statement, you write &lt;code&gt;switch&lt;/code&gt;, but whoops the compiler doesn&apos;t understand what you&apos;re talking about? that&apos;s because &lt;code&gt;switch&lt;/code&gt; is called &lt;code&gt;when&lt;/code&gt; in Kotlin. These are small, minuscule things, but they might waste your time, and there&apos;s no need for that struggle - you know what you want to do, syntax is just a tool to get there.&lt;/p&gt;
&lt;h4&gt;Copilot&lt;/h4&gt;
&lt;p&gt;Admittedly, i&apos;ve been using copilot less and less now, as cursor has been replacing it more and more, but when i write code either in Xcode or Android Studio, i still use it for auto completion of basic things. So i try not to use it for too big of a job, as i still want the repetition to build the muscle memory, but i use it for things like autocompletion of init methods, or auto completion of basic functions.&lt;/p&gt;
&lt;h2&gt;Pitfalls&lt;/h2&gt;
&lt;p&gt;Like everything else in life, this isn&apos;t a 100% solution. AI can be, and is, wrong. sometimes i&apos;ll ask it where something is, and it will hallucinate a location or a module, and i&apos;ll be sent down a rabbit hole of trying to find it. Sometimes it guesses the autocompletion of an init method and i&apos;ll be left with a bug that i didn&apos;t see coming.&lt;/p&gt;
&lt;p&gt;But that&apos;s the beauty of it, it&apos;s not a replacement for you, it&apos;s a tool to help you. It&apos;s a tool to help you learn, to help you understand, to help you get things done.&lt;/p&gt;
&lt;h2&gt;Main takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;AI is great, but it&apos;s not a replacement for you. It wants to be, but it&apos;s up to you to not let it.&lt;/li&gt;
&lt;li&gt;It&apos;s very easy to fall into the trap of using AI to do things for you. the speed of the feedback loop is too tempting. but you&apos;re not learning anything. if you want to be a better engineer, you need to learn.&lt;/li&gt;
&lt;li&gt;It might be a cliche, but it&apos;s not about the destination, it&apos;s about the journey. Programming is such a vast field, and there is so much to learn, it can be daunting. but that&apos;s the beauty of it, there is always something to learn. if you don&apos;t learn, you&apos;re going to be stagnant. and if you&apos;re stagnant, you&apos;re going to be the one left behind.&lt;/li&gt;
&lt;li&gt;This is by no means a &quot;AI BAD&quot; post. i love AI, i use it every single day, and i think that if you don&apos;t use it you&apos;re going to struggle to keep up, but you shouldn&apos;t lose sight of the fact that it&apos;s a &lt;strong&gt;tool&lt;/strong&gt;, and you&apos;re the wielder. you should be able to jump in and do whatever it is doing, at any point.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Auto-Deploy to TestFlight via GitHub Actions Tag</title><link>https://nowham.dev/posts/github-actions-testflight-deploy/</link><guid isPermaLink="true">https://nowham.dev/posts/github-actions-testflight-deploy/</guid><description>The holy grail - automatic TestFlight deployment when you tag with &quot;testflight&quot;</description><pubDate>Wed, 09 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;This is it! This is the post we&apos;ve been building up to in our &quot;GitHub Actions to TestFlight - from zero to hero&quot; series. We&apos;ve &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/&quot;&gt;set up GitHub Actions for iOS&lt;/a&gt;, we&apos;ve &lt;a href=&quot;https://nowham.dev/posts/github-actions-ios-unit-tests/&quot;&gt;automated our unit tests&lt;/a&gt;, and now we&apos;re going to put it all together to create the ultimate iOS developer dream: &lt;strong&gt;tagging your branch with &quot;testflight&quot; and having your app automatically built, tested, and deployed to TestFlight&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;No more manual builds, no more forgetting to increment version numbers, no more &quot;works on my machine&quot; deployments. Just tag, push, and relax while the robots do all the work. Let&apos;s make it happen! 🚀&lt;/p&gt;
&lt;h2&gt;The workflow we&apos;re building&lt;/h2&gt;
&lt;p&gt;Here&apos;s what&apos;s going to happen when you tag your branch with &quot;testflight&quot;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Tests run&lt;/strong&gt; - Making sure everything still works&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version numbers bump&lt;/strong&gt; - Using our &lt;a href=&quot;https://nowham.dev/posts/fastlane-version-bump-ios/&quot;&gt;version bumping knowledge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App builds&lt;/strong&gt; - Using our &lt;a href=&quot;https://nowham.dev/posts/fastlane-ios-build-guide/&quot;&gt;Fastlane building skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App uploads to TestFlight&lt;/strong&gt; - The magic moment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notifications sent&lt;/strong&gt; - So you know it worked&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All triggered by a simple &lt;code&gt;git tag testflight &amp;amp;&amp;amp; git push --tags&lt;/code&gt;. Beautiful, right?&lt;/p&gt;
&lt;h2&gt;Pre-requisites&lt;/h2&gt;
&lt;p&gt;Before we dive in, make sure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A working GitHub Actions setup from our &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/&quot;&gt;previous posts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;An App Store Connect API key (more on this below)&lt;/li&gt;
&lt;li&gt;Your app already created in App Store Connect&lt;/li&gt;
&lt;li&gt;Code signing certificates and provisioning profiles set up&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&apos;re missing any of these, don&apos;t worry - I&apos;ll walk you through the tricky parts.&lt;/p&gt;
&lt;h2&gt;Setting up App Store Connect API Key&lt;/h2&gt;
&lt;p&gt;First things first - we need a way for our GitHub Action to authenticate with App Store Connect. The old username/password method is deprecated, so we&apos;re using API keys.&lt;/p&gt;
&lt;h3&gt;Creating the API key&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;a href=&quot;https://appstoreconnect.apple.com/&quot;&gt;App Store Connect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Navigate to Users and Access → Integrations → App Store Connect API&lt;/li&gt;
&lt;li&gt;Click the + button to create a new key&lt;/li&gt;
&lt;li&gt;Give it a name like &quot;GitHub Actions&quot;&lt;/li&gt;
&lt;li&gt;Select the &quot;Developer&quot; role (or &quot;Admin&quot; if you need more permissions)&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;.p8&lt;/code&gt; file and note the &lt;strong&gt;Key ID&lt;/strong&gt; and &lt;strong&gt;Issuer ID&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Important&lt;/strong&gt;: You can only download the &lt;code&gt;.p8&lt;/code&gt; file once! Keep it safe.&lt;/p&gt;
&lt;h3&gt;Adding secrets to GitHub&lt;/h3&gt;
&lt;p&gt;Now we need to add these credentials to GitHub as secrets:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to your repository on GitHub&lt;/li&gt;
&lt;li&gt;Settings → Secrets and variables → Actions&lt;/li&gt;
&lt;li&gt;Add these repository secrets:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;APP_STORE_CONNECT_API_KEY&lt;/code&gt;: The content of your &lt;code&gt;.p8&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;APP_STORE_CONNECT_API_KEY_ID&lt;/code&gt;: Your Key ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;APP_STORE_CONNECT_ISSUER_ID&lt;/code&gt;: Your Issuer ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;APP_STORE_CONNECT_APP_ID&lt;/code&gt;: Your app&apos;s App Store ID (found in App Store Connect)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the API key content, you can get it with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat AuthKey_XXXXXXXXXX.p8 | base64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy the entire base64 string into the secret.&lt;/p&gt;
&lt;h2&gt;Code signing in CI&lt;/h2&gt;
&lt;p&gt;Code signing is probably the most painful part of iOS CI/CD. If you followed our &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/&quot;&gt;GitHub Actions setup post&lt;/a&gt;, you should already have Fastlane Match configured. If not, here&apos;s a quick refresher:&lt;/p&gt;
&lt;h3&gt;Fastlane Match Setup (Required)&lt;/h3&gt;
&lt;p&gt;Our &lt;code&gt;build&lt;/code&gt; lane uses Fastlane Match to handle code signing automatically. If you haven&apos;t set this up yet, you&apos;ll need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create a private Git repository&lt;/strong&gt; for certificates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Initialize Match&lt;/strong&gt; in your project:&lt;pre&gt;&lt;code&gt;bundle exec fastlane match init
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generate certificates&lt;/strong&gt;:&lt;pre&gt;&lt;code&gt;bundle exec fastlane match appstore
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For a complete setup guide, check out the &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/#setting-up-fastlane-match&quot;&gt;Match setup section&lt;/a&gt; in our previous post.&lt;/p&gt;
&lt;h3&gt;Required GitHub Secrets&lt;/h3&gt;
&lt;p&gt;Make sure you have these secrets configured in your repository:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MATCH_REPOSITORY&lt;/code&gt;: URL to your certificates repository&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MATCH_PASSWORD&lt;/code&gt;: Your Match encryption passphrase&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MATCH_GIT_PRIVATE_KEY&lt;/code&gt;: SSH private key for accessing your private certificates repository&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For detailed setup instructions, see the &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/#setting-up-ssh-access-for-private-match-repository&quot;&gt;SSH access setup section&lt;/a&gt; in our first post.&lt;/p&gt;
&lt;h3&gt;Alternative: Manual certificates&lt;/h3&gt;
&lt;p&gt;If you prefer not to use Match, you can create a separate Fastlane lane for manual certificate setup:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &quot;Set up manual certificates&quot;
lane :setup_certificates do
  import_certificate(
    certificate_path: &quot;ios_distribution.p12&quot;,
    certificate_password: ENV[&quot;IOS_CERTIFICATE_PASSWORD&quot;],
    keychain_name: &quot;login.keychain&quot;
  )

  install_provisioning_profile(
    path: &quot;MyApp_AppStore.mobileprovision&quot;
  )
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then call it as a separate step in your GitHub Actions workflow:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Set up code signing
  run: bundle exec fastlane setup_certificates
  env:
    IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But honestly? Match is so much easier. 😉&lt;/p&gt;
&lt;h2&gt;The TestFlight deployment workflow&lt;/h2&gt;
&lt;p&gt;Now for the main event! Here&apos;s our complete workflow:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Deploy to TestFlight

on:
  push:
    tags:
      - &apos;testflight*&apos;

jobs:
  deploy:
    runs-on: macos-latest

    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0  # We need full history for version bumping

    # Set up Xcode 16.2
    - name: Set up Xcode
      uses: maxim-lobanov/setup-xcode@v1
      with:
        xcode-version: &quot;16.2.0&quot;

    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 3.1
        bundler-cache: true

    - name: Cache Xcode derived data
      uses: actions/cache@v4
      with:
        path: ~/Library/Developer/Xcode/DerivedData
        key: ${{ runner.os }}-xcode-deriveddata-${{ hashFiles(&apos;**/*.xcodeproj/project.pbxproj&apos;) }}
        restore-keys: |
          ${{ runner.os }}-xcode-deriveddata-

    - name: Install dependencies
      run: bundle install

    - name: Setup App Store Connect API Key
      run: |
        mkdir -p ~/.appstoreconnect/private_keys/
        echo &quot;${{ secrets.APP_STORE_CONNECT_API_KEY }}&quot; | base64 --decode &amp;gt; ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8

    # Set up SSH key for Match repository access
    - name: Set up SSH key for Match
      run: |
        mkdir -p ~/.ssh
        echo &quot;${{ secrets.MATCH_GIT_PRIVATE_KEY }}&quot; &amp;gt; ~/.ssh/id_rsa
        chmod 600 ~/.ssh/id_rsa
        ssh-keyscan github.com &amp;gt;&amp;gt; ~/.ssh/known_hosts
        # Create SSH config for GitHub
        cat &amp;gt; ~/.ssh/config &amp;lt;&amp;lt; EOF
        Host github.com
          HostName github.com
          User git
          IdentityFile ~/.ssh/id_rsa
          IdentitiesOnly yes
        EOF
        # Test SSH connection
        ssh -T git@github.com || true

    - name: Set up code signing
      run: bundle exec fastlane match appstore --readonly --app_identifier com.yourcompany.yourapp
      env:
        MATCH_REPOSITORY: ${{ secrets.MATCH_REPOSITORY }}
        MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}

    - name: Run tests
      run: bundle exec fastlane test

    - name: Build and Deploy to TestFlight
      run: bundle exec fastlane deploy_testflight
      env:
        APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
        APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
        APP_STORE_CONNECT_APP_ID: ${{ secrets.APP_STORE_CONNECT_APP_ID }}

    - name: Clean up API Key
      if: always()
      run: rm -f ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Fastlane lanes&lt;/h2&gt;
&lt;p&gt;Now we need to create the Fastlane lanes that this workflow calls. Add these to your Fastfile:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/ruby
# frozen_string_literal: true

fastlane_version &apos;2.225.0&apos;

default_platform :ios
platform :ios do

  desc &quot;Build the app&quot;
  lane :build do
    # Build the app (code signing should be done separately)
    gym(
      scheme: &quot;YourAppScheme&quot;,
      configuration: &quot;Release&quot;,
      export_method: &quot;app-store&quot;,
      output_directory: &quot;./build&quot;,
      export_options: {
        uploadBitcode: false,
        uploadSymbols: true,
        compileBitcode: false
      }
    )
  end

  desc &quot;Run tests&quot;
  lane :test do
    scan(
      scheme: &quot;YourAppScheme&quot;,
      clean: true,
      devices: &quot;iPhone 15 Pro&quot;,
      output_directory: &quot;./fastlane/test_output&quot;,
      output_types: &quot;html,junit&quot;
    )
  end

  desc &quot;Deploy to TestFlight&quot;
  lane :deploy_testflight do
    # First, let&apos;s bump the build number
    # Get the latest build numbers from both TestFlight and App Store
    latest_tf_build = latest_testflight_build_number(
      app_identifier: &quot;com.yourcompany.yourapp&quot;,
      api_key_id: ENV[&quot;APP_STORE_CONNECT_API_KEY_ID&quot;],
      issuer_id: ENV[&quot;APP_STORE_CONNECT_ISSUER_ID&quot;]
    )

    latest_appstore_build = app_store_build_number(
      app_identifier: &quot;com.yourcompany.yourapp&quot;,
      api_key_id: ENV[&quot;APP_STORE_CONNECT_API_KEY_ID&quot;],
      issuer_id: ENV[&quot;APP_STORE_CONNECT_ISSUER_ID&quot;]
    )

    # Use the highest build number and increment by 1
    highest_build = [latest_tf_build, latest_appstore_build].max
    increment_build_number_in_xcodeproj(
      build_number: highest_build + 1
    )

    # Build the app (code signing was done in a previous step)
    build

    # Upload to TestFlight
    pilot(
      app_identifier: &quot;com.yourcompany.yourapp&quot;,
      api_key_id: ENV[&quot;APP_STORE_CONNECT_API_KEY_ID&quot;],
      issuer_id: ENV[&quot;APP_STORE_CONNECT_ISSUER_ID&quot;],
      skip_waiting_for_build_processing: true,
      changelog: &quot;Automated build from GitHub Actions&quot;,
      distribute_external: false,
      notify_external_testers: false
    )

    # Send a notification (optional)
    slack(
      message: &quot;🚀 New TestFlight build is processing!&quot;,
      slack_url: ENV[&quot;SLACK_WEBHOOK_URL&quot;]
    ) if ENV[&quot;SLACK_WEBHOOK_URL&quot;]
  end

end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let me break down what&apos;s happening in the &lt;code&gt;deploy_testflight&lt;/code&gt; lane:&lt;/p&gt;
&lt;h3&gt;Version bumping&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Get the latest build numbers from both TestFlight and App Store
latest_tf_build = latest_testflight_build_number(
  app_identifier: &quot;com.yourcompany.yourapp&quot;,
  api_key_id: ENV[&quot;APP_STORE_CONNECT_API_KEY_ID&quot;],
  issuer_id: ENV[&quot;APP_STORE_CONNECT_ISSUER_ID&quot;]
)

latest_appstore_build = app_store_build_number(
  app_identifier: &quot;com.yourcompany.yourapp&quot;,
  api_key_id: ENV[&quot;APP_STORE_CONNECT_API_KEY_ID&quot;],
  issuer_id: ENV[&quot;APP_STORE_CONNECT_ISSUER_ID&quot;]
)

# Use the highest build number and increment by 1
highest_build = [latest_tf_build, latest_appstore_build].max
increment_build_number_in_xcodeproj(
  build_number: highest_build + 1
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gets the latest build numbers from both TestFlight and the App Store, finds the highest one, and increments it by 1. This prevents version conflicts that could occur if the App Store has a higher build number than TestFlight. No more version conflicts! This builds on what we learned in our &lt;a href=&quot;https://nowham.dev/posts/fastlane-version-bump-ios/&quot;&gt;version bumping post&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Building the app&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Build the app (code signing was done in a previous step)
build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This calls our &lt;code&gt;build&lt;/code&gt; lane from our &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/&quot;&gt;GitHub Actions setup post&lt;/a&gt;. Since we&apos;ve already set up code signing in a previous workflow step, the build lane can focus purely on building the app. Clean and efficient!&lt;/p&gt;
&lt;h3&gt;Uploading to TestFlight&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pilot(
  app_identifier: &quot;com.yourcompany.yourapp&quot;,
  api_key_id: ENV[&quot;APP_STORE_CONNECT_API_KEY_ID&quot;],
  issuer_id: ENV[&quot;APP_STORE_CONNECT_ISSUER_ID&quot;],
  skip_waiting_for_build_processing: true,
  changelog: &quot;Automated build from GitHub Actions&quot;,
  distribute_external: false,
  notify_external_testers: false
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This uploads your app to TestFlight. The &lt;code&gt;skip_waiting_for_build_processing&lt;/code&gt; option means the workflow won&apos;t wait for Apple to process the build (which can take 10-30 minutes). The &lt;code&gt;distribute_external&lt;/code&gt; and &lt;code&gt;notify_external_testers&lt;/code&gt; options are set to false to prevent automatically distributing to external testers - you&apos;ll want to review builds manually first.&lt;/p&gt;
&lt;h2&gt;Advanced features&lt;/h2&gt;
&lt;h3&gt;Smart tagging&lt;/h3&gt;
&lt;p&gt;Instead of just using &quot;testflight&quot;, you can make your tags more informative:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;on:
  push:
    tags:
      - &apos;testflight*&apos;
      - &apos;tf-*&apos;
      - &apos;beta-*&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows tags like &lt;code&gt;testflight-v1.2.3&lt;/code&gt;, &lt;code&gt;tf-hotfix&lt;/code&gt;, or &lt;code&gt;beta-release&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Version from tag&lt;/h3&gt;
&lt;p&gt;You can extract version information from your tag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &quot;Deploy to TestFlight with version from tag&quot;
lane :deploy_testflight do
  # Extract version from git tag
  tag_name = ENV[&quot;GITHUB_REF_NAME&quot;] || sh(&quot;git describe --tags --abbrev=0&quot;).strip

  if tag_name.include?(&quot;-v&quot;)
    version = tag_name.split(&quot;-v&quot;).last
    increment_version_number(version_number: version)
  end

  # ... rest of the lane
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can tag with &lt;code&gt;testflight-v1.2.3&lt;/code&gt; and it will set your app version to 1.2.3.&lt;/p&gt;
&lt;h3&gt;Conditional notifications&lt;/h3&gt;
&lt;p&gt;Only send notifications for production builds:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Send notification only for main branch
if ENV[&quot;GITHUB_REF&quot;] == &quot;refs/heads/main&quot;
  slack(
    message: &quot;🚀 Production TestFlight build is ready!&quot;,
    slack_url: ENV[&quot;SLACK_WEBHOOK_URL&quot;]
  )
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Build artifacts&lt;/h3&gt;
&lt;p&gt;Save your .ipa file as a GitHub artifact:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Upload build artifacts
  uses: actions/upload-artifact@v4
  with:
    name: ios-app-${{ github.sha }}
    path: build/*.ipa
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using the workflow&lt;/h2&gt;
&lt;p&gt;Now for the moment of truth! Here&apos;s how to use your new automated TestFlight deployment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Make your changes&lt;/strong&gt; and commit them&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tag your commit&lt;/strong&gt;: &lt;code&gt;git tag testflight&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Push the tag&lt;/strong&gt;: &lt;code&gt;git push --tags&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Watch the magic happen&lt;/strong&gt; in the GitHub Actions tab&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can also do it all in one go:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add .
git commit -m &quot;Ready for TestFlight&quot;
git tag testflight-$(date +%Y%m%d-%H%M%S)
git push --tags
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a unique tag with a timestamp, so you can deploy multiple times per day without conflicts.&lt;/p&gt;
&lt;h2&gt;Troubleshooting&lt;/h2&gt;
&lt;h3&gt;&quot;Invalid API Key&quot;&lt;/h3&gt;
&lt;p&gt;Make sure your API key is properly base64 encoded and that you&apos;ve set all three required secrets (API key, key ID, and issuer ID).&lt;/p&gt;
&lt;h3&gt;&quot;No matching provisioning profiles found&quot;&lt;/h3&gt;
&lt;p&gt;This usually means your provisioning profile doesn&apos;t match your app&apos;s bundle identifier or capabilities. Double-check your Match setup or manual provisioning profiles.&lt;/p&gt;
&lt;h3&gt;&quot;Build already exists&quot;&lt;/h3&gt;
&lt;p&gt;This happens when you try to upload a build with the same version and build number as an existing build. Our version bumping should prevent this, but if it happens, you might need to manually increment the build number.&lt;/p&gt;
&lt;h3&gt;&quot;Processing takes too long&quot;&lt;/h3&gt;
&lt;p&gt;Apple&apos;s build processing can sometimes take a very long time. If you&apos;re hitting timeout issues, consider using &lt;code&gt;skip_waiting_for_build_processing: true&lt;/code&gt; and checking the status manually later.&lt;/p&gt;
&lt;h2&gt;Monitoring and notifications&lt;/h2&gt;
&lt;h3&gt;Slack notifications&lt;/h3&gt;
&lt;p&gt;Add Slack notifications to know when your builds succeed or fail:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;slack(
  message: &quot;✅ TestFlight deployment successful!&quot;,
  success: true,
  payload: {
    &quot;Build Number&quot; =&amp;gt; get_build_number,
    &quot;Version&quot; =&amp;gt; get_version_number,
    &quot;Git Commit&quot; =&amp;gt; ENV[&quot;GITHUB_SHA&quot;]
  },
  slack_url: ENV[&quot;SLACK_WEBHOOK_URL&quot;]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Email notifications&lt;/h3&gt;
&lt;p&gt;GitHub Actions can also send email notifications on failure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Send failure notification
  if: failure()
  uses: dawidd6/action-send-mail@v3
  with:
    server_address: smtp.gmail.com
    server_port: 465
    username: ${{ secrets.EMAIL_USERNAME }}
    password: ${{ secrets.EMAIL_PASSWORD }}
    subject: &quot;TestFlight deployment failed&quot;
    body: &quot;The TestFlight deployment for ${{ github.repository }} failed. Check the logs at ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}&quot;
    to: your-email@example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best practices&lt;/h2&gt;
&lt;h3&gt;Branch protection&lt;/h3&gt;
&lt;p&gt;Consider protecting your main branch and requiring pull request reviews before changes can be merged. This prevents accidental TestFlight deployments from broken code.&lt;/p&gt;
&lt;h3&gt;Environment separation&lt;/h3&gt;
&lt;p&gt;You might want different workflows for different environments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;testflight&lt;/code&gt; tag → Internal TestFlight&lt;/li&gt;
&lt;li&gt;&lt;code&gt;beta&lt;/code&gt; tag → External TestFlight&lt;/li&gt;
&lt;li&gt;&lt;code&gt;release&lt;/code&gt; tag → App Store&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Testing before deployment&lt;/h3&gt;
&lt;p&gt;Always run your full test suite before deploying:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &quot;Deploy to TestFlight&quot;
lane :deploy_testflight do
  # Run tests first
  scan(scheme: &quot;YourAppScheme&quot;)

  # Only proceed if tests pass
  # ... rest of deployment
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Security considerations&lt;/h2&gt;
&lt;h3&gt;Least privilege&lt;/h3&gt;
&lt;p&gt;Your API key should have the minimum permissions needed. Usually &quot;Developer&quot; role is sufficient for TestFlight uploads.&lt;/p&gt;
&lt;h3&gt;Secret rotation&lt;/h3&gt;
&lt;p&gt;Rotate your API keys and certificates regularly. Consider using different keys for different environments.&lt;/p&gt;
&lt;h3&gt;Branch restrictions&lt;/h3&gt;
&lt;p&gt;Consider restricting TestFlight deployments to specific branches:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;on:
  push:
    tags:
      - &apos;testflight*&apos;
    branches:
      - main
      - release/*
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What&apos;s next?&lt;/h2&gt;
&lt;p&gt;Congratulations! You&apos;ve just set up one of the most powerful iOS development workflows possible. You can now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Push code to GitHub&lt;/li&gt;
&lt;li&gt;Tag it with &quot;testflight&quot;&lt;/li&gt;
&lt;li&gt;Have it automatically tested, built, and deployed to TestFlight&lt;/li&gt;
&lt;li&gt;Get notified when it&apos;s done&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the kind of automation that makes iOS development feel magical. No more manual builds, no more forgetting steps, no more &quot;works on my machine&quot; problems.&lt;/p&gt;
&lt;p&gt;But we&apos;re not done yet! You could extend this further with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automatic App Store submissions (for approved builds)&lt;/li&gt;
&lt;li&gt;Integration with project management tools&lt;/li&gt;
&lt;li&gt;Automated release notes generation&lt;/li&gt;
&lt;li&gt;Performance monitoring integration&lt;/li&gt;
&lt;li&gt;Crash reporting setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The possibilities are endless!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;We&apos;ve come a long way in this series! We started with &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/&quot;&gt;basic GitHub Actions setup&lt;/a&gt;, moved on to &lt;a href=&quot;https://nowham.dev/posts/github-actions-ios-unit-tests/&quot;&gt;automated testing&lt;/a&gt;, and now we&apos;ve created a complete TestFlight deployment pipeline.&lt;/p&gt;
&lt;p&gt;This workflow leverages everything we&apos;ve learned in our Fastlane series - &lt;a href=&quot;https://nowham.dev/posts/fastlane-ios-build-guide/&quot;&gt;building apps&lt;/a&gt;, &lt;a href=&quot;https://nowham.dev/posts/fastlane-unit-tests-ios/&quot;&gt;running tests&lt;/a&gt;, and &lt;a href=&quot;https://nowham.dev/posts/fastlane-version-bump-ios/&quot;&gt;managing versions&lt;/a&gt; - and combines it with the power of GitHub Actions automation.&lt;/p&gt;
&lt;p&gt;The result? A professional-grade iOS deployment pipeline that would make any DevOps engineer proud. And the best part? It&apos;s all free (within GitHub&apos;s generous limits) and runs in the cloud, so your laptop can stay in sleep mode while your app gets built and deployed.&lt;/p&gt;
&lt;p&gt;Try it out, customize it for your needs, and let me know how it goes! And if you run into any issues, remember that both GitHub Actions and Fastlane have excellent logging - the answer is usually in the logs.&lt;/p&gt;
&lt;p&gt;Happy deploying! 🚀&lt;/p&gt;
&lt;p&gt;P.S. - If you found this series helpful, consider sharing it with other iOS developers. Automation like this can save hours every week, and everyone deserves to experience the joy of &lt;code&gt;git tag testflight &amp;amp;&amp;amp; git push --tags&lt;/code&gt; followed by a coffee break while the robots do all the work!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Run iOS Unit Tests in GitHub Actions CI</title><link>https://nowham.dev/posts/github-actions-ios-unit-tests/</link><guid isPermaLink="true">https://nowham.dev/posts/github-actions-ios-unit-tests/</guid><description>Automate your testing and never ship broken code again</description><pubDate>Thu, 19 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Welcome back to our &quot;GitHub Actions to TestFlight - from zero to hero&quot; series! In our &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/&quot;&gt;last post&lt;/a&gt;, we set up a basic GitHub Actions workflow that builds our iOS app every time we push to the main branch. That&apos;s great, but you know what&apos;s even better? Making sure our app actually works before we ship it.&lt;/p&gt;
&lt;p&gt;Today, we&apos;re going to extend our workflow to run unit tests automatically. This builds on what we learned in our &lt;a href=&quot;https://nowham.dev/posts/fastlane-unit-tests-ios/&quot;&gt;running unit tests with Fastlane&lt;/a&gt; post, but now we&apos;re going to do it in the cloud, automatically, every time we push code.&lt;/p&gt;
&lt;p&gt;Because let&apos;s be honest - we all write unit tests (&lt;strong&gt;right?&lt;/strong&gt;), but do we always remember to run them before pushing? I certainly don&apos;t. Let the robots handle it!&lt;/p&gt;
&lt;h2&gt;Why run tests in CI?&lt;/h2&gt;
&lt;h3&gt;Catch issues early&lt;/h3&gt;
&lt;p&gt;Running tests on every push and pull request means you catch issues before they make it to the main branch. This is especially important when you&apos;re working in a team - you don&apos;t want to be the person who breaks the build for everyone else.&lt;/p&gt;
&lt;h3&gt;Consistency&lt;/h3&gt;
&lt;p&gt;Your local machine might have different versions of Xcode, different simulators, or different environment variables. Running tests in CI ensures that everyone&apos;s tests run in the same environment.&lt;/p&gt;
&lt;h3&gt;Test against multiple configurations&lt;/h3&gt;
&lt;p&gt;Want to make sure your app works on iOS 15, 16, and 17? Or test against both iPhone and iPad simulators? CI makes this easy with matrix builds.&lt;/p&gt;
&lt;h2&gt;Building on our existing workflow&lt;/h2&gt;
&lt;p&gt;Remember the workflow we created in the &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/&quot;&gt;previous post&lt;/a&gt;? We&apos;re going to extend it to run tests as well. Let&apos;s start by adding a new job to our workflow:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Build and Test iOS App

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: macos-latest

    strategy:
      matrix:
        device: [&apos;iPhone 16 Pro&apos;]

    steps:
    - uses: actions/checkout@v4

    # Set up Xcode 16
    - name: Set up Xcode
      uses: maxim-lobanov/setup-xcode@v1
      with:
        xcode-version: &quot;16.2.0&quot;

    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 3.1
        bundler-cache: true

    - name: Install dependencies
      run: bundle install

    - name: Run tests
      run: bundle exec fastlane test

    - name: Upload test results
      uses: actions/upload-artifact@v4
      if: always()
      with:
        name: test-results-${{ matrix.device }}
        path: |
          fastlane/test_output/
          *.junit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let me break this down for you.&lt;/p&gt;
&lt;h2&gt;Understanding matrix builds&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;strategy:
  matrix:
    device: [&apos;iPhone 16 Pro&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;matrix&lt;/code&gt; strategy allows us to run the same job with different configurations. In this case, we&apos;re running tests with different devices. You can easily extend this to test multiple devices:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;strategy:
  matrix:
    device: [&apos;iPhone 16 Pro&apos;, &apos;iPad Pro 13-inch (M4)&apos;, &apos;iPhone SE (3rd generation)&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This would create 3 different jobs, each testing on a different device.&lt;/p&gt;
&lt;p&gt;If you need to test with different Xcode versions, you could do something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;strategy:
  matrix:
    xcode-version: [&apos;15.4&apos;, &apos;16.0&apos;]
    device: [&apos;iPhone 16 Pro&apos;]

steps:
- uses: actions/checkout@v4

- name: Set up Xcode
  uses: maxim-lobanov/setup-xcode@v1
  with:
    xcode-version: ${{ matrix.xcode-version }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This would test your app with both Xcode 15.4 and 16.0, which is useful when you want to ensure compatibility across different Xcode versions.&lt;/p&gt;
&lt;h2&gt;Setting up Fastlane for tests&lt;/h2&gt;
&lt;p&gt;Just like in our &lt;a href=&quot;https://nowham.dev/posts/fastlane-unit-tests-ios/&quot;&gt;unit testing with Fastlane post&lt;/a&gt;, we need a test lane in our Fastfile. Building on the enhanced build lane from our &lt;a href=&quot;https://nowham.dev/posts/github-actions-setup-ios/&quot;&gt;previous post&lt;/a&gt;, your Fastfile should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/ruby
# frozen_string_literal: true

fastlane_version &apos;2.225.0&apos;

default_platform :ios
platform :ios do

  desc &quot;Build the app&quot;
  lane :build do
    # Build the app (code signing should be done separately)
    gym(
      scheme: &quot;YourAppScheme&quot;,
      configuration: &quot;Release&quot;,
      export_method: &quot;app-store&quot;,
      output_directory: &quot;./build&quot;,
      export_options: {
        uploadBitcode: false,
        uploadSymbols: true,
        compileBitcode: false
      }
    )
  end

  desc &quot;Run tests&quot;
  lane :test do
    scan(
      scheme: &quot;YourAppScheme&quot;,
      clean: true,
      destination: &quot;platform=iOS Simulator,name=iPhone 16 Pro&quot;,
      output_directory: &quot;./fastlane/test_output&quot;,
      output_types: &quot;html,junit&quot;
    )
  end

end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach uses a fixed destination, which is more reliable than dynamic device selection in CI environments.&lt;/p&gt;
&lt;h2&gt;Making it more robust&lt;/h2&gt;
&lt;p&gt;Let&apos;s enhance our workflow to handle some common scenarios:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Build and Test iOS App

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: macos-latest

    strategy:
      matrix:
        device: [&apos;iPhone 16 Pro&apos;, &apos;iPhone SE (3rd generation)&apos;]
      fail-fast: false

    steps:
    - uses: actions/checkout@v4

    # Set up Xcode 16
    - name: Set up Xcode
      uses: maxim-lobanov/setup-xcode@v1
      with:
        xcode-version: &quot;16.2.0&quot;

    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 3.1
        bundler-cache: true

    - name: Cache Xcode derived data
      uses: actions/cache@v4
      with:
        path: ~/Library/Developer/Xcode/DerivedData
        key: ${{ runner.os }}-xcode-deriveddata-${{ hashFiles(&apos;**/*.xcodeproj/project.pbxproj&apos;) }}
        restore-keys: |
          ${{ runner.os }}-xcode-deriveddata-

    - name: Install dependencies
      run: bundle install

    # Optional: List available simulators for debugging
    - name: List available simulators
      run: xcrun simctl list devices available

    # Note: For unit tests, you typically don&apos;t need code signing
    # Remove these steps if your tests don&apos;t require it

    - name: Run tests
      run: bundle exec fastlane test

    - name: Upload test results
      uses: actions/upload-artifact@v4
      if: always()
      with:
        name: test-results-${{ matrix.device }}
        path: |
          fastlane/test_output/
          *.junit

    - name: Upload coverage reports
      uses: actions/upload-artifact@v4
      if: always()
      with:
        name: coverage-${{ matrix.device }}
        path: |
          *.xcresult
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Key features explained&lt;/h2&gt;
&lt;h3&gt;Caching&lt;/h3&gt;
&lt;p&gt;The cache step stores Xcode&apos;s derived data between runs, significantly speeding up builds. The cache key is based on your project file, so it invalidates when you change your project structure.&lt;/p&gt;
&lt;h3&gt;Destination parameter&lt;/h3&gt;
&lt;p&gt;Using &lt;code&gt;destination: &quot;platform=iOS Simulator,name=iPhone 16 Pro&quot;&lt;/code&gt; in the Fastfile is more reliable than device name resolution, especially in CI environments where simulator IDs can be inconsistent.&lt;/p&gt;
&lt;h3&gt;fail-fast: false&lt;/h3&gt;
&lt;p&gt;By default, if one job in a matrix fails, GitHub Actions cancels all other jobs. Setting &lt;code&gt;fail-fast: false&lt;/code&gt; means all jobs run to completion, so you see all test results, not just the first failure.&lt;/p&gt;
&lt;h2&gt;Handling different types of tests&lt;/h2&gt;
&lt;p&gt;You can create separate jobs for different types of tests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jobs:
  unit-tests:
    runs-on: macos-latest
    steps:
      # ... setup steps
      - name: Run unit tests
        run: bundle exec fastlane unit_tests

  integration-tests:
    runs-on: macos-latest
    needs: unit-tests
    steps:
      # ... setup steps
      - name: Run integration tests
        run: bundle exec fastlane integration_tests

  ui-tests:
    runs-on: macos-latest
    needs: [unit-tests, integration-tests]
    steps:
      # ... setup steps
      - name: Run UI tests
        run: bundle exec fastlane ui_tests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;needs&lt;/code&gt; keyword creates dependencies between jobs. Integration tests wait for unit tests to pass, and UI tests wait for both unit and integration tests to complete.&lt;/p&gt;
&lt;h2&gt;Advanced: Parallel testing&lt;/h2&gt;
&lt;p&gt;Remember our &lt;a href=&quot;https://nowham.dev/posts/ios-parallel-unit-tests/&quot;&gt;parallel testing post&lt;/a&gt;? We can leverage that in GitHub Actions too:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &quot;Run tests in parallel&quot;
lane :test do
  scan(
    scheme: &quot;YourAppScheme&quot;,
    clean: true,
    destination: &quot;platform=iOS Simulator,name=iPhone 16 Pro&quot;,
    parallel_testing: true,
    concurrent_workers: 4,
    output_directory: &quot;./fastlane/test_output&quot;,
    output_types: &quot;html,junit&quot;
  )
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This runs your tests faster by using multiple worker processes.&lt;/p&gt;
&lt;h2&gt;Test reporting&lt;/h2&gt;
&lt;p&gt;GitHub Actions can parse JUnit XML files and show test results directly in pull requests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Publish test results
  uses: dorny/test-reporter@v1
  if: always()
  with:
    name: Test Results
    path: &apos;fastlane/test_output/*.junit&apos;
    reporter: java-junit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a test results summary in the GitHub UI.&lt;/p&gt;
&lt;h2&gt;Troubleshooting common issues&lt;/h2&gt;
&lt;h3&gt;&quot;No simulators available&quot; or &quot;Unable to find a destination&quot;&lt;/h3&gt;
&lt;p&gt;This is the most common issue! The simulator you specified might not be available on the runner. Different Xcode versions come with different simulator versions. For example, Xcode 16.2 comes with iPhone 16 simulators, not iPhone 15.&lt;/p&gt;
&lt;p&gt;First, list available simulators to see what&apos;s actually available:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: List available simulators
  run: xcrun simctl list devices available
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then update your device names to match what&apos;s actually available. Common device names in Xcode 16.2:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;iPhone 16 Pro&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iPhone 16 Pro Max&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iPhone SE (3rd generation)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iPad Pro 13-inch (M4)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Pro tip&lt;/strong&gt;: Instead of using &lt;code&gt;device&lt;/code&gt; or &lt;code&gt;devices&lt;/code&gt;, use &lt;code&gt;destination&lt;/code&gt; for more reliable results:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scan(
  scheme: &quot;YourAppScheme&quot;,
  destination: &quot;platform=iOS Simulator,name=iPhone 16 Pro&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach is more explicit and avoids simulator ID caching issues.&lt;/p&gt;
&lt;h3&gt;&quot;Test failed due to timeout&quot;&lt;/h3&gt;
&lt;p&gt;CI environments can be slower than your local machine. You might need to increase timeouts in your test configuration.&lt;/p&gt;
&lt;h3&gt;&quot;Code signing error&quot;&lt;/h3&gt;
&lt;p&gt;For running tests, you usually don&apos;t need code signing. Make sure your test target&apos;s code signing is set to &quot;Don&apos;t Code Sign&quot; or use the iOS Simulator SDK.&lt;/p&gt;
&lt;h3&gt;Clearing simulator cache&lt;/h3&gt;
&lt;p&gt;If you&apos;re still getting old simulator ID errors after updating device names, try clearing cached data:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -rf ~/Library/Developer/Xcode/DerivedData
rm -rf YourProject.xcodeproj/project.xcworkspace/xcuserdata
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What&apos;s next?&lt;/h2&gt;
&lt;p&gt;Great! Now we&apos;ve got a workflow that builds and tests our app automatically. But we&apos;re not done yet. In the next post, we&apos;re going to put it all together and create a workflow that automatically deploys to TestFlight when we tag our branch with &quot;TestFlight&quot;.&lt;/p&gt;
&lt;p&gt;That&apos;s where things get really exciting - we&apos;ll be dealing with certificates, provisioning profiles, App Store Connect API keys, and actually shipping our app to testers.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Running tests in CI seems like overhead at first, but once you have it set up, you&apos;ll wonder how you ever lived without it. No more &quot;oops, I forgot to run the tests&quot; commits, no more broken builds, and no more manual testing every time you want to make sure everything still works.&lt;/p&gt;
&lt;p&gt;The workflow we&apos;ve created today catches issues early, tests against multiple configurations, and gives you confidence that your app works before you ship it. Best of all, it runs automatically - you don&apos;t have to remember to do anything!&lt;/p&gt;
&lt;p&gt;If you run into any issues, the GitHub Actions logs will usually tell you exactly what went wrong.&lt;/p&gt;
&lt;p&gt;Next up: the main event - deploying to TestFlight with tags! 🚀&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Set Up GitHub Actions for iOS Projects (From Scratch)</title><link>https://nowham.dev/posts/github-actions-setup-ios/</link><guid isPermaLink="true">https://nowham.dev/posts/github-actions-setup-ios/</guid><description>Your first steps into CI/CD automation for iOS apps</description><pubDate>Thu, 12 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;So, you&apos;ve been &lt;a href=&quot;https://nowham.dev/posts/fastlane-ios-build-guide/&quot;&gt;building your app with Fastlane&lt;/a&gt;, maybe even &lt;a href=&quot;https://nowham.dev/posts/fastlane-unit-tests-ios/&quot;&gt;running your unit tests&lt;/a&gt; like the responsible developer you are (&lt;strong&gt;right?&lt;/strong&gt;), and now you&apos;re thinking: &quot;This is great, but I&apos;m still doing this manually on my machine. What if I could automate this even further?&quot;&lt;/p&gt;
&lt;p&gt;Well, my friend, you&apos;ve come to the right place! In this series, we&apos;re going to take your iOS development automation to the next level by setting up GitHub Actions to automatically trigger TestFlight builds when we tag our branch with &quot;testflight&quot;. But before we get to the fun stuff, we need to lay the groundwork.&lt;/p&gt;
&lt;p&gt;This is going to be the first post in our &quot;GitHub Actions to TestFlight - from zero to hero&quot; series, and just like our &lt;a href=&quot;https://nowham.dev/posts/fastlane-intro-how/&quot;&gt;Fastlane series&lt;/a&gt;, we&apos;re going to start from the beginning and work our way up.&lt;/p&gt;
&lt;h2&gt;Why GitHub Actions?&lt;/h2&gt;
&lt;h3&gt;It&apos;s free (mostly)&lt;/h3&gt;
&lt;p&gt;GitHub Actions gives you 2,000 minutes per month for free on public repositories, and 2,000 minutes per month for free on private repositories too. For most indie developers and small teams, this is more than enough to get started. And even if you go over, the pricing is pretty reasonable.&lt;/p&gt;
&lt;h3&gt;It&apos;s integrated&lt;/h3&gt;
&lt;p&gt;Unlike other CI/CD solutions that you need to set up separately, GitHub Actions is built right into GitHub. This means no additional accounts to manage, no webhooks to configure, and no external services to worry about. It just works.&lt;/p&gt;
&lt;h3&gt;It&apos;s powerful&lt;/h3&gt;
&lt;p&gt;GitHub Actions can do pretty much anything you can think of. Build your app, run your tests, deploy to TestFlight, send notifications to Slack, update your documentation, and even make coffee (okay, maybe not that last one, but you get the idea).&lt;/p&gt;
&lt;h2&gt;Pre-requisites&lt;/h2&gt;
&lt;p&gt;Before we dive in, let&apos;s make sure we&apos;ve got everything we need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A GitHub repository with your iOS project (obviously!)&lt;/li&gt;
&lt;li&gt;Xcode project with a working build configuration&lt;/li&gt;
&lt;li&gt;Basic familiarity with Fastlane (we&apos;ve covered this in our &lt;a href=&quot;https://nowham.dev/posts/fastlane-intro-how/&quot;&gt;previous series&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;An Apple Developer account (for later posts when we actually deploy to TestFlight)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&apos;re missing any of these, no worries! Go get them set up and come back. I&apos;ll wait. ☕️&lt;/p&gt;
&lt;h2&gt;Understanding GitHub Actions&lt;/h2&gt;
&lt;p&gt;Before we start writing YAML files (yes, we&apos;re going to write YAML, but I promise it won&apos;t hurt), let&apos;s understand what GitHub Actions actually is.&lt;/p&gt;
&lt;p&gt;GitHub Actions is essentially a way to run code on GitHub&apos;s servers whenever something happens in your repository. That &quot;something&quot; could be a push to main, a pull request, a new tag, or even something as simple as someone starring your repo.&lt;/p&gt;
&lt;h3&gt;The basic concepts&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Workflows&lt;/strong&gt;: These are the main files that define what should happen. They live in &lt;code&gt;.github/workflows/&lt;/code&gt; in your repository.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jobs&lt;/strong&gt;: Each workflow can have multiple jobs that run in parallel (or sequentially if you want).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Steps&lt;/strong&gt;: Each job has multiple steps that run one after another.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Actions&lt;/strong&gt;: These are pre-built pieces of functionality that you can use in your steps. Think of them as the npm packages of the CI/CD world.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Runners&lt;/strong&gt;: These are the actual machines that run your code. GitHub provides Ubuntu, Windows, and macOS runners.&lt;/p&gt;
&lt;h2&gt;Setting up your first workflow&lt;/h2&gt;
&lt;p&gt;Let&apos;s start with something simple. We&apos;re going to create a workflow that builds your iOS app every time you push to the main branch. This is a great way to catch build errors early and make sure your app can always be built.&lt;/p&gt;
&lt;p&gt;First, create the directory structure in your repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p .github/workflows
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, let&apos;s create our first workflow file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Build iOS App

# When should this workflow run?
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    # We need a macOS runner for iOS builds
    runs-on: macos-latest
    
    steps:
    # First, we check out our code
    - uses: actions/checkout@v4
    
    # Set up Xcode 16.2
    - name: Set up Xcode
      uses: maxim-lobanov/setup-xcode@v1
      with:
        xcode-version: &quot;16.2.0&quot;
    
    # Set up Ruby (for Fastlane)
    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 3.1
        bundler-cache: true
    
    # Install dependencies
    - name: Install dependencies
      run: bundle install
    
    # Set up SSH key for Match repository access
    - name: Set up SSH key for Match
      run: |
        mkdir -p ~/.ssh
        echo &quot;${{ secrets.MATCH_GIT_PRIVATE_KEY }}&quot; &amp;gt; ~/.ssh/id_rsa
        chmod 600 ~/.ssh/id_rsa
        ssh-keyscan github.com &amp;gt;&amp;gt; ~/.ssh/known_hosts
        # Create SSH config for GitHub
        cat &amp;gt; ~/.ssh/config &amp;lt;&amp;lt; EOF
        Host github.com
          HostName github.com
          User git
          IdentityFile ~/.ssh/id_rsa
          IdentitiesOnly yes
        EOF
        # Test SSH connection
        ssh -T git@github.com || true
    
    # Set up code signing with Match
    - name: Set up code signing
      run: bundle exec fastlane match appstore --readonly --app_identifier com.yourcompany.yourapp
      env:
        MATCH_REPOSITORY: ${{ secrets.MATCH_REPOSITORY }}
        MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
    
    # Build the app using Fastlane
    - name: Build app
      run: bundle exec fastlane build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s break this down step by step:&lt;/p&gt;
&lt;h3&gt;The trigger&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells GitHub Actions to run this workflow whenever someone pushes to the main branch, or when someone creates a pull request targeting the main branch. This is a great way to catch issues early!&lt;/p&gt;
&lt;h3&gt;The runner&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;runs-on: macos-latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For iOS development, we need a macOS runner because Xcode only runs on macOS. GitHub provides macOS runners, but they&apos;re a bit more expensive than Linux runners (but still within the free tier for most use cases).&lt;/p&gt;
&lt;h3&gt;The steps&lt;/h3&gt;
&lt;p&gt;Each step does one specific thing:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Checkout the code&lt;/strong&gt;: This downloads your repository&apos;s code to the runner&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up Ruby&lt;/strong&gt;: This installs Ruby and Bundler, which we need for Fastlane&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install dependencies&lt;/strong&gt;: This runs &lt;code&gt;bundle install&lt;/code&gt; to install all the gems in your Gemfile&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build the app&lt;/strong&gt;: This runs your Fastlane build lane&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Making it work with your project&lt;/h2&gt;
&lt;p&gt;Now, this workflow assumes you&apos;ve got a Fastlane setup with a &lt;code&gt;build&lt;/code&gt; lane, like we covered in our &lt;a href=&quot;https://nowham.dev/posts/fastlane-ios-build-guide/&quot;&gt;building with Fastlane post&lt;/a&gt;. If you don&apos;t have that set up yet, you&apos;ll need to do that first.&lt;/p&gt;
&lt;p&gt;Your &lt;code&gt;Fastfile&lt;/code&gt; should look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/ruby
# frozen_string_literal: true

fastlane_version &apos;2.225.0&apos;

default_platform :ios
platform :ios do

  desc &quot;Build the app&quot;
  lane :build do
    # Build the app (code signing should be done separately)
    gym(
      scheme: &quot;YourAppScheme&quot;,
      configuration: &quot;Release&quot;,
      export_method: &quot;app-store&quot;,
      output_directory: &quot;./build&quot;,
      export_options: {
        uploadBitcode: false,
        uploadSymbols: true,
        compileBitcode: false
      }
    )
  end

end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure to replace &lt;code&gt;&quot;YourAppScheme&quot;&lt;/code&gt; with the actual scheme name from your Xcode project.&lt;/p&gt;
&lt;h3&gt;Code Signing Explained&lt;/h3&gt;
&lt;p&gt;Notice how we handle code signing as a &lt;strong&gt;separate step&lt;/strong&gt; in our GitHub Actions workflow, before the build step:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Set up code signing
  run: bundle exec fastlane match appstore --readonly
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach separates concerns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code signing setup&lt;/strong&gt; happens first, downloading certificates and provisioning profiles&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Building&lt;/strong&gt; happens second, using the certificates that are now installed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--readonly&lt;/code&gt;&lt;/strong&gt; ensures we only download existing certificates, not create new ones&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes debugging easier - if signing fails, you know exactly where to look!&lt;/p&gt;
&lt;h2&gt;Setting up Fastlane Match&lt;/h2&gt;
&lt;p&gt;Before our workflow can sign apps in CI, we need to set up Fastlane Match. Match is a tool that stores your certificates and provisioning profiles in a secure Git repository, encrypted with a passphrase.&lt;/p&gt;
&lt;h3&gt;Why Fastlane Match?&lt;/h3&gt;
&lt;p&gt;Traditional code signing is a nightmare in CI environments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manually exporting certificates and profiles&lt;/li&gt;
&lt;li&gt;Managing expiration dates&lt;/li&gt;
&lt;li&gt;Keeping multiple machines in sync&lt;/li&gt;
&lt;li&gt;Security risks of storing certificates in CI systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Match solves all of this by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Storing everything in an encrypted Git repository&lt;/li&gt;
&lt;li&gt;✅ Automatically syncing certificates across team members and CI&lt;/li&gt;
&lt;li&gt;✅ Handling certificate renewal&lt;/li&gt;
&lt;li&gt;✅ Using a single source of truth for all code signing&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Initial Match Setup&lt;/h3&gt;
&lt;p&gt;First, add Match to your Fastfile if it&apos;s not already there:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &quot;Sync certificates and profiles&quot;
lane :certificates do
  match(
    type: &quot;appstore&quot;,
    readonly: false  # Allow creating new certificates during setup
  )
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Creating the certificates repository&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create a new private Git repository&lt;/strong&gt; for your certificates. This can be on GitHub, GitLab, or any other Git provider. Name it something like &lt;code&gt;MyApp-certificates&lt;/code&gt; or &lt;code&gt;ios-certificates&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Initialize Match&lt;/strong&gt; in your project directory:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;bundle exec fastlane match init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When prompted:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enter the URL of your certificates repository&lt;/li&gt;
&lt;li&gt;Choose a strong passphrase (you&apos;ll need this later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Generating certificates&lt;/h3&gt;
&lt;p&gt;Now let&apos;s generate the certificates and profiles:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Generate App Store certificates and profiles
bundle exec fastlane match appstore
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Match will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create or download certificates from Apple Developer Portal&lt;/li&gt;
&lt;li&gt;Create provisioning profiles for your app&lt;/li&gt;
&lt;li&gt;Encrypt everything with your passphrase&lt;/li&gt;
&lt;li&gt;Store it in your certificates repository&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Make sure your Apple Developer account has admin access, or this step will fail.&lt;/p&gt;
&lt;h3&gt;Verify it worked&lt;/h3&gt;
&lt;p&gt;You should see something like this in your certificates repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certs/
  distribution/
    [TEAM_ID].cer
    [TEAM_ID].p12
profiles/
  appstore/
    AppStore_[BUNDLE_ID].mobileprovision
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All files are encrypted and can only be decrypted with your passphrase.&lt;/p&gt;
&lt;h3&gt;Team setup&lt;/h3&gt;
&lt;p&gt;If you&apos;re working with a team, each team member should run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bundle exec fastlane match appstore --readonly
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This downloads and installs the certificates locally without creating new ones.&lt;/p&gt;
&lt;h2&gt;Testing your workflow&lt;/h2&gt;
&lt;p&gt;Once you&apos;ve committed and pushed these files to your repository, GitHub Actions will automatically pick them up and start running. You can see the status of your workflows by going to the &quot;Actions&quot; tab in your GitHub repository.&lt;/p&gt;
&lt;p&gt;If everything goes well, you should see a green checkmark ✅. If something goes wrong, you&apos;ll see a red X ❌, and you can click on it to see what went wrong.&lt;/p&gt;
&lt;h2&gt;Common issues and troubleshooting&lt;/h2&gt;
&lt;h3&gt;&quot;No Gemfile found&quot;&lt;/h3&gt;
&lt;p&gt;This means you don&apos;t have a Gemfile in your repository. Create one with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source &apos;https://rubygems.org&apos;

gem &apos;fastlane&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&quot;Could not find scheme &apos;YourAppScheme&apos;&quot;&lt;/h3&gt;
&lt;p&gt;Make sure the scheme name in your Fastfile matches exactly with the scheme name in your Xcode project. Scheme names are case-sensitive!&lt;/p&gt;
&lt;h3&gt;&quot;Error cloning certificates git repo&quot; / &quot;terminal prompts disabled&quot;&lt;/h3&gt;
&lt;p&gt;This is the error you&apos;ll see if your Match repository is private and you haven&apos;t set up SSH authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fatal: could not read Username for &apos;https://github.com&apos;: terminal prompts disabled
Error cloning certificates repo, please make sure you have read access to the repository
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Follow the &lt;a href=&quot;#setting-up-ssh-access-for-private-match-repository&quot;&gt;SSH access setup section&lt;/a&gt; above to configure SSH authentication for your private certificates repository.&lt;/p&gt;
&lt;h3&gt;&quot;Enter passphrase for /Users/runner/.ssh/match_key&quot;&lt;/h3&gt;
&lt;p&gt;If you see this error, it means your SSH key was generated with a passphrase:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Agent pid 2195
Enter passphrase for /Users/runner/.ssh/match_key: 
Error: Process completed with exit code 1.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Regenerate your SSH key &lt;strong&gt;without a passphrase&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519 -C &quot;github-actions-match&quot; -f ~/.ssh/github_actions_match -N &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-N &quot;&quot;&lt;/code&gt; flag is crucial - it creates a key without a passphrase, which is required for automated CI/CD environments.&lt;/p&gt;
&lt;h3&gt;&quot;Repository not found&quot; / &quot;Could not read from remote repository&quot;&lt;/h3&gt;
&lt;p&gt;If you see this error when Match tries to clone your certificates repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ERROR: Repository not found.
fatal: Could not read from remote repository.
Please make sure you have the correct access rights and the repository exists.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This usually means the SSH key isn&apos;t being used properly by Git. The updated workflow above uses an SSH config file which is more reliable than environment variables.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Double-check&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Repository exists&lt;/strong&gt;: Make sure &lt;code&gt;git@github.com:NoamEfergan/brewbuddy_certs&lt;/code&gt; actually exists&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy key is added&lt;/strong&gt;: Verify you added the public key to the repository&apos;s deploy keys&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write access enabled&lt;/strong&gt;: Ensure &quot;Allow write access&quot; is checked for the deploy key&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Private key is correct&lt;/strong&gt;: Verify the &lt;code&gt;MATCH_GIT_PRIVATE_KEY&lt;/code&gt; secret contains the complete private key&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&quot;Permission denied (publickey)&quot;&lt;/h3&gt;
&lt;p&gt;This means your SSH key isn&apos;t properly configured:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check your deploy key&lt;/strong&gt;: Make sure you added the public key to your certificates repository&apos;s deploy keys&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check write access&lt;/strong&gt;: Ensure &quot;Allow write access&quot; is checked for the deploy key&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check the private key secret&lt;/strong&gt;: Verify &lt;code&gt;MATCH_GIT_PRIVATE_KEY&lt;/code&gt; contains the complete private key (including &lt;code&gt;-----BEGIN&lt;/code&gt; and &lt;code&gt;-----END&lt;/code&gt; lines)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&quot;No value found for &apos;app_identifier&apos;&quot;&lt;/h3&gt;
&lt;p&gt;If you see this error after Match successfully clones and decrypts the certificates repo:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;🔓  Successfully decrypted certificates repo
[!] No value found for &apos;app_identifier&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Add your app&apos;s bundle identifier to the Match command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Set up code signing
  run: bundle exec fastlane match appstore --readonly --app_identifier com.yourcompany.yourapp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace &lt;code&gt;com.yourcompany.yourapp&lt;/code&gt; with your actual bundle identifier (the same one you used when setting up Match initially).&lt;/p&gt;
&lt;h3&gt;&quot;xcodebuild: error: Unable to find a destination matching the provided destination specifier&quot;&lt;/h3&gt;
&lt;p&gt;This usually means there&apos;s an issue with the simulator or device specification. For now, don&apos;t worry about this - we&apos;ll cover device selection in more detail in the next post.&lt;/p&gt;
&lt;h2&gt;Adding code signing secrets&lt;/h2&gt;
&lt;p&gt;Now that you have Fastlane Match set up, you need to add the secrets to your GitHub repository so the CI environment can access your certificates:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to your repository on GitHub&lt;/li&gt;
&lt;li&gt;Settings → Secrets and variables → Actions&lt;/li&gt;
&lt;li&gt;Add these repository secrets:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MATCH_REPOSITORY&lt;/code&gt;: The URL to your certificates repository (e.g., &lt;code&gt;https://github.com/yourteam/ios-certificates.git&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MATCH_PASSWORD&lt;/code&gt;: Your Match encryption passphrase&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MATCH_GIT_PRIVATE_KEY&lt;/code&gt;: SSH private key for accessing your private certificates repository&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Security Note&lt;/strong&gt;: These secrets are encrypted by GitHub and only accessible to your workflow. Never commit the repository URL, passphrase, or private keys to your code repository.&lt;/p&gt;
&lt;h3&gt;Setting up SSH access for private Match repository&lt;/h3&gt;
&lt;p&gt;Since your certificates repository is private (as it should be!), you need to set up SSH authentication:&lt;/p&gt;
&lt;h4&gt;1. Generate an SSH key pair&lt;/h4&gt;
&lt;p&gt;On your local machine, generate a new SSH key specifically for GitHub Actions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519 -C &quot;github-actions-match&quot; -f ~/.ssh/github_actions_match -N &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: The &lt;code&gt;-N &quot;&quot;&lt;/code&gt; flag creates the key &lt;strong&gt;without a passphrase&lt;/strong&gt;. This is necessary for automated CI/CD environments where there&apos;s no interactive terminal to enter passphrases.&lt;/p&gt;
&lt;p&gt;This creates two files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.ssh/github_actions_match&lt;/code&gt; (private key)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.ssh/github_actions_match.pub&lt;/code&gt; (public key)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. Add the public key to your certificates repository&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Go to your certificates repository on GitHub&lt;/li&gt;
&lt;li&gt;Settings → Deploy keys&lt;/li&gt;
&lt;li&gt;Click &quot;Add deploy key&quot;&lt;/li&gt;
&lt;li&gt;Title: &quot;GitHub Actions Match Access&quot;&lt;/li&gt;
&lt;li&gt;Key: Copy the content of &lt;code&gt;~/.ssh/github_actions_match.pub&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;✅ Check &quot;Allow write access&quot; (Match needs to update the repository)&lt;/li&gt;
&lt;li&gt;Click &quot;Add key&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3. Add the private key to your main repository secrets&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Go to your main app repository on GitHub&lt;/li&gt;
&lt;li&gt;Settings → Secrets and variables → Actions&lt;/li&gt;
&lt;li&gt;Add a new secret:
&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;MATCH_GIT_PRIVATE_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Value: Copy the entire content of &lt;code&gt;~/.ssh/github_actions_match&lt;/code&gt; (the private key file)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4. Update your Match repository URL&lt;/h4&gt;
&lt;p&gt;In your &lt;code&gt;Matchfile&lt;/code&gt;, use the SSH URL instead of HTTPS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git_url(&quot;git@github.com:yourteam/ios-certificates.git&quot;)
storage_mode(&quot;git&quot;)
type(&quot;development&quot;) # The default type, can be overridden
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or if you prefer to keep the HTTPS URL in your Matchfile, you can override it in the workflow (shown below).&lt;/p&gt;
&lt;h3&gt;Testing your setup&lt;/h3&gt;
&lt;p&gt;To verify everything is working, you can test locally first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This should download and install certificates without prompting
MATCH_PASSWORD=&quot;your_passphrase&quot; bundle exec fastlane match appstore --readonly
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this works locally, it will work in CI too!&lt;/p&gt;
&lt;h2&gt;Security considerations&lt;/h2&gt;
&lt;p&gt;Now that we&apos;re dealing with certificates and sensitive information, security becomes crucial. GitHub provides a feature called &quot;Secrets&quot; where you can store sensitive information encrypted. &lt;strong&gt;Never commit certificates, private keys, or passphrases directly to your repository&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We&apos;ll dive deeper into secure code signing practices in the upcoming posts, but the foundation is now in place!&lt;/p&gt;
&lt;h2&gt;What&apos;s next?&lt;/h2&gt;
&lt;p&gt;This is just the beginning! We&apos;ve set up a basic workflow that builds your app on every push. In the next post, we&apos;re going to extend this to &lt;a href=&quot;https://nowham.dev/posts/fastlane-unit-tests-ios/&quot;&gt;run your unit tests&lt;/a&gt; as well, and we&apos;ll start getting into some more advanced GitHub Actions features like caching and matrix builds.&lt;/p&gt;
&lt;p&gt;And after that? Well, we&apos;ll get to the good stuff - automatically triggering TestFlight builds when you tag your branch with &quot;testflight&quot;. But we need to walk before we can run!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Setting up GitHub Actions for iOS might seem daunting at first, but it&apos;s really just writing some YAML files and leveraging the Fastlane knowledge you already have. The beauty of this approach is that you&apos;re not learning an entirely new tool - you&apos;re just running your existing Fastlane scripts in the cloud.&lt;/p&gt;
&lt;p&gt;Give this a try, and let me know how it goes! And if you run into any issues, don&apos;t worry - we&apos;ve all been there. The GitHub Actions logs are usually pretty helpful in figuring out what went wrong.&lt;/p&gt;
&lt;p&gt;Next up: running your unit tests with GitHub Actions! 🚀&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Terminal Productivity for iOS Devs: Aliases, Scripts &amp; Tips</title><link>https://nowham.dev/posts/ios-terminal-automation/</link><guid isPermaLink="true">https://nowham.dev/posts/ios-terminal-automation/</guid><description>Supercharge your iOS development workflow with custom terminal aliases, functions, and automation scripts</description><pubDate>Tue, 03 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;As iOS developers, we spend a significant amount of time in the terminal - whether it&apos;s running builds, managing simulators, or navigating between projects. Yet most of us are still typing the same long commands over and over again. What if I told you that with just a few simple terminal customizations, you could cut your daily command typing by 80% and automate the most tedious parts of iOS development?&lt;/p&gt;
&lt;p&gt;In this post, I&apos;ll share my personal terminal setup that I&apos;ve refined over years of iOS development. These aren&apos;t just theoretical productivity hacks - these are the actual aliases, functions, and scripts I use every single day. By the end of this post, you&apos;ll have a toolkit that will make your fellow developers wonder how you work so fast.&lt;/p&gt;
&lt;p&gt;Let&apos;s dive in!&lt;/p&gt;
&lt;h2&gt;Essential iOS Development Aliases&lt;/h2&gt;
&lt;p&gt;Let&apos;s start with the basics - aliases that replace those long, hard-to-remember commands with short, memorable ones. Add these to your &lt;code&gt;~/.zshrc&lt;/code&gt; or &lt;code&gt;~/.bash_profile&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Xcode and iOS Development
alias xc=&apos;open *.xcworkspace || open *.xcodeproj&apos;
alias xcclean=&apos;rm -rf ~/Library/Developer/Xcode/DerivedData&apos;
alias xcdevices=&apos;xcrun simctl list devices&apos;
alias xcboot=&apos;xcrun simctl boot&apos;
alias xcshutdown=&apos;xcrun simctl shutdown all&apos;
alias xcreset=&apos;xcrun simctl erase all&apos;

# Build shortcuts
alias build=&apos;xcodebuild -workspace *.xcworkspace -scheme&apos;
alias buildclean=&apos;xcodebuild clean -workspace *.xcworkspace -scheme&apos;
alias archive=&apos;xcodebuild archive -workspace *.xcworkspace -scheme&apos;

# Fastlane shortcuts (if you use Fastlane)
alias fl=&apos;fastlane&apos;
alias fltest=&apos;fastlane test&apos;
alias flbuild=&apos;fastlane build&apos;

# CocoaPods shortcuts
alias pod=&apos;bundle exec pod&apos;
alias podup=&apos;bundle exec pod update&apos;
alias podclean=&apos;bundle exec pod deintegrate &amp;amp;&amp;amp; bundle exec pod install&apos;

# Git shortcuts for iOS
alias gitclean=&apos;git clean -xfd &amp;amp;&amp;amp; git reset --hard HEAD&apos;
alias gitignore=&apos;curl -o .gitignore https://www.toptal.com/developers/gitignore/api/swift,xcode,cocoapods&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These aliases alone will save you hundreds of keystrokes per day. But we&apos;re just getting started - this is where things get really interesting.&lt;/p&gt;
&lt;h2&gt;Powerful Terminal Functions&lt;/h2&gt;
&lt;p&gt;Aliases are great for simple commands, but functions let you create more sophisticated automation. Here are my favorite iOS development functions that I honestly can&apos;t live without:&lt;/p&gt;
&lt;h3&gt;Quick Project Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Create a new iOS project structure with common files
ios_project_setup() {
    if [ -z &quot;$1&quot; ]; then
        echo &quot;Usage: ios_project_setup &amp;lt;project_name&amp;gt;&quot;
        return 1
    fi
    
    mkdir &quot;$1&quot;
    cd &quot;$1&quot;
    
    # Create basic directory structure
    mkdir -p Scripts Documentation Assets
    
    # Download .gitignore
    curl -s -o .gitignore https://www.toptal.com/developers/gitignore/api/swift,xcode,cocoapods
    
    # Create README
    echo &quot;# $1&quot; &amp;gt; README.md
    echo &quot;&quot; &amp;gt;&amp;gt; README.md
    echo &quot;## Setup&quot; &amp;gt;&amp;gt; README.md
    echo &quot;&quot; &amp;gt;&amp;gt; README.md
    echo &quot;\`\`\`bash&quot; &amp;gt;&amp;gt; README.md
    echo &quot;bundle install&quot; &amp;gt;&amp;gt; README.md
    echo &quot;bundle exec pod install&quot; &amp;gt;&amp;gt; README.md
    echo &quot;\`\`\`&quot; &amp;gt;&amp;gt; README.md
    
    # Create Gemfile for Fastlane
    echo &quot;source &apos;https://rubygems.org&apos;&quot; &amp;gt; Gemfile
    echo &quot;&quot; &amp;gt;&amp;gt; Gemfile
    echo &quot;gem &apos;fastlane&apos;&quot; &amp;gt;&amp;gt; Gemfile
    echo &quot;gem &apos;cocoapods&apos;&quot; &amp;gt;&amp;gt; Gemfile
    
    echo &quot;✅ Project $1 setup complete!&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Smart Simulator Management&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Boot the most recent iOS simulator
boot_latest_sim() {
    local device_id=$(xcrun simctl list devices available | grep &quot;iPhone&quot; | tail -1 | grep -o &quot;([A-F0-9-]*)&quot; | tr -d &quot;()&quot;)
    if [ -n &quot;$device_id&quot; ]; then
        xcrun simctl boot &quot;$device_id&quot;
        echo &quot;✅ Booted simulator: $device_id&quot;
    else
        echo &quot;❌ No available iPhone simulators found&quot;
    fi
}

# Install app on all booted simulators
install_on_all_sims() {
    if [ -z &quot;$1&quot; ]; then
        echo &quot;Usage: install_on_all_sims &amp;lt;path_to_app&amp;gt;&quot;
        return 1
    fi
    
    local app_path=&quot;$1&quot;
    xcrun simctl list devices | grep &quot;Booted&quot; | while read -r line; do
        local device_id=$(echo &quot;$line&quot; | grep -o &quot;([A-F0-9-]*)&quot; | tr -d &quot;()&quot;)
        if [ -n &quot;$device_id&quot; ]; then
            xcrun simctl install &quot;$device_id&quot; &quot;$app_path&quot;
            echo &quot;✅ Installed on $device_id&quot;
        fi
    done
}

Let me break down what these functions do, because the simulator management commands can be a bit cryptic:

- `boot_latest_sim()`: This grabs the latest iPhone simulator from your available devices and boots it up. No more manually opening Simulator.app and clicking through menus!
- `install_on_all_sims()`: Ever wanted to test your app on multiple simulator sizes at once? This installs your .app bundle on every currently running simulator. Super handy for screenshot automation or testing across different screen sizes.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Build Log Analysis&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Parse Xcode build logs for errors and warnings
analyze_build_log() {
    if [ -z &quot;$1&quot; ]; then
        echo &quot;Usage: analyze_build_log &amp;lt;log_file&amp;gt;&quot;
        return 1
    fi
    
    echo &quot;🔍 Analyzing build log: $1&quot;
    echo &quot;&quot;
    
    echo &quot;❌ ERRORS:&quot;
    grep -n &quot;error:&quot; &quot;$1&quot; | head -10
    echo &quot;&quot;
    
    echo &quot;⚠️  WARNINGS:&quot;
    grep -n &quot;warning:&quot; &quot;$1&quot; | head -10
    echo &quot;&quot;
    
    echo &quot;📊 SUMMARY:&quot;
    echo &quot;Errors: $(grep -c &quot;error:&quot; &quot;$1&quot;)&quot;
    echo &quot;Warnings: $(grep -c &quot;warning:&quot; &quot;$1&quot;)&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Project Navigation Scripts&lt;/h2&gt;
&lt;p&gt;One thing that drives me crazy is navigating between multiple iOS projects. You know the drill - you&apos;re working on three different apps, each in a different folder, and you spend half your time just &lt;code&gt;cd&lt;/code&gt;-ing around trying to remember where everything is. Here&apos;s how I solved it:&lt;/p&gt;
&lt;h3&gt;Quick Project Switcher&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Add this to your shell config
export IOS_PROJECTS_DIR=&quot;$HOME/Developer/iOS&quot;

# Quick project navigation
ios_project() {
    if [ -z &quot;$1&quot; ]; then
        echo &quot;Available projects:&quot;
        ls -1 &quot;$IOS_PROJECTS_DIR&quot; | grep -v &quot;.DS_Store&quot;
        return 0
    fi
    
    local project_path=&quot;$IOS_PROJECTS_DIR/$1&quot;
    if [ -d &quot;$project_path&quot; ]; then
        cd &quot;$project_path&quot;
        # Automatically open in Xcode if requested
        if [ &quot;$2&quot; = &quot;open&quot; ]; then
            xc
        fi
    else
        echo &quot;❌ Project &apos;$1&apos; not found in $IOS_PROJECTS_DIR&quot;
    fi
}

# Add tab completion for projects
_ios_project() {
    local projects=($(ls -1 &quot;$IOS_PROJECTS_DIR&quot; 2&amp;gt;/dev/null | grep -v &quot;.DS_Store&quot;))
    compadd -a projects
}
compdef _ios_project ios_project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can type &lt;code&gt;ios_project MyApp&lt;/code&gt; to instantly navigate to your project, or &lt;code&gt;ios_project MyApp open&lt;/code&gt; to navigate and open in Xcode.&lt;/p&gt;
&lt;h2&gt;Advanced Automation Scripts&lt;/h2&gt;
&lt;h3&gt;Automated Testing Script with Notifications&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Run tests and send notification when complete
test_and_notify() {
    local scheme=&quot;${1:-$(basename $(pwd))}&quot;
    local start_time=$(date +%s)
    
    echo &quot;🧪 Running tests for scheme: $scheme&quot;
    
    if fastlane test scheme:&quot;$scheme&quot;; then
        local end_time=$(date +%s)
        local duration=$((end_time - start_time))
        osascript -e &quot;display notification \&quot;Tests passed in ${duration}s\&quot; with title \&quot;✅ $scheme\&quot;&quot;
        echo &quot;✅ Tests passed in ${duration}s&quot;
    else
        osascript -e &quot;display notification \&quot;Tests failed\&quot; with title \&quot;❌ $scheme\&quot;&quot;
        echo &quot;❌ Tests failed&quot;
        return 1
    fi
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This one is a game-changer. How many times have you started a test run, then switched to Slack or Twitter while waiting, only to completely forget about it? This function runs your tests and sends you a macOS notification when they&apos;re done. You can even see how long they took right in the notification. No more wondering if your tests are still running or if they finished 10 minutes ago!&lt;/p&gt;
&lt;h3&gt;Dependency Update Checker&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Check for outdated dependencies
check_dependencies() {
    echo &quot;🔍 Checking for outdated dependencies...&quot;
    echo &quot;&quot;
    
    if [ -f &quot;Podfile&quot; ]; then
        echo &quot;📦 CocoaPods:&quot;
        bundle exec pod outdated
        echo &quot;&quot;
    fi
    
    if [ -f &quot;Package.swift&quot; ]; then
        echo &quot;📦 Swift Package Manager:&quot;
        swift package show-dependencies --format json | jq -r &apos;.dependencies[] | &quot;\(.identity): \(.requirement)&quot;&apos;
        echo &quot;&quot;
    fi
    
    if [ -f &quot;Gemfile&quot; ]; then
        echo &quot;💎 Ruby Gems:&quot;
        bundle outdated
    fi
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setting Up Your Terminal for Maximum Productivity&lt;/h2&gt;
&lt;p&gt;To make the most of these scripts, here&apos;s how to set up your terminal:&lt;/p&gt;
&lt;h3&gt;1. Install Essential Tools&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Install Homebrew if you haven&apos;t already
/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;

# Install essential tools
brew install jq tree bat fzf ripgrep
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Add to Your Shell Config&lt;/h3&gt;
&lt;p&gt;Add all the aliases and functions above to your &lt;code&gt;~/.zshrc&lt;/code&gt; (if using zsh) or &lt;code&gt;~/.bash_profile&lt;/code&gt; (if using bash):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Edit your shell config
nano ~/.zshrc

# Reload your config
source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Create a Scripts Directory&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Create a dedicated directory for your iOS scripts
mkdir -p ~/Scripts/iOS
echo &apos;export PATH=&quot;$PATH:$HOME/Scripts/iOS&quot;&apos; &amp;gt;&amp;gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pro Tips for Terminal Efficiency&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;fzf&lt;/code&gt; for fuzzy finding&lt;/strong&gt;: Install &lt;code&gt;fzf&lt;/code&gt; to quickly search through your command history and file system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Set up tab completion&lt;/strong&gt;: The project switcher above includes tab completion - use this pattern for your other functions too.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create keyboard shortcuts&lt;/strong&gt;: Map frequently used commands to keyboard shortcuts in your terminal app.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;bat&lt;/code&gt; instead of &lt;code&gt;cat&lt;/code&gt;&lt;/strong&gt;: It provides syntax highlighting and line numbers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Leverage &lt;code&gt;tree&lt;/code&gt; for project structure&lt;/strong&gt;: Quickly visualize your project structure with &lt;code&gt;tree -I &apos;Pods|DerivedData&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Bonus: Supercharge Your Terminal with Powerlevel10k&lt;/h2&gt;
&lt;p&gt;Now that we&apos;ve covered all the functional aspects of terminal productivity, let&apos;s talk about making your terminal not just powerful, but beautiful and informative too. I use &lt;strong&gt;Powerlevel10k&lt;/strong&gt; (p10k), which is hands down the best Zsh theme I&apos;ve ever used.&lt;/p&gt;
&lt;p&gt;Seriously, I&apos;ve tried probably a dozen different themes over the years, and nothing comes close to p10k. It&apos;s fast, it&apos;s gorgeous, and it shows you exactly the information you need without being cluttered.&lt;/p&gt;
&lt;h3&gt;What is Powerlevel10k?&lt;/h3&gt;
&lt;p&gt;Powerlevel10k is a theme for Zsh that provides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lightning fast&lt;/strong&gt; prompt rendering (100x faster than other themes)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rich information&lt;/strong&gt; at a glance - Git status, current directory, execution time, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Highly customizable&lt;/strong&gt; - configure it exactly how you want&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iOS-specific info&lt;/strong&gt; - Show Swift version, Xcode version, and more&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Installation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Install Powerlevel10k
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/powerlevel10k
echo &apos;source ~/powerlevel10k/powerlevel10k.zsh-theme&apos; &amp;gt;&amp;gt;~/.zshrc

# Restart your terminal and run the configuration wizard
p10k configure
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;My iOS Developer Configuration&lt;/h3&gt;
&lt;p&gt;Here&apos;s how I configure p10k for maximum iOS development productivity:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Add these to your ~/.p10k.zsh file after running p10k configure

# Show Swift version when in Swift projects
typeset -g POWERLEVEL9K_SWIFT_SHOW_ON_COMMAND=&apos;swift|xcodebuild|fastlane&apos;

# Show Ruby version (for Fastlane/CocoaPods)
typeset -g POWERLEVEL9K_RBENV_SHOW_ON_COMMAND=&apos;bundle|pod|fastlane|gem&apos;

# Show Node version (for React Native or web stuff)
typeset -g POWERLEVEL9K_NODE_VERSION_SHOW_ON_COMMAND=&apos;node|npm|yarn|react-native&apos;

# Customize the prompt elements (left side)
typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(
  dir                     # Current directory
  vcs                     # Git status
  swift_version           # Swift version
  rbenv                   # Ruby version (for Fastlane)
  command_execution_time  # How long the last command took
  newline                 # Line break
  prompt_char             # The prompt character
)

# Customize the right side
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(
  status                  # Exit code of last command
  background_jobs         # Background jobs indicator
  time                    # Current time
)

# Show execution time for commands longer than 3 seconds
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3

# Git status customization
typeset -g POWERLEVEL9K_VCS_CLEAN_FOREGROUND=76
typeset -g POWERLEVEL9K_VCS_MODIFIED_FOREGROUND=178
typeset -g POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND=014
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why This Matters for iOS Development&lt;/h3&gt;
&lt;p&gt;With p10k configured like this, your terminal instantly shows you:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Git status&lt;/strong&gt; - See if you have uncommitted changes at a glance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swift version&lt;/strong&gt; - Know which Swift version you&apos;re using when switching between projects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ruby version&lt;/strong&gt; - Important for Fastlane and CocoaPods compatibility&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Command execution time&lt;/strong&gt; - See how long your builds or tests took&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Current directory&lt;/strong&gt; - Never get lost in your project structure&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Pro p10k Tips&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Instant prompt&lt;/strong&gt;: p10k has an &quot;instant prompt&quot; feature that shows your prompt immediately, even before zsh fully loads. Enable it with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose&apos; &amp;gt;&amp;gt;~/.p10k.zsh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Transient prompt&lt;/strong&gt;: Makes your command history cleaner by simplifying past prompts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always&apos; &amp;gt;&amp;gt;~/.p10k.zsh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Custom segments&lt;/strong&gt;: You can create custom segments. Here&apos;s one that shows if you&apos;re in an iOS project:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Add to ~/.p10k.zsh
function prompt_ios_project() {
  if [[ -n *.xcworkspace(#qN) || -n *.xcodeproj(#qN) ]]; then
    p10k segment -f 208 -t &quot;📱&quot;
  fi
}

# Add ios_project to your POWERLEVEL9K_LEFT_PROMPT_ELEMENTS
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The result is a terminal that&apos;s not just functional, but gives you crucial information at a glance. When you&apos;re juggling multiple iOS projects with different Swift versions, Ruby versions, and Git states, this visual feedback is invaluable.&lt;/p&gt;
&lt;p&gt;Trust me, once you get used to seeing all this information in your prompt, you&apos;ll never want to go back to a boring terminal again. It&apos;s like having a dashboard for your development environment!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;These terminal customizations have literally saved me hours every week. I&apos;m not exaggerating - when you add up all the time saved from not typing long commands, not having to remember complex xcodebuild syntax, and getting instant visual feedback about your project state, it really adds up.&lt;/p&gt;
&lt;p&gt;The beauty is that you can start small - pick a few aliases that solve your biggest pain points, then gradually add more as you discover new needs. I didn&apos;t build this setup overnight; it evolved over years of &quot;ugh, I&apos;m tired of typing this same command again.&quot;&lt;/p&gt;
&lt;p&gt;The key is to pay attention to the commands you type repeatedly and ask yourself: &quot;Could I automate this?&quot; More often than not, the answer is yes, and it only takes a few minutes to set up.&lt;/p&gt;
&lt;p&gt;Start with the aliases and basic functions, then gradually build up your toolkit. Your future self (and your wrists) will thank you!&lt;/p&gt;
&lt;p&gt;What terminal shortcuts do you swear by? I&apos;d love to hear about your productivity hacks! Hit me up on &lt;a href=&quot;https://twitter.com/No_Wham&quot;&gt;Twitter&lt;/a&gt; and let me know what I should cover next.&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Use Git Hooks in iOS Projects (Without CI)</title><link>https://nowham.dev/posts/ios-git-hooks/</link><guid isPermaLink="true">https://nowham.dev/posts/ios-git-hooks/</guid><description>Make your life easier without a CI</description><pubDate>Wed, 07 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In this blog, we talk a lot about Continuous Integration (CI) and how it can help you to automate your development process. But what if you don&apos;t want to set up a CI server? What if you just want to run some scripts before you commit your code? That&apos;s where Git hooks come in. Git hooks are scripts that run automatically at certain points in the Git workflow. They can be used to enforce coding standards, run tests, or even send notifications. In this blog post, I&apos;ll show you my favorite Git hooks that I use in my iOS projects. You can use them to make your life easier without a CI server. Let&apos;s get started!&lt;/p&gt;
&lt;h3&gt;Pre-commit Hook&lt;/h3&gt;
&lt;p&gt;The pre-commit hook is run before a commit is made. This is a great place to run tests or check for coding standards.&lt;br /&gt;
I use mine to run &lt;code&gt;SwiftLint&lt;/code&gt; and &lt;code&gt;SwiftFormat&lt;/code&gt; to check for coding standards and format my code. Here&apos;s how I set it up:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
# Pre-commit hook to run SwiftLint and SwiftFormat
# Check if SwiftLint is installed, if it&apos;s not - install it.
if ! command -v swiftlint &amp;amp;&amp;gt; /dev/null
then
    echo &quot;SwiftLint could not be found, installing...&quot;
    brew install swiftlint
fi
# Check if SwiftFormat is installed, if it&apos;s not - install it.
if ! command -v swiftformat &amp;amp;&amp;gt; /dev/null
then
    echo &quot;SwiftFormat could not be found, installing...&quot;
    brew install swiftformat
fi
# Run SwiftFormat first, as it might fix the linting issues
swiftformat .
# Run SwiftLint
swiftlint
# Check the exit code of SwiftLint
if [ $? -ne 0 ]; then
    echo &quot;SwiftLint failed, please fix the issues before committing.&quot;
    exit 1
fi
echo &quot;SwiftLint passed, ready to commit!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s break it down a bit, as you might not be familiar with all the commands or with bash scripts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;command -v swiftlint &amp;amp;&amp;gt; /dev/null&lt;/code&gt;: This checks if &lt;code&gt;swiftlint&lt;/code&gt; is installed. If it&apos;s not, it installs it using Homebrew.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;swiftformat .&lt;/code&gt;: This runs &lt;code&gt;SwiftFormat&lt;/code&gt; on the current directory. It will format your code according to the rules you have set up.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;swiftlint&lt;/code&gt;: This runs &lt;code&gt;SwiftLint&lt;/code&gt; on your code. If there are any issues, it will return a non-zero exit code.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if [ $? -ne 0 ]; then ...&lt;/code&gt;: This checks the exit code of the last command. If it&apos;s not zero, it means there were issues with &lt;code&gt;SwiftLint&lt;/code&gt;, and we exit with a non-zero code to prevent the commit.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo &quot;SwiftLint passed, ready to commit!&quot;&lt;/code&gt;: If everything went well, we print a success message.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Pre-push Hook&lt;/h3&gt;
&lt;p&gt;The pre-push hook is run before you push your code to the remote repository. This is a great place to run tests or do some heavier things that you don&apos;t want to run on every commit but still want checked, such as unit tests or integration tests.&lt;br /&gt;
I use mine to run my unit tests before I push my code, using &lt;a href=&quot;./fastlane-unit-tests-ios.md&quot;&gt;Fastlane&lt;/a&gt;. Here&apos;s how I set it up:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
# Pre-push hook to run unit tests
# Check if Fastlane is installed, if it&apos;s not - install it.
if ! command -v fastlane &amp;amp;&amp;gt; /dev/null
then
    echo &quot;Fastlane could not be found, installing...&quot;
    gem install fastlane
fi
# Run Fastlane
fastlane test
# Check the exit code of Fastlane
if [ $? -ne 0 ]; then
    echo &quot;Unit tests failed, please fix the issues before pushing.&quot;
    exit 1
fi
echo &quot;Unit tests passed, ready to push!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again, let&apos;s break it down a bit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;command -v fastlane &amp;amp;&amp;gt; /dev/null&lt;/code&gt;: This checks if &lt;code&gt;fastlane&lt;/code&gt; is installed. If it&apos;s not, it installs it using RubyGems.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fastlane test&lt;/code&gt;: This runs the &lt;code&gt;test&lt;/code&gt; lane in your Fastlane setup. You can customize this to run any tests you want, or any lane that you&apos;d like.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Adding the Hooks to Your Project&lt;/h2&gt;
&lt;p&gt;To add the hooks to your project, you need to create a directory called &lt;code&gt;hooks&lt;/code&gt; in your &lt;code&gt;.git&lt;/code&gt; directory. Inside this directory, create two files: &lt;code&gt;pre-commit&lt;/code&gt; and &lt;code&gt;pre-push&lt;/code&gt;. Make sure to make them executable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x .git/hooks/pre-commit
chmod +x .git/hooks/pre-push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, every time you commit or push your code, the hooks will run automatically. You can customize them to fit your needs.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;These might seem like small things, but they can save you a lot of time and headaches in the long run. By using Git hooks, you can automate your workflow and make sure that your code is always in a good state before you commit or push it. I hope you found this blog post helpful! If you have any questions or suggestions, feel free to reach out to me on &lt;a href=&quot;https://twitter.com/No_Wham&quot;&gt;Twitter&lt;/a&gt; or leave a comment below.&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Parallelize iOS Unit Tests with Fastlane or CLI</title><link>https://nowham.dev/posts/ios-parallel-unit-tests/</link><guid isPermaLink="true">https://nowham.dev/posts/ios-parallel-unit-tests/</guid><description>Use Xcode CLI and Bash or Fastlane to run unit tests in parallel in your iOS app.</description><pubDate>Thu, 24 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In this post, I&apos;ll show you two ways to run unit tests in your iOS app: using the Xcode CLI with Bash and using Fastlane. Both approaches have their pros and cons, and you can choose the one that best fits your workflow. Whether you&apos;re working in a CI/CD environment or running tests locally, these methods will help you automate your testing process.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;For both methods, you&apos;ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Xcode installed&lt;/li&gt;
&lt;li&gt;A working iOS app with unit tests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the Fastlane method, you&apos;ll also need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://fastlane.tools/&quot;&gt;Fastlane&lt;/a&gt; installed&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Method 1: Using Xcode CLI and Bash&lt;/h2&gt;
&lt;p&gt;This method uses the Xcode CLI (&lt;code&gt;xcodebuild&lt;/code&gt;) and a Bash script to run your tests. It&apos;s lightweight and doesn&apos;t require any third-party tools.&lt;/p&gt;
&lt;h3&gt;Script&lt;/h3&gt;
&lt;p&gt;Here&apos;s the script to run your unit tests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

set -u  # Treat unset variables as errors

# Configurable variables
SCHEME=&quot;YourSchemeName&quot;
DESTINATION=&quot;platform=iOS Simulator,name=iPhone 16 Pro,OS=latest&quot;
PROJECT=&quot;YourProject.xcodeproj&quot;

# Run tests and capture exit code
set -o pipefail
xcodebuild \
-scheme &quot;$SCHEME&quot; \
-project &quot;$PROJECT&quot; \
-destination &quot;$DESTINATION&quot; \
-parallel-testing-enabled YES \
-parallel-testing-worker-count 4 \
clean test | tee xcodebuild.log | xcpretty

EXIT_CODE=${PIPESTATUS[0]}

if [ &quot;$EXIT_CODE&quot; -ne 0 ]; then
    echo &quot;❌ Unit tests failed!&quot;
    exit &quot;$EXIT_CODE&quot;
else
    echo &quot;✅ All unit tests passed!&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Running the Script&lt;/h3&gt;
&lt;p&gt;Save the script to a file (e.g., &lt;code&gt;run_tests.sh&lt;/code&gt;), make it executable, and run it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x run_tests.sh
./run_tests.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will run your tests and display the results in the terminal.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Method 2: Using Fastlane&lt;/h2&gt;
&lt;p&gt;Fastlane is a popular tool for automating iOS and Android development tasks. It provides a simple way to run tests and integrates well with CI/CD pipelines.&lt;/p&gt;
&lt;h3&gt;Setting Up Fastlane&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install Fastlane:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install fastlane
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Navigate to your project directory and initialize Fastlane:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fastlane init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Follow the prompts to set up Fastlane for your project.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Adding a Test Lane&lt;/h3&gt;
&lt;p&gt;Edit your &lt;code&gt;Fastfile&lt;/code&gt; to add a lane for running tests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;default_platform(:ios)

platform :ios do
  desc &quot;Run unit tests&quot;
  lane :run_tests do
    scan(
      scheme: &quot;YourSchemeName&quot;,
      devices: [&quot;iPhone 16 Pro&quot;],
      clean: true,
      parallel_testing: true,
      concurrent_workers: 4,
      output_directory: &quot;./fastlane/test_output&quot;,
      output_types: &quot;html,junit&quot;
    )
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Running the Test Lane&lt;/h3&gt;
&lt;p&gt;Run the test lane using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fastlane run_tests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fastlane will execute your tests and generate a report in the &lt;code&gt;./fastlane/test_output&lt;/code&gt; directory.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Comparison: Xcode CLI vs. Fastlane&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Xcode CLI + Bash&lt;/th&gt;
&lt;th&gt;Fastlane&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;td&gt;Requires Fastlane setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ease of Use&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires scripting&lt;/td&gt;
&lt;td&gt;Simple, declarative syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Customizable with &lt;code&gt;xcpretty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Built-in reports (HTML, JUnit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CI/CD Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Seamless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Both methods are effective for running unit tests in your iOS app. If you prefer a lightweight, script-based approach, the Xcode CLI with Bash is a great choice. If you want a more feature-rich solution with built-in reporting and CI/CD integration, Fastlane is the way to go.&lt;/p&gt;
&lt;p&gt;Try both methods and see which one works best for your workflow. Let me know if you have any questions or need help setting up either approach. Happy testing!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Run iOS Unit Tests on CI Without Fastlane</title><link>https://nowham.dev/posts/ios-ci-tests-no-fastlane/</link><guid isPermaLink="true">https://nowham.dev/posts/ios-ci-tests-no-fastlane/</guid><description>Use Xcode CLI and Bash to run unit tests in your iOS app.</description><pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In this post, I&apos;m going to show you how to run unit tests in your iOS app using the Xcode CLI and Bash. This is a great way to run your tests without having to rely on Fastlane or any other third-party tools. It&apos;s also a good way to get familiar with the Xcode CLI and how it works.&lt;br /&gt;
You&apos;d want to do this when you have a CI provider that doesn&apos;t have Ruby for some reason, can&apos;t install Fastlane, or just because you want to learn how to do it yourself.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Xcode installed&lt;/li&gt;
&lt;li&gt;Xcode CLI tools installed (if you&apos;ve got Xcode, you&apos;ve got these)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/xcpretty/xcpretty&quot;&gt;xcpretty&lt;/a&gt; installed (optional, but recommended for pretty output)&lt;/li&gt;
&lt;li&gt;A working iOS app with unit tests&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;You need to know the names of your project and schemes. You can find this out by opening your project in Xcode and looking at the top-left corner of the window. The project name is the name of the file with the &lt;code&gt;.xcodeproj&lt;/code&gt; extension, and the scheme is the name of the scheme you want to run tests for. For this example, we&apos;ll use &lt;code&gt;MyApp.xcodeproj&lt;/code&gt; and &lt;code&gt;MyAppTests&lt;/code&gt; as the scheme name.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

set -u  # Only treat unset variables as errors

# Configurable variables
SCHEME=&quot;YourSchemeName&quot;
DESTINATION=&quot;platform=iOS Simulator,name=iPhone 16 Pro,OS=latest&quot;
PROJECT=&quot;YourProject.xcodeproj&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Run tests&lt;/h2&gt;
&lt;p&gt;We&apos;re going to use &lt;code&gt;xcodebuild&lt;/code&gt; to run the tests. This is the command that Xcode uses under the hood to build and run your app. The command looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

set -u  # Only treat unset variables as errors

# Configurable variables
SCHEME=&quot;YourSchemeName&quot;
DESTINATION=&quot;platform=iOS Simulator,name=iPhone 16 Pro,OS=latest&quot;
PROJECT=&quot;YourProject.xcodeproj&quot;

# Run tests and capture exit code
set -o pipefail
xcodebuild test \
-scheme &quot;$SCHEME&quot; \
-project &quot;$PROJECT&quot; \
-destination &quot;$DESTINATION&quot; \
clean test | tee xcodebuild.log | xcpretty

EXIT_CODE=${PIPESTATUS[0]}

if [ &quot;$EXIT_CODE&quot; -ne 0 ]; then
    echo &quot;❌ Unit tests failed!&quot;
    exit &quot;$EXIT_CODE&quot;
else
    echo &quot;✅ All unit tests passed!&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Explanation&lt;/h2&gt;
&lt;p&gt;Ok, let&apos;s break this down a bit, as it can be quite scary to look at, especially if you&apos;re not used to the command line.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;set -u&lt;/code&gt;: This tells Bash to treat unset variables as errors. This is a good practice to avoid bugs in your scripts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SCHEME&lt;/code&gt;, &lt;code&gt;DESTINATION&lt;/code&gt;, and &lt;code&gt;PROJECT&lt;/code&gt;: These are the variables that you can configure to match your project. You can change these to match your project name, scheme name, and destination.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set -o pipefail&lt;/code&gt;: This tells Bash to return the exit code of the last command in the pipeline that failed. This is useful for capturing the exit code of &lt;code&gt;xcodebuild&lt;/code&gt; and checking if the tests passed or failed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xcodebuild test&lt;/code&gt;: This is the command that runs the tests. It takes the scheme, project, and destination as arguments.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;| tee xcodebuild.log&lt;/code&gt;: This pipes the output of &lt;code&gt;xcodebuild&lt;/code&gt; to a file called &lt;code&gt;xcodebuild.log&lt;/code&gt; and also prints it to the console. This is useful for debugging and checking the output of the tests.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;| xcpretty&lt;/code&gt;: This pipes the output of &lt;code&gt;xcodebuild&lt;/code&gt; to &lt;code&gt;xcpretty&lt;/code&gt;, which formats the output to be more readable. This is optional, but recommended for better output.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EXIT_CODE=${PIPESTATUS[0]}&lt;/code&gt;: This captures the exit code of the &lt;code&gt;xcodebuild&lt;/code&gt; command. If the tests fail, this will be a non-zero value.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if [ &quot;$EXIT_CODE&quot; -ne 0 ]; then&lt;/code&gt;: This checks if the exit code is not equal to 0. If it is not, it means the tests failed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo &quot;❌ Unit tests failed!&quot;&lt;/code&gt;: This prints a message to the console if the tests failed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exit &quot;$EXIT_CODE&quot;&lt;/code&gt;: This exits the script with the exit code of &lt;code&gt;xcodebuild&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;else&lt;/code&gt;: This is the else statement that runs if the tests pass.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo &quot;✅ All unit tests passed!&quot;&lt;/code&gt;: This prints a message to the console if the tests passed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fi&lt;/code&gt;: This closes the if statement.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Running the script&lt;/h2&gt;
&lt;p&gt;To run the script, save it to a file called &lt;code&gt;run_tests.sh&lt;/code&gt; and make it executable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x run_tests.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, run the script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./run_tests.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see the output of the tests in the console, and if the tests pass, you&apos;ll see a message saying &quot;✅ All unit tests passed!&quot;. If the tests fail, you&apos;ll see a message saying &quot;❌ Unit tests failed!&quot; and the script will exit with a non-zero exit code.&lt;/p&gt;
&lt;h2&gt;Building on top of this&lt;/h2&gt;
&lt;p&gt;This script is the bare minimum to run unit tests in your iOS app. You can build on top of this by adding more features, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Running UI tests&lt;/li&gt;
&lt;li&gt;Running tests on a specific simulator&lt;/li&gt;
&lt;li&gt;Running tests on a specific device&lt;/li&gt;
&lt;li&gt;Running tests on a specific OS version&lt;/li&gt;
&lt;li&gt;Running tests in parallel&lt;/li&gt;
&lt;li&gt;Running tests on multiple simulators&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And much more! Bash is incredibly flexible, and &lt;code&gt;xcodebuild&lt;/code&gt; is a powerful tool that can do a lot more than just run tests. You can use it to build your app, archive it, export it, and much more. You can find more information about &lt;code&gt;xcodebuild&lt;/code&gt; in the &lt;a href=&quot;https://developer.apple.com/library/archive/technotes/tn2339/_index.html&quot;&gt;Xcode documentation&lt;/a&gt;. It&apos;s quite old, but it still has a lot of useful information.&lt;/p&gt;
&lt;p&gt;For a list of all the available options for &lt;code&gt;xcodebuild&lt;/code&gt;, you can have a look &lt;a href=&quot;https://fig.io/manual/xcodebuild&quot;&gt;here&lt;/a&gt;, and you can always run &lt;code&gt;xcodebuild -help&lt;/code&gt; in the terminal to see a list of all the available options.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this post, we&apos;ve shown you how to run unit tests in your iOS app using the Xcode CLI and Bash. This is a great way to run your tests without having to rely on Fastlane or any other third-party tools. It&apos;s also a good way to get familiar with the Xcode CLI and how it works. Let me know if you have any questions or if you want to see more examples of how to use the Xcode CLI and Bash to run tests in your iOS app. Happy testing!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Automatically Bump Version &amp; Build Number with Fastlane</title><link>https://nowham.dev/posts/fastlane-version-bump-ios/</link><guid isPermaLink="true">https://nowham.dev/posts/fastlane-version-bump-ios/</guid><description>Never forget to update those ever again!</description><pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;We&apos;re nearing the end of our &quot;Fastlane from zero to hero&quot;, and in this post we&apos;re going to automate just a little bit more of the housekeeping work it takes to release an app to the app store.&lt;br /&gt;
Increasing the version number and build number is something that we all have to do, but it&apos;s also something that we all forget to do. So let&apos;s automate it!&lt;/p&gt;
&lt;h2&gt;Pre-requisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;You should have a working Fastlane setup. If you don&apos;t, check out the previous posts in this series.&lt;/li&gt;
&lt;li&gt;I&apos;m going to go off of the assumption that you&apos;ve got an &lt;a href=&quot;https://docs.fastlane.tools/app-store-connect-api/&quot;&gt;API key&lt;/a&gt;, as a JSON file in your project. If this is a subject you&apos;re not familiar with, let me know, and I can write a post about it!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Build numbers&lt;/h2&gt;
&lt;p&gt;The first thing that we&apos;re going to do is compare the current build number with the one that we have in our project. This is important because we don&apos;t want to bump the build number if it&apos;s already the same as the one in the project.&lt;br /&gt;
We&apos;re going to be getting the latest build numbers from 3 different sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The latest build number from TestFlight&lt;/li&gt;
&lt;li&gt;The latest build number from the App Store&lt;/li&gt;
&lt;li&gt;The latest build number from the local project&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Getting the latest build number from TestFlight&lt;/h3&gt;
&lt;p&gt;To get the latest build number from TestFlight, we&apos;re going to use the &lt;code&gt;latest_testflight_build_number&lt;/code&gt; action. This action will return the latest build number for the app in TestFlight. It&apos;s a built-in action from Fastlane, so we don&apos;t need to worry about installing any additional gems, or writing the complex logic ourselves.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &apos;Compare the local build number to the latest test flight on ASC&apos;
lane :compare_build_numbers do
  test_flight_version = latest_testflight_build_number(
    app_identifier: &apos;com.example.app&apos;,
    api_key_path: &apos;iTunesConnectKey.json&apos;
  ).to_i
end
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;This is assuming that you have a JSON file with your API key in the root of your project. If you don&apos;t, you can use the &lt;code&gt;api_key&lt;/code&gt; parameter to pass in the key directly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Getting the latest build number from the App Store&lt;/h3&gt;
&lt;p&gt;To get the version from App Store, we&apos;re going to use the same approach - using the built-in Fastlane actions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &apos;Compare the local build number to the latest test flight on ASC&apos;
lane :compare_build_numbers do
  test_flight_version = latest_testflight_build_number(
    app_identifier: &apos;com.example.app&apos;,
    api_key_path: &apos;iTunesConnectKey.json&apos;
  ).to_i

  live_app_version = app_store_build_number(
    app_identifier: &apos;com.example.app&apos;,
    api_key_path: &apos;iTunesConnectKey.json&apos;
  ).to_i
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Getting the latest build number from the local project&lt;/h3&gt;
&lt;p&gt;Can you guess what we&apos;re going to be using? That&apos;s right - a built-in Fastlane action!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &apos;Compare the local build number to the latest test flight on ASC&apos;
lane :compare_build_numbers do
  test_flight_version = latest_testflight_build_number(
    app_identifier: &apos;com.example.app&apos;,
    api_key_path: &apos;iTunesConnectKey.json&apos;
  ).to_i

  live_app_version = app_store_build_number(
    app_identifier: &apos;com.example.app&apos;,
    api_key_path: &apos;iTunesConnectKey.json&apos;
  ).to_i

  local_build_number = get_build_number.to_i
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Comparing and bumping&lt;/h3&gt;
&lt;p&gt;Now that we&apos;ve got all 3, we only really need 2; the local version, and which ever one of the other build numbers is highest.&lt;br /&gt;
So we can write a simple lane to compare the two, and bump the local version if it&apos;s lower than either of the other two.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lane :bump_build_number_if_needed do |options|
  local_build_number = options[:local_build_number]
  remote_build_number = options[:remote_build_number]

  if remote_build_number.to_i &amp;gt; local_build_number.to_i
    new_build_number = remote_build_number.to_i + 1
    increment_build_number(build_number: new_build_number)
  elsif remote_build_number.to_i == local_build_number.to_i
    increment_build_number
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This might look a bit scary, but let&apos;s break it down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We take in the local and remote build numbers as parameters.&lt;/li&gt;
&lt;li&gt;We compare the two, and if the remote build number is greater than the local build number, we increment the remote build number by 1, and set that as our build number.&lt;/li&gt;
&lt;li&gt;If the remote build number is equal to the local build number, we just increment the local build number by 1.&lt;/li&gt;
&lt;li&gt;If the remote build number is less than the local build number, we don&apos;t do anything ( So we don&apos;t bump when we don&apos;t need to!).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Putting it all together&lt;/h2&gt;
&lt;p&gt;Now that we&apos;ve got all the pieces, we can put them together in a lane.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &apos;Compare the local build number to the latest test flight on ASC&apos;
lane :compare_build_numbers do
  test_flight_version = latest_testflight_build_number(
    app_identifier: &apos;com.example.app&apos;,
    api_key_path: &apos;iTunesConnectKey.json&apos;
  ).to_i

  live_app_version = app_store_build_number(
    app_identifier: &apos;com.example.app&apos;,
    api_key_path: &apos;iTunesConnectKey.json&apos;
  ).to_i

  local_build_number = get_build_number.to_i

  bump_build_number_if_needed(
    local_build_number: local_build_number,
    remote_build_number: test_flight_version &amp;gt; live_app_version ? test_flight_version : live_app_version
  )
end

lane :bump_build_number_if_needed do |options|
  local_build_number = options[:local_build_number]
  remote_build_number = options[:remote_build_number]

  if remote_build_number.to_i &amp;gt; local_build_number.to_i
    new_build_number = remote_build_number.to_i + 1
    increment_build_number(build_number: new_build_number)
  elsif remote_build_number.to_i == local_build_number.to_i
    increment_build_number
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&apos;s it! Now we can run the &lt;code&gt;compare_build_numbers&lt;/code&gt; lane, and it will automatically bump the build number if it&apos;s lower than the latest build number on TestFlight or the App Store.&lt;br /&gt;
This is a great way to make sure that we never forget to bump the build number again, and it saves us a lot of time in the long run. Now let&apos;s do the same, but for your version number.&lt;/p&gt;
&lt;h2&gt;Version numbers&lt;/h2&gt;
&lt;p&gt;The version number is the same approach, but is even a little bit easier, because we can use the same built in Fastlane action to get both the App Store version &lt;em&gt;and&lt;/em&gt; the TestFlight version.&lt;/p&gt;
&lt;h3&gt;Getting remote versions&lt;/h3&gt;
&lt;p&gt;First thing we need is the remote version, which we can get using the &lt;code&gt;app_store_build_number&lt;/code&gt; action, which returns the version in it&apos;s &lt;code&gt;SharedValues&lt;/code&gt;. Let&apos;s see how that looks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &apos;Compare the local version number to the live and TF ones, and bumps it if needed.&apos;
lane :compare_version_numbers do
  app_store_build_number(
    live: true,
    app_identifier: &apos;com.example.app&apos;,
    api_key_path: &apos;iTunesConnectKey.json&apos;
  )
  live_version_number = lane_context[SharedValues::LATEST_VERSION]

  app_store_build_number(
    live: false,
    app_identifier: &apos;com.example.app&apos;,
    api_key_path: &apos;iTunesConnectKey.json&apos;
  )
  tf_version_number = lane_context[SharedValues::LATEST_VERSION]
  tf = Gem::Version.new(tf_version_number)
  live = Gem::Version.new(live_version_number)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;This is assuming that you have a JSON file with your API key in the root of your project. If you don&apos;t, you can use the &lt;code&gt;api_key&lt;/code&gt; parameter to pass in the key directly.&lt;br /&gt;
Just to break it down a little bit:&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;We call the &lt;code&gt;app_store_build_number&lt;/code&gt; action with the &lt;code&gt;live&lt;/code&gt; parameter set to &lt;code&gt;true&lt;/code&gt;, which will return the live version number.&lt;/li&gt;
&lt;li&gt;We call the &lt;code&gt;app_store_build_number&lt;/code&gt; action with the &lt;code&gt;live&lt;/code&gt; parameter set to &lt;code&gt;false&lt;/code&gt;, which will return the TestFlight version number.&lt;/li&gt;
&lt;li&gt;We convert the version numbers to &lt;code&gt;Gem::Version&lt;/code&gt; objects, which allows us to compare them easily.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Getting the local version&lt;/h3&gt;
&lt;p&gt;To get the local version, we can use the &lt;code&gt;get_version_number&lt;/code&gt; action, which will return the version number from the &lt;code&gt;Info.plist&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lane :project_version_number do
  version_number = get_version_number(
    xcodeproj: &quot;some_project.xcodeproj&quot;,
    target: &apos;SomeTarget&apos;
  )
  version_number
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Comparing and bumping&lt;/h3&gt;
&lt;p&gt;So just like before, now we&apos;ve got 3 versions, but we really only need 2. so we can write a lane that expects 2 versions, compare them, and bump them if we need to.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lane :bump_version_number_if_needed do |options|
  local_version_str = options[:local].to_s
  remote_version_str = options[:remote].to_s
  local = Gem::Version.new(local_version_str)
  remote = Gem::Version.new(remote_version_str)
  puts &quot;Remote version: #{remote}, local version: #{local}&quot;

  if remote &amp;gt; local
    puts &apos;Remote is higher than local&apos;
    increment_version_number(version_number: options[:remote])
    puts &apos;Bumping to remote + 1&apos;
    increment_version_number(bump_type: &apos;patch&apos;)
  elsif remote == local
    increment_version_number
  elsif local &amp;gt; remote
    puts &apos;Local version is higher than remote, no need to bump&apos;
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once again, this may seem a bit scary, but let&apos;s break it down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We take in the local and remote version numbers as parameters.&lt;/li&gt;
&lt;li&gt;We convert them to &lt;code&gt;Gem::Version&lt;/code&gt; objects, which allows us to compare them easily.&lt;/li&gt;
&lt;li&gt;We compare the two, and if the remote version number is greater than the local version number, we increment the remote version number by 1, and set that as our version number.&lt;/li&gt;
&lt;li&gt;If the remote version number is equal to the local version number, we just increment the local version number by 1.&lt;/li&gt;
&lt;li&gt;If the remote version number is less than the local version number, we don&apos;t do anything ( So we don&apos;t bump when we don&apos;t need to!).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this post, we&apos;ve seen how to automate the process of bumping the build number and version number using Fastlane. This is a great way to make sure that we never forget to bump the build number again, and it saves us a lot of time in the long run. You can just pick and drop this code into your Fastfile, and it will work out of the box. If you have any questions, or if there&apos;s anything else you&apos;d like to see, let me know!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>How to Unit Test SwiftUI Views Without View Models</title><link>https://nowham.dev/posts/swiftui-tests-no-viewmodel/</link><guid isPermaLink="true">https://nowham.dev/posts/swiftui-tests-no-viewmodel/</guid><description>Testing SwiftUI views without a view model, when using MV instead of MVVM</description><pubDate>Mon, 31 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The most common architecture pattern in SwiftUI is MVVM. This is a great pattern, but it can be a bit overkill for simple views. In this post, we&apos;re going to talk about how to unit test SwiftUI views without a view model. This is a great way to keep your code clean and simple, and it&apos;s also a great way to write unit tests for your views.&lt;/p&gt;
&lt;h2&gt;Reasoning&lt;/h2&gt;
&lt;p&gt;Not every view needs a view model. I know this might be a controversial statement, but hear me out. If your view is simple enough, you can just use the view itself as the model. This is especially true for views that are just displaying data and don&apos;t have any complex logic. In these cases, you can just use the view itself as the model and write your tests against it.&lt;/p&gt;
&lt;p&gt;So if you&apos;ve been writing a view with small logic, but you still want to make sure that your logic is fully covered, repeatable, and predictable, this is my way of doing it.&lt;/p&gt;
&lt;h2&gt;Static methods&lt;/h2&gt;
&lt;p&gt;This is the most obvious way to do it. You can just write a static method in your view and call it from your tests. This is a great way to keep your code clean and simple, and it&apos;s also a great way to write unit tests for your views. It also helps with forcing you to have pure functions, which is something I personally love.&lt;/p&gt;
&lt;p&gt;Here&apos;s an example of how to do this:&lt;/p&gt;
&lt;h3&gt;The View&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct ContentView: View {
  @State private var someString = &quot;Hello, world!&quot;
    var body: some View {
        VStack {
            Text(someString)
          Button(&quot;reverse string&quot;) {
            someString =  Self.reverseString(someString)
          }
        }
        .padding()
    }

  static func reverseString(_ string: String) -&amp;gt; String {
    String(string.reversed())
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see in this very important method that&apos;s clearly carrying the business logic of the app, we have a static method that reverses a string. This is a pure function, and it doesn&apos;t depend on any state. This is a great way to write unit tests for your views, and it&apos;s also a great way to keep your code clean and simple.&lt;/p&gt;
&lt;h3&gt;The Test&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import Testing
@testable import SwiftUI_unit_testing

struct ContentViewTests {

@Test
  func testStringBeingReversed() {
    let baseString = &quot;abc&quot;
    let expectedString = &quot;cba&quot;
    let reversedString = ContentView.reverseString(baseString)
    #expect(reversedString == expectedString)
  }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple enough, isn&apos;t it? All we need to do is call the static method and check if the result is what we expect. This is a great way to write unit tests for your views, and it&apos;s also a great way to keep your code clean and simple. There isn&apos;t even a need to instantiate the view!&lt;/p&gt;
&lt;h2&gt;Pitfalls&lt;/h2&gt;
&lt;p&gt;Like everything else in life, there are upsides and downsides to this approach. Here are a few things to keep in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pure functions might not be the right tool&lt;/strong&gt;: I&apos;m a fan of using whatever tool is right for the job. Sometimes, a pure function is just not the correct tool.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State observability&lt;/strong&gt;: If your view is using &lt;code&gt;@State&lt;/code&gt; or &lt;code&gt;@Binding&lt;/code&gt;, you might need to use a different approach. The updates don&apos;t show in the tests, so you could look into using UI tests to cover this.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lack of context&lt;/strong&gt;: SwiftUI views are just that - views. They&apos;re visual components, and while we can test the logic, we can&apos;t test the visual aspect of it. For that, you might want to look into snapshot testing or UI testing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this short post, we&apos;ve discussed how to unit test SwiftUI views without a view model. As you can see, like anything else, it&apos;s a decision and you should be mindful when you make it. Sometimes, a view model is just not needed. Other times, it absolutely is, if you want to test side effects of methods, different states, and so on. This should just be yet another tool in your belt. Don&apos;t fall into the tribalism of &quot;MVVM is the only way&quot; &lt;em&gt;or&lt;/em&gt; &quot;MV is the only way.&quot; Use the right tool for the job, and you&apos;ll be fine.&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Run iOS Unit Tests Automatically with Fastlane</title><link>https://nowham.dev/posts/fastlane-unit-tests-ios/</link><guid isPermaLink="true">https://nowham.dev/posts/fastlane-unit-tests-ios/</guid><description>Automate your testing and increase your confidence</description><pubDate>Tue, 25 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In our latest &lt;a href=&quot;https://nowham.dev/posts/fastlane-ios-build-guide/&quot;&gt;post&lt;/a&gt;, we spoke about how to build your app with Fastlane. In this post, we&apos;re going to talk about how to run your unit tests with Fastlane. But firstly, why would we even want to do this?&lt;/p&gt;
&lt;h2&gt;Reasoning&lt;/h2&gt;
&lt;p&gt;Unit tests are important, we all agree on that. Also, we all write unit tests (&lt;strong&gt;right?&lt;/strong&gt;), but if we don&apos;t run them - they&apos;re meaningless.&lt;br /&gt;
While manually running them is fine, it&apos;s not the most efficient way to do it. We can automate this process with Fastlane, and have it run every time we push to our CI, or every time we want to run our tests.&lt;/p&gt;
&lt;h2&gt;Pre-requisites&lt;/h2&gt;
&lt;p&gt;I&apos;m assuming you&apos;ve already got Fastlane installed. If you don&apos;t, you can follow &lt;a href=&quot;https://nowham.dev/posts/fastlane-intro-how/&quot;&gt;this&lt;/a&gt; quick guide.&lt;br /&gt;
Also, I&apos;m going to assume you&apos;ve got some unit tests in your project. If you don&apos;t, you should probably write some.&lt;/p&gt;
&lt;h2&gt;Setting up&lt;/h2&gt;
&lt;p&gt;We&apos;re going to be building off of the Fastlane file in the previous post, which at the moment looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/ruby
# frozen_string_literal: true

fastlane_version &apos;2.225.0&apos;

default_platform :ios
platform :ios do

  desc &quot;Build the app&quot;
  lane :build do
    gym(scheme: &quot;MyApp&quot;, configuration: &quot;Release&quot;, export_method: &quot;app-store&quot;, output_directory: &quot;./build&quot;)
  end

end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you need a refresher about what&apos;s going on, you can read the post &lt;a href=&quot;https://nowham.dev/posts/fastlane-ios-build-guide/&quot;&gt;here&lt;/a&gt;, but a quick summary is that we&apos;re building our app with Fastlane and exporting it in a file that is ready to be shipped to the App Store.&lt;/p&gt;
&lt;h2&gt;Explaining the tool&lt;/h2&gt;
&lt;p&gt;We&apos;re going to be using the &lt;code&gt;scan&lt;/code&gt; tool that comes with Fastlane. This tool is used to run your tests, and it&apos;s very easy to use. You can read more about it &lt;a href=&quot;https://docs.fastlane.tools/actions/scan/&quot;&gt;here&lt;/a&gt;. It&apos;s a great tool to have in your belt, and it&apos;s very easy to use. Let&apos;s go over a few of the flags that we&apos;re going to be using:&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;scheme&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This is the scheme that you want to run your tests on. If you&apos;ve got multiple schemes, you can specify which one you want to run your tests on. This is very useful if you&apos;ve got different schemes for different environments, or if you&apos;ve got different schemes for different parts of your app.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;clean&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This flag is used to clean the build folder before running the tests. This is useful if you want to make sure that you&apos;re running your tests on a clean slate, and that there&apos;s no leftover files from previous builds.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;devices&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This flag is used to specify which devices you want to run your tests on. This is useful if you&apos;ve got different devices that you want to run your tests on, or if you want to run your tests on a specific device. This is useful if you&apos;ve got snapshot tests that are per-device, for example.&lt;/p&gt;
&lt;h2&gt;Writing the lane&lt;/h2&gt;
&lt;p&gt;We&apos;re going to add a new lane to our Fastfile, called &lt;code&gt;test&lt;/code&gt;. This lane is going to run our tests, and it&apos;s going to look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/ruby
# frozen_string_literal: true

fastlane_version &apos;2.225.0&apos;

default_platform :ios
platform :ios do

  desc &quot;Build the app&quot;
  lane :build do
    gym(scheme: &quot;MyApp&quot;, configuration: &quot;Release&quot;, export_method: &quot;app-store&quot;, output_directory: &quot;./build&quot;)
  end

  desc &quot;Run the tests&quot;
  lane :test do
    scan(scheme: &quot;MyApp&quot;, clean: true, devices: [&quot;iPhone 12 Pro Max&quot;])
  end

end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Explanation&lt;/h2&gt;
&lt;p&gt;In this lane, we&apos;re running our tests on the scheme &lt;code&gt;MyApp&lt;/code&gt;, cleaning the build folder before running the tests, and running the tests on the device &lt;code&gt;iPhone 12 Pro Max&lt;/code&gt;. You can change the scheme, the devices, and any other flags that you want to use, to suit your needs. This will also produce a results file called &lt;code&gt;report.junit&lt;/code&gt; in the &lt;code&gt;./fastlane/test_output&lt;/code&gt; directory, which you can use to visualize the test results.&lt;/p&gt;
&lt;h2&gt;Running the tests&lt;/h2&gt;
&lt;p&gt;Now that we&apos;ve got our lane set up, we can run our tests by running &lt;code&gt;bundle exec fastlane test&lt;/code&gt; in our project directory. This will run our tests on the specified scheme, clean the build folder before running the tests, and run the tests on the specified device.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;And that&apos;s it! You&apos;ve now got your tests running with Fastlane. This is a great way to automate your testing and increase your confidence in your code. You can read more about the &lt;code&gt;scan&lt;/code&gt; tool &lt;a href=&quot;https://docs.fastlane.tools/actions/scan/&quot;&gt;here&lt;/a&gt;. In the next post, we&apos;re going to talk about how to run UI tests with Fastlane. Stay tuned! 🚀&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>How to Build an iOS App with Fastlane (Step-by-Step)</title><link>https://nowham.dev/posts/fastlane-ios-build-guide/</link><guid isPermaLink="true">https://nowham.dev/posts/fastlane-ios-build-guide/</guid><description>The first step in many workflows.</description><pubDate>Wed, 19 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Last week we discussed &lt;a href=&quot;https://nowham.dev/posts/fastlane-intro-how/&quot;&gt;how to setup Fastlane&lt;/a&gt;. This week we&apos;re going to be doing the first step in many workflows, building the app.&lt;br /&gt;
Building the app is what you&apos;re used to doing in Xcode, by pressing Command + R or Command + B, but when we&apos;re talking about building the app from the command line, with whatever configuration we want, we&apos;re talking about Fastlane.&lt;/p&gt;
&lt;h2&gt;The Fastfile&lt;/h2&gt;
&lt;p&gt;This will be the first time in this series that we&apos;re going to be looking at the Fastfile. The Fastfile is the file that contains all the lanes that you can run. A lane is a set of commands that you can run.&lt;br /&gt;
We have had one before, when we looked into &lt;a href=&quot;https://nowham.dev/posts/fastlane-bump-spm-version/&quot;&gt;auto-bumping SPM versions&lt;/a&gt;, but we&apos;re going to start from scratch here.&lt;/p&gt;
&lt;p&gt;Let&apos;s start with the basics:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/ruby
# frozen_string_literal: true

fastlane_version &apos;2.225.0&apos;

default_platform :ios
platform :ios do

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the basic setup of a Fastfile. We&apos;re setting the fastlane version and the default platform. We&apos;re also specifying that we&apos;re going to be working with iOS. Any new lanes or commands that we add will be added below this.&lt;br /&gt;
Like we mentioned before, we can also import any other Fastfiles we have, but we&apos;re not going to do that here. Let&apos;s keep it nice and simple!&lt;/p&gt;
&lt;h2&gt;Building the app&lt;/h2&gt;
&lt;p&gt;We&apos;re going to be using the incredibly powerful &lt;code&gt;gym&lt;/code&gt; action to build the app. This action is going to build the app and create an &lt;code&gt;.ipa&lt;/code&gt; file that we can use to distribute the app.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  desc &quot;Build the app&quot;
  lane :build do
    gym
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will just do the bare minimum, and if you run it you might actually see a failure because some things are not set up, or more importantly, this will still require your input when run, which is not something we want when we&apos;re writing automations.&lt;br /&gt;
We want these lanes to be able to run entirely on their own, either on our machine, or on a CI/CD provider (foreshadowing?!).&lt;br /&gt;
The complete list of parameters is available &lt;a href=&quot;https://docs.fastlane.tools/actions/gym/&quot;&gt;here&lt;/a&gt;, but we&apos;re going to be looking at a few of them here.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;scheme&lt;/code&gt; parameter&lt;/h3&gt;
&lt;p&gt;This is the name of the scheme that you want to build. This is the same as the scheme you would select in Xcode when you&apos;re building the app.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  desc &quot;Build the app&quot;
  lane :build do
    gym(scheme: &quot;MyApp&quot;)
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The &lt;code&gt;configuration&lt;/code&gt; parameter&lt;/h3&gt;
&lt;p&gt;This is the configuration that you want to build the app with. This is the same as the configuration you would select in Xcode when you&apos;re building the app. The default value is &lt;code&gt;Release&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  desc &quot;Build the app&quot;
  lane :build do
    gym(scheme: &quot;MyApp&quot;, configuration: &quot;Release&quot;)
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The &lt;code&gt;export_method&lt;/code&gt; parameter&lt;/h3&gt;
&lt;p&gt;This is the method that you want to use to export the app. This is the same as the method you would select in Xcode when you&apos;re exporting the app. For most iOS apps, if you plan on later on submitting to TestFlight or the app store, you would use &lt;code&gt;app-store&lt;/code&gt;.&lt;br /&gt;
If you&apos;re building an SPM package, you&apos;d use &lt;code&gt;package&lt;/code&gt;. The options available are: &lt;code&gt;app-store&lt;/code&gt;, &lt;code&gt;validation&lt;/code&gt;, &lt;code&gt;ad-hoc&lt;/code&gt;, &lt;code&gt;package&lt;/code&gt;, &lt;code&gt;enterprise&lt;/code&gt;, &lt;code&gt;development&lt;/code&gt;, &lt;code&gt;developer-id&lt;/code&gt; and &lt;code&gt;mac-application&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  desc &quot;Build the app&quot;
  lane :build do
    gym(scheme: &quot;MyApp&quot;, configuration: &quot;Release&quot;, export_method: &quot;app-store&quot;)
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The &lt;code&gt;output_directory&lt;/code&gt; parameter&lt;/h3&gt;
&lt;p&gt;This is the directory where the &lt;code&gt;.ipa&lt;/code&gt; file will be saved. The default value is &lt;code&gt;./output&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  desc &quot;Build the app&quot;
  lane :build do
    gym(scheme: &quot;MyApp&quot;, configuration: &quot;Release&quot;, export_method: &quot;app-store&quot;, output_directory: &quot;./build&quot;)
  end

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The complete Fastfile&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/ruby
# frozen_string_literal: true

fastlane_version &apos;2.225.0&apos;

default_platform :ios
platform :ios do

  desc &quot;Build the app&quot;
  lane :build do
    gym(scheme: &quot;MyApp&quot;, configuration: &quot;Release&quot;, export_method: &quot;app-store&quot;, output_directory: &quot;./build&quot;)
  end

end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;## Conclusion&lt;/p&gt;
&lt;p&gt;This is it! This is the complete Fastfile that will build your app. You can run this lane by running &lt;code&gt;bundle exec fastlane build&lt;/code&gt; in the terminal. This will build the app, and save the &lt;code&gt;.ipa&lt;/code&gt; file in the &lt;code&gt;./build&lt;/code&gt; directory.&lt;br /&gt;
That is the first step in so many of the automation steps that we&apos;re going to be looking into in the future, and we will be doing a lot of customization to this lane in the future.&lt;br /&gt;
Let me know if you have any questions, or if you want me to cover anything in particular in the future. I&apos;m always looking for new ideas!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Intro to Fastlane for iOS – How to Start Right</title><link>https://nowham.dev/posts/fastlane-intro-how/</link><guid isPermaLink="true">https://nowham.dev/posts/fastlane-intro-how/</guid><description>Get the house in order</description><pubDate>Mon, 10 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In our &lt;a href=&quot;https://nowham.dev/posts/fastlane-intro-why/&quot;&gt;last post&lt;/a&gt; we spoke about &lt;em&gt;why&lt;/em&gt; we should go with Fastlane for iOS. In this post, we will talk about &lt;em&gt;how&lt;/em&gt; to get started with Fastlane.&lt;br /&gt;
I think it&apos;s important to note that Fastlane is a great tool for you to use, regardless of if you&apos;re using it for a big company project or a small indie app. It&apos;s a great tool to have in your belt.&lt;/p&gt;
&lt;h2&gt;Pre-requisites&lt;/h2&gt;
&lt;p&gt;There are a few things that you need to have before you can start. Firstly, we need to make sure we&apos;ve got Ruby installed on our machine. This isn&apos;t a very difficult thing to do, and the way recommended by Fastlane in their docs is &lt;a href=&quot;https://github.com/rbenv/rbenv&quot;&gt;rbenv&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Secondly, you need, of course, the Xcode toolchain installed. If you&apos;ve got Xcode installed, you&apos;re good to go. If you don&apos;t, then you&apos;re reading the wrong blog post.&lt;/p&gt;
&lt;p&gt;And last but not least, is to actually install Fastlane itself. You can install it with Homebrew, but that means it&apos;s installed on your machine, so if you want it to run on some sort of a CI (Foreshadowing!?) you might want to install it with Bundler.&lt;/p&gt;
&lt;h2&gt;Installation&lt;/h2&gt;
&lt;p&gt;Installing Fastlane with Bundler is as easy as running &lt;code&gt;gem install bundler&lt;/code&gt;, followed by &lt;code&gt;bundle init&lt;/code&gt; in your project directory, and then adding &lt;code&gt;gem &apos;fastlane&apos;&lt;/code&gt; to your Gemfile. After that, you can run &lt;code&gt;bundle install&lt;/code&gt; and you&apos;re good to go.&lt;/p&gt;
&lt;p&gt;When you finish, your Gemfile should look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# frozen_string_literal: true

source &quot;https://rubygems.org&quot;

gem &quot;fastlane&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;Now that you&apos;ve got Fastlane installed, you can run &lt;code&gt;bundle exec fastlane init&lt;/code&gt; in your project directory. This will create a &lt;code&gt;fastlane&lt;/code&gt; directory in your project, with a &lt;code&gt;Fastfile&lt;/code&gt; in it. This is where you will write your Fastlane lanes. The prefix &lt;code&gt;bundle exec&lt;/code&gt; is important, as it makes sure that you&apos;re running the Fastlane version that you&apos;ve installed with Bundler.&lt;/p&gt;
&lt;p&gt;When prompted by Fastlane to choose what usage you need, select 4, as we&apos;re going to customize our Fastlane lanes, and do our setup manually.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;And that&apos;s it! You&apos;ve now got Fastlane installed on your machine, and you&apos;ve got a &lt;code&gt;Fastfile&lt;/code&gt; in your project.&lt;br /&gt;
This was a bit of a shorter post, but I think it&apos;s important to get the basics out of the way before we dive into the more complex stuff. You can read more about the setup on Fastlane&apos;s &lt;a href=&quot;https://docs.fastlane.tools/getting-started/ios/setup/&quot;&gt;docs&lt;/a&gt;.&lt;br /&gt;
In the next post, we&apos;re going to talk about how to write your first Fastlane lane, and what you can do with it. Stay tuned! 🚀&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Intro to Fastlane for iOS – Why It Matters</title><link>https://nowham.dev/posts/fastlane-intro-why/</link><guid isPermaLink="true">https://nowham.dev/posts/fastlane-intro-why/</guid><description>Your automation journey starts here</description><pubDate>Mon, 03 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;I&apos;m sure you&apos;ve heard about &lt;a href=&quot;https://fastlane.tools/&quot;&gt;Fastlane&lt;/a&gt; by now. If you haven&apos;t, this will be a very weird blogpost for you.&lt;br /&gt;
It&apos;s the industry standard for automating iOS development tasks. It&apos;s a collection of tools that help you automate the most tedious tasks in iOS development. And it&apos;s a personal favorite of mine.&lt;/p&gt;
&lt;p&gt;However, just because you&apos;ve heard of it, doesn&apos;t mean that you feel comfortable using it. Learning a new thing is daunting, and Fastlane is, for now at least, written in syntax that might be new to you as well - Ruby.&lt;/p&gt;
&lt;p&gt;But fear not! In the following few weeks, we&apos;re going to go from zero to hero when it comes to Fastlane, and automate all the things!&lt;/p&gt;
&lt;p&gt;But first, why would we even want to ?&lt;/p&gt;
&lt;h2&gt;Why Automate?&lt;/h2&gt;
&lt;h3&gt;Consistency&lt;/h3&gt;
&lt;p&gt;Humans are not good at doing the same thing over and over again. We get bored, we get tired, we get distracted. And when we do, we make mistakes. And mistakes are bad. They can lead to bugs, crashes, and even worse, security vulnerabilities.&lt;/p&gt;
&lt;p&gt;Also, when you have a team of developers, you want them to work in the same way. You want them to follow the same steps, to use the same tools, to have the same environment. This is crucial for the maintainability of your codebase.&lt;/p&gt;
&lt;p&gt;Lastly, and perhaps even more importantly, there are some tasks that are just too boring to do manually. You don&apos;t want to spend your time doing things that a computer can do for you. You want to spend your time writing code, solving problems, and creating value.&lt;/p&gt;
&lt;h3&gt;Speed&lt;/h3&gt;
&lt;p&gt;Computers are fast. They can do things in milliseconds that would take you hours. And they can do them 24/7. So why not let them?&lt;/p&gt;
&lt;p&gt;By automating the &lt;a href=&quot;https://nowham.dev/posts/fastlane-bump-spm-version/&quot;&gt;bumping of the version of your SPM package&lt;/a&gt;, you can save yourself a few minutes every time you release a new version. By automating the generation of screenshots for the App Store, you can save yourself a few hours every time you release a new app.&lt;/p&gt;
&lt;h3&gt;Laziness&lt;/h3&gt;
&lt;p&gt;I&apos;m a lazy person. I don&apos;t like doing things that I don&apos;t have to do. And I&apos;m sure you are too. So let the computer do the boring stuff for you, and you can focus on the fun stuff.&lt;/p&gt;
&lt;h2&gt;Why Fastlane?&lt;/h2&gt;
&lt;h3&gt;It&apos;s the best&lt;/h3&gt;
&lt;p&gt;Fastlane is the best tool for automating iOS development tasks. It&apos;s used by thousands of developers around the world, and it&apos;s the de facto standard for automating iOS development tasks. It is absolutely the industry standard, and regardless of which company you work for, and whatever CI/CD provider they use, you will find Fastlane there.&lt;/p&gt;
&lt;h3&gt;It&apos;s constantly updated&lt;/h3&gt;
&lt;p&gt;Fastlane is constantly updated with new features, bug fixes, and improvements. It&apos;s maintained by a team of dedicated developers who are always looking for ways to make it better. And it&apos;s open source, so you can contribute to it as well!&lt;br /&gt;
But what that means for us, is that we don&apos;t need to worry about it going the way that Cocoapods did, and we don&apos;t have to worry that the new Xcode version will brick it. And if it does, you can count on their incredible support to fix it.&lt;/p&gt;
&lt;h3&gt;It&apos;s easy to use&lt;/h3&gt;
&lt;p&gt;Syntax is a matter of opinion, and I&apos;m sure that some of you will disagree with me on this one, but I find Fastlane&apos;s syntax to be very easy to use. It&apos;s very readable, and it&apos;s very easy to understand what&apos;s going on. And if you don&apos;t understand something, you can always look it up in the &lt;a href=&quot;https://docs.fastlane.tools/&quot;&gt;documentation&lt;/a&gt;.&lt;br /&gt;
The Ruby syntax is very human readable to me, and with very minimal &lt;a href=&quot;https://nowham.dev/posts/nicer-fastlane-ux/&quot;&gt;setup&lt;/a&gt; you can have VSCode be very helpful in your Fastlane journey.&lt;/p&gt;
&lt;p&gt;But even nicer than that, it has everything you would ever need when just starting out already built into it. Do you need to run your tests? use &lt;code&gt;scan&lt;/code&gt; .do you need to manage certificates? there&apos;s &lt;code&gt;match&lt;/code&gt;. Do you need to upload your app to TestFlight? there&apos;s &lt;code&gt;pilot&lt;/code&gt;. Do you need to upload your app to the App Store? there&apos;s &lt;code&gt;deliver&lt;/code&gt;. And so on, and so on. You don&apos;t need to write it all from scratch!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;So, in conclusion, we&apos;re going to learn Fastlane because it&apos;s the best tool for automating iOS development tasks. It&apos;s constantly updated, easy to use, and has everything you would ever need already built into it. And it&apos;s going to save you time, make your codebase more maintainable, and make your life easier. I&apos;m excited about it! Are you? 🚀&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>10 Useful Fastlane Built-In Actions You Should Use</title><link>https://nowham.dev/posts/fastlane-builtins-guide/</link><guid isPermaLink="true">https://nowham.dev/posts/fastlane-builtins-guide/</guid><description>You don&apos;t need to write it all from scratch!</description><pubDate>Mon, 17 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;We&apos;re programmers. We love writing code. But, there are always going to be some edge cases that we didn&apos;t think about, something that we didn&apos;t test against (if we test at all 😉), or some way to use our code that we didn&apos;t intend for.&lt;br /&gt;
Well, luckily for us, most of the time, someone else has already thought of that edge case and has written an action that probably already does what we want to do.&lt;/p&gt;
&lt;p&gt;I&apos;m not talking about the big known actions like &lt;code&gt;scan&lt;/code&gt; or &lt;code&gt;pilot&lt;/code&gt; or &lt;code&gt;gym&lt;/code&gt;. I&apos;m talking about the little guys, the ones that you might not have heard of, or the ones that you might not have thought to use.&lt;/p&gt;
&lt;p&gt;The beauty about these actions is that you don&apos;t even need to have Fastlane setup in your project to use them! As long as you&apos;ve got Fastlane installed on your machine, you can just use them with &lt;code&gt;fastlane run &amp;lt;action&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Incrementing versions&lt;/h2&gt;
&lt;p&gt;Remember when we &lt;a href=&quot;https://nowham.dev/posts/fastlane-bump-spm-version/&quot;&gt;wrote our own lane&lt;/a&gt; to increment SPM version numbers? Well, I&apos;m sure you&apos;ve been thinking about writing the same kind of logic for incrementing your build and app version numbers. This is something we do a lot, when we release TestFlights, when we release to the store, when we release to internal testers, etc.&lt;/p&gt;
&lt;p&gt;Well, you&apos;ll be happy to know that Fastlane has a built-in action for those!&lt;br /&gt;
The first one is &lt;a href=&quot;https://docs.fastlane.tools/actions/increment_version_number/&quot;&gt;increment_version_number&lt;/a&gt;. Just running it with a simple &lt;code&gt;fastlane run increment_version_number&lt;/code&gt; will increment version&apos;s patch (assuming you&apos;re using &lt;a href=&quot;https://semver.org/&quot;&gt;semantic versioning&lt;/a&gt;, and if you don&apos;t - you should be). You can also use it to increment the minor or major versions, or even set the version to a specific number.&lt;/p&gt;
&lt;p&gt;The sister-action to this is &lt;a href=&quot;https://docs.fastlane.tools/actions/increment_build_number/&quot;&gt;increment_build_number&lt;/a&gt;. This one is a bit more straightforward, as it only increments the build number. You can also set the build number to a specific number, or even increment it by a specific number.&lt;/p&gt;
&lt;h2&gt;Git&lt;/h2&gt;
&lt;p&gt;This is a bit of an over-arching one, but I&apos;m sure that if you&apos;ve spent more than 5 minutes working on your own Fastlane code, you&apos;ve done something like &lt;code&gt;sh(&quot;git add .&quot;)&lt;/code&gt; or some other kind of hacky way to interact with git.&lt;br /&gt;
Well, there are actually built-in actions for almost all of the common git actions, so you don&apos;t need to do that anymore!&lt;/p&gt;
&lt;p&gt;There&apos;s &lt;a href=&quot;https://docs.fastlane.tools/actions/git_add/&quot;&gt;git_add&lt;/a&gt;, which allows you to add either everything, or just specific files and directories. &lt;a href=&quot;https://docs.fastlane.tools/actions/git_commit/&quot;&gt;git_commit&lt;/a&gt;, that allows you to commit the code you&apos;ve just added, and of course, &lt;a href=&quot;https://docs.fastlane.tools/actions/push_to_git_remote/&quot;&gt;git_push&lt;/a&gt;, that allows you to push your code to the remote.&lt;/p&gt;
&lt;p&gt;But there are also some other git built-in actions that you may not know about, like ones to &lt;a href=&quot;https://docs.fastlane.tools/actions/create_pull_request/&quot;&gt;create a pull request&lt;/a&gt;, or get the &lt;a href=&quot;https://docs.fastlane.tools/actions/changelog_from_git_commits/&quot;&gt;changelog between commits&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Some fun ones&lt;/h2&gt;
&lt;p&gt;It&apos;s not always business up in here. There are also some fun built-in actions to spice your life up a bit.&lt;br /&gt;
One of my all-time favorites is &lt;a href=&quot;https://docs.fastlane.tools/actions/say/&quot;&gt;say&lt;/a&gt;, which makes the machine speak the given message out loud. This is weirdly useful for when you&apos;re running something locally, like a release or a test, and you want to do it in the background and know when it&apos;s done.&lt;br /&gt;
You can be an adult and have it say things like &quot;done&quot;, but I like putting it in exceptions in my code, so when something goes wrong it says &quot;oh no, poopoo&quot;.&lt;br /&gt;
It&apos;s unironically a great way to know when a background task is done.&lt;/p&gt;
&lt;p&gt;The opposite of it is, of course, &lt;a href=&quot;https://docs.fastlane.tools/actions/rocket/&quot;&gt;rocket&lt;/a&gt;! All this action does is print out an ASCII rocket. Granted, the rocket is a bit... phallic looking.&lt;br /&gt;
But it&apos;s a fun thing to put at the end of long lanes that you know are going to take a while, just to give you a little bit of joy at the end of it.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;So these are just some of my most used built-in actions from Fastlane. There are of course more of them &lt;a href=&quot;https://docs.fastlane.tools/actions/&quot;&gt;available&lt;/a&gt;, and even MORE than those if you just search for the actions on Fastlane (but those won&apos;t be &quot;official&quot;, so might not be as battle tested).&lt;br /&gt;
And of course, you can always &lt;a href=&quot;https://nowham.dev/posts/fastlane-custom-actions/&quot;&gt;write your own Fastlane action&lt;/a&gt;!&lt;br /&gt;
Let me know if you have any favorite built-in actions, or if you&apos;ve written your own!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>How to Create Custom Fastlane Actions</title><link>https://nowham.dev/posts/fastlane-custom-actions/</link><guid isPermaLink="true">https://nowham.dev/posts/fastlane-custom-actions/</guid><description>Write your own Fastlane actions</description><pubDate>Mon, 03 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;While Fastlane provides many built-in actions, sometimes you need something specific to your workflow. Custom actions let you create reusable code that integrates seamlessly with Fastlane, but also gives you the flexibility to extend Fastlane&apos;s functionality.&lt;/p&gt;
&lt;h2&gt;Basic Structure&lt;/h2&gt;
&lt;p&gt;Create a new action by running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fastlane new_action
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will bring up the Fastlane CLI and will prompt you for some questions about your new action. In our case, we&apos;re going to create an action that bump the minor version of a given semver string. If you remember, we&apos;ve used this logic &lt;a href=&quot;https://nowham.dev/posts/fastlane-bump-spm-version/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So we&apos;re going to call our action &lt;code&gt;patch_bump&lt;/code&gt;. This will create a file called &lt;code&gt;patch_bump_action.rb&lt;/code&gt; in the &lt;code&gt;fastlane/actions&lt;/code&gt; directory. The file contains quite a lot of boilerplate and dummy code, we can remove most of it, and keep the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module Fastlane
  module Actions
    class PatchBumpAction &amp;lt; Action
      def self.run(params)
        # Add your code here
      end

      def self.description
        &quot;Bumps the patch version of a given sem-ver string&quot;
      end

      def self.available_options
        [
          FastlaneCore::ConfigItem.new(
            key: :version,
            description: &quot;The version to bump&quot;,
            optional: false
          )
        ]
      end

      def self.is_supported?(platform)
        true
      end
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Breakdown of the action syntax&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;module Fastlane&lt;/code&gt; is the namespace for all Fastlane actions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;module Actions&lt;/code&gt; is the namespace for all custom actions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;class PatchBumpAction&lt;/code&gt; is the name of the action&lt;/li&gt;
&lt;li&gt;&lt;code&gt;def self.run(params)&lt;/code&gt; is the method that will be called when the action is executed&lt;/li&gt;
&lt;li&gt;&lt;code&gt;def self.description&lt;/code&gt; is a method that returns a description of the action&lt;/li&gt;
&lt;li&gt;&lt;code&gt;def self.available_options&lt;/code&gt; is a method that returns an array of options that the action accepts. Each option is a &lt;code&gt;FastlaneCore::ConfigItem&lt;/code&gt; object. The &lt;code&gt;key&lt;/code&gt; is the name of the option, &lt;code&gt;description&lt;/code&gt; is a description of the option, and &lt;code&gt;optional&lt;/code&gt; is a boolean that indicates whether the option is required or not.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;def self.is_supported?(platform)&lt;/code&gt; is a method that returns a boolean indicating whether the action is supported on the given platform.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Implementing the action&lt;/h2&gt;
&lt;p&gt;Now that we have the basic structure of the action, we can implement the logic to bump the patch version of a given semver string. How exciting! we&apos;re going to reuse the logic we&apos;ve had in the &lt;a href=&quot;https://nowham.dev/posts/fastlane-bump-spm-version/&quot;&gt;previous post&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  def self.run(params)
    version = params[:version]
    version_number_array = version.split(&apos;.&apos;).map(&amp;amp;:to_i)
    version_number_array[-1] += 1
    version_number_array.join(&apos;.&apos;)
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a very simple implementation (and honestly? shouldn&apos;t need it&apos;s own action!), but it should give you an idea of how to implement custom actions in Fastlane.&lt;/p&gt;
&lt;h2&gt;Using the action&lt;/h2&gt;
&lt;p&gt;To use the action in your Fastfile, you can call it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lane :bump_patch do
  new_version = patch_bump(version: &quot;1.2.3&quot;)
  puts &quot;New version: #{new_version}&quot;
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So this is &lt;em&gt;very&lt;/em&gt; similar to how you would use a built-in Fastlane action. You can pass in the parameters as a hash, and the action will return the new version number. The difference is that this action can also be called as a standalone thing, outside of a Fastfile, and of course, can be tested!&lt;/p&gt;
&lt;h2&gt;Testing the action&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;RSpec&lt;/code&gt; setup&lt;/h3&gt;
&lt;p&gt;First we need to add &lt;code&gt;RSpec&lt;/code&gt; to our Gemfile:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;group :development do
  gem &apos;rspec&apos;
end

# Add these to handle the warnings and LoadError
gem &apos;logger&apos;
gem &apos;mutex_m&apos;
gem &apos;abbrev&apos;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run &lt;code&gt;bundle install&lt;/code&gt; to install the gem.&lt;/p&gt;
&lt;p&gt;After that, we want to add a spec helper file. Add the following to the &lt;code&gt;spec_helper.rb&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p spec/support
touch spec/spec_helper.rb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the following to the &lt;code&gt;spec_helper.rb&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require &apos;fastlane&apos;
require_relative &apos;../actions/patch_bump_action&apos;

RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end

  config.shared_context_metadata_behavior = :apply_to_host_groups
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells RSpec to load the Fastlane gem and our custom action before running the tests.&lt;/p&gt;
&lt;h3&gt;Writing the tests&lt;/h3&gt;
&lt;p&gt;Given how simple this action is, we can write a test for it in the &lt;code&gt;spec/actions/patch_bump_action_spec.rb&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require &apos;spec_helper&apos;

describe Fastlane::Actions::PatchBumpAction do
  describe &apos;#run&apos; do
    it &apos;bumps the patch version&apos; do
      result = Fastlane::FastFile.new.parse(&quot;lane :test do
        patch_bump(version: &apos;1.2.3&apos;)
      end&quot;).runner.execute(:test)

      expect(result).to eq(&apos;1.2.4&apos;)
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&apos;re not going to go deeper into how to test for edge cases, but you can always find more information about that, or if you want more - ask me!&lt;/p&gt;
&lt;h3&gt;Running the tests&lt;/h3&gt;
&lt;p&gt;To run the tests, you can use the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bundle exec rspec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And if everything went well, you should see something like this:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;p&gt;Remember: Custom actions live in your project&apos;s fastlane/actions/ directory. If you create something generally useful, consider turning it into a plugin!&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Always validate input parameters&lt;/li&gt;
&lt;li&gt;Use environment variables for sensitive data&lt;/li&gt;
&lt;li&gt;Keep actions focused and single-purpose&lt;/li&gt;
&lt;li&gt;Add proper error handling using UI.user_error!&lt;/li&gt;
&lt;li&gt;Write tests for your actions&lt;/li&gt;
&lt;li&gt;Document usage and parameters&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Look at all that! You&apos;ve written your first Fastlane action, and even went all 10x developer on it by adding unit tests! Well done you! 🎉&lt;br /&gt;
if you&apos;ve got any questions, or would like to know more about Fastlane, hit me up! Thanks for reading!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Run SwiftFormat on CI: Keep iOS Code Clean</title><link>https://nowham.dev/posts/swiftformat-ci-ios/</link><guid isPermaLink="true">https://nowham.dev/posts/swiftformat-ci-ios/</guid><description>Keep your codebase clean and consistent</description><pubDate>Mon, 20 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In case you haven&apos;t heard of &lt;a href=&quot;https://github.com/nicklockwood/SwiftFormat&quot;&gt;swiftformat&lt;/a&gt; yet, you&apos;ve either been living under a rock, or just have been living your life wrong.&lt;br /&gt;
It&apos;s a tremendous tool to keep your codebase clean and consistent, and it&apos;s the first thing I add to any project I&apos;m working on.&lt;br /&gt;
However, having it run on your machine is one thing, but having it run on your CI is a whole different ball game. The easiest way I found to get it done, is by adding a script to download it (if needed), and run it as a step in your CI provider.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;So there are a few small things that we need to do before we run the script. Firstly, we need to actually install SwiftFormat.&lt;br /&gt;
This can be done very easily using something like Homebrew, or by downloading the binary from the &lt;a href=&quot;https://github.com/nicklockwood/SwiftFormat/releases&quot;&gt;releases page&lt;/a&gt;.&lt;br /&gt;
I prefer the Homebrew way, as it&apos;s easier to keep up to date, and it&apos;s easier to install on a CI provider.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install swiftformat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Secondly, and this is an optional step, we need to have a &lt;code&gt;.swiftformat.yaml&lt;/code&gt; file in our project. this is the file where we specify the rules we want SwiftFormat to use / ignore.&lt;br /&gt;
You don&apos;t &lt;em&gt;have&lt;/em&gt; to do this, if you don&apos;t, SwiftFormat will use the default &lt;a href=&quot;https://github.com/nicklockwood/SwiftFormat#rules&quot;&gt;rules&lt;/a&gt;.&lt;br /&gt;
If there&apos;s interest, I can do a post explaining the rules, what they do, and how to set them up. Let me know!&lt;/p&gt;
&lt;h2&gt;Setting up the script&lt;/h2&gt;
&lt;p&gt;So we&apos;re going to use Bash to run this script. the reason we&apos;re using Bash is that it&apos;s available on almost all CI providers, and it&apos;s easy to write. Here&apos;s the script I use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
set -e

# Function to commit changes if any Swift files were changed
commit_changes() {
    echo &quot;SwiftFormat has performed some changes, committing them.&quot;
    # These two are optional, if you need to set specific user / email for Git. these can be stored in an environment variable,
    # or are provided by the CI provider.

    # git config user.email &quot;${DEPLOYMENT_EMAIL}&quot;
    # git config user.name &quot;${DEPLOYMENT_USER}&quot;
    git add ./\*.swift
    git commit -m &quot;Automatic: Committing SwiftFormat changes&quot;

    # Push changes, setting upstream if needed
    git push origin HEAD || git push --set-upstream origin HEAD
}

# Run SwiftFormat
echo &quot;Running SwiftFormat...&quot;
swiftformat .

# Check for changed Swift files and commit them if any
if git status --porcelain | grep -q &apos;\.swift&apos;; then
    echo &quot;Changed Swift files found, preparing to commit...&quot;
    commit_changes
else
    echo &quot;No changes to .swift files to commit.&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Explanation&lt;/h2&gt;
&lt;p&gt;Bash scripts run from top to bottom, so we need to write our code accordingly. We first define the functions we&apos;re going to use, then run SwiftFormat itself, and after that we can invoke the function to commit the changes.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;commit_changes()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;If you&apos;ve never written / read a Bash script before, this might look a bit daunting. But don&apos;t worry, I&apos;ll explain what&apos;s happening here.&lt;br /&gt;
Firstly, we&apos;re using the systems Bash with what is known as a &lt;code&gt;Shebang&lt;/code&gt;. After that, we&apos;re setting the &lt;code&gt;-e&lt;/code&gt; flag, which tells Bash to exit the script if any command returns a non-zero exit code. This is useful to prevent the script from continuing if something goes wrong.&lt;/p&gt;
&lt;p&gt;Next, we&apos;re writing a method. This is exactly the same as a method you&apos;d write in Swift or any other programming language.&lt;br /&gt;
What this method does, is firstly it prints out that we&apos;re starting to commit, then (commented out) if we need to - we&apos;re setting up the &lt;code&gt;git config&lt;/code&gt; with the email and username we want to use for the commit.&lt;/p&gt;
&lt;p&gt;After that, we&apos;re adding all the modified &lt;code&gt;.swift&lt;/code&gt; files that have been changed, committing them with a message, and then pushing them to the remote repository.&lt;br /&gt;
We don&apos;t want to add any temp files or anything else that have been changed, so we&apos;re using &lt;code&gt;git add ./\*.swift&lt;/code&gt; to only add modified .swift files.&lt;/p&gt;
&lt;h3&gt;Running SwiftFormat&lt;/h3&gt;
&lt;p&gt;At this step, we&apos;re printing out that we&apos;re running SwiftFormat, and then we&apos;re running it with the &lt;code&gt;swiftformat .&lt;/code&gt; command. This will run SwiftFormat on all the &lt;code&gt;.swift&lt;/code&gt; files in the current directory.&lt;br /&gt;
The command will look for a local &lt;code&gt;.swiftformat.yaml&lt;/code&gt; file, and if it doesn&apos;t find one, it will use the default rules.&lt;/p&gt;
&lt;h3&gt;Checking for changes&lt;/h3&gt;
&lt;p&gt;After running SwiftFormat, we&apos;re checking if there are any changes to the &lt;code&gt;.swift&lt;/code&gt; files. If there are, we&apos;re printing out that we&apos;re preparing to commit, and then we&apos;re calling the &lt;code&gt;commit_changes&lt;/code&gt; function.&lt;br /&gt;
The way we&apos;re doing this is with a simple &lt;code&gt;if&lt;/code&gt; statement, firstly we&apos;re using the &lt;code&gt;git status --porcelain&lt;/code&gt; command to get the status of the repository. The &lt;code&gt;--porcelain&lt;/code&gt; flag is used to get the status in a machine-readable format. We&apos;re then piping this output to &lt;code&gt;grep&lt;/code&gt; to search for any &lt;code&gt;.swift&lt;/code&gt; files that have been changed.&lt;br /&gt;
We&apos;re adding the &lt;code&gt;-q&lt;/code&gt; flag to &lt;code&gt;grep&lt;/code&gt; to make it quiet, and only return a non-zero exit code if it finds a match. If it does, we&apos;re printing out that we&apos;re preparing&lt;br /&gt;
to commit, and then we&apos;re calling the &lt;code&gt;commit_changes&lt;/code&gt; function. This is because we don&apos;t really care about &lt;em&gt;which&lt;/em&gt; files have been changed, we just care that there &lt;em&gt;are&lt;/em&gt; files that have been changed.&lt;/p&gt;
&lt;p&gt;If no files have been found, there&apos;s nothing to commit, congratulations!&lt;br /&gt;
If there are files found, we&apos;re going to the &lt;code&gt;commit_changes()&lt;/code&gt; method, which we explained above.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;And that&apos;s it! it really was &lt;em&gt;that&lt;/em&gt; easy to write a script that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run SwiftFormat&lt;/li&gt;
&lt;li&gt;Checks if there are any files changed&lt;/li&gt;
&lt;li&gt;Commits the changes if there are any&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can now plop this script into your CI provider. Depending on your provider, you may need to make it into an executable, using something like &lt;code&gt;chmod +x /path/to/your/script.sh&lt;/code&gt;. However, with a lot of providers like CircleCI, scripts can run as executables out of the box, so check with your providers docs.&lt;br /&gt;
I hope you find this useful!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>My Favorite Xcode Shortcuts for Power Users</title><link>https://nowham.dev/posts/xcode-shortcuts-pro/</link><guid isPermaLink="true">https://nowham.dev/posts/xcode-shortcuts-pro/</guid><description>Navigate Xcode like a pro</description><pubDate>Mon, 13 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Oh Xcode, what a love-hate relationship I&apos;ve got with you. So many good things in that IDE, yet so many shitty things all at once. However, love it or hate it, Xcode is our bread and butter, and unless you work with a very specific workflow, you&apos;ll be spending most of your time in Xcode. So why not make the best of it and learn some shortcuts to make your life easier?&lt;br /&gt;
Here are a few of the shortcuts I use on a daily basis. I hope you&apos;ll find them useful too!&lt;br /&gt;
I will be referring to their default key combinations, but you can always change them in the preferences (I have!)&lt;/p&gt;
&lt;h2&gt;Command palette&lt;/h2&gt;
&lt;p&gt;If you&apos;ve used something like VSCode or an IntelliJ IDE, you&apos;re probably familiar with the command palette. It&apos;s a quick way to access any command in the IDE. In Xcode, you can access it with &lt;code&gt;Cmd + Shift + A&lt;/code&gt;. The reason I&apos;m starting with this one is that it can honestly replace almost all of the following ones. I use this command all the time, to do things like reset package caches, generate initializers, or honestly anything else. It&apos;s a great way to discover new features in Xcode too. I&apos;ve remapped it to &lt;code&gt;Cmd + Shift + P&lt;/code&gt;, so my muscle memory doesn&apos;t go away when I spend time in VSCode writing Fastlane stuff (if you haven&apos;t seen it yet, &lt;a href=&quot;https://nowham.dev/posts/nicer-fastlane-ux/&quot;&gt;this&lt;/a&gt; is how I work on my Fastlane stuff.)&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Open quickly&lt;/h2&gt;
&lt;p&gt;If you&apos;ve watched &lt;em&gt;any&lt;/em&gt; YouTube tutorial, you&apos;ve undoubtedly seen the person presenting using this shortcut. It&apos;s one of those &quot;once you start using it you will never stop using it&quot;. All it does is give you a quick search bar to open any file in your project. It&apos;s a great way to navigate your project without having to use the project navigator. You can access it with &lt;code&gt;Cmd + Shift + O&lt;/code&gt;. I&apos;ve remapped it to &lt;code&gt;Cmd + P&lt;/code&gt;, because I use it &lt;em&gt;that&lt;/em&gt; much.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Edit all in scope&lt;/h2&gt;
&lt;p&gt;You know when you&apos;re refactoring and you&apos;ve realized that you&apos;ve named a variable wrong? How could it &lt;em&gt;possibly&lt;/em&gt; be called &lt;code&gt;first_name&lt;/code&gt; when it &lt;em&gt;obviously&lt;/em&gt; should be &lt;code&gt;firstName&lt;/code&gt;? Well, if that variable is also used in 10 more places in the file, you &lt;em&gt;can&lt;/em&gt; use &lt;code&gt;Cmd + F&lt;/code&gt; to find them all and replace them one by one, or use the big bulky refactor from the above-mentioned command palette, or you can use &lt;code&gt;Cmd + Ctrl + E&lt;/code&gt; to edit all in scope. It&apos;s a great way to quickly change a variable name, or a method name, or anything else that you want to change in a file. It&apos;s a great way to keep your code consistent and clean.&lt;br /&gt;
The important bit here is that this edit all is &lt;strong&gt;in scope&lt;/strong&gt;. So if you&apos;re inside a method, it&apos;ll only rename inside that method. If you&apos;re at the top level of the file, it&apos;ll rename everything in the file.&lt;/p&gt;
&lt;h2&gt;Reveal in project navigator&lt;/h2&gt;
&lt;p&gt;This is one of those that I couldn&apos;t believe that I&apos;d gone so long without knowing. You know how you&apos;re working on a file, and you want to see where it is in the project navigator? You can right-click on the file and select &quot;Reveal in project navigator&quot;, or you can use &lt;code&gt;Cmd + Shift + J&lt;/code&gt; to do it. It&apos;s a great way to quickly see where a file is in your project, and it&apos;s a great way to keep your bearings in a large project. I use it all the time when I&apos;m working on a new project and I&apos;m not sure where everything is.&lt;/p&gt;
&lt;h2&gt;Bonus one - Format File by &lt;a href=&quot;https://github.com/nicklockwood/SwiftFormat&quot;&gt;SwiftFormat&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you&apos;ve known me for more than 20 minutes, you&apos;ve heard me rave about SwiftFormat. It&apos;s my most used tool ever (the rest are &lt;a href=&quot;https://nowham.dev/posts/ios-ci-cd-dev-tools/&quot;&gt;here&lt;/a&gt;), and it&apos;s the first thing I add to any project I&apos;m working on.&lt;br /&gt;
You can use it from the command line, or you can use it as a pre-commit hook, but you can also use it as an Xcode extension. I&apos;ve chosen the latter, and remapped the key combination to &lt;code&gt;Shift + F3&lt;/code&gt;, and use that about a thousand times a day. It&apos;s a great way to keep your code consistent and clean, and it&apos;s a great way to make sure that you&apos;re not spending time on formatting your code, but on writing it.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There are a million more shortcuts in Xcode, and I&apos;m sure you&apos;ve got your own favorites. Almost all of them can be accessed from the command palette, so I&apos;d recommend starting there. I hope you&apos;ve found these useful, and I&apos;d love to know what your favorite shortcuts are! Let me know on socials!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Intermediate Fastlane: Smarter iOS Automation</title><link>https://nowham.dev/posts/intermediate-fastlane-ios/</link><guid isPermaLink="true">https://nowham.dev/posts/intermediate-fastlane-ios/</guid><description>Take your fastlane skills to the next level</description><pubDate>Mon, 06 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;So by now, if you&apos;ve been working on mobile development for any longer than a few weeks, you&apos;ve probably used Fastlane. I know I talk about it a lot in this blog, but it&apos;s genuinely one of my favorite tools out there, and I think most people don&apos;t use it to its full potential. So in this post, I&apos;m going to show you some more advanced Fastlane features that you might not have seen before.&lt;/p&gt;
&lt;h3&gt;Separate Fastfiles&lt;/h3&gt;
&lt;p&gt;When you write your normal code, you won&apos;t put everything in the same massive file, right? (&lt;em&gt;RIGHT?&lt;/em&gt;) So why would you do that with your Fastlane setup? You can split your Fastlane setup into multiple files, which can make it easier to manage and understand.&lt;/p&gt;
&lt;p&gt;I like to have all of my Fastlane files have &lt;code&gt;fastfile&lt;/code&gt; in the name, so I can easily see which files are Fastlane files. So for example, I&apos;ll have the main &lt;code&gt;Fastfile&lt;/code&gt;, and a separate &lt;code&gt;VersionBumpingFastfile&lt;/code&gt; for all of my version bumping logic (Which you can learn more about &lt;a href=&quot;https://nowham.dev/posts/fastlane-bump-spm-version/&quot;&gt;here&lt;/a&gt;!).&lt;/p&gt;
&lt;p&gt;The way you do this is by using the &lt;code&gt;import&lt;/code&gt; method in your &lt;code&gt;Fastfile&lt;/code&gt;. So if you have a file called &lt;code&gt;VersionBumpingFastfile&lt;/code&gt; in the same directory as your &lt;code&gt;Fastfile&lt;/code&gt;, you can import it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/ruby
# frozen_string_literal: true

fastlane_version &apos;2.225.0&apos;
import &apos;VersionBumpingFastfile&apos;
# The rest of your normal Fastfile here..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By doing it like this, you can now call any lane in your &lt;code&gt;VersionBumpingFastfile&lt;/code&gt; from your main &lt;code&gt;Fastfile&lt;/code&gt;, or as standalone commands from the terminal, and you get the nice separation of concerns that you&apos;re used to in your normal code.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;before_all&lt;/code&gt; and &lt;code&gt;after_all&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Think about your normal XCTest suite (I know I know, you&apos;re using the shiny new test framework, but try to remember what the olden days looked like). You have &lt;code&gt;setUp&lt;/code&gt; and &lt;code&gt;tearDown&lt;/code&gt; methods that run before and after each test, respectively. Fastlane has similar hooks that you can use to run code before and after your entire Fastlane run.&lt;/p&gt;
&lt;p&gt;This is particularly useful if you&apos;ve got things that need to happen regardless of which lane you&apos;re running. For example, you might want to set up some environment variables, or clean up some files before you start running your lanes. You can do this with the &lt;code&gt;before_all&lt;/code&gt; and &lt;code&gt;after_all&lt;/code&gt; hooks in your &lt;code&gt;Fastfile&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;before_all do
  setup_circle_ci
end

lane :beta do
  build_app(scheme: &quot;MyApp&quot;)
  upload_to_testflight
end

after_all do |lane|
  clean_build_artifacts
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sets up the environment for &lt;code&gt;CircleCI&lt;/code&gt; and cleans up files after the Fastlane run. There are also some great examples of this on the Fastlane docs &lt;a href=&quot;https://docs.fastlane.tools/advanced/Fastfile/&quot;&gt;here&lt;/a&gt;, where they use the &lt;code&gt;after_all&lt;/code&gt; block to notify a Slack channel when the Fastlane run is complete, which is mega useful.&lt;/p&gt;
&lt;h2&gt;Embrace the lane syntax&lt;/h2&gt;
&lt;p&gt;I know we&apos;re all coming from different languages and different syntax, but Fastlane has its own way of doing things. Let&apos;s look at some lane patterns that can make your code more readable and maintainable.&lt;/p&gt;
&lt;p&gt;Firstly, the &lt;code&gt;is_ci&lt;/code&gt; helper is your friend. It helps you write conditional lane logic. For example, instead of writing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lane :deploy do |options|
  if ENV[&apos;CI&apos;] == &apos;true&apos;
    match(type: &quot;appstore&quot;)
  end
  build_ios_app
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can write:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lane :deploy do |options|
  match(type: &quot;appstore&quot;) if is_ci
  build_ios_app
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also use lanes to handle error conditions elegantly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lane :push_to_git do |options|
  begin
    push_to_git_remote
  rescue =&amp;gt; ex
    handle_git_error(error: ex)
  end
end

private_lane :handle_git_error do |options|
  UI.error(options[:error])
  create_git_branch
  push_to_git_remote(force: true)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another way you can make your lanes more readable is by using early returns for validation. For example, instead of writing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lane :release do |options|
  if options[:version]
    if options[:version].match(/\d+\.\d+\.\d+/)
      increment_version_number(
        version_number: options[:version]
      )
      build_app
      upload_to_app_store
    else
      UI.user_error!(&quot;Invalid version format&quot;)
    end
  else
    UI.user_error!(&quot;Version is required&quot;)
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can write:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lane :release do |options|
  UI.user_error!(&quot;Version is required&quot;) unless options[:version]
  UI.user_error!(&quot;Invalid version format&quot;) unless options[:version].match(/\d+\.\d+\.\d+/)

  increment_version_number(
    version_number: options[:version]
  )
  build_app
  upload_to_app_store
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;These are by no means the end all be all of Fastlane tips, but they&apos;re some of the more advanced features that I think a lot of people don&apos;t know about. There are a lot more great features, some of them are documented &lt;a href=&quot;https://docs.fastlane.tools/advanced/Fastfile/&quot;&gt;here&lt;/a&gt;, and some of them are just waiting for you to discover them. I hope this post has been helpful, and that you can take your Fastlane skills to the next level!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Improve Your Fastlane Developer Experience</title><link>https://nowham.dev/posts/nicer-fastlane-ux/</link><guid isPermaLink="true">https://nowham.dev/posts/nicer-fastlane-ux/</guid><description>Have a nicer dev experience when working on Fastlane</description><pubDate>Mon, 09 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;If you&apos;ve been in mobile development for more than 2 minutes, you&apos;ve heard of &lt;a href=&quot;https://fastlane.tools/&quot;&gt;Fastlane&lt;/a&gt;.&lt;br /&gt;
Either you&apos;ve used it yourself and you&apos;re a pro, or you&apos;re just starting out and you&apos;re a bit overwhelmed by the sheer amount of things you can do with it.&lt;br /&gt;
Like any other incredible tool, the iceberg of information and tooling can be quite daunting. In this post, I&apos;ll show you a few tricks and tips that I&apos;ve picked up over the years that might help you get more out of Fastlane.&lt;/p&gt;
&lt;p&gt;You&apos;ll notice that a lot of these are Ruby-centric, but in case you missed it, Fastlane is written in Ruby, so it&apos;s a good idea to get familiar with it! (at least until &lt;a href=&quot;https://docs.fastlane.tools/getting-started/ios/fastlane-swift/&quot;&gt;Fastlane with Swift&lt;/a&gt; comes out of beta)&lt;/p&gt;
&lt;h2&gt;Editor&lt;/h2&gt;
&lt;p&gt;First thing you&apos;ll want to do is to set up your editor to work with Fastlane. You can of course use whatever editor you want, but I&apos;ll show you how to set it up in VSCode (and as an extension of that, Cursor and any other VSCode-based editor).&lt;/p&gt;
&lt;p&gt;The first plugin you&apos;re going to want to install is &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=fivethree.vscode-fastlane-snippets&quot;&gt;Fastlane Snippets&lt;/a&gt;. It&apos;s a simple plugin that allows for common shortcuts and snippets (duh) for things that you&apos;re going to be writing a lot.&lt;/p&gt;
&lt;p&gt;The second one will be the &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp&quot;&gt;Ruby LSP&lt;/a&gt; plugin from Shopify. This is a language server plugin that will help you with syntax highlighting, autocompletion, and other things that you might expect from a language server.&lt;/p&gt;
&lt;p&gt;These two alone will be more than enough to take you from the non-syntax highlighting, no autocompletion, no snippets world you&apos;re probably used to editing Fastlane things in Xcode to a much more pleasant experience.&lt;/p&gt;
&lt;h2&gt;Formatting and linting&lt;/h2&gt;
&lt;p&gt;Just like we&apos;ve got tools like &lt;a href=&quot;https://github.com/realm/SwiftLint&quot;&gt;SwiftLint&lt;/a&gt; and &lt;a href=&quot;https://github.com/nicklockwood/SwiftFormat&quot;&gt;SwiftFormat&lt;/a&gt;, we&apos;ve got tools for Ruby too.&lt;br /&gt;
The most common one is &lt;a href=&quot;https://github.com/rubocop/rubocop&quot;&gt;Rubocop&lt;/a&gt;. It&apos;s a linter and a formatter, and it&apos;s very configurable. You can set it up to be as strict or as lenient as you want, and it will help you keep your code clean and consistent. Also, unless Ruby is the main language that you use on a day-to-day basis, it will also help you with the small pitfalls that you might not be aware of.&lt;/p&gt;
&lt;p&gt;Installing Rubocop is as simple as any other gem, just add &lt;code&gt;gem &apos;rubocop&apos;, require: false&lt;/code&gt; to your Gemfile, and run &lt;code&gt;bundle install&lt;/code&gt;.&lt;br /&gt;
The &lt;code&gt;require: false&lt;/code&gt; means that the gem will be installed but not automatically loaded when your Ruby application starts.&lt;br /&gt;
Now the next time you open and save your Fastfile, you&apos;ll see all the issues that Rubocop has with your code, and you can fix them as you go. Rubocop can even auto-fix a lot of the issues for you, so you don&apos;t have to worry about it.&lt;/p&gt;
&lt;h2&gt;Ruby versioning&lt;/h2&gt;
&lt;p&gt;You may want to have different versions of Ruby installed on your machine, or just have a manager to handle the versions for you. The one I use is &lt;a href=&quot;https://github.com/rbenv/rbenv&quot;&gt;rbenv&lt;/a&gt;. Installation is mega easy using Homebrew, and it really simplifies the whole &quot;what version of Ruby do I need for this project&quot; thing.&lt;/p&gt;
&lt;h2&gt;Theme 👀&lt;/h2&gt;
&lt;p&gt;I know, I know, this is a bit of a personal preference, but I&apos;ve found that using a theme that is easy on the eyes really helps with the whole &quot;I&apos;m going to be staring at this for hours&quot; thing. I use the Monokai theme from &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=BeardedBear.beardedtheme&quot;&gt;BeardedBear&lt;/a&gt;, and I love it. They&apos;ve got a bunch of other options, even some light mode ones if you&apos;re into that sort of thing.&lt;/p&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;Congratulations! You&apos;ve now got a much nicer dev experience when working on Fastlane. You&apos;ve got syntax highlighting, autocompletion, snippets, linting, formatting, and even a nice theme to look at. You&apos;re all set to take on the world of Fastlane!&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You&apos;ve gone from this:&lt;/th&gt;
&lt;th&gt;To this:&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I hope you liked these small but impactful tips you can do to have a nicer Fastlane dev experience. I&apos;d love to know if you&apos;ve got other tools and tips that I&apos;ve missed, let me know on socials!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Swift Devs: Should You Use Skip or Kotlin Multiplatform?</title><link>https://nowham.dev/posts/skip-vs-kmp-comparison/</link><guid isPermaLink="true">https://nowham.dev/posts/skip-vs-kmp-comparison/</guid><description>Which one is better for you?</description><pubDate>Mon, 02 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Multiplatform tools are not a new thing. There are well-established players out there, like React Native or Flutter, and even some old deprecated ones, like Xamarin / MAUI. But recently, two new players have entered the game: Skip and KMP. Both of them are multiplatform tools, but they have different approaches. In this article, we will compare them and see which one is better for you.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;h3&gt;KMP (&lt;a href=&quot;https://kotlinlang.org/docs/multiplatform.html&quot;&gt;Kotlin Multiplatform&lt;/a&gt;)&lt;/h3&gt;
&lt;p&gt;A framework developed by JetBrains, the people behind Kotlin and incredible IDEs like IDEA, WebStorm, and PyCharm. KMP allows you to write code once and run it on multiple platforms, like iOS, Android, and the web. It is based on Kotlin, a modern language that is very nice to work with and is very similar to Swift in many ways.&lt;/p&gt;
&lt;p&gt;KMP takes the approach of &quot;share the logic, divide the UI&quot;. This means that you write the business logic once, and then you write the UI for each platform separately. This is a good approach if you want to have a native look and feel on each platform, but it can be a bit more work. Your UI will be SwiftUI on iOS, Jetpack Compose on Android (There is also a web side, but I&apos;m not a web dev so I can&apos;t talk about it).&lt;br /&gt;
This means that you will have to write your UI twice, which might sound like a hassle to some or a great feature to others.&lt;br /&gt;
For me, this was a great feature, as I believe that the only way to get native performance and feel from UI is to write it natively.&lt;/p&gt;
&lt;p&gt;If you wanted to go a bit deeper, you could also use Compose Multiplatform to have your UI written once, in Kotlin. However, that is still in alpha, so I can&apos;t recommend it for production.&lt;/p&gt;
&lt;p&gt;The biggest drawback for us iOS developers here will be the fact that this heavily relies on Kotlin, a new language to some. However, the language is very easy to pick up, and with AI tools, this really isn&apos;t the hurdle it once was.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://skip.tools/&quot;&gt;Skip&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Skip is the other end of the spectrum - you write &lt;em&gt;Swift&lt;/em&gt; and &lt;em&gt;SwiftUI&lt;/em&gt; code, and that in turn gets compiled into Kotlin and Compose code for you.&lt;br /&gt;
Obviously, as Swift developers, this sounds like an incredible premise - you don&apos;t need to learn a new language, you don&apos;t need to learn a new framework, you just write Swift and SwiftUI, and you get Android for free.&lt;br /&gt;
This is a bit of a double-edged sword though - you are relying on a tool to do the conversion for you, and that might not always be perfect. The beauty of it is that, like KMP, you can write UI in SwiftUI and it will get converted into Compose, &lt;strong&gt;or&lt;/strong&gt; you can write just your business logic in Swift, and write the UI in Compose, if you&apos;re more comfortable with that. And as far as cross-platform performance goes, since it converts your code to Compose for you, you&apos;re getting as close to native Android as possible.&lt;/p&gt;
&lt;h2&gt;Dev experience&lt;/h2&gt;
&lt;h3&gt;KMP&lt;/h3&gt;
&lt;p&gt;As a JetBrains tool and a Kotlin-first tool, the main IDE that you will be working in will either be IDEA (paid or community version), or my recommendation, Android Studio. This is a great IDE, and it has a lot of features that make working with KMP a breeze. JetBrains IDEs are a bit marmite - you either love them or hate them. I personally love them, but I know a lot of people who don&apos;t.&lt;br /&gt;
However, any iOS-related work that you&apos;ll be doing, whether that be any project configuration work, code signing, and indeed any SwiftUI work, will be done in Xcode. This means that you will have to switch between IDEs, which can be a bit of a hassle. JetBrains have said that they&apos;re working on a &lt;a href=&quot;https://blog.jetbrains.com/kotlin/2024/10/kotlin-multiplatform-development-roadmap-for-2025/#tooling&quot;&gt;standalone IDE for KMP&lt;/a&gt;, but that is still in the works.&lt;/p&gt;
&lt;h3&gt;Skip&lt;/h3&gt;
&lt;p&gt;Skip setup is done in the terminal, using their CLI, which is well documented on their website. It feels very similar to setting up a new Swift package, and it&apos;s very easy to get started. The main IDE that you will be working in will be Xcode, which is great for us iOS developers. You will be able to do all of your work in one place, and you won&apos;t have to switch between IDEs. This is a big plus for me, as I like to have everything in one place, even if that one place is Xcode.&lt;/p&gt;
&lt;h3&gt;Summary&lt;/h3&gt;
&lt;p&gt;While Kotlin and JetBrains IDEs are great, the comfort and familiarity of writing Swift code in Xcode is just unmatched, at least until JetBrains comes out with their standalone IDE.&lt;br /&gt;
This one goes to Skip, for me.&lt;/p&gt;
&lt;h2&gt;Support and documentation&lt;/h2&gt;
&lt;h3&gt;KMP&lt;/h3&gt;
&lt;p&gt;KMP has tremendous support from the community and from JetBrains themselves, and they are very responsive to tickets opened and issues raised. The documentation is also very good, and there are a lot of tutorials and guides out there to help you get started.&lt;br /&gt;
Obviously, being a bigger and more established company gives them the ability to have a bigger team and more resources to put into support and documentation.&lt;/p&gt;
&lt;h3&gt;Skip&lt;/h3&gt;
&lt;p&gt;The docs for Skip are great. Getting up and running with a basic project is fully covered, and won&apos;t take you long at all. Even a more complex project is well documented, and you should be able to get up and running in no time.&lt;br /&gt;
The support is also great, and when I had a question I asked it in the Slack, and was responded to by the creator on the same day, which is very impressive.&lt;br /&gt;
However, and this is by no fault of the Skip team, they are a younger product and a much smaller team, so the support and documentation might not be as good as KMP&apos;s.&lt;/p&gt;
&lt;h3&gt;Summary&lt;/h3&gt;
&lt;p&gt;This one is weirdly hard to call. They&apos;re both well documented on their own website and have good support, but KMP being just that much bigger and having a larger community, I think I have to give this one to KMP.&lt;/p&gt;
&lt;h2&gt;Price&lt;/h2&gt;
&lt;h3&gt;KMP&lt;/h3&gt;
&lt;p&gt;KMP is free, regardless of the size of the team or project using it. This is a big plus, as it means that you can try it out without any risk, and you can use it for any project, regardless of the size.&lt;/p&gt;
&lt;h3&gt;Skip&lt;/h3&gt;
&lt;p&gt;Skip has a &lt;a href=&quot;https://skip.tools/pricing/&quot;&gt;free tier&lt;/a&gt; for indie developers, which in my opinion is very generous and should be more than enough for you to have a look, play around, and even deploy a full app to both stores without ever paying.&lt;br /&gt;
However, for teams and more than one closed source project, there is a cost associated with it. This is a bit of a downside, but it&apos;s understandable, as they need to make money somehow, and they&apos;re not a big company like JetBrains.&lt;/p&gt;
&lt;h3&gt;Summary&lt;/h3&gt;
&lt;p&gt;This one goes to KMP, for being free for everyone, but I would like to give a special mention to Skip for having a very generous free tier.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;While I think Skip is an incredible tool with an endless amount of potential, for a lot of you, KMP might be the better choice. It&apos;s more established, has a bigger community, and is free for everyone. However, if you&apos;re a Swift developer, and you want to write Swift code, and you don&apos;t mind a bit of a learning curve, Skip is a great choice, and I don&apos;t think you&apos;ll be disappointed.&lt;br /&gt;
For 99.9% of indie devs, you&apos;ll be happy with both, so I would recommend building a small app (I built a &lt;a href=&quot;https://github.com/NoamEfergan/Pokemon&quot;&gt;Pokemon app&lt;/a&gt;), spend the day with the frameworks, and see which one you like better.&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Intro to Scripting with Swift (for Automation)</title><link>https://nowham.dev/posts/swift-scripting-basics/</link><guid isPermaLink="true">https://nowham.dev/posts/swift-scripting-basics/</guid><description>An oversimplified overview of how to start using Swift for scripting</description><pubDate>Fri, 22 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Reasoning&lt;/h2&gt;
&lt;p&gt;As Swift developers (whether working with iOS, macOS, visionOS, or any other OS), we&apos;re used to using Swift for building apps. But Swift has evolved and grown over the years, and now we can even use it for scripting. This allows us to leverage all of the qualities that we love about Swift, without wasting time googling syntax.&lt;/p&gt;
&lt;h2&gt;The Basics&lt;/h2&gt;
&lt;p&gt;As a Swift developer, you &lt;em&gt;already&lt;/em&gt; know how to write scripts with Swift, you just may not know it yet.&lt;br /&gt;
The basics of scripting with Swift are incredibly simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new file with a &lt;code&gt;.swift&lt;/code&gt; extension&lt;/li&gt;
&lt;li&gt;Write your Swift code&lt;/li&gt;
&lt;li&gt;Run the file with &lt;code&gt;swift &amp;lt;filename&amp;gt;.swift&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Yes, it&apos;s that simple. Let&apos;s give it a try:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Foundation

let name = &quot;Noam&quot;

func greeting(for name: String) -&amp;gt; String {
    return &quot;Hello, \(name)!&quot;
}

print(greeting(for: name))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I know, I know, what an exciting script. But it&apos;s a start! All we need to do now is create a file with this incredibly simple script and run it with &lt;code&gt;swift &amp;lt;filename&amp;gt;.swift&lt;/code&gt;.&lt;br /&gt;
You can just copy the above, and run &lt;code&gt;pbpaste &amp;gt; Greeting.swift&lt;/code&gt; in your terminal, and then run &lt;code&gt;swift Greeting.swift&lt;/code&gt;.&lt;br /&gt;
&lt;/p&gt;
&lt;p&gt;Great job! You&apos;ve just written your first Swift script. Now let&apos;s dive a bit deeper.&lt;/p&gt;
&lt;h2&gt;Passing Arguments&lt;/h2&gt;
&lt;p&gt;This greeting is good and all, but as I&apos;m sure you could&apos;ve guessed, it&apos;s a bit simple and hardcoded. Let&apos;s make it more dynamic by passing the name as an argument.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Foundation

func greeting(for name: String) -&amp;gt; String {
    return &quot;Hello, \(name)!&quot;
}

if CommandLine.arguments.count &amp;gt; 1 {
    let name = CommandLine.arguments[1]
    print(greeting(for: name))
} else {
    print(&quot;Please provide a name&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You know the drill: copy the above, save it to a file, and run it with &lt;code&gt;swift &amp;lt;filename&amp;gt;.swift &amp;lt;your name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Hurray! Look at you, passing arguments to your script like a pro.&lt;br /&gt;
You&apos;ll notice that the arguments passed in are stored in the &lt;code&gt;CommandLine.arguments&lt;/code&gt; array, and the first argument is the name of the script itself.&lt;/p&gt;
&lt;h2&gt;User Input&lt;/h2&gt;
&lt;p&gt;Now, while this script works, it requires us to &lt;em&gt;know&lt;/em&gt; that a parameter is required and what that parameter is. Let&apos;s make it more user-friendly with a &lt;code&gt;readLine&lt;/code&gt; prompt.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Foundation

func greeting(for name: String) -&amp;gt; String {
    return &quot;Hello, \(name)!&quot;
}

if CommandLine.arguments.count &amp;gt; 1 {
    let name = CommandLine.arguments[1]
    print(greeting(for: name))
} else {
    print(&quot;Please provide a name&quot;)
    if let name = readLine() {
        print(greeting(for: name))
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br /&gt;
Now we&apos;re getting somewhere! This script will prompt the user for a name if one isn&apos;t provided as an argument.&lt;/p&gt;
&lt;h2&gt;Some Pizazz&lt;/h2&gt;
&lt;p&gt;As a command line script, we can also leverage the power of the terminal. Let&apos;s add some color to our script.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Foundation

func greeting(for name: String) -&amp;gt; String {
    return &quot;Hello, \(name)!&quot;
}

if CommandLine.arguments.count &amp;gt; 1 {
    let name = CommandLine.arguments[1]
    print(&quot;\u{001B}[0;32m&quot; + greeting(for: name) + &quot;\u{001B}[0;0m&quot;)
} else {
    print(&quot;\u{001B}[0;31mPlease provide a name\u{001B}[0;0m&quot;)
    if let name = readLine() {
        print(&quot;\u{001B}[0;32m&quot; + greeting(for: name) + &quot;\u{001B}[0;0m&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;As you can see, we can use all of the fun parts of the terminal, like adding colors, supporting ASCII art, and more.&lt;/p&gt;
&lt;h2&gt;File I/O&lt;/h2&gt;
&lt;p&gt;While writing CLI tools and commands is fun, handling files is a common task for scripts. Let&apos;s write a script that writes to a file, then reads it and prints it to the console.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Foundation

let filename = &quot;test.txt&quot;

func writeToFile(_ filename: String, contents: String) {
    do {
        try contents.write(toFile: filename, atomically: true, encoding: .utf8)
        print(&quot;Wrote to file \(filename)!&quot;)
    } catch {
        print(&quot;Failed to write to file \(filename)&quot;)
    }
}

func readFromFile(_ filename: String) -&amp;gt; String? {
    do {
        return try String(contentsOfFile: filename, encoding: .utf8)
    } catch {
        print(&quot;Failed to read from file \(filename)&quot;)
        return nil
    }
}

func readAndWrite(name: String) {
    writeToFile(filename, contents: &quot;Hello, \(name)!&quot;)
    print(&quot;Contents of the file: \(readFromFile(filename)!)&quot;)
}

if CommandLine.arguments.count &amp;gt; 1 {
    let name = CommandLine.arguments[1]
    readAndWrite(name: name)
} else {
    print(&quot;\u{001B}[0;31mPlease provide a name\u{001B}[0;0m&quot;)
    if let name = readLine() {
        readAndWrite(name: name)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And like before, we&apos;ll save this to a file, and run it with &lt;code&gt;swift &amp;lt;filename&amp;gt;.swift&lt;/code&gt;.&lt;br /&gt;
&lt;/p&gt;
&lt;p&gt;As you can see, we are now both reading &lt;em&gt;and&lt;/em&gt; writing from a file. This is a common task for scripts, and Swift makes it incredibly easy and, more importantly, safe.&lt;/p&gt;
&lt;h2&gt;Additional Resources&lt;/h2&gt;
&lt;p&gt;The French Connection conference has recently released their &lt;a href=&quot;https://async.techconnection.io/frenchkit&quot;&gt;talk videos&lt;/a&gt;, including an excellent presentation by Natan Rolnik about this very topic! You can check out his talk &lt;a href=&quot;https://async.techconnection.io/talks/swift-connection/swift-connection-2024/natan-rolnik-mastering-swift-for-scripting-and-tooling&quot;&gt;here&lt;/a&gt;, and visit his blog at &lt;a href=&quot;https://swifttoolkit.dev/&quot;&gt;swifttoolkit.dev&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As you&apos;ve seen, writing scripts in Swift is incredibly easy. This can be expanded upon almost endlessly - the obvious next step is compiling an executable. You have all the power of Swift, with its type safety and elegant syntax, at your disposal. So go ahead, write some scripts, and have fun with it!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>My Favorite Dev Tools for iOS &amp; CI/CD</title><link>https://nowham.dev/posts/ios-ci-cd-dev-tools/</link><guid isPermaLink="true">https://nowham.dev/posts/ios-ci-cd-dev-tools/</guid><description>Useful tools that I use on a daily basis</description><pubDate>Mon, 18 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;I&apos;m a big tool fan boi. I love finding tools that either help make my life easier, more comfortable, or just cooler. So here&apos;s a list of some of my favorite tools that I use on a daily basis.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://github.com/jesseduffield/lazygit&quot;&gt;LazyGit&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I know I know, everyone has they&apos;re preferred git and they aren&apos;t likely to change it, and a terminal, keyboard-based, no mouse git client is a hard sell for most people. but hear me out!&lt;/p&gt;
&lt;p&gt;Firstly, it&apos;s incredibly quick. I can stage, commit, push, pull, and do everything else I need to do with git in a fraction of the time it would take me in the terminal.&lt;/p&gt;
&lt;p&gt;Secondly, because it&apos;s all in the terminal, I can do everything with my keyboard, and once you master all of the shortcuts, you&apos;ll be flying through your git workflow.&lt;/p&gt;
&lt;p&gt;Lastly, it&apos;s just so damn pretty. I mean, look at it! It&apos;s like a little game, and I love it. It makes me feel like a real hacker, and that&apos;s all I&apos;ve ever wanted.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.xcodes.app/&quot;&gt;Xcodes&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Yup, Xcodes, plural, not Xcode. This incredible tool help manage all of your Xcode installations. It&apos;s a simple GUI that allows you to download, install, and switch between different versions of Xcode with ease.&lt;br /&gt;
No more waiting 4 hours for a download from the App Store, no more downloading zip files and doing the hard work yourself. Just click a button, and you&apos;re good to go.&lt;br /&gt;
It helps as well that it&apos;s insanely quick because it uses parallel downloading, so getting a version downloaded and installed is a matter of minutes, not hours.&lt;/p&gt;
&lt;p&gt;In addition to all that, you can see your installed version &lt;strong&gt;AND&lt;/strong&gt; installed platforms (like which iOS versions you&apos;ve got), so you can clean up the ones you don&apos;t use, and minimize a bit of the insane disk space that Xcode takes up.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://github.com/github/CopilotForXcode&quot;&gt;Copilot for Xcode&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The official Github plugin for copilot. I&apos;ve used Copilot for Swift development before on VSCode, and it was nice but not particularly useful. But in Xcode, it&apos;s a whole different story.&lt;br /&gt;
It&apos;s lightyears better than the apple intelligence one, in my opinion, and it really speeds up my development.&lt;/p&gt;
&lt;p&gt;I think the main bit to have in mind when using it is - this is an assistant, not a boss. It&apos;s there to help you, not to do the work for you. So use it as a tool, not as a crutch.&lt;br /&gt;
So don&apos;t expect to give it a prompt and have it spit out and entire functioning app, but do you need help with protocol conformance? It&apos;s got you covered. Unit tests? east peasy. It&apos;s a great tool, and I highly recommend it.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://raycast.com/&quot;&gt;Raycast&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Oh Raycast, my beloved Raycast. I&apos;ve been using Raycast for a while now, and I can&apos;t imagine my life without it.&lt;br /&gt;
I know for a fact that i&apos;m not even using it to it&apos;s fullest potential, but even with the small amount that I do use, it&apos;s a game-changer.&lt;br /&gt;
I use it to quickly open meetings, search file, open apps, and even to do quick calculations. It&apos;s like spotlight, but on steroids. As a matter of fact, I&apos;ve disabled spotlight on my Mac because I never use it anymore.&lt;/p&gt;
&lt;p&gt;Not only that, but the built-in paste board has changed my life completely and i don&apos;t know how i ever lived without it.&lt;br /&gt;
Honestly I cannot recommend Raycast enough, it&apos;s a must-have for any Mac user.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://arc.net/&quot;&gt;Arc Browser&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;By now I&apos;m sure most of you have heard of Arc. I know they&apos;re a bit controversial at the moment, given that the CEO has announced that they&apos;re not going to be adding and new features to it, but honestly? I don&apos;t care.&lt;/p&gt;
&lt;p&gt;I&apos;ve been using Arc for about 18 months now, since i started working for a company that heavily realized on the Google suite ( think, Google Meet, Google Sheets etc), and a Chromium-based browser was a godsend.&lt;/p&gt;
&lt;p&gt;I was a hated of the design first, but not only did it grow on me, I&apos;m at a point where I can&apos;t go back. The smart PR folder for all of my PR tabs, the pinned tabs, the key-board shortcuts (are you seeing a pattern here?), and the built-in ad blocker. It&apos;s just so good.&lt;/p&gt;
&lt;p&gt;Plus, it&apos;s written in Swift, which is fun little tidbit that I love to tell people.&lt;/p&gt;
&lt;p&gt;So if you haven&apos;t tried Arc yet, I highly recommend it. It&apos;s a great browser, and I&apos;m sure you&apos;ll love it. Or don&apos;t. live your life. But you should try it.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;These are just 5 of the tools I use on a daily basis, but there are a ton more than are just as good and cool but haven&apos;t made the cut for this list, like iTerm, Bartender, CleanMyMac, and so many more, but these are the ones that I use every single day, and I can&apos;t imagine my life without them. I&apos;d love to hear what tools you use on a daily basis, and if you have any recommendations for me, so please let me know!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>Bump Swift Package Version Automatically with Fastlane</title><link>https://nowham.dev/posts/fastlane-bump-spm-version/</link><guid isPermaLink="true">https://nowham.dev/posts/fastlane-bump-spm-version/</guid><description>Automate the process of bumping the version of your SPM package using Fastlane</description><pubDate>Fri, 15 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;If you&apos;re using SPM (Swift Package Manager) for your SDKs/ packages / to have a modularized project, you might have had an issue with forgetting to bump the version of the package before merging, and having to do a silly PR after the fact, usually called &quot;bump_version&quot;, and have your teammates mock you and your forgetfulness.&lt;br /&gt;
Well, fear no more ! I am here to show you how to automate this process using Fastlane!&lt;/p&gt;
&lt;h2&gt;Assumptions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;You&apos;ve already got Fastlane setup for your project, and that you&apos;ve got familiarity with what Fastlane &lt;em&gt;is&lt;/em&gt; and how it works.&lt;/li&gt;
&lt;li&gt;Your SPM project is following sem-ver versioning&lt;/li&gt;
&lt;li&gt;You create releases for your SPM package on GitHub&lt;/li&gt;
&lt;li&gt;Your git config is setup correctly, and you can push to the remote without any issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Plan&lt;/h2&gt;
&lt;p&gt;So, how are we going to do this? well, easy enough! what we&apos;re going to do, is create a flow that will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check the current version of our package&lt;/li&gt;
&lt;li&gt;Check the remote version of our package&lt;/li&gt;
&lt;li&gt;If the remote version is higher than the local version, we&apos;ll bump the version of our package&lt;/li&gt;
&lt;li&gt;If the remote version is the same as the local version, we&apos;ll bump it still&lt;/li&gt;
&lt;li&gt;if the local is higher than the remote, we&apos;ll leave it as is (as it was already bumped)&lt;/li&gt;
&lt;li&gt;We&apos;ll push the new version to the remote&lt;/li&gt;
&lt;li&gt;We&apos;ll sit back and have a coffee, knowing that we&apos;ve automated this process and our imposter syndrome is at bay&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;p&gt;We&apos;re going to follow the steps above, by writing lanes and ruby methods to assist us. (ICYMI, Fastlane &lt;em&gt;is&lt;/em&gt; Ruby based, so we can use Ruby!)&lt;/p&gt;
&lt;h3&gt;Step 1: Find the current package version&lt;/h3&gt;
&lt;p&gt;Following the steps we&apos;ve outlined above, let&apos;s start by finding out what&apos;s the current version of our SPM package.&lt;br /&gt;
To do that, let&apos;s write a quick ruby method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &apos;The version of the SPM pacakge&apos;
def package_version
  version_line =
    File.open(&apos;../Package.swift&apos;) do |file|
      file.find do |line|
        line =~ /^let packageVersion =/
      end
    end
  version_line.tr!(&quot;&apos;a-zA-Z\n=\&quot;\s&quot;, &apos;&apos;)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s unpack it a bit, because it&apos;s a bit daunting to look at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First we find the &lt;code&gt;Package.swift&lt;/code&gt; file. we need to put a &lt;code&gt;../&lt;/code&gt; before it, because things are being called from the &lt;code&gt;fastlane&lt;/code&gt; folder.&lt;/li&gt;
&lt;li&gt;Once we found and opened the file, we&apos;re reading it until we find the line starting with &lt;code&gt;let packageVersion =&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Lastly, we&apos;re removing all the characters we don&apos;t need, so we&apos;re trimming the line and leaving just the version number.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 2: Find the remote package version&lt;/h3&gt;
&lt;p&gt;Great! so we have a way to get the local package number, now we need a way to get the remote one as well!&lt;br /&gt;
To do that we&apos;re going to, you&apos;ve guessed it, write another ruby method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &apos;Get the latest version that was released on git&apos;
def released_package_version
  sh(&apos;git&apos;, &apos;describe&apos;, &apos;--tags&apos;, &apos;--abbrev=0&apos;, log: false) do |status, output, _command|
    return &apos;0.0.0&apos; unless status.success?
    return output.to_s.tr(&quot;a-z&apos;\n\s&quot;, &apos;&apos;)
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again, a bit of a mouthful, but let&apos;s break it down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We&apos;re calling &lt;code&gt;git describe --tags --abbrev=0&lt;/code&gt; to get the latest tag that was released, suppressing log output for cleaner results.&lt;/li&gt;
&lt;li&gt;If the command fails, we&apos;re returning &lt;code&gt;0.0.0&lt;/code&gt; as a default value&lt;/li&gt;
&lt;li&gt;And again we&apos;re using a similar regex to trim the output and leave just the version number.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 3: Comparing&lt;/h3&gt;
&lt;p&gt;Now that we&apos;ve got methods to get both local and remote version of our package, let&apos;s compare them and see if we need to bump.&lt;br /&gt;
Can you guess what we&apos;re going to do? yes, write another method!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &apos;Bumps the version of an SPM package if needed.&apos;
def bump_version_if_required
  local_version_number = package_version
  remote_version_number = released_package_version
  # If the local version is smaller or equal to the remove version, bump it
  if Gem::Version.new(local_version_number) &amp;lt;= Gem::Version.new(remote_version_number)
    # Here we&apos;re going to bump the version
  else
    # If it&apos;s not, it was already bumped and we can ignore it
    puts &apos;Local version number is up to date&apos;
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This one is a bit more straightforward, we&apos;re comparing the two versions, and if the local version is smaller or equal to the remote version, we&apos;re going to bump it.&lt;br /&gt;
We&apos;re using the built in Gem::Version class to compare the two versions, so we don&apos;t have to deal with the sem-ver&lt;br /&gt;
comparing ourselves.&lt;/p&gt;
&lt;h3&gt;Step 4: Bumping the version&lt;/h3&gt;
&lt;p&gt;Now that we&apos;ve got the comparison, we need a way to &lt;em&gt;actually&lt;/em&gt; bump the version when it&apos;s needed.&lt;/p&gt;
&lt;p&gt;Firstly, let&apos;s write a to do the actual bumping:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def bump_version_number(version_number)
  version_number_array = version_number.split(&apos;.&apos;).map(&amp;amp;:to_i)
  version_number_array[-1] += 1
  version_number_array.join(&apos;.&apos;)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This method takes in a version, bumps it&apos;s &lt;code&gt;patch&lt;/code&gt; number and returns the new version. simple!&lt;/p&gt;
&lt;p&gt;Next, we need to write a method that will actually write the new version to the &lt;code&gt;Package.swift&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def bump_version(remote_version)
  package_version_key = &apos;let packageVersion = &apos;
  file_path = &apos;../Package.swift&apos;
  file_contents = File.read(file_path)
  matching_line = file_contents.lines.find { |line| line.include?(package_version_key) }
  new_version_number = bump_version_number(remote_version)
  puts &quot;New local version number: #{new_version_number}&quot;
  new_line = &quot;#{package_version_key}\&quot;#{new_version_number}\&quot;\n&quot;
  updated_contents = file_contents.gsub(matching_line, new_line)
  File.write(file_path, updated_contents)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is very similar to what we&apos;ve done above when we wanted to read the version number, but this time we&apos;re writing it back to the file.&lt;br /&gt;
So let&apos;s break it down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We&apos;re setting variables for things that we&apos;re going to use in a few places, because DRY! (remember, just because it&apos;s automation code, doesn&apos;t meant it doesn&apos;t need to be good code!)&lt;/li&gt;
&lt;li&gt;Reading the line we want, and storing it in a variable&lt;/li&gt;
&lt;li&gt;Bumping the version number&lt;/li&gt;
&lt;li&gt;Creating a new line with the bumped version number&lt;/li&gt;
&lt;li&gt;Replacing the old line with the new one&lt;/li&gt;
&lt;li&gt;Writing the new contents back to the file!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And that&apos;s it, voila and you&apos;ve bump the version of your SPM package! let&apos;s have a quick update at our &lt;code&gt;bump_version_if_required&lt;/code&gt; method, to include the new bumping logic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &apos;Bumps the version of an SPM package if needed.&apos;
def bump_version_if_required
  local_version_number = spm_package_version
  remote_version_number = released_package_version
  # If the local version is smaller or equal to the remove version, bump it
  if Gem::Version.new(local_version_number) &amp;lt;= Gem::Version.new(remote_version_number)
    # Here we&apos;re going to bump the version
    puts &apos;Bumping version number&apos;
    bump_version(remote_version_number)
    puts &apos;Version number bumped&apos;
    puts spm_package_version
  else
    # If it&apos;s not, it was already bumped and we can ignore it
    puts &apos;Local version number is up to date&apos;
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 5: Pushing the new version&lt;/h3&gt;
&lt;p&gt;Now that we&apos;ve bumped the version, we need to push it to the remote, so that it&apos;s up to date.&lt;br /&gt;
let&apos;s write a lane that will do that for us. We&apos;re going to use the &lt;code&gt;git&lt;/code&gt; action that Fastlane provides, to do this, so we will need to have&lt;br /&gt;
&lt;code&gt;require &apos;git&apos;&lt;/code&gt; and &lt;code&gt;fastlane_require &apos;git&apos;&lt;/code&gt; at the top of our file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lane :commit_to_git do |options|
  repo = options[:repo] || Git.open(&apos;.&apos;)
  current_branch = repo.current_branch
  git_commit(path: &apos;.&apos;, message: options[:commit_message])

  begin
    repo.push(&apos;origin&apos;, current_branch)
  rescue Git::GitExecuteError =&amp;gt; e
    raise e unless e.message.match?(/no upstream branch/)

    repo.push(&apos;origin&apos;, current_branch, set_upstream: true)
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This just takes in a parameter, &lt;code&gt;commit_message&lt;/code&gt;, and a &lt;code&gt;repo&lt;/code&gt;, and commits and pushes the changes to the remote.&lt;br /&gt;
If for some reason the remote isn&apos;t there, or we get an error, we print it out so we&apos;ll know what happened.&lt;br /&gt;
let&apos;s add that to our &lt;code&gt;bump_version_if_required&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc &apos;Bumps the version of an SPM package if needed.&apos;
def bump_version_if_required
  local_version_number = spm_package_version
  remote_version_number = released_package_version
  # If the local version is smaller or equal to the remove version, bump it
  if Gem::Version.new(local_version_number) &amp;lt;= Gem::Version.new(remote_version_number)
    # Here we&apos;re going to bump the version
    puts &apos;Bumping version number&apos;
    bump_version(remote_version_number)
    puts &apos;Version number bumped&apos;
    repo = Git.open(&apos;.&apos;)
    commit_to_git(commit_message: &quot;Bump version number from #{local_version_number} to #{spm_package_version}&quot;, repo: repo)

  else
    # If it&apos;s not, it was already bumped and we can ignore it
    puts &apos;Local version number is up to date&apos;
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 6: Putting it all together&lt;/h3&gt;
&lt;p&gt;Well done on making it so far! we&apos;ve done it, we&apos;ve created a flow for us to automate the version bumping of&lt;br /&gt;
our SPM package, and we&apos;ve done it using Fastlane! let&apos;s put it all together in a &lt;code&gt;Fastfile&lt;/code&gt;, so we can call it easily:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/ruby
# frozen_string_literal: true

fastlane_version &apos;2.225.0&apos;
fastlane_require &apos;git&apos;

# rubocop:disable Metrics/BlockLength
default_platform :ios
platform :ios do
  desc &apos;The version of the SPM pacakge&apos;
def spm_package_version
  version_line =
    File.open(&apos;../Package.swift&apos;) do |file|
      file.find do |line|
        line =~ /^let packageVersion =/
      end
    end
  version_line.tr!(&quot;&apos;a-zA-Z\n=\&quot;\s&quot;, &apos;&apos;)
end

desc &apos;Get the latest version that was released on git&apos;
def released_package_version
  # Get the most recent Git tag, suppressing log output for cleaner results
  sh(&apos;git&apos;, &apos;describe&apos;, &apos;--tags&apos;, &apos;--abbrev=0&apos;, log: false) do |status, output, _command|
    # Returns a placeholder if no Git tags have been defined, indicating no release has been made
    return &apos;&amp;lt;never released&amp;gt;&apos; unless status.success?

    # Removes any lowercase letters, apostrophes, newline, and whitespace characters from the Git tag,
    # so we get just a number,and  returns the cleaned version number as the result of the method
    return output.to_s.tr(&quot;a-z&apos;\n\s&quot;, &apos;&apos;)
  end
end

desc &apos;Bumps the version of an SPM package if needed.&apos;
def bump_version_if_required
  local_version_number = spm_package_version
  remote_version_number = released_package_version
  # If the local version is smaller or equal to the remove version, bump it
  if Gem::Version.new(local_version_number) &amp;lt;= Gem::Version.new(remote_version_number)
    # Here we&apos;re going to bump the version
    puts &apos;Bumping version number&apos;
    bump_version(remote_version_number)
    puts &apos;Version number bumped&apos;
    repo = Git.open(&apos;.&apos;)
    commit_to_git(commit_message: &quot;Bump version number from #{local_version_number} to #{spm_package_version}&quot;, repo: repo)

  else
    # If it&apos;s not, it was already bumped and we can ignore it
    puts &apos;Local version number is up to date&apos;
  end
end

def bump_version_number(version_number)
  version_number_array = version_number.split(&apos;.&apos;).map(&amp;amp;:to_i)
  version_number_array[-1] += 1
  version_number_array.join(&apos;.&apos;)
end

def bump_version(remote_version)
  package_version_key = &apos;let packageVersion = &apos;
  file_path = &apos;../Package.swift&apos;
  file_contents = File.read(file_path)
  matching_line = file_contents.lines.find { |line| line.include?(package_version_key) }
  new_version_number = bump_version_number(remote_version)
  puts &quot;New local version number: #{new_version_number}&quot;
  new_line = &quot;#{package_version_key}\&quot;#{new_version_number}\&quot;\n&quot;
  updated_contents = file_contents.gsub(matching_line, new_line)
  File.write(file_path, updated_contents)
end

lane :commit_to_git do |options|
  repo = options[:repo] || Git.open(&apos;.&apos;)
  current_branch = repo.current_branch
  git_commit(path: &apos;.&apos;, message: options[:commit_message])

  begin
    repo.push(&apos;origin&apos;, current_branch)
  rescue Git::GitExecuteError =&amp;gt; e
    raise e unless e.message.match?(/no upstream branch/)

    repo.push(&apos;origin&apos;, current_branch, set_upstream: true)
  end
end

lane :bump_spm_package do
  puts bump_version_if_required
end

end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&apos;s it! now you can call &lt;code&gt;fastlane bump_spm_package&lt;/code&gt; and it will bump the version of your SPM package for you!&lt;br /&gt;
this can be called locally or from a CI, whatever your setup is.&lt;/p&gt;
&lt;p&gt;This was a lot of fun to do, and shows how small things can really increase our quality of life, and remove a concern from us - so we can focus on what&apos;s really important, arguing on Twitter&lt;br /&gt;
Thanks for reading!&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item><item><title>How to Write Your First CircleCI Orb</title><link>https://nowham.dev/posts/circleci-orb-intro/</link><guid isPermaLink="true">https://nowham.dev/posts/circleci-orb-intro/</guid><description>A quick overview of how to write an orb for CircleCI, and how to use it in your config.</description><pubDate>Fri, 08 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Reasoning&lt;/h2&gt;
&lt;p&gt;The issue we&apos;ve faced at work is that we&apos;ve had multiple projects, living in multiple repos, each with their own CircleCI config file. While this might work (ish), it&apos;s no ideal for several reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;It&apos;s hard to maintain. If we want to change something in the config, we have to do it in multiple places.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It&apos;s hard to keep in sync. If we want to add a new job, we have to do it in multiple places.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It scratches me wrong. as a programmer, I don&apos;t like to repeat myself. I like to keep things DRY.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Approach&lt;/h2&gt;
&lt;p&gt;So to combat all of the above, i&apos;ve looked into a few options from CircleCI. The first one I looked at was Dynamic Configs [link needed]. This is a good approach for some, but it felt a bit overcomplicated for us, and it didn&apos;t feel like tool for the job. it&apos;s not about having configs that dynamically change according to the project, it&apos;s about having a set of reusable jobs that we can use across multiple projects.&lt;/p&gt;
&lt;p&gt;So naturally the next step was authoring an Orb. If you&apos;ve ever used CircleCI for iOS development, or used it to have Slack alert - you&apos;ve used Orbs before. Orbs let you setup reusable jobs, commands, and executors that you can use across multiple projects. They&apos;re a great way to keep your config DRY, and to keep your config in sync.&lt;/p&gt;
&lt;p&gt;The main attracting points for us were:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We could have a single source of truth for our jobs. If we wanted to change a job, we could do it in one place.&lt;/li&gt;
&lt;li&gt;We could have our executors setup in once place. this means that when we needed to update our Xcode version, or the MacOS version, we only needed to do it once.&lt;/li&gt;
&lt;li&gt;There&apos;s a dev release before a prod release. this means that we don&apos;t have to break everything everywhere at once; we can develop and test and only deploy when we&apos;re ready and sure of ourselves.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;Firstly, I am going to assume you&apos;ve got all the right permissions for the repo / project that your&apos;e working on.&lt;br /&gt;
i&apos;d suggest you start off using the orb template that CircleCI provides. You can find it &lt;a href=&quot;https://github.com/CircleCI-Public/Orb-Template&quot;&gt;here&lt;/a&gt;. This will give you a good starting point for your orb. This will give you the basic file structure so we can keep working on it; now let&apos;s go over the files and what they do, and why you might care about them:&lt;/p&gt;
&lt;h3&gt;@orb.yml&lt;/h3&gt;
&lt;p&gt;This is the &quot;front page&quot; of your orb, this is where you have the description of the orb, and it holds the description of the orb, the home and source URLs (where does this orb live? ), and perhaps most importantly - this is where you will define any orbs that &lt;strong&gt;your&lt;/strong&gt; orb depends on (if any). So for our usage as an iOS app, we had to use the &lt;code&gt;macos&lt;/code&gt; orb, and the &lt;code&gt;slack&lt;/code&gt; orb which we used for notifications.&lt;/p&gt;
&lt;h3&gt;Commands&lt;/h3&gt;
&lt;p&gt;This folder is where you put all of your, well, commands. Commands are the building blocks of workflow, these should be concise, and encapsulated. We will go into how to write a command in a bit.&lt;/p&gt;
&lt;h3&gt;Executors&lt;/h3&gt;
&lt;p&gt;This is where you can define what you jobs will run &lt;em&gt;on&lt;/em&gt;. Executors are the environment that your jobs will run in. This is where you can define the image, the environment variables, and the resources that your job will have access to. For our example, this is also where you define your mac version, and your xcode version, which are very important.&lt;/p&gt;
&lt;h3&gt;Includes&lt;/h3&gt;
&lt;p&gt;This is where you can any scripts you want to be bundled into the orb. These are BASH scripts that you can use in your jobs, and having them here means that you can keep your jobs clean and concise, and you have the ability to write tests for them (although I never did, because I&apos;m a bad person).&lt;/p&gt;
&lt;h3&gt;Jobs&lt;/h3&gt;
&lt;p&gt;These are the meat and potatoes of your orb - these are the things that will be visible to the config calling the orb, this is where you will define what parameters you need, what executors you will use, and what commands you will run.&lt;/p&gt;
&lt;h3&gt;Examples&lt;/h3&gt;
&lt;p&gt;Here you should put what an example usage of your orb would look like. This is a good place to show off what your orb can do, and how it can be used.&lt;/p&gt;
&lt;h2&gt;Writing an executor&lt;/h2&gt;
&lt;p&gt;Like we mentioned, executors are the machines and environments that your jobs will run on. Here&apos;s an example of an executor that we used in our orb:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;description: &amp;gt;
  Default mac executor

macos:
  xcode: &quot;16.0.0&quot;
  resource_class: macos.m1.medium.gen1

shell: /bin/bash --login -o pipefail

environment:
  FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: &quot;120&quot;
  FASTLANE_XCODEBUILD_SETTINGS_RETRIES: &quot;5&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we have the description, which is pretty self explanatory. We then have the &lt;code&gt;macos&lt;/code&gt; key, which is where we define the xcode version that we want to use, and the resource class that we want to use. We then have the &lt;code&gt;shell&lt;/code&gt; key, which is where we define the shell that we want to use. Finally, we have the &lt;code&gt;environment&lt;/code&gt; key, which is where we define any environment variables that we want to use. This is where we define the &lt;code&gt;FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT&lt;/code&gt; and &lt;code&gt;FASTLANE_XCODEBUILD_SETTINGS_RETRIES&lt;/code&gt; variables, which are used by fastlane.&lt;/p&gt;
&lt;h2&gt;Writing a command&lt;/h2&gt;
&lt;p&gt;Like we mentioned above, commands are the building blocks of jobs. They can have as many or as few steps as you&apos;d like, but they should be concise, and encapsulated. Here&apos;s an example of a command that we used in our orb:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;description: &quot;Execute fastlane based on the lane name&quot;

parameters:
  fastlane_type:
    type: string

steps:
  - run:
      name: Executing fastlane &amp;lt;&amp;lt; parameters.fastlane_type &amp;gt;&amp;gt;
      command: |
        bundle exec fastlane &amp;lt;&amp;lt; parameters.fastlane_type &amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command is pretty simple, it takes a &lt;code&gt;fastlane_type&lt;/code&gt; parameter, and then runs the fastlane command with that parameter. As you can see it&apos;s quite reusable, this can be called from where ver and doesn&apos;t depend on anything or any project.&lt;/p&gt;
&lt;h2&gt;Writing a job&lt;/h2&gt;
&lt;p&gt;Now that you&apos;ve written a command, you can write a job. Jobs are the things that will be visible to the config calling the orb, and if your commands relies on other things happening before it, or after it, this is where you&apos;ll define that flow. Here&apos;s an example of a job that we used in our orb:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;description: &amp;gt;
  Execute any lane

parameters:
  fastlane:
    type: string

executor: mac_executor

steps:
  - load_workspace
  - restore_cache_gems
  - fastlane_execute_lane:
      fastlane_type: &amp;lt;&amp;lt;parameters.fastlane&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This job is a bit more complex, it takes a &lt;code&gt;fastlane&lt;/code&gt; parameter, and then runs a series of commands before running the &lt;code&gt;fastlane&lt;/code&gt; command. This is where you can define the flow of your jobs, and how they should be run. Lets explain a bit of what&apos;s going on here:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. The description is to explain to whatever is calling this job what the does the job do. pretty self explanatory.
2. The parameters are the things that you can pass to the job. In this case, we&apos;re passing a `fastlane` parameter, which is a string.
3. This needs to run on a mac machine, so we use a mac executer that we&apos;ve defined in the `executors` folder.
4. We then run a series of commands, `load_workspace`, `restore_cache_gems`, and then `fastlane_execute_lane` which is the command we defined above. To call a job, you just need to use it&apos;s file name. so we saved the above job as `fastlane_execute_lane`, so we call it as `fastlane_execute_lane`.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Orb development&lt;/h2&gt;
&lt;p&gt;Congratulations! You&apos;ve written your first orb, with a command, a job, and an executor. Now you&apos;re ready to use it&apos;s dev version and try it out. if you&apos;ve used the template provided above, you&apos;ll notice that this orb project has a &lt;code&gt;.circleci&lt;/code&gt; folder, which holds a config. we&apos;re not going go over how things work in that config, but what you need to know for now is - if you push your branch, a workflow will be triggered that will create a dev version of your build. This will happen in the &lt;code&gt;publish&lt;/code&gt; job, and when it&apos;s done you&apos;ll be able to see the hash of your new dev orb ( which will be available for 90 days), or you can just use &lt;code&gt;@dev:alpha&lt;/code&gt; to use the latest dev version of your orb.&lt;br /&gt;
Now you can import it in whatever config that you want to use it in, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
version: 2.1

orbs:
our-orb: our-org/orb-name@dev:alpha

// The rest of your config...


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you&apos;ve got your orb in your config, you can start using it&apos;s jobs in your workflows.&lt;br /&gt;
Here&apos;s an example of how you might use the job we defined above:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fastlane:
  jobs:
    - checkout_code
    - our-orb/fastlane:
        fastlane: &quot;test&quot;
        requires:
          - checkout_code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So by doing this, we&apos;ve defined a workflow that will run the &lt;code&gt;checkout_code&lt;/code&gt; job, and then run the &lt;code&gt;fastlane&lt;/code&gt; job that we defined in our orb. This is a very simple example, but you can see how you can start to build up your workflows using the jobs that you&apos;ve defined in your orb. You can also pass those variables through the pipeline, you can have them passed in as ENV variables, or you can hard code them, it&apos;s up to you.&lt;/p&gt;
&lt;p&gt;Once your&apos;e done, if you merge your orb into master / main, and create a tag with a sem-ver version, your orb will be published and available for use in any config that you want to use it in.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This was a very high level overview of how to write an orb, and how to use it in your config. There&apos;s a lot more that you can do with orbs, and a lot more that you can do with CircleCI. I&apos;ve really enjoyed working with them, and the CircleCI documentation is nice and descriptive, but can be a tad overwhelming.&lt;br /&gt;
I hope you&apos;ve enjoyed this brief post, and I&apos;d love to hear if you&apos;ve got questions or comments. I&apos;m always looking to learn more, and to help others learn more too.&lt;/p&gt;
</content:encoded><author>NoamIfergan@gmail.com</author></item></channel></rss>