<?xml version="1.0" encoding="UTF-8" standalone="no"?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0">

<channel>
	<title>Wulf's Bookmarks</title>
	<atom:link href="https://adamwulf.me/feed/" rel="self" type="application/rss+xml"/>
	<link>https://adamwulf.me</link>
	<description>Browse and discuss Adam's bookmarks at http://welcome.totheinter.net/bookmarks/</description>
	<lastBuildDate>Mon, 17 Mar 2025 05:19:01 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8.1</generator>
	<item>
		<title>Building a MCP server in Swift</title>
		<link>https://adamwulf.me/2025/03/building-a-mcp-server-in-swift/</link>
					<comments>https://adamwulf.me/2025/03/building-a-mcp-server-in-swift/#respond</comments>
		
		<dc:creator><![CDATA[Adam Wulf]]></dc:creator>
		<pubDate>Sun, 16 Mar 2025 22:33:10 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://adamwulf.me/?p=6001</guid>

					<description><![CDATA[Cursor is a fantastic tool for developing with AI, but it&#8217;s also a wonderful tool for interacting with any folder of files with AI. I use it to search through and chat with a folder of 1000s of Markdown files (downloaded from my Notion bookmarks database). Cursor indexes the files with its vector index, can [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Cursor is a fantastic tool for developing with AI, but it&#8217;s also a wonderful tool for interacting with any folder of files with AI. I use it to search through and chat with a folder of 1000s of Markdown files (downloaded from my Notion bookmarks database). Cursor indexes the files with its vector index, can read the file contents, summarize multiple files, and link to the source files &#8211; it&#8217;s changed how I interact with my bookmarks.</p>



<p>So when <a href="https://www.cursor.com/changelog/-cursor-rules-better-codebase-understanding-new-tab-model">Cursor added MCP support</a> &#8211; I was very excited. Instead of opening a Cursor window pointed at my bookmarks folder, I could build my own MCP server that indexed the bookmarks, and provide tools so that local bookmark search was available in <em>all</em> of my Cursor windows. I&#8217;ve successfully nerd-sniped myself into the MCP abyss for the foreseeable weekends.</p>



<h2 class="wp-block-heading">Just gimme the code</h2>



<p><em>Ok ok!</em> Here&#8217;s my <a href="https://github.com/adamwulf/mcp-template">barebones MCP server in Swift</a> that uses <code><a href="https://github.com/loopwork-ai/mcp-swift-sdk">loopwork-ai/mcp-swift-sdk</a></code>.</p>



<h2 class="wp-block-heading">MCP in Swift</h2>



<p>It&#8217;s not hard to find <a href="https://github.com/modelcontextprotocol/servers">example MCP servers</a>, it&#8217;s a bit harder to <a href="https://youtu.be/oAoigBWLZgE?si=fFSeHOgqh6CIsm0w&amp;t=299">find MCP servers that work</a>, and it&#8217;s very hard to find <a href="https://github.com/loopwork-ai/iMCP">MCP servers written in Swift</a>. I found two SDKs on Github that look promising, the first one I found is <a href="https://github.com/gsabran/mcp-swift-sdk">https://github.com/gsabran/mcp-swift-sdk</a>, and the second is the similarly-named-but-different <a href="https://github.com/loopwork-ai/mcp-swift-sdk">https://github.com/loopwork-ai/mcp-swift-sdk</a>. Both are under active development, so by the time you read this the &#8216;best&#8217; one of these two may change, or even have more libraries available on Github.</p>



<p>I started with <code>gsabran</code>&#8216;s repo, and now I&#8217;m happier with <code>loopwork-ai</code>&#8216;s repository. Maybe it&#8217;s because their repo is better, or maybe it&#8217;s because I understand things better now, who could say?</p>



<p>A big benefit of <code>loopwork-ai</code>&#8216;s repo &#8211; it also contains a fully functional Mac app that attaches to the MCP server. So how does that work? what is an &#8220;MCP server&#8221; anyway?</p>



<h2 class="wp-block-heading">MCP Architecture</h2>



<p>Servers need to accept input requests and provide responses somehow. MCP is based on <a href="https://www.jsonrpc.org/specification">JSON-RPC v2.0</a>, which makes the format of its input and output very clear. The <a href="https://spec.modelcontextprotocol.io/latest">MCP specification</a> gives clear definitions for the various MCP specific messages.</p>



<p>When I hear the word &#8220;server,&#8221; I think of HTTP servers like Apache or Nginx running on port 80, or 443 for SSL, or 8080, or etc etc. However, the recommended and most common MCP server runs on <code>stdio</code> input and output streams. Huh?! It&#8217;s not how I typically think of a server. The specification does also define SSE &#8211; an MCP protocol on HTTP, though the spec says that that &#8220;Clients&nbsp;<strong>SHOULD</strong>&nbsp;support <code>stdio</code> whenever possible.&#8221;</p>



<p>I suspect this is motivated as much by security and authentication as it is by simplicity. If clients spawn the MCP process locally and communication through <code>stdin</code> and <code>stdout</code>, there&#8217;s a much smaller surface area for bad actors. If I instead run a small HTTP server locally on my Mac &#8211; that&#8217;s a much bigger potential hole in my security, both by accidentally exposing the MCP outside my device, as well as any security holes in the HTTP server I use.</p>



<p>At the end of the day, clients and servers are just trading JSON back and forth, so <a href="https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#custom-transports">custom transports are entirely possible</a>.</p>



<h2 class="wp-block-heading">Specification Summary</h2>



<p>Very simply, MCP servers provide prompts, resources, and tools. Clients connect to servers and discover what&#8217;s provided, and then surface those to the LLM and/or user as they see fit. The easiest way to think of an MCP server is a bag of functions &#8211; servers list the functions (tools) to the client, and the client calls those functions with some input.</p>



<p>Importantly, the MCP server does not see the context of the chat with the LLM &#8211; it only gets the parameters sent for a specific tool invocation. Clients are encouraged to verify with the user before calling a tool, so this means minimal context is sent to an MCP server, and all tools calls ideally go through human approval to mitigate the risk of leaking sensitive data.</p>



<h2 class="wp-block-heading">Authentication</h2>



<p>The MCP specification does not specify authentication, just like HTTP. Any authorization that you want to enforce for a client/server session you must handle yourself. For <a href="https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio"><code>stdio</code> servers</a>, you can track state in your running server without issue, as its only ever connected to a single client. Since it&#8217;s using input/output streams, there&#8217;s generally no authentication for stdio connections which is much easier.</p>



<p>For <code>SSE</code> servers, you&#8217;ll need to handle authentication yourself, potentially with HTTP headers or something similar, potentially with a <code>Bearer</code> token header using OAuth, etc.</p>



<h2 class="wp-block-heading">SSE Transport</h2>



<p>A quick primer on the <a href="https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse">SSE protocol</a> &#8211; while the stdio transport allows only a single client to connect to the single server, an SSE enabled MCP server can potentially serve multiple clients. It does this by exposing a single GET endpoint. When a new connection is made, the server immediately sends a POST endpoint for that client to send requests to. Since all clients are using the same GET request, the server is responsible for separating out which requests belong to which GET connection. One strategy would be to use a separate POST endpoint per connection.</p>



<p>This <a href="https://github.com/modelcontextprotocol/specification/discussions/102">discussion in the modelcontextprotocol/specification Github</a> is a deep dive on the tradeoffs of stateful and stateless servers and possible future changes to allow for traditional stateless HTTP architecture, or using connections like WebSockets instead of long-lived GET.</p>



<h2 class="wp-block-heading">Debugging Tools</h2>



<p>The MCP documentation lists an <a href="https://modelcontextprotocol.io/docs/tools/debugging">number of helpful debugging tools</a>.</p>



<ol class="wp-block-list">
<li><strong><a href="https://github.com/modelcontextprotocol/inspector">MCP Inspector</a></strong>: This is a small server that runs on your machine and can launch and connect to MCP servers. I strongly suggest getting your tool working with this inspector before moving on to Claude or Cursor integration.</li>



<li><strong><a href="https://modelcontextprotocol.io/quickstart/user">Claude Desktop Developer Tools</a></strong>: Claude itself can be helpful for debugging your server as you develop. Add your server into <code>claude_desktop_config.json</code> and restart Claude. Then <code>tail</code> logs at <code><br>tail -f /Users/{username}/Library/Logs/Claude/mcp-server-{yourservername}.log</code></li>



<li><strong>Server logs</strong>: I use Swift Logging for my server, as does the <code>loopwork-ai/mcp-swift-sdk</code>, which makes seeing logs in Xcode&#8217;s console very simple. The MCP specification uses <code>stdin</code> and <code>stdout</code>, so anything you send to <code>stderr</code> will generally show up in the client&#8217;s logs somewhere.</li>



<li><strong>Xcode</strong>: After launching the client (which then spawns the server), I connect the debugger to it and can then use all of the normal tools in Xcode: breakpoints, logging, etc. I also use the built executables path (Xcode → Product → Show Build Folder) as the server in Claude/Cursor so that I can rebuild in Xcode → relaunch Claude and see the new version.</li>
</ol>



<h2 class="wp-block-heading">MCP Tools Walkthrough</h2>



<p>When the server initializes, it tells the connecting client what capabilities it has, and for the <code>tool</code> capability there&#8217;s an option to notify when those tools change:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "capabilities": {
    "tools": {
      "listChanged": true
    }
  }
}</pre>



<p>Once the client hears the capabilities, it requests the tool list from the server. From what I&#8217;ve seen, clients are not very well behaved, and will ask for tools, prompts, and resources regardless of the capabilities described by the server. The specification for requesting the tool list is a request from the client:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {
    "cursor": "optional-cursor-value"
  }
}</pre>



