<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Objectionable-C</title><link>http://objectionable-c.com/</link><description>Recent content on Objectionable-C</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Thu, 13 Aug 2026 08:00:00 -0800</lastBuildDate><atom:link href="http://objectionable-c.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Ask not what Siri can do for you</title><link>http://objectionable-c.com/posts/maybe-dont-ask-siri/</link><pubDate>Thu, 13 Aug 2026 08:00:00 -0800</pubDate><guid>http://objectionable-c.com/posts/maybe-dont-ask-siri/</guid><content:encoded>&lt;p>iOS 27 introduces Siri AI, and Apple&amp;rsquo;s really excited about it. So excited that your app can get new &amp;ldquo;Ask Siri&amp;rdquo; options in &lt;code>UIMenu&lt;/code>s with no apparent opt out.&lt;/p>
&lt;p>
 &lt;img src="1.jpeg" alt="">
&lt;/p>
&lt;p>It seems like the OS is using some heuristics for choosing when to show this and when not to. I see it in some places in my apps, and not in others.&lt;/p>
&lt;p>
 &lt;img src="slide.jpeg" alt="">
&lt;/p>
&lt;p>Kind of odd that there&amp;rsquo;s not an official way to opt out of these, so I did some digging to find if there&amp;rsquo;s some way to omit them.&lt;/p>
&lt;p>I discovered that these &lt;code>UIAction&lt;/code>s are a bit odd, they lack a title and have a transient identifier.&lt;/p>
&lt;p>
 &lt;img src="action.jpeg" alt="">
&lt;/p>
&lt;p>However, they are all created using the &lt;code>+_intelligentAssistantActionWithIdentifier:configuration:&lt;/code> class method on &lt;code>UIAction&lt;/code>.&lt;/p>
&lt;p>
 &lt;img src="bt.jpeg" alt="">
&lt;/p>
&lt;p>The injection of these happens via a C function &lt;em>after&lt;/em> you&amp;rsquo;ve been queried for your menu items, so the best way I&amp;rsquo;ve found thus far to omit them is by swizzling &lt;code>+_intelligentAssistantActionWithIdentifier:configuration:&lt;/code> to return &lt;code>nil&lt;/code> or to hide it using &lt;code>UIMenuElementAttributesHidden&lt;/code>.&lt;/p>
&lt;p>
 &lt;img src="swizzlers.jpeg" alt="">
&lt;/p>
&lt;p>Of course swizzling private methods should be done cautiously, but if you feel inclined this is a way to omit those &amp;ldquo;Ask Siri&amp;rdquo; entries!&lt;/p></content:encoded></item><item><title>Iconic loading</title><link>http://objectionable-c.com/posts/iconic-loading/</link><pubDate>Fri, 30 Jan 2026 19:00:00 -0800</pubDate><guid>http://objectionable-c.com/posts/iconic-loading/</guid><content:encoded>&lt;p>Showing your app&amp;rsquo;s icon &lt;em>in&lt;/em> your app: seems like something you might want to do right?&lt;/p>
&lt;p>Prior to iOS 18‘s introduction of home screen dark mode it was very easy to get your app&amp;rsquo;s icon, just use &lt;code>+imageNamed:&lt;/code>. But in iOS 18 and higher to show your app&amp;rsquo;s icon you might need to package a separate copy or fetch it from a server just for this purpose.&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="toot.jpeg" alt="">
 &lt;figcaption>https://mastodon.social/@kylebshr/112598769741895570&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>Well, it turns out that there is a place where iOS stashes a copy of your app&amp;rsquo;s icon that is accessible without these gymnastics. Xcode&amp;rsquo;s build process has created a separate copy of app icons for years that are embedded as files in your bundle.&lt;/p>
&lt;p>
 &lt;img src="icons.jpeg" alt="">
&lt;/p>
&lt;p>These files are placed here presumably to be loaded by the system outside of springboard.&lt;/p>
&lt;p>You can find the names of these files by looking at particular keys in your bundle&amp;rsquo;s &lt;code>infoDictionary&lt;/code>.&lt;/p>
&lt;p>
 &lt;img src="plist.jpeg" alt="">
&lt;/p>
&lt;p>From there you can enumerate the available files and pick the largest one to show. This is how I&amp;rsquo;m doing it.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-objc" data-lang="objc">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">@implementation&lt;/span> &lt;span style="color:#a6e22e">NSBundle&lt;/span> (AppIcon)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- (UIImage &lt;span style="color:#f92672">*&lt;/span>)&lt;span style="color:#a6e22e">appIcon&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NSDictionary &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">const&lt;/span> bundleIcons &lt;span style="color:#f92672">=&lt;/span> self.infoDictionary[&lt;span style="color:#e6db74">@&amp;#34;CFBundleIcons&amp;#34;&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NSArray&lt;span style="color:#f92672">&amp;lt;&lt;/span>NSString &lt;span style="color:#f92672">*&amp;gt;&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">const&lt;/span> imageNames &lt;span style="color:#f92672">=&lt;/span> bundleIcons[&lt;span style="color:#e6db74">@&amp;#34;CFBundlePrimaryIcon&amp;#34;&lt;/span>][&lt;span style="color:#e6db74">@&amp;#34;CFBundleIconFiles&amp;#34;&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> UIImage &lt;span style="color:#f92672">*&lt;/span>appIconImage &lt;span style="color:#f92672">=&lt;/span> nil;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CGFloat maxWidth &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0.0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> (NSString &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">const&lt;/span> imageName &lt;span style="color:#66d9ef">in&lt;/span> imageNames) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> UIImage &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">const&lt;/span> image &lt;span style="color:#f92672">=&lt;/span> [UIImage imageNamed:imageName inBundle:self compatibleWithTraitCollection:nil];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (image.size.width &lt;span style="color:#f92672">&amp;gt;&lt;/span> maxWidth) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> appIconImage &lt;span style="color:#f92672">=&lt;/span> image;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> maxWidth &lt;span style="color:#f92672">=&lt;/span> image.size.width;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> appIconImage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">@end&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This even works with iOS 26&amp;rsquo;s new &lt;code>.icon&lt;/code> file format!&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="new-format.jpeg" alt="">
 &lt;figcaption>Even .icon files are exported to the bundle&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>I&amp;rsquo;ve been using this trick in &lt;a href="https://apps.apple.com/app/id989565871">Opener&lt;/a> for ages, and we&amp;rsquo;re now using it in &lt;a href="https://apps.apple.com/app/id6443709020">Retro&lt;/a>! It could be a good way to slim down your app a tiny bit.&lt;/p>
&lt;p>&lt;em>Note: This only applies to your app&amp;rsquo;s &lt;em>default&lt;/em> icon, if you provide alternate icons you&amp;rsquo;ll still need another way to show them.&lt;/em>&lt;/p>
&lt;p>Credit to &lt;a href="https://stackoverflow.com/a/23158902/3943258">this Stack Overflow answer&lt;/a> where I originally got this idea from ages ago.&lt;/p></content:encoded></item><item><title>Ultra memory-friendly thumbnails on iOS</title><link>http://objectionable-c.com/posts/qlthumbnailgenerator/</link><pubDate>Fri, 08 Aug 2025 05:00:00 -1000</pubDate><guid>http://objectionable-c.com/posts/qlthumbnailgenerator/</guid><content:encoded>&lt;p>This week &lt;a href="https://retro.app">Retro&lt;/a> has &lt;a href="https://x.com/ryanolsonk/status/1953114758720127454">a fresh new look&lt;/a> to the Friends widget. Where we used to only show a single post, we now show a grid of recent posts from your friends.&lt;/p>
&lt;p>
 &lt;img src="widgets.jpg" alt="">
&lt;/p>
&lt;p>It’s not super well documented, but widgets have size restrictions on the images you can add to timeline entries. This combined with the very low memory limits imposed on iOS extensions (~30MB) has historically made loading images into widgets a bit of a headache. We got crashes while downscaling very often, even when using the efficient &lt;code>CGImageSource&lt;/code> thumbnailing methods.&lt;/p>
&lt;p>So, how do we go from showing one image per widget to showing a whole grid without blowing up the memory limit?&lt;/p>
&lt;p>In the past I’ve heard of the &lt;a href="https://developer.apple.com/documentation/quicklookthumbnailing?language=objc">QuickLook framework’s thumbnailing methods&lt;/a> being designed explicitly for the low memory requirements of extensions, but nothing I’d worked on had warranted using them. To enable Retro’s grid widget I decided to give them another shot.&lt;/p>
&lt;p>Per the docs (&lt;a href="https://developer.apple.com/documentation/quicklookthumbnailing/qlthumbnailgenerator/savebestrepresentation(for:to:contenttype:completion:)?language=objc#:~:text=Use%20this%20method%20to%20create%20and%20save%20the%20thumbnail%20image%20outside%20of%20your%20process%20as%20it%20doesn%E2%80%99t%20impose%20the%20same%20constraints%20on%20memory%20usage.">here&lt;/a> and &lt;a href="https://developer.apple.com/documentation/quicklookthumbnailing/qlthumbnailgenerator/savebestrepresentation(for:to:as:completion:)?language=objc#:~:text=This%20is%20primarily%20intended%20for%20file%20provider%20extensions%20which%20need%20to%20upload%20thumbnails%20and%20have%20a%20small%20memory%20limit.">here&lt;/a>), in order to get out-of-process thumbnailing you have to use the &lt;a href="https://developer.apple.com/documentation/quicklookthumbnailing/qlthumbnailgenerator/savebestrepresentation(for:to:as:completion:)?language=objc">&lt;code>-saveBestRepresentationForRequest:toFileAtURL:...&lt;/code>&lt;/a> method that operates on files. You give it a file, and it hands you a thumbnail file back. I wasn’t expecting this API to work given that I hadn’t heard much about it before, but to my surprise it does exactly what it claims! The widget, even when processing many images, now barely consumes memory where it used to have a nonzero chance of running out.&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="before2.jpeg" alt="">
 &lt;figcaption>Before, OOMing&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="after.jpeg" alt="">
 &lt;figcaption>After, comfortably under the memory limit&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>If you’re working in a memory constrained environment like widgets or extensions on iOS I highly recommend checking this out.&lt;/p>
&lt;h2 id="july-2026-update">July 2026 Update&lt;/h2>
&lt;p>&lt;a href="https://tapbots.social/@paul">Paul Haddad&lt;/a> reached out to me about using this API. Apparently there&amp;rsquo;s an undocumented upper limit on the size of thumbnails you can request from QuickLook of around 3.5 MP. If you request a thumbnail that&amp;rsquo;s too large you&amp;rsquo;ll get something an error &lt;code>QLThumbnailErrorDomain&lt;/code> with code &lt;code>0&lt;/code> and underlying error code &lt;code>102&lt;/code>. 3.5 MP is plenty for a widget, but might not work for other cases, something to bear in mind!&lt;/p></content:encoded></item><item><title>Trim the Fat: Static file tricks to slim down your app</title><link>http://objectionable-c.com/posts/shrink-static-files/</link><pubDate>Fri, 25 Jul 2025 05:00:00 -1000</pubDate><guid>http://objectionable-c.com/posts/shrink-static-files/</guid><content:encoded>&lt;p>I care deeply about keeping apps as small as possible. It’s our responsibility as iOS developers to preserve resources on our user’s devices.&lt;/p>
&lt;p>I’ve detailed some strategies about &lt;a href="http://objectionable-c.com/posts/brotli-ios/">compressing files&lt;/a> and &lt;a href="http://objectionable-c.com/posts/woff2-ios/">compressing particular file formats&lt;/a> that you can use to reduce your app’s size, but what about files you can’t compress or change the format of? For example: third party libraries often bundle in plists and JSON, and you couldn’t compress those files without forking the library and changing how they’re loaded. Some libraries are closed source so you couldn’t even do this if you wanted to.&lt;/p>
&lt;p>Well, it turns out there’s a little slack you can get from these files.&lt;/p>
&lt;h2 id="plists">Plists&lt;/h2>
&lt;p>Plists have several different encodings under the hood: XML, binary, and JSON. Of these, binary often is the most compact and quickest to load, but sometimes the bulkier formats are bundled into your built app instead.&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="xmlplist.png" alt="">
 &lt;figcaption>You can use the file command to inspect they underlying types of files. In this case a framework plist was using XML instead of binary&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>
 &lt;img src="trenchcoat.jpg" alt="">
&lt;/p>
&lt;p>When loading plists the particular encoding doesn’t matter, so we can safely re-encode plists as binary as a build step to save on space. Furthermore, many other formats like &lt;code>.xcprivacy&lt;/code> and &lt;code>.strings&lt;/code> are actually plists under the hood, and we can re-encode those too.&lt;/p>
&lt;p>
 &lt;img src="scooby.jpg" alt="">
&lt;/p>
&lt;h2 id="json">JSON&lt;/h2>
&lt;p>The size of JSON files can be easily reduced by stripping out whitespace.&lt;/p>
&lt;pre tabindex="0">&lt;code>// These are equivalent
{
 &amp;#34;foo&amp;#34;: [
 &amp;#34;bar&amp;#34;,
 &amp;#34;baz&amp;#34;
 ],
 &amp;#34;xyz&amp;#34;: 123
}