<p>The server would reply with its list of tools according to the specification. This example shows a <code>helloPerson</code> tool that accepts a name:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "helloPerson",
        "description": "Returns a friendly greeting message",
        "inputSchema": {
          "type": "object",
          "properties": {
            "name": [
              "type": "string",
              "description": "Name of the person to say hello to",
            ]
          },
          "required": ["name"]
        }
      }
    ],
    "nextCursor": "next-page-cursor"
  }
}</pre>



<p>At this point, the client has all of the information it needs to start calling the function:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "helloPerson",
    "arguments": {
      "name": "Adam"
    }
  }
}</pre>



<p>And the server can send its response to <code>stdout</code>. Note that the response contains the <code>id</code> of the request that it is responding to. This allows multiple requests to be in-flight, and the client can match responses with the corresponding request:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Hello, Adam!"
      }
    ],
    "isError": false
  }
}</pre>



<h2 class="wp-block-heading">Gotchas</h2>



<h2 class="wp-block-heading has-medium-font-size">stdio Format</h2>



<p>Note that all of the JSON examples in this post and in the documentation are formatted for humans, but aren&#8217;t verbatim allowed responses. <a href="https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio">All server responses</a> must <em>not</em> contain any newlines, and <em>must</em> be separated by a single newline.</p>



<p><code>stdin</code> is reserved for input RPC requests, and <code>stdout</code> is reserved for RPC responses. Nothing else can be sent on those streams &#8211; any other logging must be sent on <code>stderr</code>. Those <code>print()</code> statements you use when debugging? Gotta find another way! I&#8217;ve been using <a href="https://github.com/apple/swift-log">Swift Logging</a> instead.</p>



<h2 class="wp-block-heading has-medium-font-size">Tools Changed Notification</h2>



<p>The MCP specification allows for servers to notify clients when the list of tools updates. When the client hears this notification, it should send a new <code>list/tools</code> request to get the new list of available tools.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "jsonrpc": "2.0",
  "method": "notifications/tools/list_changed"
}</pre>



<p>Once I had a barebones MCP server working with Claude, I spent quite a bit of time trying to diagnose why it wasn&#8217;t responding to my updated tools notification. Welp, it turns out <a href="https://github.com/orgs/modelcontextprotocol/discussions/76">Claude doesn&#8217;t support updating tools</a> yet, and it appears Cursor doesn&#8217;t either.</p>



<p>Making it more confusing, the <a href="https://github.com/modelcontextprotocol/inspector">modelcontextprotocol/inspector</a> does not show <code>tools/list_changed</code> notifications either. After some digging, I found that <a href="https://github.com/modelcontextprotocol/inspector/blob/main/client/src/lib/hooks/useConnection.ts#L252-L266">only a few notification types will appear</a> in the UI. So when you work on your MCP and can&#8217;t seem to get this notification to work &#8211; don&#8217;t worry, it&#8217;s [possibly] not you. It seems we&#8217;re so early in MCP ecosystem that tool list updates just aren&#8217;t widely support yet. The workaround? Restart your client 😬.</p>



<h2 class="wp-block-heading has-medium-font-size">List Tools</h2>



<p>Tools are the meat and potatoes of MCP servers. The client requests available tools from the server with the <code>tools/list</code> method request, which accepts a single optional cursor parameter. When implementing <code>Codable</code> in Swift, be careful when decoding optional parameters. When I began using <code>loopwork-ai</code>&#8216;s repository, there was a subtle decoding issue, and to show the issue, take a look at possible request JSON&#8217;s from a client:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// Specification for the List Tools requests to the server
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {
    "cursor": "optional-cursor-value"
  }
}</pre>



<p>Note that the cursor parameter is optional, so which of the following is a valid request? All of them? Probably? The right answer is &#8220;whatever the client that you need to integrate with sends you&#8230;&#8221;</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {
    "cursor": null
  }
}

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": null
}

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}</pre>



<p>When I started working with <code>loopwork-ai</code>&#8216;s sdk, the decoding was a bit too strict and failed to decode this message correctly. I&#8217;ve <a href="https://github.com/loopwork-ai/mcp-swift-sdk/pull/13">submitted a PR for the fix</a> to make it more lenient, and have a <a href="https://github.com/adamwulf/mcp-swift-sdk/tree/feature/wait-until-complete">branch on my <code>mcp-swift-sdk</code> fork</a> that I use for my development with a few other niceties.</p>



<h2 class="wp-block-heading">Follow Along</h2>



<p>I&#8217;m working on a barebones template for building MCP servers in Swift on the Mac. I&#8217;ve started a repository at <code><a href="https://github.com/adamwulf/mcp-template">adamwulf/mcp-template</a></code> which uses <code>loopwork-ai</code>&#8216;s SDK. My goal is to provide a simple <code>EasyMCP</code> server that manages the MCP server lifecyle, provides easy tool registration, and provides an App Store safe way to ship an MCP command line tool that connects to a Mac app for its functionality.</p>



<p>So far, I have a simple wrapper around the <code>loopwork-ai</code> SDK that handles the MCP <code>stdio</code> transport setup and provides a simple tool registration method:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="swift" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">        // Build the MCP server with logging
        let logger = Logger(label: "com.milestonemade.easymcp")
        let mcp = EasyMCP(logger: logger)

        // Register a simple tool that accepts a single parameter.
        // The name, description, and inputSchema will be sent to
        // the client so that the LLM knows what the tool does.
        try await mcp.register(tool: Tool(
            name: "helloPerson",
            description: "Returns a friendly greeting message",
            inputSchema: [
                "type": "object",
                "properties": [
                    "name": [
                        "type": "string",
                        "description": "Name of the person to say hello to",
                    ]
                ],
                "required": ["name"]
            ]
        )) { input in
            // It's an async closure, so you can await whatever you need to for long running tasks
            await someOtherAsyncStuffIfYouWant()
            // Return your result and flag if it is/not an error
            return Result(content: [.text(hello(input["name"]?.stringValue ?? "world"))], isError: false)
        }

        try await mcp.start()
        try await mcp.waitUntilComplete()</pre>



<h2 class="wp-block-heading">It&#8217;s Alive!</h2>



<p>After two weekends of fiddling, I have the <code>mcpexample</code> command line server in <code><a href="https://github.com/adamwulf/mcp-template">mcp-template</a></code> working in Claude and Cursor! 🎉</p>