{&amp;#34;foo&amp;#34;:[&amp;#34;bar&amp;#34;,&amp;#34;baz&amp;#34;],&amp;#34;xyz&amp;#34;:123}
&lt;/code>&lt;/pre>&lt;p>For large JSON files this whitespace can be significant. For example: in &lt;a href="https://opener.link/get">Opener’s&lt;/a> rule set JSON whitespace accounts for 45% of the original file size. We can safely strip out this whitespace as a build step to reduce JSON file size while preserving the original data. Just like plists, there are other formats like &lt;code>.ahap&lt;/code> that are just JSON under the hood that we can also tidy up.&lt;/p>
&lt;h2 id="putting-it-together">Putting it together&lt;/h2>
&lt;p>Using these strategies I’ve made a couple of scripts you can use to reduce your app’s size, available &lt;a href="https://github.com/timonus/appscripts">here&lt;/a>. You can add them to your app using a build script step like the following.&lt;/p>
&lt;p>
 &lt;img src="script.png" alt="">
&lt;/p>
&lt;pre tabindex="0">&lt;code>if [ &amp;#34;${CONFIGURATION}&amp;#34; = &amp;#34;Release&amp;#34; ]; then

python3 Scripts/plist-minify.py
python3 Scripts/json-minify.py

fi
&lt;/code>&lt;/pre>&lt;p>I only run these for release builds since they slow down the build slightly.&lt;/p>
&lt;h2 id="examples">Examples&lt;/h2>
&lt;p>I use these scripts in all the apps I work on. In &lt;a href="https://retro.app">Retro&lt;/a> they’re currently saving ~400 KB, not too bad for a build step that requires little maintenance!&lt;/p>
&lt;p>What’s fun about these scripts is, since they run on fully built apps, you can run them on any app. Here are some other examples for apps I don’t have the source for.&lt;/p>
&lt;ul>
&lt;li>In Cash app they save 333 KB&lt;/li>
&lt;li>In Instagram they save 924 KB&lt;/li>
&lt;li>In eufy they save 1.2 MB&lt;/li>
&lt;li>In Xcode 16.4 they save 40 MB&lt;/li>
&lt;/ul>
&lt;p>If you’re looking for some low hanging fruit to reduce your app’s size I recommend trying these out!&lt;/p></content:encoded></item><item><title>Dub Dub Clickbait 2025</title><link>http://objectionable-c.com/posts/dub-dub-clickbait-2025/</link><pubDate>Fri, 06 Jun 2025 05:00:00 -0700</pubDate><guid>http://objectionable-c.com/posts/dub-dub-clickbait-2025/</guid><content:encoded>&lt;p>This is the least connected I’ve felt to WWDC in years, in fact I booked a flight during the keynote 🙃. That’s not to say my love for Apple has waned, I think maybe the dud that was Apple Intelligence last year just has me less tuned-in as most years. Nonetheless, I’m excited to see what they announce.&lt;/p>
&lt;p>There’s always shiny new features during dub dub, but what I really love is quality-of-life refinements to mature APIs that I’ve used for ages. The canonical example I use is when iOS 15 introduced a &lt;a href="https://developer.apple.com/documentation/uikit/uiimage?changes=latest_minor&amp;amp;language=objc#Loading-images-for-display">dedicated image decoding API&lt;/a>, or iOS 13’s introduction of &lt;a href="https://github.com/timonus/UIImageViewAnimatedGIF">first-party animated GIF playback&lt;/a>. APIs that make you go “finally!”&lt;/p>
&lt;p>A couple years ago I started recording little API wishes I had for Apple to put together a sort of “bingo card” for future WWDCs. I feel like publishing it is a little clickbait-y, but might as well in case it resonates with folks or developers at Apple might see it. Here’s what I’d love to see in no particular order:&lt;/p>
&lt;ul>
&lt;li>Add a built in hex-to-&lt;code>UIColor&lt;/code> method&lt;/li>
&lt;li>Add a “Toast” UI component&lt;/li>
&lt;li>Add images to actions in &lt;code>UIAlertController&lt;/code>&lt;/li>
&lt;li>Extended swipe to pop navigation controller gestures to full screen easily (like &lt;a href="https://github.com/timonus/TJExtendedNavigationPop">this&lt;/a>)&lt;/li>
&lt;li>Aesthetic text wrapping (like &lt;a href="https://github.com/kylebshr/AestheticText">this&lt;/a>)&lt;/li>
&lt;li>Easily detect if text in &lt;code>UILabel&lt;/code>/&lt;code>UITextView&lt;/code> is truncated, provide alternates to fit&lt;/li>
&lt;li>Tappable links in &lt;code>UILabel&lt;/code> (like &lt;a href="https://github.com/timonus/UILabelTapHandling">this&lt;/a>)&lt;/li>
&lt;li>Better share/action extension disabling rules&lt;/li>
&lt;li>Auto-sizing &lt;code>UISheetPresentationController&lt;/code>&lt;/li>
&lt;li>Nav bar header customization via appearance proxy&lt;/li>
&lt;li>Open up translate sheet to UIKit (like &lt;a href="https://x.com/SebJVidal/status/1801887531048349749">this&lt;/a>)&lt;/li>
&lt;li>More fine grained control of &lt;code>UIVisualEffectView&lt;/code>&lt;/li>
&lt;li>Get size of keyboard arbitrarily, without it even being shown&lt;/li>
&lt;li>Expose &lt;code>NSDataCompressionAlgorithmBrotli&lt;/code> (&lt;a href="https://objectionable-c.com/posts/brotli-ios/#:~:text=Hopefully%20Apple%20makes%20this%20enum%20value%20public%20in%20the%20future!%20(FB16918276)">this&lt;/a>)&lt;/li>
&lt;li>A better way of creating launch screens (maybe more control over &lt;a href="https://sarunw.com/posts/launch-screen-using-plist/">&lt;code>UILaunchScreen&lt;/code>&lt;/a>)&lt;/li>
&lt;li>Make it so wireless debugging can be turned off&lt;/li>
&lt;li>Allow typing &lt;code>:emoji-name:&lt;/code> to autocomplete emojis in text fields&lt;/li>
&lt;li>Allow double tap on hardware action button to trigger a specific action&lt;/li>
&lt;li>Add “Unsubscribe” action to email notifications&lt;/li>
&lt;/ul></content:encoded></item><item><title>Your Links, Your Way — Ten Years of Opener</title><link>http://objectionable-c.com/posts/opener-10/</link><pubDate>Sun, 01 Jun 2025 13:40:00 -0700</pubDate><guid>http://objectionable-c.com/posts/opener-10/</guid><content:encoded>&lt;p>Today &lt;a href="https://apps.apple.com/app/id989565871">Opener&lt;/a> turns 10 years old. It&amp;rsquo;s safe to say Opener is the side project that I&amp;rsquo;m most proud of, and has had the most success on the App Store. I wanted to take some time to reflect on the past 10 years building it. This blog post won&amp;rsquo;t be too technical, it&amp;rsquo;s mostly a collection of little stories and thoughts.&lt;/p>
&lt;h2 id="history">History&lt;/h2>
&lt;h3 id="early-inspiration">Early Inspiration&lt;/h3>
&lt;p>The idea for Opener came about from a few things:&lt;/p>
&lt;ul>
&lt;li>In my work at &lt;a href="https://flipboard.com">Flipboard&lt;/a> at the time we were spending a lot of time building out rendering for various different content types. I remember we&amp;rsquo;d just added support for &lt;a href="http://soundcloud.com/">SoundCloud&lt;/a> and we were talking about the possibility of building the SoundCloud waveform in our app and I thought: &amp;ldquo;This isn&amp;rsquo;t the best use of our time as a news app. We should really allow people to view this detail &lt;em>in SoundCloud&lt;/em> if they want.&amp;rdquo;&lt;/li>
&lt;li>I attended an event at Twitter HQ where they discussed Twitter Cards and how you could include metadata in them so Twitter could open links in your app. I remember thinking it would be cool if that worked everywhere, not just in Twitter.&lt;/li>
&lt;li>There was an iOS Workflow (pre-Apple acquisition) / bookmarklet that was floating around that allowed people to open Tweets in Tweetbot and other apps. I remember feeling like I wished this worked for &lt;em>every&lt;/em> link and &lt;em>every&lt;/em> app.&lt;/li>
&lt;li>My friend Jordan Kay tweeted about a Mac app he was working on that would open URLs in native apps named &lt;a href="https://github.com/jordanekay/Lattice">Lattice&lt;/a>. (Coincidentally Opener now works great on Macs with Apple Silicon!)&lt;/li>
&lt;/ul>
&lt;p>The introduction of &lt;a href="https://www.theverge.com/2014/6/2/5773080/ios-8-apps-can-talk-to-each-other">app extensions&lt;/a> in iOS 8 was the catalyst that made it impossible for me to not build Opener. I saw app extensions as the &lt;em>perfect&lt;/em> vessel for this functionality, allowing in-context actions to be taken on links.&lt;/p>
&lt;h3 id="elevator-pitch">Elevator Pitch&lt;/h3>
&lt;p>The elevator pitch for Opener is as true today as it was in 2015, so I&amp;rsquo;ll say it again.&lt;/p>
&lt;blockquote>
&lt;p>People love the apps on their phones, and companies spend millions of dollars developing those apps, yet when you tap links from those companies instead of opening the app it launches a logged out, slow, subpar web experience. What if you could open links directly in apps where you get the best possible experience? That&amp;rsquo;s what Opener does.&lt;/p>
&lt;/blockquote>
&lt;h3 id="stories">Stories&lt;/h3>
&lt;ul>
&lt;li>I remember the week I started Opener. I was in my parents house in Broomfield, Colorado for Easter week when I got the first prototype working. &lt;a href="early-screenshot.jpg">This&lt;/a> is the earliest screenshot of it I can find.&lt;/li>
&lt;li>I learned a &lt;em>crapload&lt;/em> about regular expressions while building Opener since it requires a lot of them. Turns out this is a really useful skill to have, and I&amp;rsquo;ve used it a bunch of times in my career since.&lt;/li>
&lt;li>I clearly remember the day Opener launched, June 1, 2015. It was a Monday and I had my summer intern starting that day, so I wasn&amp;rsquo;t really thinking about it. I used &lt;a href="https://buffer.com">Buffer&lt;/a> to pre-schedule all the &lt;a href="https://medium.com/p/497136c3e09c">posts&lt;/a> about the launch, but towards the end of the day a few people I worked with were like &amp;ldquo;hey, whoa, you launched this today? This is cool.&amp;rdquo; Over the week a few blogs wrote about it, which made me really happy. At the end of the week our CTO Eric Feng gave me a high five for being &lt;a href="https://techcrunch.com/2015/06/03/opener-for-ios-lets-you-open-web-links-in-your-apps-instead/">&amp;ldquo;crunched&amp;rdquo;&lt;/a>.&lt;/li>
&lt;li>WWDC 2015 was the week after Opener launched. I was &lt;em>terrified&lt;/em> when Apple announced universal links in iOS 9, which could effectively replace Opener. As it turned out over time, universal links only really work for first-party apps, and they don&amp;rsquo;t work that well in general anyway. Opener still serves people very well when they want control over opening links, or when universal links bug out as they&amp;rsquo;re prone to.&lt;/li>
&lt;li>I designed the &lt;a href="opener-icon-og.jpeg">original icon&lt;/a> in code as a &lt;code>UIView&lt;/code>, but it wasn&amp;rsquo;t exactly beautiful. Fortunately I started dating somebody in 2016 (now my wife) who was much better at design than me and she agreed to give the icon &lt;a href="https://medium.com/@timonus/opener-14-610343db047b#d67e">an overhaul&lt;/a>. The icon she designed is still the one that app uses today!
&lt;ul>
&lt;li>&lt;em>Side note&lt;/em>: I had &lt;em>hundreds&lt;/em> of &lt;a href="sticker.JPG">stickers&lt;/a> of the original icon printed and I&amp;rsquo;ve given away maybe 50 total. If you live and the US and you want one shoot me &lt;a href="https://mastodon.social/@timonus">a DM&lt;/a> and I&amp;rsquo;ll mail it to you.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>In iOS 14 Apple limited the number of URL schemes you could include in &lt;code>LSApplicationQueriesSchemes&lt;/code> to 50. I was nervous that people might be frustrated that Opener could no longer detect which apps were installed since it supported over 200 apps, so I continued to link Opener with the iOS 14 SDK for almost a whole year. I had a dedicated Mac mini that I kept on the old OS so I could continue to use the old Xcode to build it. Finally, when I looked, it turned out that 95% of links opened in Opener are within its top 50 most popular apps. I released the update that limited the list to 50 and I&amp;rsquo;ve gotten nearly zero complaints about it since.&lt;/li>
&lt;li>The week in 2022 when I was in the hospital after my first child was born I was alerted to the fact that &lt;a href="https://www.reddit.com/r/apple/comments/y577fi/comment/ismkis2/">somebody had cloned Opener&lt;/a>. I was in an emotional state already, and this made me &lt;em>pissed&lt;/em>. Up until this time I&amp;rsquo;d left Opener&amp;rsquo;s rule set as open source for people who wanted to contribute, but when reading the source for this clone&amp;rsquo;s JS I saw it clearly just copied the whole thing. I rapidly made Opener&amp;rsquo;s rule set private and hardened up security about how the app loads it. Since then a few more clones have popped up, but it seems like they&amp;rsquo;ve mostly become stale and forgotten.&lt;/li>
&lt;li>I sent the source code for Opener &lt;a href="https://www.reddit.com/r/space/comments/l52iym/you_can_send_something_to_the_moon_for_free_im/">to the moon&lt;/a> thanks to a kind person on Reddit! Sadly, it didn&amp;rsquo;t make it and burned up returning to Earth in January 2024. Still cool though.&lt;/li>
&lt;li>I applied for and &lt;a href="https://retro.app/m/5J4LF67">got to attend&lt;/a> Apple‘s developer labs for the Vision Pro on the Vision Pro’s launch day to test out Opener! While I was there the cranky engineer running the lab told me that Apple wasn’t going to add an API for detecting if an iPad app is running on Vision Pro. Afterwards I &lt;a href="https://medium.com/p/08a85db0fb31">figured out how to detect this anyways&lt;/a> and it still seems to be &lt;a href="https://developer.apple.com/forums/thread/733029?answerId=786093022#786093022">how people do it&lt;/a> these days. A few months after the Vision Pro launch I bought and returned one within the two week window to fix a bug one of my users was only encountering on Vision Pro.&lt;/li>
&lt;/ul>
&lt;h2 id="stats-and-factoids">Stats and Factoids&lt;/h2>
&lt;p>The stat I care most about for Opener is how many links are being opened in it. While I don’t have a 100% accurate picture of this number over the last 10 years, I’ve been able to patch together a pretty good approximation.&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="opens.png" alt="">
 &lt;figcaption>8.2 million total links opened&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>Here are the total copies of Opener sold over the past 10 years.&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="units.png" alt="">
 &lt;figcaption>83,693 total units sold&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>Here’s the proceeds I’ve made from Opener in this time.&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="proceeds.png" alt="">
 &lt;figcaption>$122,997 total proceeds&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>A vast majority of the links opened go through the action extension instead of the app itself. I still hold that app extensions are my favorite feature ever added to iOS because of this.&lt;/p>
&lt;p>
 &lt;img src="targets.png" alt="">
&lt;/p>
&lt;h1 id="the-future">The Future&lt;/h1>
&lt;p>Opener is feature rich, stable, and capable of meeting my users’ needs. I’m very cautious about not &lt;a href="https://en.wikipedia.org/wiki/Enshittification">enshittifying&lt;/a> it by tacking on unneeded features, and when I do add new features they’re appropriately buried under the app’s core functionality. I plan to be the best platform citizen I can, and to take advantage of new platform features that make sense for Opener as best as I possibly can. I also plan to continue to add support for opening more and more apps over time. This app is a labor of love, and I plan to keep it a well oiled machine for as long as I’m around.&lt;/p>
&lt;p>Here’s to the next 10 years!&lt;/p>
&lt;p>
 &lt;img src="cheers.gif" alt="">
&lt;/p></content:encoded></item><item><title>No-nonsense UISceneDelegate migration reference</title><link>http://objectionable-c.com/posts/uiscenedelegate-migration/</link><pubDate>Fri, 16 May 2025 05:00:00 -0700</pubDate><guid>http://objectionable-c.com/posts/uiscenedelegate-migration/</guid><content:encoded>&lt;p>
 &lt;img src="meme.jpg" alt="">
&lt;/p>
&lt;p>I&amp;rsquo;ve avoided migrating to using the &lt;code>UIScene&lt;/code>-based application lifecycle since it was announced with iOS 13 because:&lt;/p>
&lt;ul>
&lt;li>None of my apps support multiple windows (the main reason for &lt;code>UIScene&lt;/code> to exist afaict)&lt;/li>
&lt;li>I preferred the older, simpler methods of dealing with app events via &lt;code>UIApplicationDelegate&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>I&amp;rsquo;ve also seen numerous apps break in weird ways due to differences in behavior between &lt;code>UIApplicationDelegate&lt;/code> and &lt;code>UISceneDelegate&lt;/code>. Most notably, several events that get their own methods in &lt;code>UISceneDelegate&lt;/code> are handled completely differently on cold start when a scene is being spun up. This often causes &lt;a href="https://mastodon.social/@timonus/114433444611806352">URL handling&lt;/a> and shortcut handling to break on cold start. Another common source of issues is that app delegate lifecycle methods (&lt;code>didBecomeActive&lt;/code>/&lt;code>didEnterBackground&lt;/code>/etc.) are no longer called, even though the notifications for those events are still fired 🤔.&lt;/p>
&lt;p>It seems that &lt;a href="https://developer.apple.com/documentation/technotes/tn3187-migrating-to-the-uikit-scene-based-life-cycle">as of iOS 18.4&lt;/a> Apple is starting to force developers towards using the &lt;code>UIScene&lt;/code>-based lifecycle, so I took a stab at porting my apps over.&lt;/p>
&lt;p>I&amp;rsquo;ve created a project named &lt;a href="https://github.com/timonus/tjscenedelegate">&lt;code>TJSceneDelegate&lt;/code>&lt;/a> that should more or less acts as a drop in replacement in any app. When installed it forwards all events to &lt;code>UIApplicationDelegate&lt;/code>, the thought is that you should then be able to forget about scenes and use the app delegate callbacks like you used to. At the moment I&amp;rsquo;ve ported all but one of my side projects over to this, and I&amp;rsquo;m going to migrate the final one soon.&lt;/p>
&lt;p>I hope this can serve as a solution or a reference for others migrating to the &lt;code>UIScene&lt;/code>-based lifecycle.&lt;/p></content:encoded></item><item><title>Save space using compact fonts</title><link>http://objectionable-c.com/posts/woff2-ios/</link><pubDate>Wed, 30 Apr 2025 05:00:00 -0700</pubDate><guid>http://objectionable-c.com/posts/woff2-ios/</guid><content:encoded>&lt;p>&lt;em>Disclaimer: I&amp;rsquo;m not a font expert. The strategy pitched in this post may have drawbacks I haven&amp;rsquo;t yet discovered.&lt;/em>&lt;/p>
&lt;p>Last week at work we were importing a new font to use in some countries that was rather large. The designer I was working with managed to slim it down a bit, but it still wound up being 14 MB. This got me thinking if there was a way to reduce the size further.&lt;/p>
&lt;p>I asked my favorite coding companion AI, Claude, for advice. &lt;a href="https://claude.ai/share/43d27ecf-a8c7-4424-a7f5-9f9f099fad2c">Claude mentioned&lt;/a> trying out the newer font format &lt;code>.woff2&lt;/code>, developed by Google for compactness. I initially dismissed this idea thinking that iOS only supports &lt;code>.ttf&lt;/code> and &lt;code>.otf&lt;/code>, but upon trying it I&amp;rsquo;ve found that &lt;code>.woff2&lt;/code> does work on iOS!&lt;/p>
&lt;p>It&amp;rsquo;s trivial to convert existing fonts to &lt;code>.woff2&lt;/code> with a tool you can get through homebrew.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-shell" data-lang="shell">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Install Google&amp;#39;s woff2 tool&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>brew install woff2
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Convert a font to woff2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>woff2_compress &lt;span style="color:#f92672">[&lt;/span>your_font_here&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You&amp;rsquo;ll then need to update and references to the old font name containing its former file extension, most commonly in the info.plist &lt;code>UIAppFonts&lt;/code> field. Once you do this everything should work!&lt;/p>
&lt;p>This wound up saving:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>10.9 MB&lt;/strong> on the new font we imported (14.1 MB → 3.2 MB, &lt;strong>78% reduction&lt;/strong>)&lt;/li>
&lt;li>An additional &lt;strong>~800 KB&lt;/strong> (1.2 MB → ~400 KB, &lt;strong>~65% reduction&lt;/strong>) in the other fonts we use in &lt;a href="https://retro.app">Retro&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>I also applied this strategy to &lt;a href="https://github.com/RedHatOfficial/RedHatFont">the font&lt;/a> I&amp;rsquo;m using in &lt;a href="https://apps.apple.com/app/id1355171732">Burst&lt;/a> saving 71KB, close to &lt;strong>10% of the app&amp;rsquo;s overall size&lt;/strong>.&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="font-savings.jpg" alt="">
 &lt;figcaption>Font savings in Burst&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>I can&amp;rsquo;t find definitive documentation of when iOS added support for &lt;code>.woff2&lt;/code>, but it&amp;rsquo;s been supported in Safari &lt;a href="https://caniuse.com/?search=woff2">since iOS 10&lt;/a>. This seems like it could be a reasonable proxy for OS support, and I&amp;rsquo;ve tested it working as far back as iOS 15.8.4.&lt;/p>
&lt;p>If you&amp;rsquo;re looking for places to reduce your app&amp;rsquo;s size and you&amp;rsquo;re using custom fonts, leveraging &lt;code>.woff2&lt;/code> could be a good way to get some savings!&lt;/p>
&lt;h2 id="may-2026-update">May 2026 Update&lt;/h2>
&lt;p>Apple is &lt;a href="https://mastodon.social/@chadpod/116558693951370273">starting to reject&lt;/a> apps embedding woff2 fonts claiming they&amp;rsquo;re unsupported. They seem to be detecting these fonts by inspecting file contents, not just looking for the file extension, so a simple rename will still result in rejection. woff2 files have a predictable prefix, so you can still embed them by manipulating the contents of the bundled file then changing them back and loading at runtime.&lt;/p></content:encoded></item><item><title>Storing custom metadata in PHAssets</title><link>http://objectionable-c.com/posts/phasset-custom-metadata/</link><pubDate>Fri, 18 Apr 2025 05:00:00 -0700</pubDate><guid>http://objectionable-c.com/posts/phasset-custom-metadata/</guid><content:encoded>&lt;p>&lt;code>PHPhotoLibrary&lt;/code> is a powerful resource that lets you interact with one of iOS&amp;rsquo; most vital components as a developer. We&amp;rsquo;re lucky to have it compared with a lot of other OS features that provide little or no developer access.&lt;/p>
&lt;p>In the past I&amp;rsquo;ve wanted to add small bits of custom information to &lt;code>PHAsset&lt;/code>s, however &lt;code>PHAsset&lt;/code> doesn&amp;rsquo;t provide a way to do this. As a workaround you could add custom Exif or video metadata to the asset at write time, then load the asset, extract that data, and parse it at read time, but that could be relatively slow since fetching &lt;code>PHAsset&lt;/code> resource data can be iCloud-backed. Additionally, if downscaled data is returned from &lt;code>PHAsset&lt;/code>, that metadata could be stripped. I wanted a quick way to stash a small amount of information in &lt;code>PHAsset&lt;/code>s.&lt;/p>
&lt;p>What I found is &lt;code>PHAssetResource&lt;/code>. &lt;code>PHAssetResource&lt;/code> can be used when writing &lt;code>PHAsset&lt;/code>s to add fine-grained file representations of assets, but there&amp;rsquo;s a field within it that can be used to add arbitrary strings: the &lt;code>originalFilename&lt;/code>. You can set the original filename to whatever you&amp;rsquo;d like like so&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-objc" data-lang="objc">&lt;span style="display:flex;">&lt;span>PHAssetCreationRequest &lt;span style="color:#f92672">*&lt;/span>creationRequest &lt;span style="color:#f92672">=&lt;/span> [PHAssetCreationRequest creationRequestForAsset];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PHAssetResourceCreationOptions &lt;span style="color:#f92672">*&lt;/span>options &lt;span style="color:#f92672">=&lt;/span> [PHAssetResourceCreationOptions new];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>options.originalFilename &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#75715e">/*Filename of your choice, including custom info*/&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[creationRequest addResourceWithType:... data:... options:options];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;em>Note: I recommend using the correct file extension for whatever type your using when setting &lt;code>originalFilename&lt;/code> as mismatches can lead the Photos framework to fail.&lt;/em>&lt;/p>
&lt;p>And you can get the original filename from a &lt;code>PHAsset&lt;/code> &lt;em>without&lt;/em> loading the entire data from the asset.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-objc" data-lang="objc">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> (PHAssetResource &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">const&lt;/span> resource &lt;span style="color:#66d9ef">in&lt;/span> [PHAssetResource assetResourcesForAsset:asset]) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NSString &lt;span style="color:#f92672">*&lt;/span>originalFilename &lt;span style="color:#f92672">=&lt;/span> resource.originalFilename;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Use originalFilename
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>PHAssetResource&lt;/code> seems to be a part of &lt;code>PHAsset&lt;/code>&amp;rsquo;s metadata, so it doesn&amp;rsquo;t require an additional load of the full underlying data to retrieve. It&amp;rsquo;s quick.&lt;/p>
&lt;p>Here&amp;rsquo;s how I&amp;rsquo;m leveraging this:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://apps.apple.com/app/id515789135">Close-up&lt;/a> includes IDs in filenames when writing to &lt;code>PHPhotoLibrary&lt;/code> so that I can later tell if the photo has already been written and skip re-writing it in certain cases.&lt;/li>
&lt;li>&lt;a href="https://apps.apple.com/app/id1355171732">Burst&lt;/a> includes a hash of the params used to create media in filenames so that I can reuse assets previously saved to the photo library instead of generating new ones.&lt;/li>
&lt;li>&lt;a href="https://retro.app">Retro&lt;/a> includes photo IDs in filenames so we can count photos that are uploaded then saved to the local device as &amp;ldquo;already uploaded&amp;rdquo; instead of as new assets.&lt;/li>
&lt;/ul>
&lt;p>
 &lt;img src="examples.jpg" alt="">
&lt;/p>
&lt;p>If you&amp;rsquo;re looking to store a bit of extra info in &lt;code>PHAsset&lt;/code>s this could be a great way to do it!&lt;/p></content:encoded></item><item><title>Tagged pointer string keys</title><link>http://objectionable-c.com/posts/tagged-pointer-string-keys/</link><pubDate>Mon, 31 Mar 2025 05:00:00 -0700</pubDate><guid>http://objectionable-c.com/posts/tagged-pointer-string-keys/</guid><content:encoded>&lt;p>&lt;a href="https://mikeash.com/pyblog/friday-qa-2015-07-31-tagged-pointer-strings.html">This blog post&lt;/a> by Mike Ash about how tagged pointer strings work in Objective-C is fantastic, one of my favorites. It details under what circumstances &lt;code>NSString&lt;/code>s become tagged pointers instead of full objects. Tagged pointers have &lt;a href="https://www.mikeash.com/pyblog/friday-qa-2012-07-27-lets-build-tagged-pointers.html#:~:text=Tagged%20Pointer%20Uses,uses%20as%20well.">performance and space efficiency benefits&lt;/a> over full objects by bypassing heap allocations.&lt;/p>
&lt;p>After reading this a few years ago I was inspired to find a &amp;ldquo;nail&amp;rdquo; for the proverbial &amp;ldquo;hammer&amp;rdquo; it discusses: I wanted to find places where I could leverage tagged pointer strings in a substantial way. One or two uses of tagged pointer strings probably doesn&amp;rsquo;t make a big difference, so I was looking for subsystems where many strings were allocated that I could switch over to tagged pointer strings.&lt;/p>
&lt;p>Image caches commonly use hexadecimal hashes of the image URLs they&amp;rsquo;re loading as keys at runtime and for filenames when storing on disk. My image cache, &lt;a href="https://github.com/timonus/TJImageCache">&lt;code>TJImageCache&lt;/code>&lt;/a>, was doing just this. Migrating &lt;code>TJImageCache&lt;/code>&amp;rsquo;s keys to use tagged pointer strings seemed liked the perfect use of this strategy since many cache keys are created and held in memory and the cost of switching to new keys was very low.&lt;/p>
&lt;p>The hex strings of common hashes are too long to be tagged pointer strings, commonly 32-64 characters, so I had to come up with a new way of converting hashes into strings. I wanted to generate maximally unique keys that were still tagged pointer strings and also valid, &amp;ldquo;clean&amp;rdquo; filenames (meaning there were no &lt;code>.&lt;/code>s or spaces in them even though those are valid). Following the guidelines from Mike&amp;rsquo;s post, it seems like constructing 11 character strings from the following 30 character table creates the most unique key while still being a good filename.&lt;/p>
&lt;pre tabindex="0">&lt;code>eilotrmapdnsIcufkMShjTRxgC4013
&lt;/code>&lt;/pre>&lt;p>I implemented the following function to create these keys from URL strings&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-objc" data-lang="objc">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">char&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">const&lt;/span> kHashCharacterTable &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;eilotrmapdnsIcufkMShjTRxgC4013&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>NSString &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">TJImageCacheHash&lt;/span>(NSString &lt;span style="color:#f92672">*&lt;/span>string)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">unsigned&lt;/span> &lt;span style="color:#66d9ef">char&lt;/span> result[CC_SHA256_DIGEST_LENGTH];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CC_SHA256([string UTF8String], (CC_LONG)string.length, result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> [NSString stringWithFormat:&lt;span style="color:#e6db74">@&amp;#34;%c%c%c%c%c%c%c%c%c%c%c&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kHashCharacterTable[result[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kHashCharacterTable[result[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kHashCharacterTable[result[&lt;span style="color:#ae81ff">2&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kHashCharacterTable[result[&lt;span style="color:#ae81ff">3&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kHashCharacterTable[result[&lt;span style="color:#ae81ff">4&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kHashCharacterTable[result[&lt;span style="color:#ae81ff">5&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kHashCharacterTable[result[&lt;span style="color:#ae81ff">6&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kHashCharacterTable[result[&lt;span style="color:#ae81ff">7&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kHashCharacterTable[result[&lt;span style="color:#ae81ff">8&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kHashCharacterTable[result[&lt;span style="color:#ae81ff">9&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kHashCharacterTable[result[&lt;span style="color:#ae81ff">10&lt;/span>] &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">30&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>(&lt;a href="https://github.com/timonus/TJImageCache/blob/6c06ad9484fcc889dbf78f37bbbf8bc666eaac99/TJImageCache/TJImageCache.m#L79-L104">Source&lt;/a>)&lt;/p>
&lt;p>I then plugged this in and voila, tagged pointer strings keys showing in &lt;code>TJImageCache&lt;/code>&amp;rsquo;s internals.&lt;/p>
&lt;p>
 &lt;img src="example.png" alt="">
&lt;/p>
&lt;p>Another place where this strategy could be beneficial is when generating random identifiers. I haven&amp;rsquo;t done this in any apps in practice, but it could make sense for a logging framework or something that winds up creating a lot of IDs. These IDs certainly aren&amp;rsquo;t as random as UUIDs (&lt;a href="https://chatgpt.com/share/67ea0a74-54b4-8008-9f5f-289977e79cf2">54 bits of entropy versus 122&lt;/a>), but they may be good enough depending on your use case.&lt;/p>
&lt;hr>
&lt;p>Is any of this necessary? Definitely not, but hey it feels good to make things a little more efficient. If you look around in your projects you might find uses for tagged pointer strings too!&lt;/p></content:encoded></item><item><title>gzip yer uploads</title><link>http://objectionable-c.com/posts/gzip-yer-uploads/</link><pubDate>Mon, 24 Mar 2025 06:00:00 -0700</pubDate><guid>http://objectionable-c.com/posts/gzip-yer-uploads/</guid><content:encoded>&lt;p>&lt;code>NSURLSession&lt;/code> opaquely handles HTTP negotiations about accepting compressed content and does the decompression (using formats like gzip and &lt;a href="http://objectionable-c.com/posts/brotli-ios">brotli&lt;/a>) before you receive responses, which is great. &lt;code>Foundation&lt;/code>, Apple&amp;rsquo;s OSes, and the web are doing work that we as developers don&amp;rsquo;t have to so our users consume less data and have a faster experience. Hooray! I&amp;rsquo;ve always been impressed that this happens pretty much auto-magically without having to think about it.&lt;/p>
&lt;p>However, I had a thought late in 2024 about transmitting data the other way: uploads. While looking I realized there&amp;rsquo;s not automatic upload compression built into &lt;code>NSURLSession&lt;/code>. I&amp;rsquo;m sure there are good reasons for this, but I figured that some of the APIs I was using could accept compressed uploads. It turns out I was right, &lt;a href="https://developer.mixpanel.com/reference/import-events">Mixpanel&lt;/a> and Dropbox both accept compressed data.&lt;/p>
&lt;p>Here&amp;rsquo;s how I&amp;rsquo;m doing the compression itself&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-objc" data-lang="objc">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#import &amp;lt;zlib.h&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Function that gzips data
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Thanks Claude https://tijo.link/BGdVNx
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">static&lt;/span> NSData &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">_gzipCompressData&lt;/span>(NSData &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">const&lt;/span> data, NSError &lt;span style="color:#f92672">**&lt;/span>error) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#f92672">!&lt;/span>data.length) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (error) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">*&lt;/span>error &lt;span style="color:#f92672">=&lt;/span> [NSError errorWithDomain:&lt;span style="color:#e6db74">@&amp;#34;CompressionErrorDomain&amp;#34;&lt;/span> code:&lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> userInfo:&lt;span style="color:#ae81ff">@{&lt;/span>NSLocalizedDescriptionKey: &lt;span style="color:#e6db74">@&amp;#34;Invalid input data&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">}&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> nil;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> z_stream strm;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> strm.zalloc &lt;span style="color:#f92672">=&lt;/span> Z_NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> strm.zfree &lt;span style="color:#f92672">=&lt;/span> Z_NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> strm.opaque &lt;span style="color:#f92672">=&lt;/span> Z_NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Initialize deflate with gzip format
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> (deflateInit2(&lt;span style="color:#f92672">&amp;amp;&lt;/span>strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, &lt;span style="color:#ae81ff">15&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">16&lt;/span>, &lt;span style="color:#ae81ff">8&lt;/span>, Z_DEFAULT_STRATEGY) &lt;span style="color:#f92672">!=&lt;/span> Z_OK) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (error) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">*&lt;/span>error &lt;span style="color:#f92672">=&lt;/span> [NSError errorWithDomain:&lt;span style="color:#e6db74">@&amp;#34;CompressionErrorDomain&amp;#34;&lt;/span> code:&lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> userInfo:&lt;span style="color:#ae81ff">@{&lt;/span>NSLocalizedDescriptionKey: &lt;span style="color:#e6db74">@&amp;#34;Failed to initialize compression&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">}&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> nil;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Set up input
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> strm.avail_in &lt;span style="color:#f92672">=&lt;/span> (uInt)data.length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> strm.next_in &lt;span style="color:#f92672">=&lt;/span> (Bytef &lt;span style="color:#f92672">*&lt;/span>)data.bytes;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Prepare output buffer (compress can increase size)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> NSMutableData &lt;span style="color:#f92672">*&lt;/span>compressedData &lt;span style="color:#f92672">=&lt;/span> [NSMutableData dataWithLength:data.length &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">1.1&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">12&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> strm.avail_out &lt;span style="color:#f92672">=&lt;/span> (uInt)compressedData.length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> strm.next_out &lt;span style="color:#f92672">=&lt;/span> compressedData.mutableBytes;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Compress
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> (deflate(&lt;span style="color:#f92672">&amp;amp;&lt;/span>strm, Z_FINISH) &lt;span style="color:#f92672">!=&lt;/span> Z_STREAM_END) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deflateEnd(&lt;span style="color:#f92672">&amp;amp;&lt;/span>strm);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (error) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">*&lt;/span>error &lt;span style="color:#f92672">=&lt;/span> [NSError errorWithDomain:&lt;span style="color:#e6db74">@&amp;#34;CompressionErrorDomain&amp;#34;&lt;/span> code:&lt;span style="color:#ae81ff">3&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> userInfo:&lt;span style="color:#ae81ff">@{&lt;/span>NSLocalizedDescriptionKey: &lt;span style="color:#e6db74">@&amp;#34;Compression failed&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">}&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> nil;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Cleanup and finalize
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> [compressedData setLength:strm.total_out];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deflateEnd(&lt;span style="color:#f92672">&amp;amp;&lt;/span>strm);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> compressedData;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>(You could also use a library like &lt;a href="https://github.com/nicklockwood/GZIP">this one&lt;/a> if you don&amp;rsquo;t want to hand write the compression.)&lt;/p>
&lt;p>Then, if using a data task:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-objc" data-lang="objc">&lt;span style="display:flex;">&lt;span>NSData &lt;span style="color:#f92672">*&lt;/span>compressedData &lt;span style="color:#f92672">=&lt;/span> _gzipCompressData(originalData, &lt;span style="color:#f92672">&amp;amp;&lt;/span>err);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>NSURLRequest &lt;span style="color:#f92672">*&lt;/span>request &lt;span style="color:#f92672">=&lt;/span> ...;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[request setValue:&lt;span style="color:#e6db74">@&amp;#34;gzip&amp;#34;&lt;/span> forHTTPHeaderField:&lt;span style="color:#e6db74">@&amp;#34;Content-Encoding&amp;#34;&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[request setHTTPBody:compressedData];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Start a data task using request and an NSURLSession
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>or an upload task:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-objc" data-lang="objc">&lt;span style="display:flex;">&lt;span>NSData &lt;span style="color:#f92672">*&lt;/span>compressedData &lt;span style="color:#f92672">=&lt;/span> _gzipCompressData(originalData, &lt;span style="color:#f92672">&amp;amp;&lt;/span>err);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>NSURLRequest &lt;span style="color:#f92672">*&lt;/span>request &lt;span style="color:#f92672">=&lt;/span> ...;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[request setValue:&lt;span style="color:#e6db74">@&amp;#34;gzip&amp;#34;&lt;/span> forHTTPHeaderField:&lt;span style="color:#e6db74">@&amp;#34;Content-Encoding&amp;#34;&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[urlSession uploadTaskWithRequest:request fromData:compressedData];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With this you can shave a few percent off your upload sizes saving bandwidth and time!&lt;/p>
&lt;p>&lt;em>Here are examples where I&amp;rsquo;m doing this in &lt;a href="https://github.com/timonus/TJMixpanelLogger/blob/b0ba0e704c11a97410a459f7b31d705caa335c6e/TJMixpanelLogger.m#L314-L323">&lt;code>TJMixpanelLogger&lt;/code>&lt;/a> and &lt;a href="https://github.com/timonus/TJDropbox/blob/a35deac894646d5468ac06c645a8451c680d5962/TJDropbox/TJDropbox.m#L975-L986">&lt;code>TJDropbox&lt;/code>&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>The case for launch xibs</title><link>http://objectionable-c.com/posts/launch-xib-size/</link><pubDate>Fri, 21 Mar 2025 06:00:00 -0700</pubDate><guid>http://objectionable-c.com/posts/launch-xib-size/</guid><content:encoded>&lt;p>I&amp;rsquo;m obsessed with keeping apps small, and I recently found an unlikely place to save a couple KB: launch screens. The default on iOS for a while has been to create a launch storyboard for your app, but launch storyboards are bundles that wrap a few files.&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="innards.jpeg" alt="">
 &lt;figcaption>The innards of a compiled storyboard&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>Many launch storyboards don&amp;rsquo;t use any functionality that isn&amp;rsquo;t also available in the simpler, older xib format. I copied and pasted the launch screens from within the .storyboard into a freshly made .xib and the results saved 1.2 KB.&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="new-xib.jpeg" alt="">
 &lt;figcaption>Make a new empty xib file with the same filename as your storyboard&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="copy-paste.jpeg" alt="">
 &lt;figcaption>Copypasta&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>
 &lt;img src="delete-old.jpeg" alt="">
&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="trim.gif" alt="">
 &lt;figcaption>Remove the launch screen file extension from your info.plist if there is one&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="compare.jpeg" alt="">
 &lt;figcaption>Resulting file size savings&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>It&amp;rsquo;s just a couple KB, but in apps that are &amp;lt;2 MB like &lt;a href="https://apps.apple.com/app/id989565871">Opener&lt;/a> every KB counts!&lt;/p>
&lt;p>
 &lt;img src="freerealestate.jpeg" alt="">
&lt;/p>
&lt;p>You could save even more space by using the relatively new &lt;a href="https://sarunw.com/posts/launch-screen-using-plist/">&lt;code>UILaunchScreen&lt;/code> info.plist key&lt;/a>, but that lacks some of the customizability of launch storyboards/xibs.&lt;/p>
&lt;p>&lt;em>Side note: Opener has two launch screens, one for iPhone and one for iPad, so this savings is doubled&lt;/em>&lt;/p></content:encoded></item><item><title>Wielding Brotli on iOS</title><link>http://objectionable-c.com/posts/brotli-ios/</link><pubDate>Tue, 18 Mar 2025 05:00:00 -0700</pubDate><guid>http://objectionable-c.com/posts/brotli-ios/</guid><content:encoded>&lt;p>&lt;a href="https://en.wikipedia.org/wiki/Brotli">Brotli&lt;/a> is a compression format introduced by Google some time ago that is really excellent at compressing text. Brotli is widely supported across the web and has been supported on all of Apple&amp;rsquo;s platforms since 2017. A lot of the web traffic you receive is probably brotli compressed, but many of the iOS developers I talk to have never heard of it.&lt;/p>
&lt;p>Brotli decompression happens automatically when fetching things from the network using &lt;code>NSURLSession&lt;/code>, just as it does with gzip. If you were motivated to manually decompress brotli data you could import the Compression framework and write some C code using &lt;code>compression_decode_buffer&lt;/code> to do it, but that&amp;rsquo;s not super approachable. It turns out there&amp;rsquo;s a simpler way: &lt;code>NSData&lt;/code> actually has a hidden enum value that does brotli decompression for you!&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-objc" data-lang="objc">&lt;span style="display:flex;">&lt;span>NSData &lt;span style="color:#f92672">*&lt;/span>decompressedData &lt;span style="color:#f92672">=&lt;/span> [brotliData decompressedDataUsingAlgorithm:NSDataCompressionAlgorithmZlib &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> error:nil];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I &lt;a href="https://mastodon.social/@timonus/109634546423390529">discovered this&lt;/a> in 2023 and have taken advantage of it the reduce the size of bundled assets in many of my apps. Here are some examples:&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="brotli-ios-chart1.jpeg" alt="">
 &lt;figcaption>Opener&amp;#39;s rule set JSON file. Original size: 168 KB.&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>
 &lt;figure>
 &lt;img src="brotli-ios-chart2.jpeg" alt="">
 &lt;figcaption>Opener&amp;#39;s en localized strings. Original size: 13 KB&lt;/figcaption>
 &lt;/figure>
&lt;/p>
&lt;p>I care deeply about conserving storage on my users&amp;rsquo; devices, and this has become an excellent tool to use for reducing app size. Hopefully Apple makes this enum value public in the future! (FB16918276)&lt;/p>
&lt;p>You can install brotli from homebrew and use it to compress files with ease! I often do this as a build step that applies only to release builds so that I can continue using the original files while debugging the app.&lt;/p>
&lt;p>If you&amp;rsquo;d ever like to compress to brotli within an app, the Compression framework has a method for doing so using &lt;a href="https://developer.apple.com/documentation/compression/compression_brotli">&lt;code>COMPRESSION_BROTLI&lt;/code>&lt;/a> at level 2. If you&amp;rsquo;d ever like to compress to brotli yourself using level 11 (the highest) there are several open source iOS projects for it. I forked a popular one &lt;a href="https://github.com/timonus/Brotli">here&lt;/a> and have been using it.&lt;/p>
&lt;p>I plan to reference this in future posts about more aggressive app shrinkage ideas in the future. Happy compressing!&lt;/p>
&lt;p>&lt;em>Update: Here&amp;rsquo;s &lt;a href="https://github.com/lonepalm/Elegant-Emoji-Picker/commit/0a852f91c46c86e5c7f002a71bc244a5432ae0e1">an example&lt;/a> of where we&amp;rsquo;re using this to save some space in &lt;a href="https://retro.app">Retro&lt;/a>.&lt;/em>&lt;/p></content:encoded></item></channel></rss>