<figure class="wp-block-video"><video controls src="https://adamwulf.me/wp-content/uploads/2025/03/CleanShot-2025-03-15-at-01.52.57.mp4"></video></figure>
]]></content:encoded>
					
					<wfw:commentRss>https://adamwulf.me/2025/03/building-a-mcp-server-in-swift/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure length="1104993" type="video/mp4" url="https://adamwulf.me/wp-content/uploads/2025/03/CleanShot-2025-03-15-at-01.52.57.mp4"/>

			</item>
		<item>
		<title>Podcast Transcripts with WavoAI, Cursor, Hugo</title>
		<link>https://adamwulf.me/2025/01/podcast-transcripts-with-wavoai-cursor-hugo/</link>
					<comments>https://adamwulf.me/2025/01/podcast-transcripts-with-wavoai-cursor-hugo/#respond</comments>
		
		<dc:creator><![CDATA[Adam Wulf]]></dc:creator>
		<pubDate>Sun, 12 Jan 2025 00:55:53 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://adamwulf.me/?p=5991</guid>

					<description><![CDATA[During its 84 episode run, the Metamuse podcast was a much loved for its discussion on local first software, deep work, creativity, and authenticity. Hosted by Muse founders Adam Wiggins and Mark McGranaghan, the podcast featured guests ranging from Obsidian CEO Stephan Ango, Roam&#8217;s founder Conor White-Sullivan, MindNode&#8217;s founder Markus Müller-Simhofer, and too many more [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>During its 84 episode run, the <a href="https://museapp.com/podcast/">Metamuse podcast</a> was a much loved for its discussion on local first software, deep work, creativity, and authenticity. Hosted by <a href="https://museapp.com/">Muse</a> founders <a href="https://adamwiggins.com/">Adam Wiggins</a> and <a href="https://markmcgranaghan.com/">Mark McGranaghan</a>, the podcast featured guests ranging from <a href="https://museapp.com/podcast/81-evergreen-notes/">Obsidian CEO Stephan Ango</a>, <a href="https://museapp.com/podcast/75-collective-intelligence/">Roam&#8217;s founder Conor White-Sullivan</a>, <a href="https://museapp.com/podcast/66-business-of-apps/">MindNode&#8217;s founder Markus Müller-Simhofer</a>, and too many more to count. Wonderful conversations with the leaders of much loved deep-work products and researchers at the forefront of human-computer interaction.</p>



<p>Since going solo with Muse in late 2023, I&#8217;ve wanted to get the transcripts of all of these episodes online, but I knew that writing transcripts manually was an insurmountable task. <a href="https://openai.com/index/whisper/">Whisper</a> had recently been released, and I was hopeful for AI transcription to help solve this. While Whisper&#8217;s transcription accuracy is fantastic, it does not do <a href="https://en.wikipedia.org/wiki/Speaker_diarisation">speaker diarisation</a> &#8211; it doesn&#8217;t separate the transcript per speaker.</p>



<p>Recently I found <a href="https://wavoai.com/">WavoAI</a>, an online transcription service with a generous free tier. Their transcripts are good quality, and most importantly, they do provide speaker diarisation.</p>



<p>Below is the process I followed to add transcripts to <a href="https://museapp.com/podcast/">every podcast episode page on the Muse website</a>, and take a look at <a href="https://museapp.com/podcast/1-tool-switching/">Metamuse Episode 1 page</a> to see the result.</p>



<h2 class="wp-block-heading">Step 1: Transcribe with WavoAI</h2>



<p>The <a href="https://wavoai.com/">WavoAI</a> website is wonderfully simple. There&#8217;s an upload button, and a list of all your transcripts &#8211; that&#8217;s it. They do have a monthly cap of total transcript duration, so over a couple of months I would upload each episode until I had all 84 episodes transcribed.</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="434" src="https://adamwulf.me/wp-content/uploads/2025/01/WavoAI-1024x434.png" alt="" class="wp-image-5993" srcset="https://adamwulf.me/wp-content/uploads/2025/01/WavoAI-1024x434.png 1024w, https://adamwulf.me/wp-content/uploads/2025/01/WavoAI-300x127.png 300w, https://adamwulf.me/wp-content/uploads/2025/01/WavoAI-768x326.png 768w, https://adamwulf.me/wp-content/uploads/2025/01/WavoAI.png 1386w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Interestingly, the export for the transcript is a <code>*.docx</code> file of all things. So I clicked through and downloaded each <code>docx</code> transcript file, and made sure to name it the same as the podcast episode mp3 file so it was easy to reference.</p>



<h2 class="wp-block-heading">Step 2: Convert to Markdown</h2>



<p>Next, I used a short script to automatically translate those <code>docx</code> files into <code>md</code> Markdown files:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">for file in docx/*.docx; do
    filename=$(basename "$file" .docx)
    pandoc "$file" -t markdown -o "md/${filename}.md"
    # Use the following sed command for macOS
    sed -i '' -e 's/\\$//' -e "s/\\\\'/'/g" "md/${filename}.md"
done</pre>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-full"><img decoding="async" width="718" height="458" src="https://adamwulf.me/wp-content/uploads/2025/01/podcast-docx.png" alt="The original list of docx transcripts from WavoAI." class="wp-image-5994" srcset="https://adamwulf.me/wp-content/uploads/2025/01/podcast-docx.png 718w, https://adamwulf.me/wp-content/uploads/2025/01/podcast-docx-300x191.png 300w" sizes="(max-width: 718px) 100vw, 718px" /></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-full"><img decoding="async" width="716" height="450" src="https://adamwulf.me/wp-content/uploads/2025/01/podcast-md.png" alt="A list of the converted podcast transcripts into markdown files." class="wp-image-5995" srcset="https://adamwulf.me/wp-content/uploads/2025/01/podcast-md.png 716w, https://adamwulf.me/wp-content/uploads/2025/01/podcast-md-300x189.png 300w" sizes="(max-width: 716px) 100vw, 716px" /></figure>
</div>
</div>



<h2 class="wp-block-heading">Step 3: Use Cursor to update Hugo site</h2>



<p>The Muse website is built with the <a href="https://gohugo.io/">Hugo framework</a>. Each podcast episode lives as a Markdown file in a <code>podcast</code> directory. I&#8217;ve been using Cursor for many menial coding tasks, and I wanted to see how it would do integrating the podcast transcript files into the podcast episode files. I admittedly don&#8217;t know too much about Hugo, so I very much threw this problem over the wall to the AI to see how it would do. I have Cursor setup to use Claude-3.5-Sonnet.</p>



<p>Here&#8217;s my first prompt to Cursor:</p>



<pre class="wp-block-code"><code>separate from this repo, I also have a folder full of transcripts for all of the podcast episodes. This site uses Hugo to be built. Is there a way to auto-process the transcript files using Hugo to include them as a sub-section of the respective podcast episode page? or would i need to edit each podcast episode page individually to manually copy/paste the entire transcript in?</code></pre>



<p>That&#8217;s all it took and I was off to the races! It suggested adding a <code>transcripts</code> subfolder to the <code>podcast</code> folder. Since all the filenames already matched, it built a new partial for the transcript section:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="html" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{{ $transcriptPath := printf "podcast/transcripts/%s.md" .File.BaseFileName }}
{{ with site.GetPage $transcriptPath }}
&lt;div class="transcript">
  &lt;h3>Transcript&lt;/h3>
  {{ $content := .Content }}
  {{ $pattern := `(\d{2}):(\d{2}):(\d{2}) - ` }}
  {{ $replacement := `&lt;a href="#t=$1$2$3" class="timestamp" data-time="$1:$2:$3">$1:$2:$3&lt;/a> - ` }}
  {{ $processed := replaceRE $pattern $replacement $content }}
  {{ $processed | safeHTML }}
&lt;/div>
{{ end }} </pre>



<p>And added a single line to the <code>single.html</code> podcast episode page: <code>{{ partial "podcast-transcript" . }}</code></p>



<p>Barely 30s after opening cursor, I had all of the episode transcripts imported into the website and showing up correctly! Not bad!</p>



<h2 class="wp-block-heading">Step 4: Clean up and deploy</h2>



<p>At this point, it was good enough to ship, but I spent a bit more time to keep it extra tidy. The WavoAI did a great job with the transcript, but obviously wouldn&#8217;t know my last name is spelled with a <code>u</code> instead of an <code>o</code>. I fixed up other egregious misspellings &#8211; the word &#8220;Muse&#8221; was often transcribed as &#8220;MU&#8221; or &#8220;Mu&#8217;s&#8221;, etc.</p>



<p>The transcription file also includes <code>00:00:00</code> styled timestamps each time the speaker changes. I asked Cursor if it it could write a script to use those timestamps to jump the inline podcast player to that timestamp. A bit of back and forth, and now the podcast episode page has links for each timestamp which update the hash of the page URL. This means anyone can jump straight to a quote by clicking in the transcript, and can also share the URL with anyone so that they can jump straight to the quote too! Here&#8217;s a <a href="https://museapp.com/podcast/30-computers-and-creativity/#t=002951">great moment in Molly Mielke&#8217;s episode</a> where she&#8217;s talking about the importance of interoperability in tools for thought.</p>



<h2 class="wp-block-heading">Wrap up</h2>



<p>This has been something I&#8217;ve wanted to do for the Metamuse podcast library since day 1 of going solo. It&#8217;s been a long wait to find the right tooling to be able to do this without spending weeks of my own time manually annotating speakers. It just wouldn&#8217;t be feasible for me to do without the help of both WavoAI and Cursor. Even the Hugo work, I could&#8217;ve eventually gotten there on my own, but it would&#8217;ve taken me hours instead of minutes to learn the necessary Hugo-isms to pull it all together.</p>



<p>I&#8217;m very thankful to have this project ticked off my todo list! It makes the podcast episode pages accessible to the hearing impaired, and I&#8217;m hopeful it&#8217;ll increase SEO traffic to Muse too, and also make it just a bit easier to share loved moments of your favorite episodes.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://adamwulf.me/2025/01/podcast-transcripts-with-wavoai-cursor-hugo/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Testing Background Uploads in iOS</title>
		<link>https://adamwulf.me/2025/01/testing-background-uploads-in-ios/</link>
					<comments>https://adamwulf.me/2025/01/testing-background-uploads-in-ios/#respond</comments>
		
		<dc:creator><![CDATA[Adam Wulf]]></dc:creator>
		<pubDate>Tue, 07 Jan 2025 23:46:36 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://adamwulf.me/?p=5896</guid>

					<description><![CDATA[When implementing background uploads on iOS, I found this incredibly helpful article by Antoine van der Lee. Any feature that relies on uncontrollable iOS system behavior is a tough one to implement, and his article was my map through the void. Since it was published, some things have changed slightly in iOS, so I&#8217;m writing [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>When implementing background uploads on iOS, I found this incredibly <a href="https://www.avanderlee.com/swift/urlsession-common-pitfalls-with-background-download-upload-tasks/">helpful article by Antoine van der Lee.</a> Any feature that relies on uncontrollable iOS system behavior is a tough one to implement, and his article was my map through the void. Since it was published, some things have changed slightly in iOS, so I&#8217;m writing this post to add some corrections about implementing background uploads in iOS.</p>



<h2 class="wp-block-heading">Invalidating URLSession</h2>



<p>One tip it gives at the end is to invalidate the URLSession on app launch so that you can start from a clean slate:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<h4 class="wp-block-heading" id="h-starting-from-scratch">Starting from scratch</h4>



<p>First of all, it’s good to start with a clean slate. We can do this by executing the following piece of code on launch to invalidate our URLSession and its tasks:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="swift" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">#if DEBUG
    if debuggingBackgroundTasks {
        /// Cancel any running tasks and invalidate the session.
        URLSession.shared.invalidateAndCancel()
    }
#endif</pre>
</blockquote>



<p>I&#8217;m not sure this is a good idea, or at the very least requires relaunching your app after invalidating all tasks. The issue is that this is invalidating <code>URLSession.shared</code>, not the background <code>URLSession</code> we created to handle background uploads.</p>



<p>The <a href="https://developer.apple.com/documentation/foundation/urlsession/1411538-invalidateandcancel">Apple Documentation for <code>URLSession.invalidateAndCancel()</code></a> states:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Calling this method on the session returned by the&nbsp;<a href="doc://com.apple.documentation/documentation/foundation/urlsession/1409000-shared"><code>shared</code></a>&nbsp;method has no effect.</p>
</blockquote>



<p>If we invalidate our background <code>URLSession</code> instead, we run into a new problem: Only a single URLSession for a given configuration identifier will ever be created. This means that if we invalidate and then re-create our background <code>URLSession</code>, we actually get back the same <code>URLSession</code> that we just invalidated!</p>



<pre class="EnlighterJSRAW" data-enlighter-language="swift" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">        backgroundSession.invalidateAndCancel()
        let config = URLSessionConfiguration.background(withIdentifier: Self.backgroundSessionIdentifier)
        config.sessionSendsLaunchEvents = true
        config.isDiscretionary = false
        backgroundSession = URLSession(configuration: config)  // &lt;--- This returns the same `backgroundSession` that we just invalidated!</pre>



<p>The <a href="https://developer.apple.com/documentation/foundation/urlsession/1411538-invalidateandcancel">Apple Documentation for <code>URLSession.invalidateAndCancel()</code></a> states:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Once invalidated, references to the delegate and callback objects are broken. After invalidation, session objects cannot be reused.</p>
</blockquote>



<p>This does mention we shouldn&#8217;t use the same session, but doesn&#8217;t mention that the exact same <code>URLSession</code> will be returned from the <code>init(configuration:)</code>, which I found surprising.</p>



<p>Instead of invalidating the session to clear out old upload tasks, simply iterate through the tasks and cancel them individually:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="swift" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">let tasks = await yourBackgroundURLSession.allTasks
for task in tasks {
    task.cancel()
}</pre>



<h2 class="wp-block-heading">Forcing App Background</h2>



<p>One other note, in the article Antoine recommends calling <code>exit(0)</code> from <code>applicationDidEnterBackground(<code>_:</code>)</code>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<h4 class="wp-block-heading" id="h-make-your-app-suspend-sooner">Make your app suspend sooner</h4>



<p>Waiting for your app to suspend is a waste of time. By using&nbsp;<code>exit(0)</code>&nbsp;we can force our app to suspend:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="swift" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">func applicationDidEnterBackground(_ application: UIApplication) {
    #if DEBUG
        if debuggingBackgroundTasks {
            /// Exit to make our app suspend directly.
            exit(0)
        }
    #endif
}</pre>
</blockquote>



<p>However, <a href="https://developer.apple.com/documentation/uikit/uiapplicationdelegate/applicationdidenterbackground(_:)">the Apple documentation for that method</a> notes that <code>applicationDidEnterBackground(_:)</code> will not be called for applications using scenes:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>If you’re using scenes (see&nbsp;<a href="https://developer.apple.com/documentation/uikit/scenes">Scenes</a>), UIKit will not call this method. Use&nbsp;<a href="https://developer.apple.com/documentation/uikit/uiscenedelegate/scenedidenterbackground(_:)"><code>scene<wbr>Did<wbr>Enter<wbr>Background(_:)</code></a>&nbsp;instead to perform any final tasks. UIKit posts a&nbsp;<a href="https://developer.apple.com/documentation/uikit/uiapplication/didenterbackgroundnotification"><code>did<wbr>Enter<wbr>Background<wbr>Notification</code></a>&nbsp;regardless of whether your app uses scenes.</p>
</blockquote>



<p>Oops! I&#8217;d completely forgotten about this, and I was left wondering why my app delegate method wasn&#8217;t call. The fix is easy, I switched to use <code><br>sceneDidEnterBackground(_ scene: UIScene)</code> in your scene delegate.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p>If you want to cancel all background tasks and start from scratch, call <code>cancel()</code> on your background <code>URLSession.allTasks</code>. Otherwise, if you invalidate your background session, the background session will not be able to be re-used in the same app session that it was invalidated in. To start new background tasks, you would need to relaunch the app and not <code>invalidateAndCancel()</code> a second time.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://adamwulf.me/2025/01/testing-background-uploads-in-ios/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Translating an iOS/Mac app with AI and humans</title>
		<link>https://adamwulf.me/2024/12/translating-an-ios-mac-app-with-ai-and-humans/</link>
					<comments>https://adamwulf.me/2024/12/translating-an-ios-mac-app-with-ai-and-humans/#respond</comments>
		
		<dc:creator><![CDATA[Adam Wulf]]></dc:creator>
		<pubDate>Sun, 15 Dec 2024 22:55:08 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://adamwulf.me/?p=5865</guid>

					<description><![CDATA[I&#8217;ve long wanted to get Muse translated into multiple languages &#8211; Muse has been English-only since launch. The problem isn&#8217;t so much the one time translation cost at the beginning, but having a strategy to translate for every single update going forward. Getting Muse into German isn&#8217;t a problem, keeping Muse in German is the [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;ve long wanted to get Muse translated into multiple languages &#8211; Muse has been English-only since launch. The problem isn&#8217;t so much the one time translation cost at the beginning, but having a strategy to translate for every single update going forward. Getting Muse into German isn&#8217;t a problem, keeping Muse in German is the problem.</p>



<p>Unlike just a few years ago, we now live in a world with high quality AI translations! As good as these AIs are, they&#8217;re still not human, and they&#8217;ll still miss important context about how these translations are used and viewed in the app, so having a human in the loop is still incredibly important. However &#8211; they do reduce the human-time cost of adding and maintaining multiple languages.</p>



<p>I&#8217;ve recently shipped a French translation for Muse, and I have German and Spanish cooking and expect those to release in the next week or two. Thats three languages in less than a month of time, and better yet, less than a few days of my coding time. Here&#8217;s how I&#8217;m able to translate Muse with minimal coder time and minimal volunteer time.</p>



<h2 class="wp-block-heading">Step 1: Prep the codebase for localization</h2>



<p>SwiftUI provides much of localization for free with its <code>Text</code> node, however all of Muse&#8217;s codebase is UIKit. Following <a href="https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog">Apple&#8217;s localization documentation</a>, the first step is to add a Localizable.xcstrings String Catalog file into the project.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large is-resized"><img decoding="async" width="1024" height="726" src="https://adamwulf.me/wp-content/uploads/2024/12/localizable-strings-catalog-1024x726.png" alt="" class="wp-image-5885" style="width:477px;height:auto" srcset="https://adamwulf.me/wp-content/uploads/2024/12/localizable-strings-catalog-1024x726.png 1024w, https://adamwulf.me/wp-content/uploads/2024/12/localizable-strings-catalog-300x213.png 300w, https://adamwulf.me/wp-content/uploads/2024/12/localizable-strings-catalog-768x544.png 768w, https://adamwulf.me/wp-content/uploads/2024/12/localizable-strings-catalog.png 1462w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p>When you build your project, Xcode will automatically update this strings catalog with all of the localized strings in your codebase. As long as you use <code>Text</code> from SwiftUI or <code>NSLocalizedString</code>, the string keys will appear automatically.</p>



<p>Next, add the new language into the project&#8217;s settings:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img decoding="async" width="1024" height="632" src="https://adamwulf.me/wp-content/uploads/2024/12/add-language-1024x632.png" alt="" class="wp-image-5886" srcset="https://adamwulf.me/wp-content/uploads/2024/12/add-language-1024x632.png 1024w, https://adamwulf.me/wp-content/uploads/2024/12/add-language-300x185.png 300w, https://adamwulf.me/wp-content/uploads/2024/12/add-language-768x474.png 768w, https://adamwulf.me/wp-content/uploads/2024/12/add-language-1536x947.png 1536w, https://adamwulf.me/wp-content/uploads/2024/12/add-language.png 1670w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p>Now, you&#8217;ll see the new language appear in the string catalog that you&#8217;ve just added. Xcode shows a fancy editor for the strings file, but what happens if we open as source code instead? the <code>Localizable.xcstrings</code> file is really just a JSON file! And what tool is good with JSON? LLMs.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large is-resized"><img decoding="async" width="1024" height="578" src="https://adamwulf.me/wp-content/uploads/2024/12/open-as-source-code-1-1024x578.png" alt="" class="wp-image-5889" style="width:532px;height:auto" srcset="https://adamwulf.me/wp-content/uploads/2024/12/open-as-source-code-1-1024x578.png 1024w, https://adamwulf.me/wp-content/uploads/2024/12/open-as-source-code-1-300x169.png 300w, https://adamwulf.me/wp-content/uploads/2024/12/open-as-source-code-1-768x434.png 768w, https://adamwulf.me/wp-content/uploads/2024/12/open-as-source-code-1.png 1202w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<h2 class="wp-block-heading">Step 2: Cursor and Claude</h2>



<p>Now that we have our application&#8217;s translations as a single JSON file, let&#8217;s use AI to automatically translate our app. Open the root folder of your project in Cursor, and open to the <code>Localizable.xcstrings</code> file.</p>



<p>Next, use Command+L to open the Chat interface, and ask to add a new translation to the file. Here&#8217;s the prompt I used to translate Muse into German:</p>



<pre class="wp-block-code"><code>Here are the key guidelines for German localization of Muse:

Canonical translation:
- English is the original translation and should be the primary reference

Brand &amp; Common App-Specific Terms:
- "Muse" (never translate)
- "Board(s)" (keep English term)
- "Card(s)" (keep English term)
- "Workspace(s)" (keep English term)
- "Backstage Pass" (keep English)
- File types like "PDF", "URL"
- Technical terms like "debug", "database", "sync", "cache" (only if commonly kept in English when used in German)

Formality &amp; Tone:
- Use informal "du" form rather than formal "Sie"
- Friendly but professional tone
- Direct address to user
- Keep technical explanations simple and clear

Gender &amp; Inclusivity:
- Use gender-inclusive forms with -In suffix where applicable (e.g., "BenutzerIn", "MitarbeiterIn")
- When possible, use gender-neutral terms

Capitalization &amp; Grammar:
- Capitalize nouns per German rules
- Compound words with English terms maintain English spelling (e.g., "Workspace-Karten")
- Maintain English terms in their original form when part of compound words
- Use German quotation marks („") for quotes

UI Elements:
- Button text should be concise
- Menu items use infinitive form
- Error messages should be clear and direct
- Maintain consistent terminology throughout

Special Considerations:
- Preserve emoji and formatting tags (e.g., &lt;highlight>, &lt;small>)
- Maintain placeholder syntax (e.g., %@, %d)
- Keep technical command references (e.g., ⌘, ⇧) unchanged
- Preserve HTML tags and links exactly as in source

Length:
- German translations tend to be longer than English
- Keep UI elements as concise as possible while maintaining clarity
- Consider space constraints in buttons and menus


If you see a string that does not match the guidelines, please generate a replacement JSON that can be applied to the file to replace it with a corrected version. explain the change.</code></pre>



<p>That&#8217;s quite a prompt! My goal is for the prompt to give the LLM enough context to know how to translate my app with the correct tone and localization decisions. Each language has its own nuance: gender, plurals, formality, etc. Explain to the LLM how you want your app to be translated for the given language.</p>



<p>For extremely long <code>xcstrings</code> files, I will select the range of keys that I want translated, use Command+Shift+L to add that snippet to the chat context, and ask to translate only that portion. Then I&#8217;ll copy/paste into Cursor to replace the selection. Why not use Apply? For extremely long files, Apply can both take too long and use too many tokens for the model&#8217;s limited output size.</p>



<figure class="wp-block-video"><video controls src="https://adamwulf.me/wp-content/uploads/2024/12/translate-ai-cursor.mp4"></video></figure>



<p>For all of Muse&#8217;s translations, I used Claude Sonnet 3.5.</p>



<p>After updating the xcstrings file, save it in Cursor and open it in Xcode to verify that the JSON is still in the valid format. Save again in Xcode, as Xcode will format it with alphabetical language keys, whitespace, etc. Commit to your repo and off you go!</p>



<p>For Muse, a codebase with ~700 localized keys, it takes me ~2 hours to translate all of them with AI. I could probably optimize my flow to take less than an hour, but that&#8217;s only 10-15 hours total for 5 languages!</p>



<h2 class="wp-block-heading">Step 3: Upload to POEditor</h2>



<p>At this point, we have a fairly accurate translation from an AI &#8211; but we don&#8217;t know how accurate it actually is. In my first pass at German, I didn&#8217;t know about its gender or formality, so after a volunteer looked it over and gave their first feedback, I re-ran the AI with a better prompt to get a more accurate translation. Now the volunteers have fewer strings to manually edit.</p>



<p>But how do we get these strings to our volunteers or translation team? I use <a href="https://poeditor.com/">POEditor</a> to coordinate with volunteers on Muse&#8217;s translations.</p>



<p>To get the translations from Xcode into POEditor, use the Export Localizations item in the Product menu.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img decoding="async" width="624" height="252" src="https://adamwulf.me/wp-content/uploads/2024/11/product-menu.png" alt="" class="wp-image-5876" style="width:345px;height:auto" srcset="https://adamwulf.me/wp-content/uploads/2024/11/product-menu.png 624w, https://adamwulf.me/wp-content/uploads/2024/11/product-menu-300x121.png 300w" sizes="(max-width: 624px) 100vw, 624px" /></figure></div>


<p>This will export a folder at the location you choose with 1 bundle file per language that your app supports. POEditor doesn&#8217;t handle <code>xcloc</code> bundle files, but it does handle <code>xliff</code> files. To get to your <code>xliff</code> file, right click the bundle and choose Show Package Contents, and find the <code>xliff</code> file in the Localized Contents subfolder.</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img decoding="async" width="832" height="398" data-id="5877" src="https://adamwulf.me/wp-content/uploads/2024/12/show-package-contents.png" alt="" class="wp-image-5877" srcset="https://adamwulf.me/wp-content/uploads/2024/12/show-package-contents.png 832w, https://adamwulf.me/wp-content/uploads/2024/12/show-package-contents-300x144.png 300w, https://adamwulf.me/wp-content/uploads/2024/12/show-package-contents-768x367.png 768w" sizes="(max-width: 832px) 100vw, 832px" /></figure>



<figure class="wp-block-image size-large"><img decoding="async" width="706" height="400" data-id="5878" src="https://adamwulf.me/wp-content/uploads/2024/12/language-xliff.png" alt="" class="wp-image-5878" srcset="https://adamwulf.me/wp-content/uploads/2024/12/language-xliff.png 706w, https://adamwulf.me/wp-content/uploads/2024/12/language-xliff-300x170.png 300w" sizes="(max-width: 706px) 100vw, 706px" /></figure>
</figure>



<p>Upload this <code>xliff</code> file to the same language in POEditor. Note: There&#8217;s two different places to upload in POEditor, there&#8217;s an import link at the project level, and another import link in the specific language &#8211; you want the import link in the language.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large is-resized"><img decoding="async" width="1024" height="895" src="https://adamwulf.me/wp-content/uploads/2024/12/poe-import-1024x895.png" alt="" class="wp-image-5879" style="width:400px;height:auto" srcset="https://adamwulf.me/wp-content/uploads/2024/12/poe-import-1024x895.png 1024w, https://adamwulf.me/wp-content/uploads/2024/12/poe-import-300x262.png 300w, https://adamwulf.me/wp-content/uploads/2024/12/poe-import-768x671.png 768w, https://adamwulf.me/wp-content/uploads/2024/12/poe-import.png 1432w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p>Choose to overwrite any existing translations in POEditor, save and upload, and enjoy all of your strings in POEditor!</p>



<h2 class="wp-block-heading">Step 4: Review translations</h2>



<p>Next, add your contributors to POEditor for the language. These are the people who&#8217;ll be able to view and make changes to all of the translations.</p>



<p>Once your volunteers or translation team has validated and corrected your translation, it&#8217;s time to get their work back into Xcode.</p>



<h2 class="wp-block-heading">Step 5: Export from POEditor</h2>



<p>In POEditor, navigate into the language and choose its Export link. In the export menu, choose the <code>xliff</code> file format, and export to overwrite the exact same file you uploaded to POEditor.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large is-resized"><img decoding="async" width="1024" height="896" src="https://adamwulf.me/wp-content/uploads/2024/12/export-from-poe-1024x896.png" alt="" class="wp-image-5880" style="width:445px;height:auto" srcset="https://adamwulf.me/wp-content/uploads/2024/12/export-from-poe-1024x896.png 1024w, https://adamwulf.me/wp-content/uploads/2024/12/export-from-poe-300x263.png 300w, https://adamwulf.me/wp-content/uploads/2024/12/export-from-poe-768x672.png 768w, https://adamwulf.me/wp-content/uploads/2024/12/export-from-poe.png 1348w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<figure class="wp-block-image size-full is-resized"><img decoding="async" width="603" height="222" src="https://adamwulf.me/wp-content/uploads/2024/12/xcode-import-localization.png" alt="" class="wp-image-5881" style="width:368px;height:auto" srcset="https://adamwulf.me/wp-content/uploads/2024/12/xcode-import-localization.png 603w, https://adamwulf.me/wp-content/uploads/2024/12/xcode-import-localization-300x110.png 300w" sizes="(max-width: 603px) 100vw, 603px" /></figure>



<p>Then, from Xcode, choose Import Localizations from the Product menu and select the Localizations folder that it had exported before. Since you&#8217;ve just overwritten the language&#8217;s <code>xliff</code> file inside that same folder, Xcode will now read in and use all of those new translations.</p>



<p>Congratulations! You&#8217;ve just translated your app into another language using AI, POEditor, and a small focused dose of human help.</p>



<h2 class="wp-block-heading">Wrap up</h2>



<p>This is a long post, but the core idea is simple:</p>



<ul class="wp-block-list">
<li>Use <code>xcstring</code> files in Xcode to get auto-updated translation keys, including <a href="https://developer.apple.com/documentation/xcode/localizing-strings-that-contain-plurals">localized plurals</a></li>



<li>Use Cursor and AI to get a draft translation for your new language</li>



<li>Upload to POEditor and validate with human helpers</li>



<li>Download and import back into Xcode, and enjoy!</li>
</ul>



<p><a href="https://museapp.com/">Muse</a> is available today in English and French, and soon in German and Spanish as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://adamwulf.me/2024/12/translating-an-ios-mac-app-with-ai-and-humans/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure length="6005441" type="video/mp4" url="https://adamwulf.me/wp-content/uploads/2024/12/translate-ai-cursor.mp4"/>

			</item>
		<item>
		<title>Finding unused code with Periphery</title>
		<link>https://adamwulf.me/2024/12/finding-unused-code-with-periphery/</link>
					<comments>https://adamwulf.me/2024/12/finding-unused-code-with-periphery/#respond</comments>
		
		<dc:creator><![CDATA[Adam Wulf]]></dc:creator>
		<pubDate>Sat, 14 Dec 2024 23:38:00 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://adamwulf.me/?p=5868</guid>

					<description><![CDATA[The Muse codebase is over 5 years old with over 350,000 lines of Swift, and I&#8217;m sure is filled with more than a few archeological code-fossils. Like any startup (frankly, like literally every code project), it&#8217;s difficult to prune old unused code while keeping up velocity of new features. Code cleanliness is always a tradeoff [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>The <a href="https://museapp.com/" data-type="link" data-id="https://museapp.com/">Muse</a> codebase is over 5 years old with over 350,000 lines of Swift, and I&#8217;m sure is filled with more than a few archeological code-fossils. Like any startup (frankly, like literally every code project), it&#8217;s difficult to prune old unused code while keeping up velocity of new features. Code cleanliness is always a tradeoff with velocity, and often comes in second place. That&#8217;s why I love tooling that can help automate this otherwise brutally slow manual task.</p>



<p>I was recently told about <code><a href="https://github.com/peripheryapp/periphery">periphery</a></code>, a command line tool to find unused code in Swift projects. What&#8217;s a day off work for but for having fun playing with new tools? First up, install:</p>



<pre class="wp-block-code"><code>$ brew install periphery</code></pre>



<p>God bless <code>brew</code>, amirite.</p>



<p>Next up, the README suggests we run its setup for a handheld first run:</p>



<pre class="wp-block-code"><code>$ periphery scan --setup
Welcome to Periphery!
This guided setup will help you select the appropriate configuration for your project.

* Inspecting project...

Select build targets to analyze:
? Delimit choices with a single space, e.g: 1 2 3, or 'all' to select all options
1 AppKitBridge
2 Muse
3 Muse Integration Tests
4 Muse Tests
5 MuseShare
6 SparklePlugin

...</code></pre>



<p>Super easy to get setup, what a pleasure! I selected the targets I needed, then the schemes. Next it asks about Objective-C code, and I selected Yes to assume obj-c code is in use. Muse has only a little, but some interactions with UIKit or AppKit still reach into Objective-C.</p>



<pre class="wp-block-code"><code>Assume Objective-C accessible declarations are in use?
? Declarations exposed to the Objective-C runtime explicitly with @objc, or implicitly by inheriting NSObject will be assumed to be in use. Choose 'No' if your project is pure Swift.
(Y)es/(N)o &gt; y</code></pre>



<p>Next, it asked about assuming all public declarations are in use &#8211; which would be very useful when building a framework or library, for for an app like Muse I answered No.</p>



<pre class="wp-block-code"><code>Assume all 'public' declarations are in use?
? You should choose 'Yes' here if your public interfaces are not used by any selected build target, as may be the case for a framework/library project.
(Y)es/(N)o &gt; n</code></pre>



<p>Last, I saved the configuration to <code>.periphery.yml</code> and let the first scan run. Luckily, it found the codebase squeaky clean! 😉</p>



<figure class="wp-block-video"><video controls src="https://adamwulf.me/wp-content/uploads/2024/12/muse-periphery-results.mp4"></video></figure>



<p>I use <a href="https://github.com/krzysztofzablocki/Sourcery">Sourcery</a> to auto-generate some connector files between the custom Muse sync codebase and Muse-the-application codebase. There&#8217;s also a fair bit of old CoreData code that&#8217;s still in the codebase so that very old Muse libraries can be migrated to the current sync world. For each of these, I don&#8217;t need them included in <code>periphery</code>&#8216;s output.</p>



<p>To remove them, I used the <code><a href="https://github.com/peripheryapp/periphery?tab=readme-ov-file#excluding-files">--report-exclude</a></code> command line option to remove a few paths that I don&#8217;t need in the report. I&#8217;m also not concerned with redundant <code>public</code>, so I included <code><br><a href="https://github.com/peripheryapp/periphery?tab=readme-ov-file#redundant-public-accessibility">--disable-redundant-public-analysis</a></code> too. Last, I&#8217;m not worried about unused function parameters, so i used <code><a href="https://github.com/peripheryapp/periphery?tab=readme-ov-file#function-parameters">--retain-unused-protocol-func-params</a></code>. I ran with <code>--verbose</code> which <a href="https://github.com/peripheryapp/periphery?tab=readme-ov-file#configuration">shows the <code>.yml</code> configuration</a> changes I needed to make to always run with these excluded.</p>



<p>Last, I setup an Aggregate target in Xcode so that I can run <code>periphery</code> and see unused code in the Issue navigator. I updated the Build Settings to use iOS, added a User Defined setting for <code>SUPPORTS_MACCATALYST=YES</code>, and added a Run Script phase with the following:</p>



<pre class="wp-block-code"><code>export PATH="$PATH:/opt/homebrew/bin"

if which periphery &gt;/dev/null; then
    periphery scan --config "${SRCROOT}/.periphery.yml"
else
    echo "warning: periphery not installed, install from https://github.com/peripheryapp/periphery"
fi</code></pre>



<p>Now, when building the Periphery aggregate target within Xcode, I can see all of the unused code inline right inside Xcode.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="553" src="https://adamwulf.me/wp-content/uploads/2024/12/Muse-unused-code-results-1024x553.png" alt="" class="wp-image-5870" srcset="https://adamwulf.me/wp-content/uploads/2024/12/Muse-unused-code-results-1024x553.png 1024w, https://adamwulf.me/wp-content/uploads/2024/12/Muse-unused-code-results-300x162.png 300w, https://adamwulf.me/wp-content/uploads/2024/12/Muse-unused-code-results-768x415.png 768w, https://adamwulf.me/wp-content/uploads/2024/12/Muse-unused-code-results-1536x830.png 1536w, https://adamwulf.me/wp-content/uploads/2024/12/Muse-unused-code-results-2048x1107.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Just a little bit of clean-up work to do! 😅</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="72" src="https://adamwulf.me/wp-content/uploads/2024/12/CleanShot-2024-12-14-at-17.31.12@2x-1-1024x72.png" alt="" class="wp-image-5872" srcset="https://adamwulf.me/wp-content/uploads/2024/12/CleanShot-2024-12-14-at-17.31.12@2x-1-1024x72.png 1024w, https://adamwulf.me/wp-content/uploads/2024/12/CleanShot-2024-12-14-at-17.31.12@2x-1-300x21.png 300w, https://adamwulf.me/wp-content/uploads/2024/12/CleanShot-2024-12-14-at-17.31.12@2x-1-768x54.png 768w, https://adamwulf.me/wp-content/uploads/2024/12/CleanShot-2024-12-14-at-17.31.12@2x-1.png 1052w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://adamwulf.me/2024/12/finding-unused-code-with-periphery/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure length="7458314" type="video/mp4" url="https://adamwulf.me/wp-content/uploads/2024/12/muse-periphery-results.mp4"/>

			</item>
		<item>
		<title>Localize Screenshots using Figma</title>
		<link>https://adamwulf.me/2024/11/localize-screenshots-using-figma/</link>
					<comments>https://adamwulf.me/2024/11/localize-screenshots-using-figma/#respond</comments>
		
		<dc:creator><![CDATA[Adam Wulf]]></dc:creator>
		<pubDate>Tue, 19 Nov 2024 03:44:34 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://adamwulf.me/?p=5842</guid>

					<description><![CDATA[I&#8217;ve recently translated Muse into French 🇫🇷! As part of localizing the app, I also needed to translate the App Store description and screenshots. All of the App Store screenshots for Muse are setup in Figma, and I was hoping beyond hope that I could setup multiple languages in Figma to make it easy to [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;ve recently translated <a href="https://museapp.com/">Muse</a> into French 🇫🇷! As part of localizing the app, I also needed to translate the App Store description and screenshots. All of the App Store screenshots for Muse are setup in Figma, and I was hoping beyond hope that I could setup multiple languages in Figma to make it easy to export for all storefronts.</p>



<p>Thankfully, Figma came through! The strategy is to use <a href="https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes">Figma&#8217;s <em>variables</em> and <em>modes</em></a>. The basics flow is to:</p>



<ol class="wp-block-list">
<li>Setup a variable for each piece of text that you want to translate, and set it&#8217;s initial value to the English translation</li>



<li>Update each text node in your Figma design to use that variable instead of static text</li>



<li>Setup a new <em>mode</em> for the variable for each new language, and then update each variable to include a translation for each language</li>



<li>Toggle the page&#8217;s default <em>mode</em> to set the language for all screenshots</li>
</ol>



<h2 class="wp-block-heading">Figma Variables and Modes</h2>



<p>With nothing selected on the Figma page, click the button to open up the <em>Local variables</em> window. This is where you&#8217;ll configure each text snippet.</p>



<figure class="wp-block-image size-full is-resized"><img decoding="async" width="544" height="398" src="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.10.50@2x-1.png" alt="The local variables button in the Figma sidebar" class="wp-image-5850" style="width:302px;height:auto" srcset="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.10.50@2x-1.png 544w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.10.50@2x-1-300x219.png 300w" sizes="(max-width: 544px) 100vw, 544px" /></figure>



<p>Once the variable window is open, add a variable for each piece of text that appears in your design. For Muse, one of my screenshots is below, and you can see I&#8217;ve translated its two header lines as a separate <em>variable</em>.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="844" src="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.30.48@2x-1024x844.png" alt="An App Store screenshot for Muse with its default English translation" class="wp-image-5851" srcset="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.30.48@2x-1024x844.png 1024w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.30.48@2x-300x247.png 300w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.30.48@2x-768x633.png 768w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.30.48@2x.png 1090w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>You can see that I&#8217;ve created separate variables for &#8220;Nested boards&#8221; and &#8220;A cozy space to think.&#8221; For simplicity, I set the Name and English value so the same text so that it&#8217;s easier for me to recognize the variables elsewhere in Figma.</p>



<p>After setting up the English version, click the + button to add a new <em>variable mode</em>. Use this new <em>variable mode</em> for your second language&#8217;s text.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="498" src="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.12.31@2x-1-1024x498.png" alt="Figma's variable definition window, showing a row per variable, and a column per mode." class="wp-image-5852" srcset="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.12.31@2x-1-1024x498.png 1024w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.12.31@2x-1-300x146.png 300w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.12.31@2x-1-768x374.png 768w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.12.31@2x-1-1536x747.png 1536w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.12.31@2x-1-2048x997.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Update Text Nodes to Use Variables</h2>



<p>Now that you have your variables setup with all of your translations, it&#8217;s time to setup the text nodes in your designs to use them. To do that, select a text node and then click the <em>Apply variable</em> button.</p>



<figure class="wp-block-image size-full is-resized"><img decoding="async" width="542" height="112" src="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.13.14@2x-1.png" alt="The Text node attributes, showing the Apply variable button." class="wp-image-5853" style="width:319px;height:auto" srcset="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.13.14@2x-1.png 542w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.13.14@2x-1-300x62.png 300w" sizes="(max-width: 542px) 100vw, 542px" /></figure>



<p>That will open a dialog where you can choose the <em>variable</em> that matches your text.</p>



<figure class="wp-block-image size-full is-resized"><img decoding="async" width="542" height="488" src="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.36.26@2x.png" alt="The variable menu appears to select which variable to assign to this text node." class="wp-image-5854" style="width:318px;height:auto" srcset="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.36.26@2x.png 542w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.36.26@2x-300x270.png 300w" sizes="(max-width: 542px) 100vw, 542px" /></figure>



<p>Now your text node has its text pulled from that <em>variable</em>.</p>



<figure class="wp-block-image size-full is-resized"><img decoding="async" width="542" height="182" src="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.13.29@2x.png" alt="The text node now shows it's attached to the variable's value." class="wp-image-5847" style="width:325px;height:auto" srcset="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.13.29@2x.png 542w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.13.29@2x-300x101.png 300w" sizes="(max-width: 542px) 100vw, 542px" /></figure>



<h2 class="wp-block-heading">Toggle Language</h2>



<p>That&#8217;s all there is to setup multiple languages in your Figma design. To switch between your languages, deselect everything on your page, and then click the <em>Apply variable mode</em> button to switch between your language modes.</p>



<figure class="wp-block-image size-full"><img decoding="async" width="894" height="388" src="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.13.58@2x.png" alt="Figma's &quot;Apply variable mode&quot; button pressed, showing the menu to switch modes." class="wp-image-5848" srcset="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.13.58@2x.png 894w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.13.58@2x-300x130.png 300w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-16-at-01.13.58@2x-768x333.png 768w" sizes="(max-width: 894px) 100vw, 894px" /></figure>



<p>Now it&#8217;s easy for me to switch between and export both English and French screenshots for the App Store!</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="789" src="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.40.32@2x-1024x789.png" alt="App Store screenshot of Muse, in French" class="wp-image-5855" srcset="https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.40.32@2x-1024x789.png 1024w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.40.32@2x-300x231.png 300w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.40.32@2x-768x592.png 768w, https://adamwulf.me/wp-content/uploads/2024/11/CleanShot-2024-11-18-at-21.40.32@2x.png 1108w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
]]></content:encoded>
					
					<wfw:commentRss>https://adamwulf.me/2024/11/localize-screenshots-using-figma/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Gmail IMAP configuration in Zoho</title>
		<link>https://adamwulf.me/2024/10/gmail-imap-configuration-in-zoho/</link>
					<comments>https://adamwulf.me/2024/10/gmail-imap-configuration-in-zoho/#respond</comments>
		
		<dc:creator><![CDATA[Adam Wulf]]></dc:creator>
		<pubDate>Fri, 11 Oct 2024 18:22:39 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://adamwulf.me/?p=5833</guid>

					<description><![CDATA[I&#8217;ve been using Zoho for my personal email for years, and I love it. I moved for a few reasons, privacy being a big one. If you&#8217;re not paying for the product, you are the product! I had used Gmail since it came out in college, and it was time to take control of my [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;ve been using Zoho for my personal email for years, and I love it. I moved for a few reasons, privacy being a big one. If you&#8217;re not paying for the product, you are the product! I had used Gmail since it came out in college, and it was time to take control of my online privacy and data security.</p>



<p>After moving to Zoho, I found that they also had fantastic customer support! I can file a ticket and get a human response who actually helps to resolve my question. It&#8217;s been such a breath of fresh air.</p>



<p>Recently, I started co-teaching a product management class at Rice. As part of that, I re-activated my rice.edu email address. Rice uses Google for their email handling, so I added that email into my Zoho account as an additional IMAP email connection.</p>



<p>This lets me check my rice.edu email from the same zoho web app that I use for my personal email, while still keeping the two email inboxes and emails separate. It&#8217;s been wonderful, and I do this for a few other email accounts as well.</p>



<p>What was odd &#8211; emails in my Rice account were appearing twice in Zoho (!) while my other IMAP added accounts were behaving just fine. I contacted Zoho support, who suggested this was an issue on the Google side, and recommended I find the message identifiers to confirm if these were &#8220;different&#8221; emails or not &#8211; this could help diagnose the issue.</p>



<p>I found the exact same message id, and exact same .eml contents for the duplicate emails. I noticed while doing this &#8211; one email was in Inbox and the other was in Important.</p>



<p>I remembered that Gmail uses labels instead of folders, and this can cause strange side effects for IMAP clients. I talked out my problem and evidence so far with Claude, <a href="/wp-content/uploads/2024/10/Gmail-IMAP-in-Zoho-AI-chat-2.pdf">which suggested I change Gmail&#8217;s IMAP settings</a>. I found the toggle in Gmail settings → full settings → labels → IMAP access and turned it off for the Important label.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="432" src="https://adamwulf.me/wp-content/uploads/2024/10/CleanShot-2024-10-11-at-12.52.55@2x-1024x432.png" alt="A screenshot showing Gmail's Settings. This shows the Labels section where the &quot;Show in IMAP&quot; toggle is turned off for the &quot;Important&quot; label." class="wp-image-5834" srcset="https://adamwulf.me/wp-content/uploads/2024/10/CleanShot-2024-10-11-at-12.52.55@2x-1024x432.png 1024w, https://adamwulf.me/wp-content/uploads/2024/10/CleanShot-2024-10-11-at-12.52.55@2x-300x127.png 300w, https://adamwulf.me/wp-content/uploads/2024/10/CleanShot-2024-10-11-at-12.52.55@2x-768x324.png 768w, https://adamwulf.me/wp-content/uploads/2024/10/CleanShot-2024-10-11-at-12.52.55@2x-1536x648.png 1536w, https://adamwulf.me/wp-content/uploads/2024/10/CleanShot-2024-10-11-at-12.52.55@2x-2048x864.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Next sync &#8211; tada! Zoho removed all the duplicates from that Important flag and is correctly showing only a single email in my Unread filter.</p>



<p>Wins: Zoho support for helping me find clues, AI for helping me diagnose the issue with those clues, Gmail for having the correct settings to undo their non-standard IMAP implementation, and Zoho for a stellar email product!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://adamwulf.me/2024/10/gmail-imap-configuration-in-zoho/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Think On My Feet</title>
		<link>https://adamwulf.me/2024/03/think-on-my-feet/</link>
					<comments>https://adamwulf.me/2024/03/think-on-my-feet/#respond</comments>
		
		<dc:creator><![CDATA[Adam Wulf]]></dc:creator>
		<pubDate>Mon, 04 Mar 2024 01:06:34 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://adamwulf.me/?p=5819</guid>

					<description><![CDATA[I keep a list of random ideas, and any time I need to inject a bit of creativity into my weekend I try to pick off a small easy win and see if I can create it. Today&#8217;s idea: Think On Your Feet: Custom printed socks with thought bubble emojis all over them. So you [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>I keep a list of random ideas, and any time I need to inject a bit of creativity into my weekend I try to pick off a small easy win and see if I can create it. Today&#8217;s idea:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Think On Your Feet:</p>



<p>Custom printed socks with thought bubble emojis all over them. So you can ‘think on your feet.’</p>
</blockquote>



<p>After filtering through many search results for &#8220;custom socks,&#8221; most of which were aimed at youth sports leagues, I finally found that none other than Shutterfly offers the option. A little work creating a &#8216;photograph&#8217; of the Thought Bubble emoji and one Shutterfly template later, voila!</p>


<div class="wp-block-image">
<figure class="aligncenter size-medium"><a href="https://www.shutterfly.com/share-product/?shareid=0cda6f87-dcd8-4130-9343-16782fa6b0c4&amp;cid=SHARPRDWEBMPRLNK"><img decoding="async" width="226" height="300" src="https://adamwulf.me/wp-content/uploads/2024/03/Think-On-My-Feet-226x300.png" alt="" class="wp-image-5820" srcset="https://adamwulf.me/wp-content/uploads/2024/03/Think-On-My-Feet-226x300.png 226w, https://adamwulf.me/wp-content/uploads/2024/03/Think-On-My-Feet-770x1024.png 770w, https://adamwulf.me/wp-content/uploads/2024/03/Think-On-My-Feet-768x1021.png 768w, https://adamwulf.me/wp-content/uploads/2024/03/Think-On-My-Feet.png 1080w" sizes="(max-width: 226px) 100vw, 226px" /></a></figure></div>


<p><a href="https://www.shutterfly.com/share-product/?shareid=0cda6f87-dcd8-4130-9343-16782fa6b0c4&amp;cid=SHARPRDWEBMPRLNK">Get a pair</a> for the deep thinker in your life!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://adamwulf.me/2024/03/think-on-my-feet/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Removing Xcode Simulator Touch Indicators</title>
		<link>https://adamwulf.me/2024/01/removing-xcode-simulator-touch-indicators/</link>
					<comments>https://adamwulf.me/2024/01/removing-xcode-simulator-touch-indicators/#respond</comments>
		
		<dc:creator><![CDATA[Adam Wulf]]></dc:creator>
		<pubDate>Fri, 26 Jan 2024 03:09:53 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://adamwulf.me/?p=5803</guid>

					<description><![CDATA[I&#8217;ve been working on creating new Muse onboarding and tutorial videos, and I&#8217;m using my HandShadows Swift package to show the gestures visually during the video. Muse is a gesture heavy app, and showing plain dots for the finger locations doesn&#8217;t always make it clear what&#8217;s actually happening. These shadows make each gesture much more [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;ve been working on creating new <a href="https://museapp.com/">Muse</a> onboarding and tutorial videos, and I&#8217;m using my <a href="https://github.com/adamwulf/HandShadows">HandShadows</a> Swift package to show the gestures visually during the video. Muse is a gesture heavy app, and showing plain dots for the finger locations doesn&#8217;t always make it clear what&#8217;s actually happening. These shadows make each gesture much more clear.</p>



<figure class="wp-block-video"><video autoplay controls loop src="https://adamwulf.me/wp-content/uploads/2024/01/to-move-between-boards-ipad.mp4"></video></figure>



<p>To record these demos, I&#8217;m using <a href="https://www.telestream.net/screenflow/overview.htm">Screenflow</a>, a wonderfully simple screen recorder and video editor.</p>



<p>For reasons beyond my understanding, I found that using Screenflow to record the simulator gave higher quality video than using the Simulator&#8217;s built in Record Screen functionality. Similarly, it&#8217;s better quality than recording the screen directly on the iPad as well.</p>



<p>I&#8217;ll have to eventually solve the quality problem, as it&#8217;s not currently possible to perform two simultaneous gestures on the simulator. For those future demo videos that do use simultaneous gestures, I&#8217;ll need to record directly on the iPad, but until then the simulator works great.</p>



<p>However! By default, the simulator shows two dots for the touch points of a two finger gesture. I&#8217;m using HandShadows precisely to <em>not</em> show touch points, so I needed a way to either edit these points out or turn them off entirely.</p>



<p>Luckily, there is are a few hidden user defaults for the Simulator to toggle touch points. I found the first one through Aman Mittal&#8217;s <a href="https://amanhimself.dev/blog/show-touch-indicator-on-ios-simulator/">blog post</a> about turning <em>on</em> the touch point for single touch gestures. To do that, you can do the following:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="bash" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">defaults write com.apple.iphonesimulator ShowSingleTouches 1</pre>



<p>This turns touch points on for a single finger, which is very useful if you&#8217;re using the simulator&#8217;s touch indicators for videos, but in my case I wanted to turn them all <em>off</em>.</p>



<p>I asked colleagues if they knew any other hidden simulator defaults, and one suggested to run <code>strings</code> on the simulator executable and see what came up. So that&#8217;s just what I did:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="bash" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">strings /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator | grep Show</pre>



<p>And what appears? Hope!</p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">...
ShowSingleTouches
ShowPinches
ShowPinchPivotPoint
...</pre>



<p>And sure enough, toggling the <code>ShowPinches</code> removes the two finger gesture touch points from the simulator!</p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">defaults write com.apple.iphonesimulator ShowPinches 0</pre>



<p>And an interesting 3rd option too: <code>ShowPinchPivotPoint</code>. Let&#8217;s see what the simulator looks like with all of these turned on:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">defaults write com.apple.iphonesimulator ShowSingleTouches 1
defaults write com.apple.iphonesimulator ShowPinches 1
defaults write com.apple.iphonesimulator ShowPinchPivotPoint 1</pre>



<p>When toggling these settings from the command line, it seems that the Simulator app needs to be completely quit and restarted before they take effect.</p>



<figure class="wp-block-video"><video autoplay controls loop src="https://adamwulf.me/wp-content/uploads/2024/01/simulator-touch-points.mp4"></video></figure>



<p>These settings are a very helpful new tool in the toolbox!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://adamwulf.me/2024/01/removing-xcode-simulator-touch-indicators/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure length="806469" type="video/mp4" url="https://adamwulf.me/wp-content/uploads/2024/01/to-move-between-boards-ipad.mp4"/>
<enclosure length="2283651" type="video/mp4" url="https://adamwulf.me/wp-content/uploads/2024/01/simulator-touch-points.mp4"/>

			</item>
		<item>
		<title>Integrating Sparkle framework in a sandboxed Mac Catalyst app</title>
		<link>https://adamwulf.me/2023/06/integrating-sparkle-framework-in-sandboxed-mac-catalyst-app/</link>
					<comments>https://adamwulf.me/2023/06/integrating-sparkle-framework-in-sandboxed-mac-catalyst-app/#respond</comments>
		
		<dc:creator><![CDATA[Adam Wulf]]></dc:creator>
		<pubDate>Thu, 15 Jun 2023 23:03:09 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://adamwulf.me/?p=5777</guid>

					<description><![CDATA[I&#8217;m working to take an App Store-only Catalyst app and allow it to be distributed outside the App Store. Part of that is making sure that we can auto-update the app, and we&#8217;re using the Sparkle framework for the task. Since Sparkle is an AppKit framework, and our app is a UIKit Catalyst app, I [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;m working to take an App Store-only Catalyst app and allow it to be distributed outside the App Store. Part of that is making sure that we can auto-update the app, and we&#8217;re using the Sparkle framework for the task.</p>



<p>Since Sparkle is an AppKit framework, and our app is a UIKit Catalyst app, I needed some help joining these two worlds together, and I found this fantastic step-by-step tutorial by <a href="https://twitter.com/EskilSviggum">Eskil Sviggum</a>: <a href="https://betterprogramming.pub/configuring-app-updates-for-mac-catalyst-apps-with-sparkle-beef7a90a515">https://betterprogramming.pub/configuring-app-updates-for-mac-catalyst-apps-with-sparkle-beef7a90a515</a>.</p>



<p>After embedding the SparklePlugin.bundle into the app, tying together the UIKit and AppKit so that it could attempt to check for updates at launch, I immediately got the following error when launching the app in Debug mode from Xcode:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">2023-06-15 17:54:13.028099-0500 Muse[55654:1170102] [loading] Error loading /Users/adamwulf/Library/Developer/Xcode/DerivedData/Muse-cfdmmqzcfayzhrabrpyekznonhrk/Build/Products/Debug-maccatalyst/Muse.app/Contents/PlugIns/SparklePlugin.bundle/Contents/MacOS/SparklePlugin (194):  dlopen(/Users/adamwulf/Library/Developer/Xcode/DerivedData/Muse-cfdmmqzcfayzhrabrpyekznonhrk/Build/Products/Debug-maccatalyst/Muse.app/Contents/PlugIns/SparklePlugin.bundle/Contents/MacOS/SparklePlugin, 0x0109): Library not loaded: @rpath/Sparkle.framework/Versions/B/Sparkle
  Referenced from: &lt;3B2706F0-30A4-3FF2-84B3-191F25C6A606> /Users/adamwulf/Library/Developer/Xcode/DerivedData/Muse-cfdmmqzcfayzhrabrpyekznonhrk/Build/Products/Debug-maccatalyst/Muse.app/Contents/PlugIns/SparklePlugin.bundle/Contents/MacOS/SparklePlugin
  Reason: tried: '/Users/adamwulf/Library/Developer/Xcode/DerivedData/Muse-cfdmmqzcfayzhrabrpyekznonhrk/Build/Products/Debug/Sparkle.framework/Versions/B/Sparkle' (file system sandbox blocked open()), 
...</pre>



<p>The issue is that the embedded plugin bundle includes an embedded Sparkle.framework, and that inner framework isn&#8217;t being codesigned with the same identity, which means the sandboxed app is refusing to load it. I found <a href="https://github.com/sparkle-project/Sparkle/issues/1885">this thread</a> pointing to the <a href="https://sparkle-project.org/documentation/sandboxing/#code-signing">framework&#8217;s documentation about codesigning</a>, and the solution is to add an extra code signing step to the build phases.</p>



<p>I modified it slightly so that it pointed explicitly at the Sparkle.framework in the <code>BUILT_PRODUCTS_DIR</code>. Once I did that, I got the following build error:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">Showing All Messages
Apple Development: ambiguous (matches "Apple Development: Adam Wulf (SomeTeam)" and "Apple Development: Adam Wulf (OtherTeam)" in /Users/adamwulf/Library/Keychains/login.keychain-db)</pre>



<p>My Apple ID is part of multiple teams, so I have multiple developer profiles that were matching. The fix is to use <code>EXPANDED_CODE_SIGN_IDENTITY_NAME</code> instead of <code>CODE_SIGN_IDENTITY</code>.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="bash" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">PluginsPath="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME%.*}.app/Contents/PlugIns/SparklePlugin.bundle/Contents/Frameworks"

echo "Codesign Sparkle"
echo $PluginsPath

codesign -f -s "$EXPANDED_CODE_SIGN_IDENTITY_NAME" -o runtime "${PluginsPath}/Sparkle.framework/Versions/B/XPCServices/Installer.xpc"
codesign -f -s "$EXPANDED_CODE_SIGN_IDENTITY_NAME" -o runtime --entitlements Entitlements/Downloader.entitlements "${PluginsPath}/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc"

codesign -f -s "$EXPANDED_CODE_SIGN_IDENTITY_NAME" -o runtime "${PluginsPath}/Sparkle.framework/Versions/B/Autoupdate"
codesign -f -s "$EXPANDED_CODE_SIGN_IDENTITY_NAME" -o runtime "${PluginsPath}/Sparkle.framework/Versions/B/Updater.app"

codesign -f -s "$EXPANDED_CODE_SIGN_IDENTITY_NAME" -o runtime "${PluginsPath}/Sparkle.framework"
</pre>



<p>After that change to the signing build phase script, it could build + run + check for updates just fine!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://adamwulf.me/2023/06/integrating-sparkle-framework-in-sandboxed-mac-catalyst-app/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>