<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Christian Selig</title>
    <link>https://christianselig.com/</link>
    <description>Recent content on Christian Selig</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <copyright>© Christian Selig {year}</copyright>
    <lastBuildDate>Fri, 27 Jan 2023 00:00:00 +0000</lastBuildDate><atom:link href="https://christianselig.com/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Rivian R2 wishes as an R1 owner</title>
      <link>https://christianselig.com/2026/02/rivian-r2-wishes/</link>
      <pubDate>Mon, 09 Feb 2026 10:27:44 -0400</pubDate>
      
      <guid>https://christianselig.com/2026/02/rivian-r2-wishes/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2026/02/rivian-r2-wishes/hero.jpeg&#34; alt=&#34;Green Rivian R1S in the snow&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;After 7 years with a Tesla Model 3, we picked up a gen 2 Rivian R1S in April of 2025. We still have the Model 3 as a second vehicle, but it&amp;rsquo;s been really cool experiencing a new electric vehicle from a very passionate new company.&lt;/p&gt;
&lt;p&gt;2026 is a really exciting time for Rivian, as in the first half of this year they&amp;rsquo;re launching their R2 vehicle - a smaller, less expensive SUV offering that should have a lot more mass-market appeal.&lt;/p&gt;
&lt;p&gt;With a bunch of jourrnalists getting previews of the vehicle today, I thought I&amp;rsquo;d share what I&amp;rsquo;m really hoping for in this new vehicle having experienced their existing vehicle for almost a year, and a Tesla for the better part of a decade. None of these are in any particular order.&lt;/p&gt;
&lt;h2 id=&#34;better-audio&#34;&gt;Better audio&lt;/h2&gt;
&lt;p&gt;We sprung for the &amp;ldquo;Premium Audio&amp;rdquo; package in our R1S and… it&amp;rsquo;s not premium at all. Whatever system just came with our Model 3 sounds demonstrably better, from where it feels like the sound is coming from (much more expansive) to the bass, the R1S just feels a lot weaker. Honestly for the base audio system it would be probably decent, but for a &amp;ldquo;Premium Audio&amp;rdquo; system it just falls short. They have been making it better and better with softwware updates of all things over the course of our vehicle&amp;rsquo;s life, where it&amp;rsquo;s noticeable better now than it was at the beginning, a recent update in December basically sounds like they figured out how to turn on the subwoofer in the trunk a little bit, but it&amp;rsquo;s still just not that much.&lt;/p&gt;
&lt;p&gt;With the Tesla sometimes I&amp;rsquo;d park and finish out a song before leaving the car because it just sounded so good, have never had that happen in the Rivian so I hope they bring some of that experience to the R2 even through another &amp;ldquo;Premium Audio&amp;rdquo; package for those who care.&lt;/p&gt;
&lt;h2 id=&#34;no-dual-motor-epa-shenanigans&#34;&gt;No dual motor EPA shenanigans&lt;/h2&gt;
&lt;p&gt;So our R1S is dual motor, meaning it has a motor for the front wheels and a separate motor for the back wheels, meaning you get AWD. Rivian allows you to get more range by only using the front motor, turning it into effectively a front wheel drive vehicle and as only one motor is active you get a bit better range (about 10%). Sounds great, right? Choose between more efficient front-wheel drive on trips, but just use AWD around town by default.&lt;/p&gt;
&lt;p&gt;Well, the devil is in the details. In order to be able to market this slightly improved range, the EPA requires Rivian to automatically revert back to this front wheel drive/higher efficiency mode after a few hours. Kinda like gas vehicles and how they turn off the engine at traffic lights, and if you disable that it just keeps turning itself back on. So even if it&amp;rsquo;s the winter and you&amp;rsquo;re like, &amp;ldquo;Dang, roads are a little dicey, I want to be in AWD&amp;rdquo;, if you park the car for awhile and forget to set it to AWD, it just reverts to front wheel drive. It&amp;rsquo;s like if your iPhone kept reverting to &amp;ldquo;Low Battery Mode&amp;rdquo; every 2 hours even after you keep toggling it off so Apple could advertise that model having 10% better battery life.&lt;/p&gt;
&lt;p&gt;Note: the vehicle isn&amp;rsquo;t hard-locked to front wheel drive in this mode, if it slips it alerts you that it&amp;rsquo;s switching over to AWD (I don&amp;rsquo;t want to wait for it to slip), and if you floor it on the highway for instance it&amp;rsquo;ll engage the rear motors for extra grunt, but because it has to link up the rear motors to an already moving system, you can sometimes get a kinda weird clunk feeling as the rear motor connects itself at speed which isn&amp;rsquo;t very satisfying. It almost feels like a wheel slip in the rain.&lt;/p&gt;
&lt;p&gt;This is maddening, and is only the case on their dual motor vehicles. For tri and quad motor vehicles, they just don&amp;rsquo;t market them as having that extra 10% range, so all the modes are actually sticky! If you say &amp;ldquo;front wheel drive mode&amp;rdquo; (they call it &amp;ldquo;Conserve&amp;rdquo;) it stays there indefinitely, if you say &amp;ldquo;AWD mode&amp;rdquo;, it stays there indefinitely.&lt;/p&gt;
&lt;p&gt;Is this dumb? Yes. Do I blame the EPA? Yes. Do I also blame Rivian? Also yes, they&amp;rsquo;re making this trade off to be able to market extra range.&lt;/p&gt;
&lt;p&gt;If Rivian does this same stuff for the R2 dual motor just so they can advertise a few extra miles, I really hope they have a $10 option you can configure when you order called &amp;ldquo;Give me less marketed range with no actual range decrease but have the vehicle actually do what I tell it to&amp;rdquo;, but maybe with a catchier title.&lt;/p&gt;
&lt;h2 id=&#34;spare-compact-tire&#34;&gt;Spare compact tire&lt;/h2&gt;
&lt;p&gt;Our Tesla lacks a spare tire of any sort, instead electing to include a repair method and roadside assistance. A few years back a nasty pothole absolutely destroyed the sidewall of one of our tires on a drive home, and where it was sidewall damage it simply wasn&amp;rsquo;t repairable, so we had to call Tesla roadside assistance. Unfortuantely Tesla roadside assistance was absolutely useless, taking ages to respond and then ultimately not having any providers in the area, so we just ditched the car and got a ride home with a friend and dealt with it the next day.&lt;/p&gt;
&lt;p&gt;After that I was like &amp;ldquo;I do not want another vehicle without a spare tire on board&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;With our R1S on the other hand, we had another unlucky event where we popped a tire (also the sidewall if you&amp;rsquo;d believe it, I have some great luck) and sure enough, since we elected to get a compact spare tire it was super easy to deal with, just grab the included jack, throw the compact spare on, then the next day we just slowly drove over to a shop for a new tire. Completely uneventful.&lt;/p&gt;
&lt;p&gt;I was worried with the smaller body that you wouldn&amp;rsquo;t be able to in the R2, but &lt;a href=&#34;https://www.youtube.com/watch?v=at1HS1CNhe4&#34;&gt;Jerry Rig Everything&lt;/a&gt; showed room for a compact spare in the sub trunk, nice!&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2026/02/rivian-r2-wishes/spare-tire.jpeg&#34; alt=&#34;Empty space for a compact spare tire in the trunk of the Rivian R2&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;digital-rear-view-mirror&#34;&gt;Digital rear view mirror&lt;/h2&gt;
&lt;p&gt;I don&amp;rsquo;t see this in any of the videos so it seems unlikely but I&amp;rsquo;m holding out hope it&amp;rsquo;s an option.&lt;/p&gt;
&lt;p&gt;Picture this: you approach your vehicle, since it sees it&amp;rsquo;s your phone it knows who you are, so it positions your seat, steering wheel, mirrors, Apple Music/Spotify account, and temperature preferences. You didn&amp;rsquo;t have to do a thing, the vehicle is just smart! Except… your spouse is 7 inches shorter than you so when you look in the rear view mirror you&amp;rsquo;re staring at the back seat.&lt;/p&gt;
&lt;p&gt;Is having to adjust your rear view mirror a big deal? No, but having the vehicle do everything else for you almost draws &lt;em&gt;more&lt;/em&gt; attention to the final thing it&amp;rsquo;s missing that you still have to adjust every single time. This is a solved problem in inexpensive vehicles, just have a &amp;ldquo;digital rear view mirror&amp;rdquo; that requires no adjusting as it just shows a camera feed of the rear of the vehicle where the mirror is.&lt;/p&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2026/02/rivian-r2-wishes/digital-rear-view.jpeg&#34; alt=&#34;Digital rear view mirror in a Toyota RAV4&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Digital rear view mirror in a RAV4 by u/MildSpaghettiSauce&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Rivian&amp;rsquo;s cameras are legitimately so good, that at night it&amp;rsquo;s easier to use them for side blind spot monitoring when you change lanes than the actual side mirrors, because they let in enough light that you can actually make out details through the pseudo &amp;ldquo;Night Mode&amp;rdquo; camera vision that you can&amp;rsquo;t with the mirrors alone, it&amp;rsquo;s wild. I want that for the rear view mirror too!&lt;/p&gt;
&lt;p&gt;Do you still prefer an analog mirror? That&amp;rsquo;s totally cool, all the vehicles that offer this let you just toggle back to a good ol&amp;rsquo; reflective mirror. No harm no foul.&lt;/p&gt;
&lt;h2 id=&#34;v2h-story&#34;&gt;V2H story&lt;/h2&gt;
&lt;p&gt;Not a lot of people realize one of the most powerful part of EVs: they&amp;rsquo;re mobile powerstations that can (theoretically) power your entire home. Take a Tesla Model 3 for instance, it has a 75 kWh battery. Tesla also sells Powerwalls to help you back up your home, each Powerwall is around $6,000 and has 13.5 kWh of battery capacity. Yes, that means your Model 3 is the equivalent of more than 5 Powerwalls, or $33,000 in equivalent batteries.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s nuts! Ever have a power outage? With the average home in Canada using about 30 kWh per day, that could power your house through potentially multiple days and genuinely save lives.&lt;/p&gt;
&lt;p&gt;My Rivian R1S is a lot better than my Tesla here in that it actually has normal, 120V AC outlets, but they can only output a measly 1.5 kW, so even powering a hungry kettle could result in the breaker tripping. Much better than the max 120W my Tesla can do through a 12V cigarette outlet (good god how is that the best they can do), but it&amp;rsquo;s still not enough &lt;em&gt;output&lt;/em&gt; to power a house effectively.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s kinda like being in a drought with a massive water tower, but water only comes out in drips. Better than nothing, but we need &lt;em&gt;output speed&lt;/em&gt; too.&lt;/p&gt;
&lt;p&gt;The R1 &lt;em&gt;can&lt;/em&gt; output much more by pulling DC energy directly from the battery through CCS protocols like the ISO 15118 standard, and sure enough despite Rivian not talnking about it, folks with these systems &lt;a href=&#34;https://www.youtube.com/watch?v=pTGjzF8UkVc&amp;amp;list=PLY8KTALx6NoQ_CdtunOIgkSY8LAx9Mq_T&#34;&gt;have been able to connect the R1S directly to their house&lt;/a&gt; with the appropriate cable and send up to 24 kW to power their entire home, crazy stuff, hope Rivian talks about this more in the future as clearly the vehicle supports it even if they are quiet about it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m kinda curious what the R2&amp;rsquo;s story is here. The &lt;a href=&#34;https://youtu.be/jE4MUQrkfKo?t=4822&#34;&gt;CEO of Rivian said in an interview&lt;/a&gt; (~1:20:20) that the R1 and R2 both have bidirectional EV charging in the realm of 20 kW (which again we&amp;rsquo;re finally seeing folks be able to take advantage of in the R1 recently), but unlike being limited to 1.5 kW of AC output in the R1, the CEO says the R2 will be able to do &amp;ldquo;10 or 11 kW&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s massive versus the 1.5 kW the R1 can do, but I&amp;rsquo;m not sure that made it to production in the end? &lt;a href=&#34;https://youtu.be/xUl_0087dyM?t=1582&#34;&gt;Doug Demuro showed a Rivian graphic with a V2L adapter&lt;/a&gt; (at 26:22) but it&amp;rsquo;s only listed as 2.4 kW. Still a lot better than 1.5, but a far cry fro the 10 or 11 kW that RJ Scaringe said earlier.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2026/02/rivian-r2-wishes/ac.jpeg&#34; alt=&#34;V2L poster at Rivian for the R2 for an adapter claiming 2,400W&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Either way, talk more about this Rivian! This is one of the coolest parts about EVs!&lt;/p&gt;
&lt;h2 id=&#34;better-suspension&#34;&gt;Better suspension&lt;/h2&gt;
&lt;p&gt;The R1S has a fancy air suspension, so picture a bagpipe over each corner of the vehicle that lets it inflate or deflate to change the height of the vehicle and theoretically make for a cushier ride.&lt;/p&gt;
&lt;p&gt;I say theoretically because honestly I find the gen 2 R1S kinda rough suspension wise. Like, hitting the same potholes around where I live in the Tesla Model 3 (with a much simpler coil suspension, no bagpipes) versus the Rivian R1S I honestly find the Tesla makes me wince less. I thought it might have been the massive 22&amp;quot; wheels that came on the Rivian and the correspondingly small sidewall on the tire, but we switched to a 20&amp;quot; wheel for the winter with a much larger sidewall and it&amp;rsquo;s better but still not great.&lt;/p&gt;
&lt;p&gt;Would love to see Rivian tune this so that the R2 is a super smooth ride, I&amp;rsquo;ve heard the newer Tesla Model Ys are incredible here and also just have a coil suspension like the R2 will have.&lt;/p&gt;
&lt;h2 id=&#34;faster-charging&#34;&gt;Faster charging&lt;/h2&gt;
&lt;p&gt;Brands love to brag about maximum charge speeds, our Tesla for instance got a software update to enable 250 kW charging, which is super fast. To put that in perspective, most new homes in North America have 200 amp service going into their home, 250 kW is the equivalent of upgrading to 1,000 amp service, and then pouring every drop of that power into your car.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;But&lt;/em&gt;, while that top speed is impressive, it holds that for like, minutes maybe before crashing down to much slower charging speeds. Our R1S is no different here, with good peak speeds but it doesn&amp;rsquo;t exactly hold them super well, there&amp;rsquo;s been tons of reports that the cooling method for the batteries in the R1S is just underwhelming, where the Rivian R1 models have a two battery packs stacked on top of each other with a single liquid cooling plate in the middle (so only the top or the bottom of the battery is touching the cooling surface), and that single plate often seems a little underpowered for cooling such a massive pack quickly. Kinda like thermal throttling in laptops! We typically see around 45 minutes for the battery to charge 10-80% at fast chargers.&lt;/p&gt;
&lt;p&gt;The R2 on the other hand appears to be moving to a smarter cooling method where the cooling liquid flows almost through a ribbon, weaving along the &lt;em&gt;sides&lt;/em&gt; of each cell in the battery pack, meaning a lot more cooling surface area.&lt;/p&gt;
&lt;p&gt;And indeed, this seems to have paid off, with a Rivian employee saying they&amp;rsquo;re now &lt;a href=&#34;https://youtu.be/M0iJXfooaws?t=92&#34;&gt;under 30 minutes for 10-80% on the R2&lt;/a&gt;. Class-leading? No, Hyundai is under 20 minutes, but a fair bit better.&lt;/p&gt;
&lt;p&gt;And for folks without EV experience, honestly, this is mostly a non-issue. 99.5% of your charging is done at home where speeds don&amp;rsquo;t matter (it just charges overnight like your phone), it&amp;rsquo;s only if you&amp;rsquo;re going on a bit of a roadtrip where this comes into play.&lt;/p&gt;
&lt;h2 id=&#34;better-usb-c&#34;&gt;Better USB-C&lt;/h2&gt;
&lt;p&gt;Okay this isn&amp;rsquo;t a big deal, but the R1S has a ton of USB-C ports everywhere, like there must be close to a dozen, which is awesome, but if you try to charge a power bank or a laptop or something over USB-C it&amp;rsquo;s just… not very fast. I haven&amp;rsquo;t actually measured but I&amp;rsquo;m assuming they&amp;rsquo;re only doing 12V (at most) instead of at least 20V which most chargers are at nowadays, which I really hope they improve. My MacBook charges so slow.&lt;/p&gt;
&lt;h2 id=&#34;better-phone-charger&#34;&gt;Better phone charger&lt;/h2&gt;
&lt;p&gt;This is the one part of the Rivian that I&amp;rsquo;m like &amp;ldquo;how did this even make it out of the factory&amp;rdquo;. And if you&amp;rsquo;d believe it the phone charger in my vehicle is the second revision, so this is their attempt at fixing it somehow.&lt;/p&gt;
&lt;p&gt;Basically they have a flat little area near the arm rests where you can place one or two phones to have them wirelessly charge. Sounds fine, right?&lt;/p&gt;
&lt;p&gt;I haven&amp;rsquo;t measured it, but from experience I believe the charging &amp;ldquo;sweet spot&amp;rdquo; is approximately 4 atoms wide. If the car is in motion at all, it moves from those four atoms and tries super hard to charge it but ultimately cannot, and just making the phone get super hot and &lt;em&gt;lose&lt;/em&gt; a bunch of battery life.&lt;/p&gt;
&lt;p&gt;One time we went camping and I was like &amp;ldquo;Okay, the vehicle is not moving, surely I can just set it here while I sleep and it&amp;rsquo;ll charge&amp;rdquo;. Nope, somehow even stationary the phone did not charge beyond 20% overnight and was super hot in the morning. What.&lt;/p&gt;
&lt;p&gt;They have managed to make a phone charger that is worse than not having one at all. I can&amp;rsquo;t even place my phone in the arm rest while I drive because it just cooks it, at least if nothing was there it could just be a storage location.&lt;/p&gt;
&lt;p&gt;No, mine is not broken, this is a common complaint from just about every Rivian owner, and they need to make this better for the R2. Just hot glue a MagSafe puck in there at minimum.&lt;/p&gt;
&lt;h2 id=&#34;better-phone-as-a-key-paak&#34;&gt;Better Phone as a Key (PAAK)&lt;/h2&gt;
&lt;p&gt;Rivian and Tesla do the (honestly very cool thing) where it uses your phone to detect your proximity to the car and lock/unlock it and recognize who the driver is to set preferences. It&amp;rsquo;s great, no having to carry around bulky car keys (don&amp;rsquo;t worry, there is a backup little credit card style key you carry in your pocket in case your phone dies or disappears).&lt;/p&gt;
&lt;p&gt;Rivian even recently updated theirs to use the first-party &amp;ldquo;Apple CarKey&amp;rdquo; functionality so you get bonuses like being able to unlock it for a few hours even after your phone&amp;rsquo;s battery died, and it uses &amp;ldquo;Ultra Wide Band&amp;rdquo; (UWB) so it can position your phone in relation to the car down to the centimeter. Tesla doesn&amp;rsquo;t do this and has their own proprietary thing that I think is based on Bluetooth but might use a bit of UWB on newer models (not on my car).&lt;/p&gt;
&lt;p&gt;But… Tesla&amp;rsquo;s is still much better. There&amp;rsquo;s two aspects of nailing good &amp;ldquo;phone as a key&amp;rdquo; support: firstly, unlocking the car as you approach (duh), and secondly, knowing who approached the car so you can set their preferences (seat, steering wheel, mirror positions, temperature preferences, music streaming account, etc.)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Unlocking: B+ for Rivian here, sometimes even with the recent update I have to stand by the door for a few seconds and be like &amp;ldquo;Um, hello&amp;rdquo; before it sees me there and unlocks. Same phone, walk over to my Tesla, always unlocks instantly even though the Rivian should have a massive advantage with UWB.&lt;/li&gt;
&lt;li&gt;Identifying driver: D for Rivian here. Again, with UWB and centimeter-level positioning over the driver in relation to the vehicle Rivian should be able to know exactly who is approaching the driver door when my girlfriend and I (who both have Rivian keys) approach the vehicle, but 95% of the time when my girlfriend is with me (despite her always preferring to be passenger) it &lt;em&gt;always&lt;/em&gt; sets her as the driver. Even weirder, sometimes she&amp;rsquo;ll yell out the window if I&amp;rsquo;m putting something in the frunk &amp;ldquo;Oh it actually recognized you this time!&amp;rdquo; with me set as the profile, but then when I sit down in the seat it reverts to her. What.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rivian needs to do better here for the R2. The pain is especially compounded by the fact that if you leave in a bit of a rush and somehow don&amp;rsquo;t realize the driver profile is wrong, Rivian won&amp;rsquo;t let you change your driver settings unless you slow down to under 3 mph, so you better get ready to pull over and stop the car if you need to adjust something. With Tesla you can always just swap profile and have your steering wheel, seat, mirrors, etc. move to where they should be, and that feels a lot safer than having to muck around with changing things via a touch screen or sit their tweaking controls on the side of the car seat.&lt;/p&gt;
&lt;h2 id=&#34;smoother-software&#34;&gt;Smoother software&lt;/h2&gt;
&lt;p&gt;Somehow despite my Model 3 being a 2018 model and probably running a Raspberry Pi compared to the hardware Rivian runs, moving around the OS in the Tesla still feels faster. With the Rivian there&amp;rsquo;s still lag just bouncing around screens with stuff sometimes taking a second or two to show up. I don&amp;rsquo;t get it.&lt;/p&gt;
&lt;p&gt;I really hope this improves on the R2, and thankfully it looks like it does, &lt;a href=&#34;https://www.youtube.com/watch?v=xUl_0087dyM&amp;amp;t=1040s&#34;&gt;Doug DeMuro bounces around the R2&amp;rsquo;s UI here&lt;/a&gt; and it&amp;rsquo;s much faster than my gen 2 R1S with everything loading virtually instantly. Yay.&lt;/p&gt;
&lt;h2 id=&#34;carplay&#34;&gt;CarPlay&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve never had a vehicle with CarPlay, we rented a vehicle with it and it was kinda neat but Teslas and Rivians already have like every piece of software I&amp;rsquo;d want when interfacing with a vehcile (good, traffic-based maps and popular music streaming services), so with the exception of Overcast I don&amp;rsquo;t personally care about CarPlay at all.&lt;/p&gt;
&lt;p&gt;That being said, I know a &lt;strong&gt;ton&lt;/strong&gt; of people do so I kinda hope Rivian looks into it even just as a little windowed experience because I think it would make a lot more people interested.&lt;/p&gt;
&lt;h2 id=&#34;alexa-is-so-bad&#34;&gt;Alexa is so bad&lt;/h2&gt;
&lt;p&gt;About a month ago at their Autonomy Day Rivian previewed (among many other cool things) &lt;a href=&#34;https://youtu.be/mIK1Y8ssXnU?t=3038&#34;&gt;their new &amp;ldquo;Rivian Assistant&amp;rdquo;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is sorely needed, their current system uses Alexa and. it. is. so. bad.&lt;/p&gt;
&lt;p&gt;With our Tesla, you can say &amp;ldquo;Navigate to Blah&amp;rdquo; and it will just automatically plot it and you&amp;rsquo;re off to the races.&lt;/p&gt;
&lt;p&gt;Best case with Rivian you&amp;rsquo;re like, &amp;ldquo;Alexa, navigate to Bob&amp;rsquo;s Cool Donuts in Dartmouth&amp;rdquo;, and Alexa is like &amp;ldquo;Would you like to navigate to Bob&amp;rsquo;s Cool Donuts, Dartmouth, Nova Scotia&amp;rdquo;, &amp;ldquo;Yes&amp;rdquo; where it repeats literally the only possible match back to you requiring confirmation every time instead of just… taking you there.&lt;/p&gt;
&lt;p&gt;A more typical case is &amp;ldquo;Alexa, navigate to Bob&amp;rsquo;s Donuts in Dartmouth&amp;rdquo;, &amp;ldquo;Would you like directions to Bob&amp;rsquo;s Donuts in Toledo, Ohio&amp;rdquo;. No Alexa, I want the one that&amp;rsquo;s a five minute drive, not the one a three day drive away. &amp;ldquo;Oh, okay, try being more specific next time&amp;rdquo;&lt;/p&gt;
&lt;h2 id=&#34;better-heat-pump&#34;&gt;Better heat pump&lt;/h2&gt;
&lt;p&gt;They do a &lt;em&gt;decent&lt;/em&gt; job isolating the sound from the outside if you&amp;rsquo;re in the vehicle, but if it&amp;rsquo;s cold outside and you heat up your Rivian R1S, you can hear that sucker from a full block away in this super high pitched whine. I&amp;rsquo;ve understandably had multiple people ask &amp;ldquo;Is it okay?&amp;rdquo;.&lt;/p&gt;
&lt;h2 id=&#34;better-time-placement&#34;&gt;Better time placement&lt;/h2&gt;
&lt;p&gt;The UX for knowing the current time in the cabin is not great. There is no clock on the main driver instrument display, and it&amp;rsquo;s in the furthest place possible on the middle display, so it&amp;rsquo;s not exactly easy to check. There&amp;rsquo;s tons of space on the driver display so I either wish they shoved it there, or if you put in directions, I wish it showed you the current time there. Right now it just says &amp;ldquo;ETA 2:58 PM&amp;rdquo; which if you&amp;rsquo;ve been driving for 45 minutes and kinda have lost track of time is not particularly helpful, that could be in 5 minutes or in 25 minutes for all I know.&lt;/p&gt;
&lt;p&gt;It looks a bit better on the R2 (seen in &lt;a href=&#34;https://www.youtube.com/watch?v=EfReqcUJfBU&#34;&gt;MKBHD&amp;rsquo;s video&lt;/a&gt;) where they&amp;rsquo;re placing it on the middle display a lot closer to the driver, but I still wish they just put it on the driver display.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2026/02/rivian-r2-wishes/time.jpeg&#34; alt=&#34;R2 UI showing the time on the left side of the main display&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;better-second-row-release&#34;&gt;Better second row release&lt;/h2&gt;
&lt;p&gt;The R1S and many other modern vehicles have really stupid and downright dangerous second row releases, where the main door handles are electric, so it there&amp;rsquo;s an electrical issue and you need to get out of the vehicle, you obviously need a manual release. On my Tesla, there&amp;rsquo;s a (shocker) pull handle that is obvious and you can just pull to get out of the vehicle. Easy peasy (don&amp;rsquo;t worry Tesla has since changed to an inexplicably stupider, hidden design like Rivian)&lt;/p&gt;
&lt;p&gt;The R1 is incredibly stupid and you literally have to pop off trim in the rear door to get access to a cable you can yank. Yeah, good luck remembering that in dire circumstances, so I threw one of those window breakers in the back cubby.&lt;/p&gt;
&lt;p&gt;The R2 is better here, &lt;a href=&#34;https://youtu.be/at1HS1CNhe4?t=382&#34;&gt;Jerry Rig Everything&lt;/a&gt; shows a little button patch you can pop off much easier to get to the cable, but still, just put the manual release that the front doors have, this is a basic safety feature and should not be complicated.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2026/02/rivian-r2-wishes/release.jpeg&#34; alt=&#34;Zack from Jerry Rig Everything&amp;#39;s hand the rear hatch release thing for the manual door release cable&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;smaller&#34;&gt;Smaller&lt;/h2&gt;
&lt;p&gt;This we know will be the case, with the Rivian R2 being about 2 feet shorter than the R1S.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2026/02/rivian-r2-wishes/r2-size.jpeg&#34; alt=&#34;Size showing the R2 length at 4,715 mm versus 5,100 mm of the R1S&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;The R1S is a properly &lt;em&gt;large&lt;/em&gt; vehicle, which makes it very capable, but I dunno, I do find myself wishing it was a little smaller quite often, so I honestly think if the R2 is compelling enough I might be trading in my R1.&lt;/p&gt;
&lt;h2 id=&#34;-does-this-sound-negative&#34;&gt;… Does this sound negative?&lt;/h2&gt;
&lt;p&gt;Reading it back a bit, this post sounds a wee bit negative even though the intention is just to talk about things I hope they improve.&lt;/p&gt;
&lt;p&gt;So just to be totally clear, I love the thing. It&amp;rsquo;s spacious, incredibly capable, looks great, has amazing range, and is just a lot of fun to drive with a ton of creature comforts that I miss every time I drive the Tesla. But there&amp;rsquo;s always room for improvement!&lt;/p&gt;
&lt;h2 id=&#34;im-so-excited&#34;&gt;I&amp;rsquo;m so excited&lt;/h2&gt;
&lt;p&gt;I genuinely think Rivian is doing such cool things, and the company behind it seems to have a real passion for building cool products instead of just sitting on Twitter all day, so I&amp;rsquo;m super excited to see what this mass-market vehicle does for them and I&amp;rsquo;m hoping for the best.&lt;/p&gt;
&lt;p&gt;Looks like they&amp;rsquo;re launching in spring (I guessed summer!) &lt;a href=&#34;https://x.com/Rivian/status/2021255369906131207&#34;&gt;with more pricing and configuration details March 12th&lt;/a&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>CKSyncEngine questions and answers</title>
      <link>https://christianselig.com/2026/01/cksyncengine/</link>
      <pubDate>Wed, 07 Jan 2026 18:01:47 -0400</pubDate>
      
      <guid>https://christianselig.com/2026/01/cksyncengine/</guid>
      <description>


&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2026/01/cksyncengine/hero.jpeg&#34; alt=&#34;Link Amiibo from Ocarina of Time, out of focus&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;I didn&amp;#39;t know what to put as a header so here are some iClouds (interesting clouds) in Maine&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I&amp;rsquo;ve had a lot of fun working with &lt;code&gt;CKSyncEngine&lt;/code&gt; over the last month or so. I truly think it&amp;rsquo;s one of the best APIs Apple has built, and they&amp;rsquo;ve managed to take a very complex topic (cloud syncing) and make it very digestible and easy to integrate, without having to get into the weeds of &lt;code&gt;CKOperation&lt;/code&gt; and whatnot like you had to in previous years.&lt;/p&gt;
&lt;p&gt;That being said, there&amp;rsquo;s a fair bit of work you still have to do (through no fault of Apple, it&amp;rsquo;s just that a lot of cloud sync work is application-specific), such as how to handle conflicts, how to integrate the &lt;code&gt;CKRecord&lt;/code&gt;s into your flow, responding to errors, etc.&lt;/p&gt;
&lt;p&gt;More interesting for a blog post, perhaps, I also had a fair few questions going into it (having very little CloudKit knowledge prior to this), and I thought I&amp;rsquo;d document those questions and the corresponding answers, as well as general insights I found to potentially save a future &lt;code&gt;CKSyncEngine&lt;/code&gt; user some time, as I really couldn&amp;rsquo;t find easy answers to these anywhere (nor did modern LLMs have any idea).&lt;/p&gt;
&lt;h2 id=&#34;apple-sample-project&#34;&gt;Apple sample project&lt;/h2&gt;
&lt;p&gt;When in doubt, it&amp;rsquo;s always nice to see how Apple does things in their nicely published &lt;code&gt;CKSyncEngine&lt;/code&gt; sample project: &lt;a href=&#34;https://github.com/apple/sample-cloudkit-sync-engine&#34;&gt;https://github.com/apple/sample-cloudkit-sync-engine&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Other awesome resources are Jordan Morgan&amp;rsquo;s &lt;a href=&#34;https://superwall.com/blog/syncing-data-with-cloudkit-in-your-ios-app-using-cksyncengine-and-swift-and-swiftui&#34;&gt;blog post at Superwall&lt;/a&gt;, as well as the awesome work by Pointfree on their &lt;a href=&#34;https://github.com/pointfreeco/sqlite-data&#34;&gt;SQLiteData library&lt;/a&gt; which is open source and integrates &lt;code&gt;CKSyncEngine&lt;/code&gt; as the syncing layer.&lt;/p&gt;
&lt;p&gt;These are great resources to understand how to implement &lt;code&gt;CKSyncEngine&lt;/code&gt; which this article won&amp;rsquo;t be going over. I want to go over questions and edge cases you may encounter.&lt;/p&gt;
&lt;h2 id=&#34;conflict-resolution&#34;&gt;Conflict resolution&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;ve used &lt;code&gt;NSUbiquitousKeyValueStore&lt;/code&gt; (my only prior exposure to iCloud), &lt;code&gt;CKSyncEngine&lt;/code&gt; is thankfully &lt;strong&gt;a lot&lt;/strong&gt; smarter with conflict resolution (and by &amp;ldquo;conflict resolution&amp;rdquo; I mean &amp;ldquo;what happens when two devices try to save the same piece of data to the cloud&amp;rdquo;).&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;NSUbiquitousKeyValueStore&lt;/code&gt; if you had super valuable, years old data stored at key &amp;ldquo;blah&amp;rdquo; and you downloaded the app onto a new device and somehow set new data to the key &amp;ldquo;blah&amp;rdquo; (for instance, existing data hadn&amp;rsquo;t been downloaded yet) you would completely blow away the existing &amp;ldquo;blah&amp;rdquo; data, potentially jeopardizing years of data. Not great, which made me wary of storing much of value there without a ton of checks.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CKSyncEngine&lt;/code&gt; is a lot smarter, where you&amp;rsquo;re dealing with &lt;code&gt;CKRecord&lt;/code&gt;s directly (more on that below) and thus can save metadata from them, so if you try to overwrite &amp;ldquo;blah&amp;rdquo; and your metadata is not up to date, &lt;code&gt;CKSyncEngine&lt;/code&gt; will return a failure with the newest version of that data asking you what you want to do (overwrite your local data with the newer cloud version? tag your version with the newer cloud metadata and re-upload it so it works?), rather than blindly overwriting it. This makes it virtually impossible for a new device to come onto the scene and write &amp;ldquo;bad data&amp;rdquo; up, messing up existing data.&lt;/p&gt;
&lt;p&gt;(And &lt;code&gt;serverRecordChanged&lt;/code&gt; is the error in &lt;code&gt;failedRecordSaves&lt;/code&gt; you hook into!)&lt;/p&gt;
&lt;p&gt;It does beg the question though, &amp;ldquo;What do you do when there&amp;rsquo;s a conflict&amp;rdquo; and that&amp;rsquo;s what I alluded to earlier with Apple not being able to do &lt;em&gt;everything&lt;/em&gt; for you, and you need to make some decisions here. For me, it depends on the data. For the vast majority of the data, always having the &amp;ldquo;server version win&amp;rdquo; is perfectly fine for my use case, so I overwrite the local version with the cloud version.&lt;/p&gt;
&lt;p&gt;But there&amp;rsquo;s some situations where I want to be a little choosier, for instance for integer that can never decrease in value (a good example would be how many times you&amp;rsquo;ve died in a video game), I have a system where it just chooses the higher value between the cloud version and the local version, and chooses that.&lt;/p&gt;
&lt;p&gt;You could write a long blog post just on this though, the important part is to choose the right system for your application. An app that creates a lot of singular data but rarely ever modifies it will need a dramatically different system than one that has a large, single body of data that is frequently being edited on multiple devices concurrently.&lt;/p&gt;
&lt;p&gt;And remember that &lt;code&gt;CKSyncEngine&lt;/code&gt; being effectively a database means you can store a lot more information than the paltry 1,024 keys/1MB total limit that &lt;code&gt;NSUbiquitousKeyValueStore&lt;/code&gt; allows, so you can create a much more robust system that&amp;rsquo;s appropriate to your app, but not necessarily any more complicated!&lt;/p&gt;
&lt;h2 id=&#34;deletion-conflict-resolution&#34;&gt;Deletion conflict resolution&lt;/h2&gt;
&lt;p&gt;Note that deletions just fire without any conflict resolution at the CKSyncEngine level; if you say to delete something with recordID &lt;code&gt;&amp;quot;blah&amp;quot;&lt;/code&gt;, &lt;code&gt;CKSyncEngine&lt;/code&gt; will trust you know what you&amp;rsquo;re doing and just delete it (and not compare metadata or anything as it doesn&amp;rsquo;t even ask for it).&lt;/p&gt;
&lt;h2 id=&#34;ckrecord-handling&#34;&gt;CKRecord handling&lt;/h2&gt;
&lt;p&gt;One of the only awkward parts of &lt;code&gt;CKSyncEngine&lt;/code&gt; is that it operates through &lt;code&gt;CKRecord&lt;/code&gt;s, which are quite old a construct (much more Objective-C than Swift) you have to decide how to incorporate that into your existing data store. They&amp;rsquo;re basically a big old string dictionary of data with some metadata.&lt;/p&gt;
&lt;p&gt;For me, I mostly use GRDB (SQLite), and you have a nice, easy, hybrid solution where you have your local records with an extra column called something like &lt;code&gt;cloudKitInfo&lt;/code&gt;, which is just the &lt;code&gt;CKRecord&lt;/code&gt; distilled down into its pure informational metadata. This strips out all the &lt;code&gt;CKRecord&lt;/code&gt; of large image and text data, and you&amp;rsquo;re basically just getting the bare essentials: the metadata fields like its record change tag for conflict resolution when you upload it&lt;/p&gt;
&lt;p&gt;If you don&amp;rsquo;t save these metadata fields you&amp;rsquo;re going to have a Very Bad Time™ when you go to upload, as your items being uploaded will have no matching metadata, so CloudKit will think you don&amp;rsquo;t have the most up to date version of that record and give you a conflict error every time.&lt;/p&gt;
&lt;p&gt;So my process generally looks like:&lt;/p&gt;
&lt;p&gt;When you get a new CKRecord from iCloud to sync with your local store, you extract all the data you care about from the dictionary fields (e.g.: &lt;code&gt;item.postTitle = ckRecord[&amp;quot;postTitle&amp;quot;]&lt;/code&gt;) into your local Swift object, and then extract the CloudKit specific metadata.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;extension&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;CKRecord&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;systemFieldsData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;archiver&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NSKeyedArchiver&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;requiringSecureCoding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;encodeSystemFields&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;with&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;archiver&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;archiver&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;finishEncoding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;archiver&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;encodedData&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cloudKitInfo&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ckRecord&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;systemFieldsData&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;saveToSQLite&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, when you go to upload an item after you changed it, you create a &lt;code&gt;CKRecord&lt;/code&gt; by initializing it with your existing &lt;code&gt;cloudKitInfo&lt;/code&gt;, then set the fields.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;unarchiver&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NSKeyedUnarchiver&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forReadingFrom&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cloudKitSystemFields&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;unarchiver&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;requiresSecureCoding&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;restoredRecord&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CKRecord&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;coder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;unarchiver&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;restoredRecord&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;postTitle&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;myNewPostTitle&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This has the nice effect of letting you do basically everything in Swift, and just tacking on the necessary parts of the &lt;code&gt;CKRecord&lt;/code&gt; to make the system work properly, without duplicating the entire &lt;code&gt;CKRecord&lt;/code&gt; with all of the heavy data fields it may contain.&lt;/p&gt;
&lt;h2 id=&#34;backwardforward-compatibility&#34;&gt;Backward/forward compatibility&lt;/h2&gt;
&lt;p&gt;One big worry I had was what if in version 1.0 of my app I have a structure like the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;IceCream&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;lastEatenOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Date&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And then in version 1.1 of the app I add a new field:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;IceCream&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;lastEatenOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Date&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;tastiness&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Float&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// New!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If a user has two devices, one that is updated to version 1.1 and another on 1.0, if I save a new &lt;code&gt;IceCream&lt;/code&gt; on version 1.1 of the app with both a &lt;code&gt;name&lt;/code&gt; of &lt;code&gt;&amp;quot;chocolate&amp;quot;&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; a &lt;code&gt;tastiness&lt;/code&gt; of &lt;code&gt;0.95&lt;/code&gt;, and sync that back to the device on version 1.0, where they eat the ice cream, then sync that back up, crucially &lt;strong&gt;that version of the app doesn&amp;rsquo;t know about the &lt;code&gt;tastiness&lt;/code&gt; variable&lt;/strong&gt;! So it might effectively sync back up &lt;code&gt;IceCream(name: &amp;quot;chocolate&amp;quot;, lastEatenOn: .now)&lt;/code&gt;, and then when version 1.1 gets that, the &lt;code&gt;tastiness&lt;/code&gt; is effectively lost data! Noooooo!&lt;/p&gt;
&lt;p&gt;How do we handle this? I dreamt up some complex solutions, but it turns out it&amp;rsquo;s incredibly easy thanks to the way &lt;code&gt;CKRecord&lt;/code&gt; works. &lt;code&gt;CKSyncEngine&lt;/code&gt; never documents this anywhere directly, but it obviously uses CloudKit under the hood, and CloudKit has dinstinct saving policies under &lt;code&gt;CKModifyRecordsOperation.RecordSavePolicy&lt;/code&gt; &lt;a href=&#34;https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation/savepolicy&#34;&gt;documented here&lt;/a&gt;. And no matter what policy you choose (we don&amp;rsquo;t get a choice with &lt;code&gt;CKSyncEngine&lt;/code&gt;) all of them detail the same behavior:&lt;/p&gt;
&lt;p&gt;CloudKit only saves the fields on &lt;code&gt;CKRecord&lt;/code&gt;s that you &lt;em&gt;explicitly set&lt;/em&gt;. In other words, on version 1.0, when we create our &lt;code&gt;CKRecord&lt;/code&gt; that represents our local data, it would look something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;ckRecord&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// create CKRecord instance&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;ckRecord&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;chocolate&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;ckRecord&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;lastEatenOn&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Date&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;now&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that we didn&amp;rsquo;t set &lt;code&gt;tastiness&lt;/code&gt; &lt;em&gt;at all&lt;/em&gt;, so when it goes up to iCloud, the &lt;code&gt;tastiness&lt;/code&gt; field won&amp;rsquo;t be touched at all as it&amp;rsquo;s not present, &lt;em&gt;it will just remain what it was&lt;/em&gt;. The only way the &lt;code&gt;tastiness&lt;/code&gt; field would get reset is it we explicitly set it to &lt;code&gt;nil&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So when version 1.1 pulls down this change that version 1.0 made, the &lt;code&gt;CKRecord&lt;/code&gt; it pulls down &lt;em&gt;will still have the tastiness field intact&lt;/em&gt;. It&amp;rsquo;s essentially a factor that old versions of the app can only touch what fields they know exist, so no harm no foul.&lt;/p&gt;
&lt;p&gt;The only catch is you can&amp;rsquo;t go in the other direction: don&amp;rsquo;t delete &lt;code&gt;tastiness&lt;/code&gt; in verson 1.2 of the app if earlier versions expect it to always exist. Give it some innocent default value.&lt;/p&gt;
&lt;h2 id=&#34;enums-are-bad&#34;&gt;Enums are bad&lt;/h2&gt;
&lt;p&gt;Enums are a &lt;em&gt;finite&lt;/em&gt; set of values, so unless you&amp;rsquo;re positive that it will &lt;em&gt;never&lt;/em&gt; change, don&amp;rsquo;t use enums in values meant to be cloud-synced.&lt;/p&gt;
&lt;p&gt;Why? Say you have this enum in version 1.0 of your app:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;enum&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;IceCreamFlavor&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;chocolate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;strawberry&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And in version 1.1 you add a new flavor:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;enum&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;IceCreamFlavor&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;chocolate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;strawberry&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;vanilla&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// New!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What happens when version 1.0 has to decode &lt;code&gt;IceCreamFlavor.vanilla&lt;/code&gt;? It will have no idea what that case is, and fail to decode, which you could just treat as a nil value, but if you then try to sync that nil value up, you risk overwriting the existing, good value with nil data (unlike the &amp;ldquo;Backward/forward compatibility case&amp;rdquo; above where it was a value stored in a &lt;em&gt;different field&lt;/em&gt;, this is all operating under the same field/key). Bad.&lt;/p&gt;
&lt;p&gt;Instead, just store it as a string, and you could try to initialize an enum of known values with the string&amp;rsquo;s raw value if you desire.&lt;/p&gt;
&lt;h2 id=&#34;multiple-cksyncengine-instances&#34;&gt;Multiple CKSyncEngine instances&lt;/h2&gt;
&lt;p&gt;You have to be really careful with multiple instances of &lt;code&gt;CKSyncEngine&lt;/code&gt;s.&lt;/p&gt;
&lt;p&gt;At a high level in CloudKit you have &lt;code&gt;CKContainer&lt;/code&gt;, which houses three &lt;code&gt;CKDatabase&lt;/code&gt; instances: a private one (probably most commonly used), a public one, and a shared one.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CKSyncEngine&lt;/code&gt; only allows one instance to manage an individual database, so that means it&amp;rsquo;s totally fine to have separate &lt;code&gt;CKSyncEngine&lt;/code&gt; instances for a private and shared database. (&lt;strong&gt;Not&lt;/strong&gt; for the public database however, as &lt;code&gt;CKSyncEngine&lt;/code&gt; does not support public databases.)&lt;/p&gt;
&lt;p&gt;But you should not have multiple &lt;code&gt;CKSyncEngine&lt;/code&gt; instances managing a single private database (I naively tried to do this to have a nice separation of concerns between different types of data in the app). The instances trip over each othre very quickly, with it not being clear which instance receives the sync events.&lt;/p&gt;
&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; get around this by creating multiple &lt;code&gt;CKContainer&lt;/code&gt;s, and having a &lt;code&gt;CKSyncEngine&lt;/code&gt; per each one, but that feels messy and from what I understand not really how Apple intended containers to be used. Keeping everything under one instance isn&amp;rsquo;t too bad even with different kinds of data, as you can use different zones or record types to keep things sufficiently separated.&lt;/p&gt;
&lt;h2 id=&#34;should-you-not-call-cksyncengine-methods-if-the-user-isnt-signed-into-icloud&#34;&gt;Should you not call CKSyncEngine methods if the user isn&amp;rsquo;t signed into iCloud?&lt;/h2&gt;
&lt;p&gt;Apple&amp;rsquo;s sample project still does! It seems harmless. From my testing, they get enqueued, but are never actioned upon (they never fail unlike normal &lt;code&gt;CKRecordOperation&lt;/code&gt;s, they just sit waiting forever), and then the queue is wiped when the user signs in.&lt;/p&gt;
&lt;h2 id=&#34;what-happens-if-they-sign-outsign-in-while-your-app-is-quit&#34;&gt;What happens if they sign out/sign in while your app is quit?&lt;/h2&gt;
&lt;p&gt;No worries, you get the appropriate &lt;code&gt;accountChange&lt;/code&gt; event on the next app launch.&lt;/p&gt;
&lt;h2 id=&#34;what-is-the-difference-between-the-account-change-notifications&#34;&gt;What is the difference between the account change notifications?&lt;/h2&gt;
&lt;p&gt;You can either get &lt;code&gt;signedIn&lt;/code&gt;, &lt;code&gt;signedOut&lt;/code&gt;, or &lt;code&gt;switchAccount&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;signedIn&lt;/code&gt; happens when they had no account and signed into one. &lt;code&gt;signedOut&lt;/code&gt; happens when they had an existing account and signed out.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;switchAccounts&lt;/code&gt; is a &amp;ldquo;coalescing&amp;rdquo; one (you won&amp;rsquo;t get &lt;code&gt;signedIn&lt;/code&gt;/&lt;code&gt;signedOut&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;switchAccounts&lt;/code&gt;), where if your app is running/backgrounded you will get &lt;code&gt;signedOut&lt;/code&gt; then &lt;code&gt;signedIn&lt;/code&gt; if the user changes accounts, and you &lt;em&gt;won&amp;rsquo;t&lt;/em&gt; get a &lt;code&gt;switchAccounts&lt;/code&gt; notification. You only get &lt;code&gt;switchAccounts&lt;/code&gt; if your app was quit and you relaunch the app at which point you&amp;rsquo;ll get the &lt;code&gt;switchAccounts&lt;/code&gt; notification (but neither of the other two).&lt;/p&gt;
&lt;h2 id=&#34;how-does-state-serialization-work&#34;&gt;How does state serialization work?&lt;/h2&gt;
&lt;p&gt;Every time &lt;em&gt;anything&lt;/em&gt; happens with &lt;code&gt;CKSyncEngine&lt;/code&gt; you&amp;rsquo;re given a &lt;code&gt;stateUpdate&lt;/code&gt; event, which you&amp;rsquo;re expected to persist to disk. This encodes the entirety of your &lt;code&gt;CKSyncEngine&lt;/code&gt;&amp;rsquo;s state into a serialized value, so when the app launches the next time it can start off right where it was.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s essentially a super charged git commit tag/checkpoint, so iOS knows where your &lt;code&gt;CKSyncEngine&lt;/code&gt; exists in time (does it need to pull down any new changes?) and maintains any pending changes/deletions that might have not completed. If your app crashes part way through applying a change, your app simply will not have been issued the new &amp;ldquo;save checkpoint&amp;rdquo; notification, so the next time your app relaunches it will simply be restored to the last &lt;code&gt;CKSyncEngine&lt;/code&gt; state you saved and retry.&lt;/p&gt;
&lt;p&gt;It also initializes synchronously, so if you had any pending items in your serialized state and you initialize &lt;code&gt;CKSyncEngine&lt;/code&gt;, you can view your pending items immediately.&lt;/p&gt;
&lt;p&gt;Also note that if you initialize &lt;code&gt;CKSyncEngine&lt;/code&gt; without any state serialization, you &lt;em&gt;always&lt;/em&gt; get an &amp;ldquo;account change: &lt;code&gt;signedIn&lt;/code&gt;&amp;rdquo; notification even if the user didn&amp;rsquo;t explicitly just sign into their iCloud account.&lt;/p&gt;
&lt;h2 id=&#34;cksyncengine-re-initialization&#34;&gt;CKSyncEngine re-initialization&lt;/h2&gt;
&lt;p&gt;Per Apple&amp;rsquo;s sample project, re-initialize your &lt;code&gt;CKSyncEngine&lt;/code&gt; (and delete any old state serialization) when either the user signs out, or switches accounts, but &lt;em&gt;not&lt;/em&gt; when they transition from signed out to signed in, presumably because in the latter case there&amp;rsquo;s nothing really to invalidate in the &lt;code&gt;CKSyncEngine&lt;/code&gt; when there is in the other two states.&lt;/p&gt;
&lt;h2 id=&#34;how-does-error-handling-work&#34;&gt;How does error handling work?&lt;/h2&gt;
&lt;p&gt;Apple&amp;rsquo;s sample project indicates that there are a number of transient errors that &lt;code&gt;CKSyncEngine&lt;/code&gt; handles automatically for you, like rate limiting issues, no internet connection, iCloud being down, etc. Nice!&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;networkFailure&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;networkUnavailable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;zoneBusy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;serviceUnavailable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;notAuthenticated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;operationCancelled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;requestRateLimited&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In &lt;em&gt;most&lt;/em&gt; of these cases it means the item just gets immediately added back to the pending items queue and CloudKit will pause the queue for a certain amount of time before retrying.&lt;/p&gt;
&lt;p&gt;Other ones, you &lt;em&gt;do&lt;/em&gt; need to handle yourself, even if they seem like they should be automatic. A good example is &lt;code&gt;quotaExceeded&lt;/code&gt; which you get if the user ran out of iCloud storage and you tried to save something.&lt;/p&gt;
&lt;p&gt;In this case Apple pauses the queue until the user frees up space or buys more (or after several minutes, specified by &lt;code&gt;retryAfterSeconds&lt;/code&gt;) but &lt;em&gt;does not add your item back&lt;/em&gt;, which seems weird to me, so just add it back. But you also can&amp;rsquo;t &lt;em&gt;just&lt;/em&gt; add it back, as that would put it at the end of the queue, so you have to insert it back at the beginning of the queue so it&amp;rsquo;s the next item that will be retried (since it just failed). Only, there&amp;rsquo;s no API for this, so grab all the items in the queue, then empty the queue, then re-add all items &lt;em&gt;back&lt;/em&gt; to the queue with your failed item at the front.&lt;/p&gt;
&lt;p&gt;For other failures, like &lt;code&gt;quotaExceeded&lt;/code&gt;, they&amp;rsquo;re immediately removed from pending items once they fail, so if you want them to be retried you have to add them back manually.&lt;/p&gt;
&lt;p&gt;(Remember, the pending queue survives app restarts as it&amp;rsquo;s serialized to disk through state serialization, see above.)&lt;/p&gt;
&lt;h2 id=&#34;embedding-record-types-into-record-ids&#34;&gt;Embedding record types into record IDs&lt;/h2&gt;
&lt;p&gt;A small point worth noting is that weirdly CKSyncEngine does &lt;em&gt;not&lt;/em&gt; provide the actual recordType (only the string ID) when requesting the fully built &lt;code&gt;CKRecord&lt;/code&gt;s (which we need in order to tell which SQLite table the ID belongs to), so we can prepend the table to the beginning of the ID string, for instance &lt;code&gt;IceCream:9arsnt89rna9stda5&amp;quot;&lt;/code&gt; so we &lt;em&gt;can&lt;/em&gt; discern it at runtime.&lt;/p&gt;
&lt;h2 id=&#34;let-things-be-automatic&#34;&gt;Let things be automatic&lt;/h2&gt;
&lt;p&gt;You can manually pull/push to &lt;code&gt;CKSyncEngine&lt;/code&gt; with &lt;code&gt;fetchChanges()&lt;/code&gt; and &lt;code&gt;sendChanges()&lt;/code&gt; but be careful. You can&amp;rsquo;t call these inside the &lt;code&gt;CKSyncEngineDelegate&lt;/code&gt; methods per &lt;code&gt;CKSyncEngineDelegate&lt;/code&gt; documentation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;CKSyncEngine&lt;/code&gt; delivers events serially, which means the delegate doesn’t receive the next event until it finishes handling the current one. To maintain this ordering, don’t call sync engine methods from your delegate that may cause the engine to generate additional events. For example, don’t invoke &lt;code&gt;fetchChanges(_:)&lt;/code&gt; or &lt;code&gt;sendChanges(_:)&lt;/code&gt; from within &lt;code&gt;handleEvent(_:syncEngine:)&lt;/code&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;You can get stuck in weird, infinite loops. In practice I&amp;rsquo;ve found &lt;code&gt;CKSyncEngine&lt;/code&gt; is really great at queuing up changes almost instantly without you having to babysit it and manually pull/fetch, just let it do its own thing and you should get great performance and not run into infinite loop bugs by trying to do things yourself.&lt;/p&gt;
&lt;p&gt;(Also note that the quote is kinda confusing, but it refers to those fetch and send changes methods specifically, adding new items to the queue within the delegate is fine and something Apple does in their sample project.)&lt;/p&gt;
&lt;h2 id=&#34;zone-deletion-reasons&#34;&gt;Zone deletion reasons&lt;/h2&gt;
&lt;p&gt;When a &amp;ldquo;zone was deleted&amp;rdquo; event occurs, ensure you inspect the reason, of which there are 3:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;deleted&lt;/code&gt; means we (the programmer) did it programmatically, commonly done as it&amp;rsquo;s the easiest/quickest way to delete all the records in a zone&lt;/li&gt;
&lt;li&gt;&lt;code&gt;purged&lt;/code&gt; means the user went through the iOS Settings app and wiped iCloud data for our app, which per Apple&amp;rsquo;s recommendation means we should delete all local data as well (otherwise it would just sync back up after they explicitly asked for it to be wiped, likely because they were running low on storage), and in the &lt;code&gt;purged&lt;/code&gt; case we also delete our local system state serialization change token as it&amp;rsquo;s no longer valid (this is a full reset).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;encryptedDataReset&lt;/code&gt; means the user had to reset their encrypted data during account recovery and per Apple&amp;rsquo;s recommendation we treat this as something the user likely did not want to have to do, so reset/delete our system state serialization token and reupload all their data to minimize data loss.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;responding-to-account-status-changes&#34;&gt;Responding to account status changes&lt;/h2&gt;
&lt;p&gt;CloudKit also has a NotificationCenter API for monitoring account changes (&lt;code&gt;Notification.Name.CKAccountChanged&lt;/code&gt;) but you don&amp;rsquo;t really need this at all if you&amp;rsquo;re using &lt;code&gt;CKSyncEngine&lt;/code&gt;, everything comes through the &lt;code&gt;accountChange&lt;/code&gt; event that the &lt;code&gt;NotificationCenter&lt;/code&gt; API would otherwise provide (just distilled down to &lt;code&gt;signedIn&lt;/code&gt;, &lt;code&gt;signedOut&lt;/code&gt;, or &lt;code&gt;switchAccounts&lt;/code&gt; where the NotificationCenter API is a bit more granular). You can use both, but I haven&amp;rsquo;t found a need.&lt;/p&gt;
&lt;p&gt;Note that you should react appropriately to the &lt;em&gt;kind&lt;/em&gt; of account change that occurred. For instance, following Apple&amp;rsquo;s sample project recommendation, if you receive a notification that they &lt;code&gt;signedOut&lt;/code&gt;, that could mean they signed out of their iCloud account to give their sibling an old iPhone to play around with, and they may have private data they don&amp;rsquo;t want their sibling to have access to, so we should take this as a queue to delete local data (if they want the data back, when they sign back into iCloud it will be re-downloaded).&lt;/p&gt;
&lt;p&gt;Also note you can get the status of the user&amp;rsquo;s iCloud account at any point using &lt;code&gt;try await CKContainer.default().accountStatus()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;batch-sizes&#34;&gt;Batch sizes&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CKRecord&lt;/code&gt;s can be a max size of 1 MB, but also note that uploaded batches are limited to 1 MB in size, so if you enqueue 10 items to be uploaded, each 1 MB, iCloud will upload them in sequential, 1 MB batches (I sort of expected a single, 10 MB upload that included all the records).&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s uploads, but conversely on the download size, iCloud is happy to download batches much larger than 1 MB in size! I&amp;rsquo;ve comfortably seen 100 MB+, which can happen when syncing an initial, large library.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If I think of any more notes I&amp;rsquo;ll add them, but hopefully a bunch of these things (that I had to find out through trial and error) save some other folks time when implementing &lt;code&gt;CKSyncEngine&lt;/code&gt;!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>The one software tweak the iPhone Air needs</title>
      <link>https://christianselig.com/2025/09/iphone-air-macro-need/</link>
      <pubDate>Fri, 26 Sep 2025 13:38:10 -0300</pubDate>
      
      <guid>https://christianselig.com/2025/09/iphone-air-macro-need/</guid>
      <description>&lt;p&gt;&lt;em&gt;One trick doctors hate that will make your iPhone…&lt;/em&gt; Sorry.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been &lt;em&gt;loving&lt;/em&gt; my iPhone Air. A week in I think it&amp;rsquo;s my favorite iPhone since the iPhone X.&lt;/p&gt;
&lt;p&gt;It has that indescribable feeling that the original MacBook Air had. That, &amp;ldquo;Wow, computers can be like this?&amp;rdquo; feeling that&amp;rsquo;s hard to quantify when you&amp;rsquo;re just looking at a spec sheet. Picking it up still makes me smile, and I love that the screen is bigger than any iPhone I&amp;rsquo;ve ever had, while the device overall feels &lt;em&gt;smaller&lt;/em&gt; because it&amp;rsquo;s so thin.&lt;/p&gt;
&lt;p&gt;Even the battery has been surprisingly good, I feel like I have more at the end of the day than I did with my 15 Pro that I&amp;rsquo;m upgrading from, and &lt;a href=&#34;https://www.apple.com/iphone/compare/?modelList=iphone-15-pro,iphone-air&#34;&gt;Apple’s numbers seem to back this up&lt;/a&gt;, showing 23 hours of video playback on the 15 Pro and an increase to 27 on the Air.&lt;/p&gt;
&lt;p&gt;The only area I&amp;rsquo;ve kinda been disappointed on is the camera situation. No, not the telephoto, I really never used that personally. And not the ultrawide, for me that just felt too wide. But the ultrawide &lt;em&gt;did&lt;/em&gt; allow for awesome macro capabilities that this iPhone Air is sorely lacking. &lt;em&gt;At least currently&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2025/09/iphone-air-macro-need/link-out-of-focus.jpeg&#34; alt=&#34;Link Amiibo from Ocarina of Time, out of focus&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;The iPhone Air&amp;rsquo;s minimum focus distance is just too short. Don&amp;rsquo;t get me wrong, it&amp;rsquo;s a hair better than my 15 Pro&amp;rsquo;s main sensor, allowing you to get maybe 15% closer to the subject, but it still does that annoying thing where when you want to take a picture of a small object and have it take up the full field of view, it often goes blurry &lt;em&gt;right&lt;/em&gt; when you get it framed up.&lt;/p&gt;
&lt;p&gt;But then I was like, duh, it&amp;rsquo;s a 48 MP sensor, so I can zoom into 2x to get twice as close and still get a nice 12 MP photo. So you just pull the phone back a bit, hit 2x, and bam, you have a beautifully framed close shot, that&amp;rsquo;s actually in focus.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2025/09/iphone-air-macro-need/link-in-focus.jpeg&#34; alt=&#34;Link Amiibo from Ocarina of Time, in focus&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;an-easy-solution&#34;&gt;An &amp;ldquo;easy&amp;rdquo; solution&lt;/h2&gt;
&lt;p&gt;Look, I won&amp;rsquo;t claim camera sensor software is in any way easy, but all the other iPhones do an awesome job of detecting when the main sensor reached its minimum focus distance and then hopping over the the ultrawide to get a nice macro shot that&amp;rsquo;s still in focus.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d &lt;em&gt;love&lt;/em&gt; if Apple implemented similar software magic on the Air, where instead of having to manually hit that 2x when it gets blurry, Apple detected you hit the minimum focus distance and instructed you to &amp;ldquo;back up a bit&amp;rdquo; and then automatically made it in focus through cropping in on the main sensor.&lt;/p&gt;
&lt;p&gt;Would it change the world? No, but it&amp;rsquo;d take out a manual step I&amp;rsquo;m finding myself doing somewhat frequently.&lt;/p&gt;
&lt;p&gt;Will this level up your macro photography so that you can take pictures of the pollen on the leg of a bee? No, absolutely not. But getting about twice as close to your subject is a massive difference, especially since I find right now the Air&amp;rsquo;s minimum focus distance is just on the edge of where I want it to be when holding things close.&lt;/p&gt;
&lt;p&gt;Hopefully the brilliant folks at &lt;a href=&#34;https://halide.cam&#34;&gt;Halide&lt;/a&gt;, &lt;a href=&#34;https://notbor.ing/product/camera&#34;&gt;(Not Boring)&lt;/a&gt;, or &lt;a href=&#34;https://obscura.camera/obscura/index.html&#34;&gt;Obscura&lt;/a&gt; (listed in alphabetical order so I don&amp;rsquo;t have to rank my friends) can integrate something like this into their awesome apps if Apple themselves do not.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>App Clip Local Experiences have consumed my day</title>
      <link>https://christianselig.com/2025/09/why-app-clip-no-work/</link>
      <pubDate>Mon, 08 Sep 2025 16:03:58 -0300</pubDate>
      
      <guid>https://christianselig.com/2025/09/why-app-clip-no-work/</guid>
      <description>&lt;p&gt;Okay, I have to be doing something astronomically stupid, right? This &lt;em&gt;should&lt;/em&gt; be working? I&amp;rsquo;m playing around with an App Clip and want to just run it on the device as a test, but no matter how I set things up nothing ever works. If you see what I&amp;rsquo;m doing wrong let me know and I&amp;rsquo;ll update this, and hopefully we can save someone else in the future a few hours of banging their head!&lt;/p&gt;
&lt;h2 id=&#34;xcode&#34;&gt;Xcode&lt;/h2&gt;
&lt;p&gt;App Clips require some setup in App Store Connect, so Apple provides a way when you&amp;rsquo;re just testing things to side step all that: &lt;a href=&#34;https://developer.apple.com/documentation/appclip/testing-the-launch-experience-of-your-app-clip#Test-App-Clip-invocations-with-a-local-experience&#34;&gt;App Clip Local Experiences&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I create a new sample project called &lt;code&gt;IceCreamStore&lt;/code&gt;, which has the bundle ID &lt;code&gt;com.christianselig.IceCreamStore&lt;/code&gt;. I then go to File &amp;gt; New &amp;gt; Target… &amp;gt; App Clip. I choose the Product Name &amp;ldquo;IceCreamClip&amp;rdquo;, and it automatically gets the bundle ID &lt;code&gt;com.christianselig.IceCreamStore.Clip&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I run both the main target and the app clip target on my iOS 18.6 phone and everything shows up perfectly, so let&amp;rsquo;s go onto actually configuring the Local Experience.&lt;/p&gt;
&lt;h2 id=&#34;local-experience-setup&#34;&gt;Local Experience setup&lt;/h2&gt;
&lt;p&gt;I go to Settings.app &amp;gt; Developer &amp;gt; App Clips Testing &amp;gt; Local Experiences &amp;gt; Register Local Experience, and then input the following details:&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2025/09/why-app-clip-no-work/hero.jpeg&#34; alt=&#34;Screenshot of iOS Settings app page for App Clip Local Experiences, with the inputted values available in text below&#34; class=&#34;width-75&#34; loading=&#34;lazy&#34; /&gt;

&lt;ul&gt;
&lt;li&gt;URL Prefix: &lt;a href=&#34;https://boop.com/beep/&#34;&gt;https://boop.com/beep/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Bundle ID: com.christianselig.IceCreamStore.Clip (note thne Apple guide above says to use the &lt;em&gt;Clip&amp;rsquo;s&lt;/em&gt; bundle ID, but I have tried both)&lt;/li&gt;
&lt;li&gt;Title: Test1&lt;/li&gt;
&lt;li&gt;Subtitle: Test2&lt;/li&gt;
&lt;li&gt;Action: Open&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Upon saving, I then send myself a link to &lt;a href=&#34;https://boop.com/beep/123&#34;&gt;https://boop.com/beep/123&lt;/a&gt; in iMessage, and upon tapping on it… nothing, it just tries to open that URL in Safari rather than in an App Clip (as it presumably should?). Same thing if I paste the URL into Safari&amp;rsquo;s address bar directly.&lt;/p&gt;
&lt;p&gt;I also tried generating an &lt;a href=&#34;https://developer.apple.com/documentation/appclip/creating-app-clip-codes-with-the-app-clip-code-generator&#34;&gt;App Clip Code&lt;/a&gt;, but upon scanning it with my device I get &amp;ldquo;No usable data found&amp;rdquo;.&lt;/p&gt;
&lt;h2 id=&#34;help&#34;&gt;Help&lt;/h2&gt;
&lt;p&gt;What&amp;rsquo;s the deal here, what am I doing wrong? Is my App Store Connect account conspiring against me? I&amp;rsquo;ve tried on multiple iPhones on both iOS 18 and 26, and the incredible &lt;a href=&#34;https://x.com/MattHeaney23&#34;&gt;Matt Heaney&lt;/a&gt; (wrangler of App Clips) even kindly spent a bunch of time also pulling his hair out over this. We even tried to see if my devices were somehow banned from using App Clips, but nope, production apps using App Clips work fine!&lt;/p&gt;
&lt;p&gt;If you figure this out you would be my favorite person. 😛&lt;/p&gt;
&lt;h2 id=&#34;update-solution-sorta&#34;&gt;Update: solution. Sorta?&lt;/h2&gt;
&lt;p&gt;Okay, seems the solution is two-fold:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make sure in addition to the main app target being installed, you manually switch to the App Clip target and install that itself directly too&lt;/li&gt;
&lt;li&gt;Generate an App Clip Code via &lt;a href=&#34;https://developer.apple.com/documentation/appclip/creating-app-clip-codes-with-the-app-clip-code-generator&#34;&gt;the generator CLI&lt;/a&gt; (or a &lt;a href=&#34;https://github.com/MattHeaney23/AppClipped&#34;&gt;nice GUI&lt;/a&gt;) and scan that, rather than trying to open from URLs directly&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I will say I do love how Apple stuff 99% of the time does &amp;ldquo;just work&amp;rdquo;, but dang those times when it doesn&amp;rsquo;t I really wish they showed some diagnostics I could see as to why.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>High quality, low filesize GIFs</title>
      <link>https://christianselig.com/2025/08/high-quality-low-filesize-gifs/</link>
      <pubDate>Sat, 02 Aug 2025 12:12:53 -0300</pubDate>
      
      <guid>https://christianselig.com/2025/08/high-quality-low-filesize-gifs/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2025/08/high-quality-low-filesize-gifs/hero.jpeg&#34; alt=&#34;A group of small kittens on a carpet&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;While the GIF format is a little on the older side, it&amp;rsquo;s still a really handy format in 2025 for sharing short clips where an actual video file might have some compatibility issues.&lt;/p&gt;
&lt;p&gt;For instance, I find when you just want a short little video on your website, a GIF is still so handy versus a video, where some browsers will refuse to autoplay them, or seem like they&amp;rsquo;ll autoplay them fine until Low Battery Mode is activated, etc. With GIFs it&amp;rsquo;s just… easy, and sometimes easy is nice. They&amp;rsquo;re super handy for showing a screen recording of a cool feature in your app, for instance.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s not nice is the size of GIFs. They have a reputation of being absolutely enormous from a filesize perspective, and they often are, but that doesn&amp;rsquo;t &lt;em&gt;have&lt;/em&gt; to be the case, you can be smart about your GIF and optimize its size substantially. Over the years I&amp;rsquo;ve tried lots of little apps that promise to help to no avail, so I&amp;rsquo;ve developed a little script to make this easier that I thought might be helpful to share.&lt;/p&gt;
&lt;h2 id=&#34;naive-approach&#34;&gt;Naive approach&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s show where GIFs get that bad reputation so we can have a baseline.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll use trusty ol&amp;rsquo; &lt;code&gt;ffmpeg&lt;/code&gt; (in the age of LLMs it is a super handy utility), which if you don&amp;rsquo;t have already you can install via &lt;code&gt;brew install ffmpeg&lt;/code&gt;. It&amp;rsquo;s a handy (and in my opinion downright essential) tool for doing just about anything with video.&lt;/p&gt;
&lt;p&gt;For a video we&amp;rsquo;ll use this cute video of some kittens I took at our local animal shelter:&lt;/p&gt;



    &lt;video class=&#34;&#34; controls autoplay muted loop playsinline&gt;&lt;source src=&#34;https://christianselig.com/2025/08/high-quality-low-filesize-gifs/kitties.mp4&#34;&gt;&lt;/source&gt;&lt;/video&gt;

&lt;p&gt;It&amp;rsquo;s 4K, 30 FPS, 5 seconds long, and thanks to its H265/HEVC video encoding it&amp;rsquo;s only 19.5 MB. Not bad!&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s just chuck it into ffmpeg and tell it to output a GIF and see how it does.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ffmpeg -i kitties.mp4 kitties.gif
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, let that run and- oh &lt;em&gt;no&lt;/em&gt;.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2025/08/high-quality-low-filesize-gifs/gif-size-naive.jpg&#34; alt=&#34;A screenshot of macOS Finder showing the GIF at 409.4MB&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;For your sake I&amp;rsquo;m not even going to attach the GIF here in case folks are on mobile data, but the resulting file is &lt;strong&gt;409.4MB&lt;/strong&gt;. Almost half a gigabyte for a 5 second GIF of kittens. We gotta do better.&lt;/p&gt;
&lt;h2 id=&#34;better&#34;&gt;Better&lt;/h2&gt;
&lt;p&gt;We can do better.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s throw a bunch of confusing parameters at &lt;code&gt;ffmpeg&lt;/code&gt; (that I&amp;rsquo;ll break down) to make this a bit more manageable.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ffmpeg -i kitties.mp4 -filter_complex &lt;span class=&#34;s2&#34;&gt;&amp;#34;fps=24,scale=iw*sar:ih,scale=1000:-1,split[a][b];[a]palettegen[p];[b][p]paletteuse=dither=floyd_steinberg&amp;#34;&lt;/span&gt; kitties2.gif
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, lot going on here, let&amp;rsquo;s break it down.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fps=24&lt;/code&gt;: we&amp;rsquo;re dropping down to 24 fps from 30 fps, many folks upload full YouTube videos at this framerate so it&amp;rsquo;s more than acceptable for a GIF.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scale=iw*sar:ih&lt;/code&gt;: sometimes video files have weird situations where the aspect ratio of each &lt;em&gt;pixel&lt;/em&gt; isn&amp;rsquo;t square, which GIFs don&amp;rsquo;t like, so this is just a correction step so that doesn&amp;rsquo;t potentially trip us up&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scale=1000:-1&lt;/code&gt;: we don&amp;rsquo;t need our GIF to be 4K, and I&amp;rsquo;ve found 1,000 pixels across to be a great middle ground for GIFs. The -1 at the end just means scale the height to the appropriate value rather than us having to do the math ourselves.&lt;/li&gt;
&lt;li&gt;The rest is related to the color palette, we&amp;rsquo;re telling &lt;code&gt;ffmpeg&lt;/code&gt; to scan the entire video to build an appropriate color palette up, and to use the Floyd-Steinberg algorithm to do so. I find this algorithm gives us the highest quality output (which is also handy for compressing it more in further steps)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives us a dang good looking GIF that clocks in at about 10% the file size at 45.8MB.&lt;/p&gt;



&lt;a href=&#34;https://christianselig.com/2025/08/high-quality-low-filesize-gifs/kitties2.gif&#34;&gt;Link to GIF in lieu of embedding directly&lt;/a&gt;
&lt;p&gt;Nice!&lt;/p&gt;
&lt;h2 id=&#34;even-better&#34;&gt;Even better&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ffmpeg&lt;/code&gt; is great, but where it&amp;rsquo;s geared toward videos it doesn&amp;rsquo;t do every GIF optimization imaginable. You could stop where we are and be happy, but if you want to shave off a few more megabytes, we can leverage &lt;code&gt;gifsicle&lt;/code&gt;, a small command line utility that is built around optimizing GIFs.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll install &lt;code&gt;gifsicle&lt;/code&gt; via &lt;code&gt;brew install gifsicle&lt;/code&gt; and throw our GIF into it with the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;gifsicle -O3 --lossy&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;65&lt;/span&gt; --gamma&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.2 kitties2.gif -o kitties3.gif
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So what&amp;rsquo;s going on here?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;O3&lt;/code&gt; is essentially gifsicle&amp;rsquo;s most efficient mode, doing fancy things like delta frames so changes between frames are stored rather than each frame separately&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lossy=65&lt;/code&gt; defines the level of compression, 65 has been a good middle ground for me (200 I believe is the highest compression level)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gamma=1.2&lt;/code&gt; is a bit confusing, but essentially the gamma controls how the &lt;code&gt;lossy&lt;/code&gt; parameter reacts to (and thus compresses) colors. &lt;code&gt;1&lt;/code&gt; will allow it to be quite aggressive with colors, while &lt;code&gt;2.2&lt;/code&gt; (the default) is much less so. Through trial and error I&amp;rsquo;ve found &lt;code&gt;1.2&lt;/code&gt; causes nice compression without much of a loss in quality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The resulting GIF is now 23.8MB, shaving a nice additional 22MB off, so we&amp;rsquo;re now at a meager 5% of our original filesize.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2025/08/high-quality-low-filesize-gifs/kitties-final.gif&#34; alt=&#34;Three kittens playing with a pink feather toy on a carpet&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;That&amp;rsquo;s a lot closer to the 4K, 20MB input, so for a GIF I&amp;rsquo;ll call that a win. And for something like a simpler screen recording it&amp;rsquo;ll be even smaller!&lt;/p&gt;
&lt;h2 id=&#34;make-it-easy&#34;&gt;Make it easy&lt;/h2&gt;
&lt;p&gt;Rather than having to remember that command or come back here and copy paste it all the time, add the following to your &lt;code&gt;~/.zshrc&lt;/code&gt; (or create it if you don&amp;rsquo;t have one already):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;gifify&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Defaults&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;local&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;lossy&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;65&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;fps&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;24&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;1000&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;gamma&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.2
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;while&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[[&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$#&lt;/span&gt; -gt &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$1&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; in
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            --lossy&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;lossy&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$2&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;shift&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            --fps&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;   &lt;span class=&#34;nv&#34;&gt;fps&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$2&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;   &lt;span class=&#34;nb&#34;&gt;shift&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            --width&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$2&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;shift&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            --gamma&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;gamma&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$2&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;shift&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            --help&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;-h&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;              &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Usage: gifify [--lossy N] [--fps N] [--width N] [--gamma VAL] &amp;lt;input video&amp;gt; &amp;lt;output.gif&amp;gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;              &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Defaults: --lossy 65  --fps 24  --width 1000  --gamma 1.2&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;              &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;              &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            --&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; shift&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;break&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            --*&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Unknown option: &lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$1&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;2&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            *&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;  &lt;span class=&#34;nb&#34;&gt;break&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;esac&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;((&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$#&lt;/span&gt; &amp;lt; &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;))&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Usage: gifify [--lossy N] [--fps N] [--width N] [--gamma VAL] &amp;lt;input video&amp;gt; &amp;lt;output.gif&amp;gt;&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;local&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$1&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;local&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;out&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$2&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;local&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;tmp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;mktemp -t gifify.XXXXXX&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;.gif&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;trap&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;rm -f &amp;#34;$tmp&amp;#34;&amp;#39;&lt;/span&gt; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;[gifify] FFmpeg: starting encode → &amp;#39;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$in&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#39; → temp GIF (fps=&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;fps&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;, width=&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;)…&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; ! ffmpeg -hide_banner -loglevel error -nostats -y -i &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$in&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;        -filter_complex &lt;span class=&#34;s2&#34;&gt;&amp;#34;fps=&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;fps&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;,scale=iw*sar:ih,scale=&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;:-1,split[a][b];[a]palettegen[p];[b][p]paletteuse=dither=floyd_steinberg&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$tmp&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;[gifify] FFmpeg failed.&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;[gifify] FFmpeg: done. Starting gifsicle (lossy=&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;lossy&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;, gamma=&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;gamma&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;)…&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; ! gifsicle -O3 --gamma&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$gamma&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; --lossy&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$lossy&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$tmp&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; -o &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$out&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;[gifify] gifsicle failed.&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;local&lt;/span&gt; bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;bytes&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;stat -f%z &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$out&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; stat -c%s &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$out&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[[&lt;/span&gt; -n &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$bytes&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nb&#34;&gt;local&lt;/span&gt; mb
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nv&#34;&gt;mb&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;LC_ALL&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;C &lt;span class=&#34;nb&#34;&gt;printf&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;%.2f&amp;#34;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;$((&lt;/span&gt; bytes &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1000000&lt;/span&gt;.0 &lt;span class=&#34;k&#34;&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;[gifify] gifsicle: done. Wrote &amp;#39;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$out&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#39; (&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;mb&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; MB).&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;[gifify] gifsicle: done. Wrote &amp;#39;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$out&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#39;.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will allow you to easily call it as either &lt;code&gt;gifify &amp;lt;input-filename.mp4&amp;gt; &amp;lt;output-gifname.gif&amp;gt;&lt;/code&gt; and default to the values above, or if you want to tweak them you can use any optional parameters with &lt;code&gt;gifify --fps 30 --gamma 1.8 --width 600 --lossy 100 &amp;lt;input-filename.mp4&amp;gt; &amp;lt;output-gifname.gif&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For instance:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Using default values we used above&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;gifify cats.mp4 cats.gif
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Changing the lossiness and gamma&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;gifify --lossy &lt;span class=&#34;m&#34;&gt;30&lt;/span&gt; --gamma 2.2 cats.mp4 cats.gif
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Much easier.&lt;/p&gt;
&lt;p&gt;May your GIFs be beautiful and efficient.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>You should repaste your MacBook (but don&#39;t)</title>
      <link>https://christianselig.com/2025/07/repaste-macbook/</link>
      <pubDate>Thu, 10 Jul 2025 19:06:19 -0300</pubDate>
      
      <guid>https://christianselig.com/2025/07/repaste-macbook/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2025/07/repaste-macbook/hero.jpeg&#34; alt=&#34;Bare Apple Silicon die from MacBook without any thermal paste&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;My favorite memory of my M1 Pro MacBook Pro was the whole sensation of &amp;ldquo;holy crap, you &lt;em&gt;never&lt;/em&gt; hear the fans in this thing&amp;rdquo;, which was very novel in 2021.&lt;/p&gt;
&lt;p&gt;Four years later, this MacBook Pro is still a delight. It&amp;rsquo;s the longest I&amp;rsquo;ve ever owned a laptop, and while I&amp;rsquo;d love to pick up the new M4 goodness, this dang thing still seems to just shrug at basically anything I throw at it. Video editing, code compiling, CAD models, the works. (My desire to update &lt;em&gt;is&lt;/em&gt; helped though by the fact I got the 2TB SSD, 32GB RAM option, and upgrading to those on new MacBooks is still eye wateringly expensive.)&lt;/p&gt;
&lt;p&gt;But my MacBook is starting to show its age in one area: it&amp;rsquo;s not quiet anymore.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re doing anything too intensive like compiling code for awhile, or converting something in Handbrake, the age of the fans being quiet is long past. The fans are properly loud. (And despite having two cats, it&amp;rsquo;s not them! I clean out the fans pretty regularly.)&lt;/p&gt;
&lt;h2 id=&#34;enter-the-thermal-paste&#34;&gt;Enter the thermal paste&lt;/h2&gt;
&lt;p&gt;Everyone online seems to point toward one thing: the thermal paste on computers tends to dry up over the years.&lt;/p&gt;
&lt;p&gt;What the heck is thermal paste? Well, components on your computer that generate a lot of heat are normally made to touch something like a copper heatsink that is really good at &lt;em&gt;pulling&lt;/em&gt; that heat away from it. The issue is, when you press these two metal surfaces against each other, even the best machining isn&amp;rsquo;t perfect and you there&amp;rsquo;s microscopic gaps between them meaning there&amp;rsquo;s just air at those parts, and air is a terrible conductor of heat.&lt;/p&gt;
&lt;p&gt;The solution is to put a little bit of thermal paste (basically a special grey toothpaste gunk that is really good at transferring heat) between them, and it fills in any of those microscopic gaps.&lt;/p&gt;
&lt;p&gt;The problem with this solution is after hundreds and hundreds of days of intense heat, the paste can dry up into something closer to almost a powder, and it&amp;rsquo;s not nearly as good at filling in those gaps.&lt;/p&gt;
&lt;h2 id=&#34;replacement-time&#34;&gt;Replacement time&lt;/h2&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2025/07/repaste-macbook/logic-board.jpeg&#34; alt=&#34;The MacBook&amp;#39;s detatched logic board&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;The logic board!&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;MacBook thermal paste isn&amp;rsquo;t anything crazy (for the most part, see below), custom PC builders use thermal paste all the time so incredibly performant options are available online. I grabbed a tube of Noctua NT-H2 for about $10 and set to taking apart my MacBook to swap out the aging thermal paste. And thankfully, &lt;a href=&#34;https://www.ifixit.com/Guide/MacBook+Pro+14-Inch+2021+Heat+Sink+Replacement/150898&#34;&gt;iFixit has a tremendous, in depth guide&lt;/a&gt; on the disassembly required, so I got to it.&lt;/p&gt;
&lt;p&gt;Indeed, that grey thermal paste looked quite old, but also above and below it (on the RAM chips) I noticed something that didn&amp;rsquo;t quite seem like thermal paste, it was far more… grainy almost?&lt;/p&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2025/07/repaste-macbook/thermal-paste.jpeg&#34; alt=&#34;Existing kinda dry thermal paste on MacBook&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Spottiness is due to half of it being on the heatsink&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;It turns out, ending with my generation of MacBooks (lucky me!) Apple used a very special kind of thermal compound often called &amp;ldquo;&lt;a href=&#34;https://www.ifixit.com/products/tcrs-carbon-black-thermal-compound&#34;&gt;Carbon Black&lt;/a&gt;&amp;rdquo;, which is basically designed to be able to bridge an even thicker gap than traditional thermal paste. I thought about replacing it, but it seems really hard to come across that special thermal compound (and do not do it with normal thermal paste) and my RAM temperatures always seemed fine (65°C is fine… right?) so I just made sure to not touch that.&lt;/p&gt;
&lt;p&gt;For the regular grey thermal paste, I used some cotton swabs and isopropyl alcohol to remove the dried up existing thermal paste, then painted on a bit of the new stuff.&lt;/p&gt;
&lt;h2 id=&#34;disaster&#34;&gt;Disaster&lt;/h2&gt;
&lt;p&gt;To get to the underside of the CPU, you basically need to disassemble the entire MacBook. It&amp;rsquo;s honestly not that hard, but iFixit warned that the fan cables (which also need to be unclipped) are incredibly delicate. And they&amp;rsquo;re not wrong, seriously they have the structural integrity of the half-ply toilet paper available at gas stations.&lt;/p&gt;
&lt;p&gt;So, wouldn&amp;rsquo;t you know it, I moved the left fan&amp;rsquo;s cable a bit too hard and it completely tore in half. Gah.&lt;/p&gt;
&lt;p&gt;I found a replacement fan online (yeah you can&amp;rsquo;t just buy the cable, need a whole new fan) and in the meantime I just kept an eye on my CPU thermals. As long as I wasn&amp;rsquo;t doing anything too intensive it honestly always stayed around 65° which was warm, but not terrifying (MacBook Airs completely lack a fan, after all).&lt;/p&gt;
&lt;h2 id=&#34;take-two&#34;&gt;Take two&lt;/h2&gt;
&lt;p&gt;A few days later, the fans arrived, and I basically had to redo the entire disassembly process to get to the fans. At least I was a lot faster this time.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2025/07/repaste-macbook/fans.jpeg&#34; alt=&#34;Two replacement MacBook fans&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;The fan was incredibly easy to swap out (hats off there, Apple!) and I screwed everything back together and began reconnecting all the little connectors.&lt;/p&gt;
&lt;p&gt;Until I saw it: the tiny (made of the same half ply material as the fan cable) Touch ID sensor cable was inexpicably torn in half, the top half just hanging out. I didn&amp;rsquo;t even touch this thing really, and I hadn&amp;rsquo;t even got to the stage of reconnecting it (I was about to!), it comes from underneath the logic board and I guess just the movement of sliding the logic board back in sheared it in half.&lt;/p&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2025/07/repaste-macbook/rainy.jpeg&#34; alt=&#34;Person casually sitting on a bench in a flood&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;me&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Bah. I looked up if I could just grab another replacement cable here, and sure enough you can… but the Touch ID chip is cryptographically paired to your MacBook so you&amp;rsquo;d have to take it into an Apple Store. Estimates seemed to be in the hundreds of dollars, so if anyone has any experience there let me know, but for now I&amp;rsquo;m just going to live happily without a Touch ID sensor… or the button because the button also does not work.&lt;/p&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2025/07/repaste-macbook/touch-id.jpeg&#34; alt=&#34;Torn off Touch ID sensor sitting beside a Sharpie pen for scale&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;RIP little buddy&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;(And yeah I&amp;rsquo;m 99.9% sure I can&amp;rsquo;t solder this back together, there&amp;rsquo;s a bunch of tiny lanes that make up the cable that you would need experience with proper micro-soldering to do.)&lt;/p&gt;
&lt;p&gt;Honestly, the disassembly process for my MacBook was surprisingly friendly and not very difficult, I just really wish they beefed up some of the cables even slightly so they weren&amp;rsquo;t &lt;em&gt;so&lt;/em&gt; delicate.&lt;/p&gt;
&lt;h2 id=&#34;the-results&#34;&gt;The results&lt;/h2&gt;
&lt;p&gt;I was going to cackle if I went through all that just to have identical temperatures as before, but I&amp;rsquo;m very happy to say they actually improved a fair bit. I ran a Cinebench test before disassembling the MacBook the very first time to establish a baseline:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Max CPU temperature&lt;/strong&gt;: 102°C&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Max fan speed&lt;/strong&gt;: 6,300 RPM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cinbench score&lt;/strong&gt;: 12,252&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After the new thermal paste (and the left fan being new):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Max CPU temperature&lt;/strong&gt;: 96°C&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Max fan speed&lt;/strong&gt;: 4,700 RPM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cinbench score&lt;/strong&gt;: 12,316&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now just looking at those scores you might be like… so? But let me tell you, dropping 1,600 RPM on the fan is a noticeable change, it goes from &amp;ldquo;Oh my god this is annoyingly loud&amp;rdquo; to &amp;ldquo;Oh look the fans kicked in&amp;rdquo;, and despite slower fan speeds there was still a decent drop in CPU temperature! And a 0.5% higher Cinebench score!&lt;/p&gt;
&lt;p&gt;But where I also really notice it is in idling: just writing this blog post my CPU was right at 46°C the whole time, where previously my computer idled right aroud 60°C. The whole computer just feels a bit &lt;em&gt;healthier&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&#34;so-should-you-do-it&#34;&gt;So… should you do it?&lt;/h2&gt;
&lt;p&gt;Honestly, unless you&amp;rsquo;re very used to working on small, delicate electronics, probably not. But if you do have that experience and are very careful, or have a local repair shop that can do it for a reasonable fee (and your MacBook is a few years old so as to warrant it) it&amp;rsquo;s honestly a really nice tweak that I feel will hopefully at least get me to the M5 generation.&lt;/p&gt;
&lt;p&gt;I do miss Touch ID, though.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>A slept on upscaling tool for macOS</title>
      <link>https://christianselig.com/2025/06/awesome-upscaling-tool/</link>
      <pubDate>Mon, 30 Jun 2025 11:48:17 -0300</pubDate>
      
      <guid>https://christianselig.com/2025/06/awesome-upscaling-tool/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2025/06/awesome-upscaling-tool/hero.jpeg&#34; alt=&#34;A PC case sitting on a desk, and the left half of the image is very pixelated with the right half being very sharp&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;I upload YouTube videos &lt;a href=&#34;https://www.youtube.com/@cselig&#34;&gt;from time to time&lt;/a&gt;, and a fun comment I often get is &amp;ldquo;Whoa, this is 8K!&amp;rdquo;. Even better, I&amp;rsquo;ve had comments from the like, seven people with 8K TVs that the video looks awesome on their TV.&lt;/p&gt;
&lt;p&gt;And you guessed it, I don&amp;rsquo;t record my videos in 8K! I record them in 4K and upscale them to 8K after the fact.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2025/06/awesome-upscaling-tool/badge.jpeg&#34; alt=&#34;Screenshot of my YouTube video that has the 8K metadata badge underneath&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;There&amp;rsquo;s no shortage of AI video upscaling tools today, but they&amp;rsquo;re of varying quality, and some are great but &lt;a href=&#34;https://www.topazlabs.com/experience/video-1-1&#34;&gt;quite expensive&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The legendary Finn Voorhees created a really cool tool though, called &lt;a href=&#34;https://github.com/finnvoor/fx-upscale&#34;&gt;fx-upscale&lt;/a&gt;, that smartly leverages Apple&amp;rsquo;s built-in &lt;a href=&#34;https://developer.apple.com/documentation/metalfx/&#34;&gt;MetalFX framework&lt;/a&gt;. For the unfamiliar, this library is an extension of Apple&amp;rsquo;s Metal graphics library, and adds functionality similar to NVIDIA&amp;rsquo;s DLSS where it intelligently upscales video using machine learning (AI), so rather than just stretching an image, it uses a model to try to infer what the frame would look like at a higher resolution.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s primarily geared toward video game use, but Finn&amp;rsquo;s library shows it does an excellent job for video too.&lt;/p&gt;
&lt;p&gt;I think this is a really killer utility, and use it for all my videos. I even have a license for Topaz Video AI, which arguably works better, but takes an order of magnitude longer. For instance my recent 38 minute, 4K video took about an hour to render to 8K via &lt;code&gt;fx-upscale&lt;/code&gt; on my M1 Pro MacBook Pro, but would take over 24 hours with Topaz Video AI.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Install with homebrew&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;brew install finnvoor/tools/fx-upscale
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Outputs a file named my-video Upscaled.mov&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fx-upscale my-video.mov --width &lt;span class=&#34;m&#34;&gt;7680&lt;/span&gt; --codec h265
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anyway, just wanted to give a tip toward a really cool tool! Finn&amp;rsquo;s even got a version &lt;a href=&#34;https://apps.apple.com/ca/app/unsqueeze/id6475134617&#34;&gt;in the Mac App Store called Unsqueeze&lt;/a&gt; with an actual GUI that&amp;rsquo;s even easier to use, but I really like the command line version because you get a bit more control over the output.&lt;/p&gt;
&lt;p&gt;8K is kinda overkill for most use cases, so to be clear you can go from like, 1080p to 4K as well if you&amp;rsquo;re so inclined. I just really like 8K for the future proofing of it all, in however many years when 8K TVs are more common I&amp;rsquo;ll be able to have some of my videos already able to take advantage of that. And it takes long enough to upscale that I&amp;rsquo;d be surprised to see TVs or YouTube offering that upscaling natively in a way that looks as good given the amount of compute required currently.&lt;/p&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2025/06/awesome-upscaling-tool/comparison.gif&#34; alt=&#34;A very zoomed in GIF showing a stark comparison between the sharpness of 4K versus 8K&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Obviously very zoomed in to show the difference easier&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;If you ask me, for indie creators, even when 8K displays are more common, the future of recording still probably won&amp;rsquo;t be in native 8K. 4K recording gives so much detail still that have more than enough details to allow AI to do a compelling upscale to 8K. I think for my next camera I&amp;rsquo;m going to aim for recording in 6K (so I can still reframe in post), and then continue to output the final result in 4K to be AI upscaled. I&amp;rsquo;m coming for you, &lt;a href=&#34;https://shop.panasonic.com/products/lumix-s1ii-full-frame-camera-partially-stacked-sensor-dc-s1m2?srsltid=AfmBOorctIOPRkIMr4fjvBRv1_C_Wa3S21-rVsKthGra2GeSLbDoAanh&#34;&gt;Lumix S1ii&lt;/a&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Embedding Godot games in iOS apps is easy now</title>
      <link>https://christianselig.com/2025/05/godot-ios-interop/</link>
      <pubDate>Sat, 31 May 2025 09:54:14 -0300</pubDate>
      
      <guid>https://christianselig.com/2025/05/godot-ios-interop/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2025/05/godot-ios-interop/hero.jpg&#34; alt=&#34;The Swift and Godot logos side by side over top a blurred image of Hyrule Field from Ocarina of Time&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Recently there&amp;rsquo;s been very exciting developments in the Godot game engine, that have allowed really easy and powerful integration into an existing normal iOS or Mac app. I couldn&amp;rsquo;t find a lot of documentation or discussion about this, so I wanted to shine some light on why this is so cool, and how easy it is to do!&lt;/p&gt;
&lt;h2 id=&#34;whats-godot&#34;&gt;What&amp;rsquo;s Godot?&lt;/h2&gt;
&lt;p&gt;For the uninitiated, Godot is an engine for building games, other common ones you might know of are Unity and Unreal Engine.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s risen in popularity &lt;em&gt;a lot&lt;/em&gt; over the last couple years due to its open nature: it&amp;rsquo;s completely open source, MIT licensed, and worked on in the open. But beyond that, it&amp;rsquo;s also a really well made tool for building games with (both 2D and 3D), with a great UI, beautiful iconography, a ton of tutorials and resources, and as a bonus, it&amp;rsquo;s very lightweight.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve had a lot of fun playing around with it (considering potentially integrating it into &lt;a href=&#34;https://pixelpals.app&#34;&gt;Pixel Pals&lt;/a&gt;), and while Unity and Unreal Engine are also phenomenal tools, Godot has felt lightweight and approachable in a really nice way. As an analogy, Godot feels closer to Sketch and Figma whereas Unity and Unreal feel more like Photoshop/Illustrator or other kinda bulky Adobe products.&lt;/p&gt;
&lt;p&gt;Even Apple &lt;a href=&#34;https://www.macrumors.com/2025/04/23/apple-visionos-godot-game-engine/&#34;&gt;has taken interest in it&lt;/a&gt;, contributing a substantial pull request for visionOS support in Godot.&lt;/p&gt;
&lt;h2 id=&#34;why-use-it-with-ios&#34;&gt;Why use it with iOS?&lt;/h2&gt;
&lt;p&gt;You&amp;rsquo;ve always been able to build a game in Godot and export it to run on iOS, but recently &lt;a href=&#34;https://docs.godotengine.org/en/stable/tutorials/scripting/gdextension/index.html&#34;&gt;thanks to advancements in the engine&lt;/a&gt; and &lt;a href=&#34;https://github.com/migueldeicaza/SwiftGodotKit&#34;&gt;work by amazing folks like Miguel de Icaza&lt;/a&gt;, you can now embed a Godot game in an existing normal SwiftUI or UIKit app just as you would an extra &lt;code&gt;UITextView&lt;/code&gt; or &lt;code&gt;ScrollView&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Why is this important? Say you want to build a game or experience, but you don&amp;rsquo;t want it to feel just like another port, you want it to integrate nicely with iOS and feel at home there through use of some native frameworks and UI here and there to anchor the experience (share sheets, local notifications, a simple SwiftUI sheet for adding a friend, etc.). Historically your options have been very limited or difficult.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You no longer have to have &amp;ldquo;a Godot game&amp;rdquo; or &amp;ldquo;an iOS app&amp;rdquo;, you can have the best of both worlds&lt;/strong&gt;. A fun game built entirely in Godot, while having your share sheets, Settings screens, your paywall, home screen widgets, onboarding, iCloud sync, etc. all in native Swift code. Dynamically choosing which tool you want for the job.&lt;/p&gt;
&lt;p&gt;(Again, this was technically possible before and with other engines, but was much, much more complicated. Unity&amp;rsquo;s in particular seems to have been &lt;a href=&#34;https://docs.unity3d.com/6000.1/Documentation/Manual/UnityasaLibrary-iOS.html&#34;&gt;last updated during the first Obama administration&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;And truly, this doesn&amp;rsquo;t only benefit &amp;ldquo;game apps&amp;rdquo;. Heck, if the user is doing something that will take awhile to complete (uploading a video, etc.) you could give them a small game to play in the interim. Or just for some fun you could embed a little side scroller easter egg in one of your Settings screens to delight spelunking users. Be creative!&lt;/p&gt;
&lt;h2 id=&#34;spritekit&#34;&gt;SpriteKit?&lt;/h2&gt;
&lt;p&gt;A quick aside. It wouldn&amp;rsquo;t be an article about game dev on iOS without mentioning SpriteKit, &lt;a href=&#34;https://developer.apple.com/documentation/spritekit/&#34;&gt;Apple&amp;rsquo;s native 2D game framework&lt;/a&gt; (Apple also has &lt;a href=&#34;https://developer.apple.com/documentation/scenekit/&#34;&gt;SceneKit&lt;/a&gt; for 3D).&lt;/p&gt;
&lt;p&gt;SpriteKit is well done, and actually what I built most of Pixel Pals in. But it has a lot of downsides versus a proper, dedicated game engine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Godot has a wealth of tutorials on YouTube and elsewhere, bustling Discord communities for help, where SpriteKit being a lot more niche can be quite hard to find details on&lt;/li&gt;
&lt;li&gt;The obvious one: SpriteKit only works on Apple platforms, so if you want to port your game to Android or Windows you&amp;rsquo;re probably not going to have a great time, where Godot is fully cross platform&lt;/li&gt;
&lt;li&gt;Godot being a full out game engine has a lot more tools for game development than can be handy, from animation tools, to sprite sheet editors, controls that make experimenting a lot easier, handy tools for creating shaders, and so much more than I could hope to go over in this article. If you ever watch a YouTube video of someone building a game in a full engine, the wealth of tools they have for speeding up development is bonkers.&lt;/li&gt;
&lt;li&gt;Godot is updated frequently by a large team of employees and volunteers, SpriteKit conversely isn&amp;rsquo;t exactly one of Apple&amp;rsquo;s most loved frameworks (I don&amp;rsquo;t think it&amp;rsquo;s been mentioned at WWDC in years) and kinda feels like something Apple isn&amp;rsquo;t interested in putting much more work into. Maybe that&amp;rsquo;s because it does everything Apple wants and is considered &amp;ldquo;finished&amp;rdquo; (if so I think that would be incorrect, see previous point for many things that it would be helpful for SpriteKit to have), but if you were to encounter a weird bug I&amp;rsquo;d feel much better about the likelihood of it getting fixed in Godot than SpriteKit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;m a big fan of using the right tool for the job. For iOS apps, most of the time that&amp;rsquo;s building something incredible in SwiftUI and UIKit. But for building a game, be it small or large, using something purpose built to be &lt;em&gt;incredible&lt;/em&gt; at that seems like the play to me, and Godot feels like a great candidate there.&lt;/p&gt;
&lt;h2 id=&#34;setup&#34;&gt;Setup&lt;/h2&gt;
&lt;p&gt;Simply add the &lt;a href=&#34;https://github.com/migueldeicaza/SwiftGodotKit&#34;&gt;SwiftGodotKit package&lt;/a&gt; to your Xcode project by selecting your project in the sidebar, ensuring your project is selected in the new sidebar, selecting the Package Dependencies tab, click the +, then paste the GitHub link.&lt;/p&gt;
&lt;p&gt;After adding it, you will also need to select the target that you added it to in the sidebar, select the Build Settings tab, then select &amp;ldquo;Other Linker Flags&amp;rdquo; and add &lt;code&gt;-lc++&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Lastly, with that same target, under the General tab add &lt;code&gt;MetalFX.framework&lt;/code&gt; to Frameworks, Libraries, and Embedded Content. (Yeah you got me, I don&amp;rsquo;t know why we have to do that.)&lt;/p&gt;
&lt;p&gt;After that, you should be able to &lt;code&gt;import SwiftGodotKit&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;usage&#34;&gt;Usage&lt;/h2&gt;
&lt;p&gt;Now we&amp;rsquo;re ready to use Godot in our iOS app! What excites me most and I want to focus on is &lt;strong&gt;embedding an existing Godot game in your iOS app and communicating back and forth with it from your iOS app&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This way, you can do the majority of the game development in Godot without even opening Xcode, and then sprinkle in delightful iOS integration by communicating between iOS and Godot where needed.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2025/05/godot-ios-interop/godot-editor.jpg&#34; alt=&#34;Screenshot of the Godot editor with the Cassette Beats game visible inside&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;To start, we&amp;rsquo;ll build a very simple game called IceCreamParlor, where we select from a list of ice cream options in SwiftUI, which then gets passed into Godot. Godot will have a button the user can tap to send a message back to SwiftUI with the total amount of ice cream. This will not be an impressive &amp;ldquo;game&amp;rdquo; by any stretch of the imagination, but should be easy to set up and understand the concepts so you can apply it to an actual game.&lt;/p&gt;
&lt;p&gt;To accomplish our communication, in essence we&amp;rsquo;ll be recreating iOS&amp;rsquo; &lt;code&gt;NotificationCenter&lt;/code&gt; to send messages back and forth between Godot and iOS, and like &lt;code&gt;NotificationCenter&lt;/code&gt;, we&amp;rsquo;ll create a simple singleton to accomplish this.&lt;/p&gt;
&lt;p&gt;Those messages will be sent via &lt;strong&gt;&lt;a href=&#34;https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html&#34;&gt;Signals&lt;/a&gt;&lt;/strong&gt;. This is Godot&amp;rsquo;s system for, well, &lt;em&gt;signaling&lt;/em&gt; an event occurred, and can be used to signify everything from a button press, to a player taking damage, to a timer ending. Keeping with the &lt;code&gt;NotificationCenter&lt;/code&gt; analogy, this would the be &lt;code&gt;Notification&lt;/code&gt; that gets posted (except in Godot, it&amp;rsquo;s used for everything, where in iOS land you really wouldn&amp;rsquo;t use &lt;code&gt;NotificationCenter&lt;/code&gt; for a button press.)&lt;/p&gt;
&lt;p&gt;And similar to &lt;code&gt;Notification&lt;/code&gt; that has a &lt;code&gt;userInfo&lt;/code&gt; field to provide more information about the notification, Godot signals can also take an argument that provides more information. (For example if the notification was &amp;ldquo;player took damage&amp;rdquo; the argument might be an integer that includes how much damage they took.) Like &lt;code&gt;userInfo&lt;/code&gt;, this is optional however and you can also fire off a signal with &lt;em&gt;no&lt;/em&gt; further information, something like &amp;ldquo;userUnlockedPro&amp;rdquo; for when they activate Pro after your SwiftUI paywall.&lt;/p&gt;
&lt;p&gt;For our simple example, we&amp;rsquo;re going to send a &amp;ldquo;selectedIceCream&amp;rdquo; signal from iOS to Godot, and a &amp;ldquo;updatedIceCreamCount&amp;rdquo; signal from Godot to iOS. The former will have a string argument for which ice cream was selected, and the latter will have an integer argument with the updated count.&lt;/p&gt;
&lt;h3 id=&#34;setting-up-our-godot-project&#34;&gt;Setting up our Godot project&lt;/h3&gt;
&lt;p&gt;Open Godot.app (available to download from &lt;a href=&#34;http://godotengine.org&#34;&gt;their website&lt;/a&gt;) and create a new project, I&amp;rsquo;ll type in IceCreamParlor, choose the Mobile renderer, then click Create.&lt;/p&gt;
&lt;p&gt;Godot defaults to a 3D scene, so I&amp;rsquo;ll switch to 2D at the top, and then in the left sidebar click 2D Scene to create that as our root node. I&amp;rsquo;ll right-click the sidebar to add a child node, and select &lt;code&gt;Label&lt;/code&gt;. We&amp;rsquo;ll set the text to the &amp;ldquo;Ice cream:&amp;rdquo;. In the right sidebar, we&amp;rsquo;ll go to Theme Overrides and increase the font size to 80 to make it more visible, and we&amp;rsquo;ll also rename it in the left sidebar from &lt;code&gt;Label&lt;/code&gt; to &lt;code&gt;IceCreamLabel&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll also do the same to add a &lt;code&gt;Button&lt;/code&gt; to the scene, which we&amp;rsquo;ll call &lt;code&gt;UpdateButton&lt;/code&gt; and sets its text to &amp;ldquo;Update Ice Cream Count&amp;rdquo;.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2025/05/godot-ios-interop/ice-cream-canvas.png&#34; alt=&#34;The Godot canvas with the aforementioned label and button added&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;If you click the Play button in the top right corner of Godot, it will run and you can click the button, but as of now it doesn&amp;rsquo;t do anything.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll select our root node (&lt;code&gt;Node2D&lt;/code&gt;) in the sidebar, right click, and select &amp;ldquo;Attach Script&amp;rdquo;. Leave everything as default, and click Create. This will now present us with an area where we can actually code in GDScript, and we can refer to the objects in our scene by prefixing their name with a dollar sign.&lt;/p&gt;
&lt;p&gt;Inside our script, we&amp;rsquo;ll implement the &lt;code&gt;_ready&lt;/code&gt; function, which is essentially Godot&amp;rsquo;s equivalent of &lt;code&gt;viewDidLoad&lt;/code&gt;, and inside we&amp;rsquo;ll connect to our simple signal we discussed earlier. We&amp;rsquo;ll do this by grabbing a reference to our singleton, then reference the signal we want, then connect to it by passing a function we want to be called when the signal is received. And of course the function takes a &lt;code&gt;String&lt;/code&gt; as a parameter because our signal includes what ice cream was selected.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-gdscript&#34; data-lang=&#34;gdscript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;extends&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Node2D&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ice_cream&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Array&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;_ready&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;singleton&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Engine&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;get_singleton&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;GodotSwiftMessenger&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;singleton&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ice_cream_selected&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;connect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;_on_ice_cream_selected_signal_received&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;_on_ice_cream_selected_signal_received&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;new_ice_cream&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# We received a signal! Probably should do something…&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that we haven&amp;rsquo;t actually &lt;em&gt;created&lt;/em&gt; the singleton yet, but we will shortly. Also note that normally in Godot, you have to declare custom signals like the ones we&amp;rsquo;re using, but we&amp;rsquo;re going to declare them in Swift. As long as they&amp;rsquo;re declared somewhere, Godot is happy!&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s also hook up our button by going back to our scene, selecting our button in the canvas, selecting the &amp;ldquo;Node&amp;rdquo; tab in the right sidebar, and double-clicking the &lt;code&gt;pressed()&lt;/code&gt; option. We can then select that same Node2D script and name the function &lt;code&gt;_on_update_button_pressed&lt;/code&gt; to add a function that executes when the button is pressed (fun fact: the button being pressed event is also powered by signals).&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-gdscript&#34; data-lang=&#34;gdscript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;_on_update_button_pressed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;setting-up-our-iosswift-project&#34;&gt;Setting up our iOS/Swift project&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s jump over to Xcode and create a new SwiftUI project there as well, also calling it IceCreamParlor. We&amp;rsquo;ll start by adding the Swift package for SwiftGodotKit to Swift Package Manager, and as mentioned above we&amp;rsquo;ll add &lt;code&gt;-lc++&lt;/code&gt; to our &amp;ldquo;Other Linker Flags&amp;rdquo; under &amp;ldquo;Build Settings&amp;rdquo;, add &lt;code&gt;MetalFX&lt;/code&gt;, then go to &lt;code&gt;ContentView.swift&lt;/code&gt; and add &lt;code&gt;import SwiftGodotKit&lt;/code&gt; at the top.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2025/05/godot-ios-interop/package-manager.png&#34; alt=&#34;Adding SwiftGodotKit to Xcode via the Swift Package Manager&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;From here, let&amp;rsquo;s create a simple SwiftUI view so we can choose from some ice cream options.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;some&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;View&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;HStack&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;Button&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;label&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Chocolate&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;Button&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;label&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Strawberry&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;Button&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;label&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Vanilla&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;buttonStyle&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bordered&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We&amp;rsquo;ll also create a new file in Xcode called &lt;code&gt;GodotSwiftMessenger.swift&lt;/code&gt;. This will be where we implement our singleton that is akin to &lt;code&gt;NotificationCenter&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SwiftGodot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Godot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;GodotSwiftMessenger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Object&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;shared&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GodotSwiftMessenger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Signal&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;iceCreamSelected&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SignalWithArguments&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Signal&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;iceCreamCountUpdated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SignalWithArguments&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We first import SwiftGodot (minus the Kit), essentially because this part is purely about interfacing with Godot through Godot, and doesn&amp;rsquo;t care about whether or not it&amp;rsquo;s embedded in an iOS app. For more details on SwiftGodot see its section below.&lt;/p&gt;
&lt;p&gt;Then, we annotate our class with the &lt;code&gt;@Godot&lt;/code&gt; Swift Macro, which basically just says &amp;ldquo;Hey make Godot aware that this class exists&amp;rdquo;. The class is a subclass of &lt;code&gt;Object&lt;/code&gt; as everything in Godot needs to inherit from &lt;code&gt;Object&lt;/code&gt;, it&amp;rsquo;s essentially the parent class of everything.&lt;/p&gt;
&lt;p&gt;Following that is your bog standard Swift singleton initialization.&lt;/p&gt;
&lt;p&gt;Then, with another Swift Macro, we annotate a variable we want to be our signal which signifies that it&amp;rsquo;s a Signal to Godot. You can either specify its type as &lt;code&gt;Signal&lt;/code&gt; or &lt;code&gt;SignalWithArguments&amp;lt;T&amp;gt;&lt;/code&gt; depending on whether or not the specific signal also sends any data alongside it. We&amp;rsquo;ll use that &amp;ldquo;somethingHappened&amp;rdquo; signal we mentioned early, which includes a string for more details on &lt;em&gt;what&lt;/em&gt; happened.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note that we used &amp;ldquo;ice_cream_selected&amp;rdquo; in Godot but &amp;ldquo;iceCreamSelected&amp;rdquo; in Swift, this is because the underscore convention is used in Godot, and SwiftGodotKit will automatically map the camelCase Swift convention to it.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now we need to tell Godot about this singleton we just made. We want Godot to know about it &lt;em&gt;as soon as possible&lt;/em&gt;, otherwise if things aren&amp;rsquo;t hooked up, Godot might emit a signal that we wouldn&amp;rsquo;t receive in Swift, or vice-versa.&lt;/p&gt;
&lt;p&gt;So, we&amp;rsquo;ll hook it up very early in our app cycle. In SwiftUI, you might do this in the init of your main App struct as I&amp;rsquo;ll show below, and in UIKit in &lt;code&gt;applicationDidFinishLaunching&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;IceCreamParlor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;App&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;init&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;initHookCb&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;level&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;level&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scene&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;register&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GodotSwiftMessenger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;Engine&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;registerSingleton&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;GodotSwiftMessenger&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;instance&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GodotSwiftMessenger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;shared&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;some&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Scene&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;WindowGroup&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;ContentView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In addition to the boilerplate code Xcode gives us, we&amp;rsquo;ve added an extra step to the initializer, where we set a callback on &lt;code&gt;initHookCb&lt;/code&gt;. This is just a callback that fires as Godot is setup, and it specifies what &lt;code&gt;level&lt;/code&gt; of setup has occurred. We want to wait until the &lt;code&gt;level&lt;/code&gt; setup is reached, which means the game is ready to go (you could set it up at an even earlier level if you see that as beneficial).&lt;/p&gt;
&lt;p&gt;Then, we just tell Godot about this type by calling &lt;code&gt;register&lt;/code&gt;, and then we register the singleton itself with a name we want it to be accessible under.&lt;/p&gt;
&lt;p&gt;Again, we want to do this early, as if Godot was already setup in our app, &lt;em&gt;and then&lt;/em&gt; we set &lt;code&gt;initHookCb&lt;/code&gt;, its contents would never fire and thus we wouldn&amp;rsquo;t register anything. But don&amp;rsquo;t worry, this hook won&amp;rsquo;t fire until we first initialize our Godot game in iOS ourself, so as long as this code is called before then, we&amp;rsquo;re golden.&lt;/p&gt;
&lt;p&gt;Lastly, everything is registered in iOS land, but there&amp;rsquo;s still nothing that emits or receives signals. Let&amp;rsquo;s change that by going to &lt;code&gt;ContentView.swift&lt;/code&gt;, and change our &lt;code&gt;body&lt;/code&gt; to the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SwiftUI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SwiftGodotKit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SwiftGodot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;ContentView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;View&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;State&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;totalIceCream&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;State&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;godotApp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GodotApp&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GodotApp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;packFile&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;main.pck&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;some&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;View&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;VStack&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;GodotAppView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;environment&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;\&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;godotApp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;godotApp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Total Ice Cream: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalIceCream&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;HStack&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;Button&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;GodotSwiftMessenger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;shared&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;iceCreamSelected&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;emit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;chocolate&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;label&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Chocolate&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;Button&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;GodotSwiftMessenger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;shared&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;iceCreamSelected&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;emit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;strawberry&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;label&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Strawberry&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;Button&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;GodotSwiftMessenger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;shared&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;iceCreamSelected&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;emit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;vanilla&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;label&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Vanilla&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;buttonStyle&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bordered&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;onAppear&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;GodotSwiftMessenger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;shared&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;iceCreamCountUpdated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;connect&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;newTotalIceCream&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;totalIceCream&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;newTotalIceCream&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s quite a bit going on here, but let&amp;rsquo;s break it down because it&amp;rsquo;s really quite simple.&lt;/p&gt;
&lt;p&gt;We have two new state variables, the first is to keep track of the new ice cream count. Could we just do this ourselves purely in SwiftUI? Totally, but for fun we&amp;rsquo;re going to be totally relying on Godot to keep us updated there, and we&amp;rsquo;ll just reflect that in SwiftUI to show the communication. Secondly and more importantly, we need to declare a variable for our actual game file so we can embed it.&lt;/p&gt;
&lt;p&gt;We do this embedding at the top of the &lt;code&gt;VStack&lt;/code&gt; by creating a &lt;code&gt;GodotAppView&lt;/code&gt;, a handy SwiftUI view we can now leverage, and we do so by just setting its environment variable to the game we just declared.&lt;/p&gt;
&lt;p&gt;Then, we change our buttons to actually emit the selections via signals, and when the view appears, we make sure we connect to the signal that keeps us updated on the count so we can reflect that in the UI. Note that we &lt;em&gt;don&amp;rsquo;t&lt;/em&gt; also connect to the &lt;code&gt;iceCreamSelected&lt;/code&gt; signal, because we don&amp;rsquo;t care to receive it in SwiftUI, we&amp;rsquo;re just firing that one off for Godot to handle.&lt;/p&gt;
&lt;h3 id=&#34;communicating&#34;&gt;Communicating&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s update our gdscript in Godot to take advantage of these changes.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-gdscript&#34; data-lang=&#34;gdscript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;_on_ice_cream_selected_signal_received&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;new_ice_cream&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;ice_cream&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;new_ice_cream&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;$IceCreamLabel&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Ice creams: &amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;, &amp;#34;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;join&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ice_cream&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;_on_update_button_pressed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;singleton&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Engine&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;get_singleton&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;GodotSwiftMessenger&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;singleton&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ice_cream_count_updated&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;emit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ice_cream&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Not too bad! We now receive the signal from SwiftUI and update our UI and internal state in Godot accordingly, as well as the UI by making our ice cream into a comma separated list. And then when the user taps the update button, we then send (emit) that signal back to SwiftUI with the updated count.&lt;/p&gt;
&lt;h3 id=&#34;running&#34;&gt;Running&lt;/h3&gt;
&lt;p&gt;To actually see this live, first make sure you have &lt;strong&gt;an actual iOS device plugged in&lt;/strong&gt;. Unfortunately Godot doesn&amp;rsquo;t work with the iOS simulator.&lt;/p&gt;
&lt;p&gt;Secondly, in Godot, select the Project menu bar item, then Export, then click the Add button and select &amp;ldquo;iOS&amp;rdquo;. This will bring you to a screen with a bunch of options, but my understanding is that this is 99% geared toward if you&amp;rsquo;re building your app &lt;strong&gt;entirely&lt;/strong&gt; in Godot (which we are not): you can plug in all the things you&amp;rsquo;d otherwise plug into Xcode &lt;em&gt;here&lt;/em&gt; instead, and Godot will handle them for you. That doesn&amp;rsquo;t apply to us, we&amp;rsquo;re going to do all that normally in Xcode anyway, we just want the juicy game files, so ignore all that and select &amp;ldquo;Export PCK/ZIP…&amp;rdquo; at the bottom. It&amp;rsquo;ll ask you where you want to save it, and I just keep it in the Godot project directory, make sure &amp;ldquo;Godot Project Pack (*.pck)&amp;rdquo; is selected in the dropdown, and then save it as &lt;code&gt;main.pck&lt;/code&gt;.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2025/05/godot-ios-interop/export.png&#34; alt=&#34;Godot&amp;#39;s export screen with iOS preselected&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;That&amp;rsquo;s our &amp;ldquo;game&amp;rdquo; bundled up, as meager as it is!&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll then drop that into Xcode, making sure to add it to our target, then we can run it on the device!&lt;/p&gt;
&lt;p&gt;Here we&amp;rsquo;ll see choosing the ice cream flavor at the bottom in SwiftUI beams it into the Godot game that&amp;rsquo;s just chilling like a SwiftUI view, and then we can tap the update button in Godot land to beam the new count right back to SwiftUI to be displayed.&lt;/p&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2025/05/godot-ios-interop/game-on-phone.png&#34; alt=&#34;Screenshot of the simplistic game running on an iPhone with the Godot game on top, and the SwiftUI buttons at the bottom.&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Not exactly a AAA game but enough to show the basics of communication 😄&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Look at you go! Take this as a leaping off point for all the cool SwiftUI and Godot interoperability that you can accomplish, be it tappings a Settings icon in Godot to bring up a beautifully designed, native SwiftUI settings screen, or confirmation to you your game when the user updated to the Pro version of your game through your SwiftUI paywall.&lt;/p&gt;
&lt;h2 id=&#34;bonus-swiftgodot-minus-the-kit&#34;&gt;Bonus: SwiftGodot (minus the &amp;ldquo;Kit&amp;rdquo;)&lt;/h2&gt;
&lt;p&gt;An additional fun option (that sits at the heart of SwiftGodotKit) is &lt;a href=&#34;https://github.com/migueldeicaza/SwiftGodot&#34;&gt;SwiftGodot&lt;/a&gt;, which allows you to actually build your entire Godot game with Swift as the programming language if you so choose. Swift for iOS apps, Swift on the server, Swift for game dev. Swift truly is everywhere.&lt;/p&gt;
&lt;p&gt;For me, I&amp;rsquo;m liking playing around in GDScript, which is Godot&amp;rsquo;s native programming language, but it&amp;rsquo;s a really cool option to know about.&lt;/p&gt;
&lt;h2 id=&#34;embed-size&#34;&gt;Embed size&lt;/h2&gt;
&lt;p&gt;A fear might be that embedding Godot into your app might bloat the binary and result in an enormous app download size. Godot is very lightweight, adding it to your codebase adds a relatively meager (at least by 2025 standards) 30MB to your binary size. That&amp;rsquo;s a lot larger than SpriteKit&amp;rsquo;s 0MB, but for all the benefits Godot offers that&amp;rsquo;s a pretty compelling trade.&lt;/p&gt;
&lt;p&gt;(30MB was measured by handy blog sponsor, &lt;a href=&#34;https://www.emerge.tools&#34;&gt;Emerge Tools&lt;/a&gt;.)&lt;/p&gt;
&lt;h2 id=&#34;tips&#34;&gt;Tips&lt;/h2&gt;
&lt;h3 id=&#34;logging&#34;&gt;Logging&lt;/h3&gt;
&lt;p&gt;If you log something in Godot/GDScript via &lt;code&gt;print(&amp;quot;something&amp;quot;)&lt;/code&gt; that will also print to the Xcode console, handy!&lt;/p&gt;
&lt;h3 id=&#34;quickly-embedding-the-pck-into-ios&#34;&gt;Quickly embedding the pck into iOS&lt;/h3&gt;
&lt;p&gt;Exporting the pck file from Godot to Xcode is quite a few clicks, so if you&amp;rsquo;re doing it a lot it would be nice to speed that up. We can use the command line to make this a lot nicer.&lt;/p&gt;
&lt;p&gt;Godot.app also has a headless mode you can use by going inside the .app file, then Contents &amp;gt; MacOS &amp;gt; Godot. But typing the full path to that binary is no fun, so let&amp;rsquo;s symlink the binary to &lt;code&gt;/usr/local/bin&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo ln -s &lt;span class=&#34;s2&#34;&gt;&amp;#34;/Applications/Godot.app/Contents/MacOS/Godot&amp;#34;&lt;/span&gt; /usr/local/bin/godot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we can simply type &lt;code&gt;godot&lt;/code&gt; anywhere in the Terminal to either open the Godot app, or we can use &lt;code&gt;godot --headless&lt;/code&gt; for some command line goodness.&lt;/p&gt;
&lt;p&gt;My favorite way to do this, is to do something like the following within your Godot project directory:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;godot --headless --export-pack &lt;span class=&#34;s2&#34;&gt;&amp;#34;iOS&amp;#34;&lt;/span&gt; /path/to/xcodeproject/target/main.pck
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will handily export the pck and add it to our Xcode project, overwriting any existing pck file, from which point we can simply compile our iOS app.&lt;/p&gt;
&lt;h2 id=&#34;wrapping-it-up&#34;&gt;Wrapping it up&lt;/h2&gt;
&lt;p&gt;I really think Godot&amp;rsquo;s new interoperability with iOS is an incredibly exciting avenue for building games on iOS, be it a full fledged game or a small little easter egg integrated into an existing iOS app, and hats off to all the folks who did the hard work getting it working.&lt;/p&gt;
&lt;p&gt;Hopefully this serves as an easy way to get things up and running! It might seem like a lot at first glance, but most of the code shown above is just boilerplate to get an example Godot and iOS project up and running, the actual work to embed a game and communicate across them is so delightfully simple!&lt;/p&gt;
&lt;p&gt;(Also big shout out to Chris Backas and Miguel de Icaza for help getting this tutorial off the ground.)&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Curing Mac mini M4 fomo with 3D printing</title>
      <link>https://christianselig.com/2024/11/mac-mini-m4-prints/</link>
      <pubDate>Tue, 12 Nov 2024 18:12:32 -0400</pubDate>
      
      <guid>https://christianselig.com/2024/11/mac-mini-m4-prints/</guid>
      <description>


&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2024/11/mac-mini-m4-prints/hero.jpg&#34; alt=&#34;A 3D printed Mac mini sitting on an oak desk with a Stitch plushy sitting next to it&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Spoiler: 3D printed! The colored ports really sell the effect&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;If you&amp;rsquo;re anything like me, you&amp;rsquo;ve found the &lt;a href=&#34;https://www.apple.com/mac-mini/&#34;&gt;new, tinier Mac mini&lt;/a&gt; to be absolutely adorable. But you might also be like me that you either already have an awesome M1 Mac mini that you have no real reason to replace, or the new Mac mini just isn&amp;rsquo;t something you totally &lt;em&gt;need&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;While that logic might be sound, but it doesn&amp;rsquo;t make you want one any less.&lt;/p&gt;
&lt;p&gt;To help cure this FOMO, I made a cute little 3D printable Mac mini that can sit on your desk and be all cute. But then I had an even better idea, the new Mac mini is powerful sure, but it can&amp;rsquo;t hold snacks. Or a plant. Or your phone. Or pens/pencils. So I also made some versions you can print that add some cute utility to your desk in the form of the new Mac mini. They&amp;rsquo;re free of course! Just chuck &amp;rsquo;em into your (or your friend&amp;rsquo;s) 3D printer.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/11/mac-mini-m4-prints/holding.jpg&#34; alt=&#34;My hand holding the 3D printed Mac mini showing its back&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;It even has all the little details modeled, like the power button, ports (including rear), and fan holes!&lt;/p&gt;
&lt;p&gt;They&amp;rsquo;re pretty easy to print, it&amp;rsquo;s in separate parts for ease of  printing the bottom a different color (black) versus the top, then just put a dab of glue (or just use gravity) to keep them together. If you have a multi-color 3D printer, you can color the ports and power LED to make it look extra cool (or just do it after the fact with paint).&lt;/p&gt;
&lt;p&gt;Here are the different options for your desk!&lt;/p&gt;
&lt;h2 id=&#34;secret-item-stash&#34;&gt;Secret item stash&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/11/mac-mini-m4-prints/inside.jpg&#34; alt=&#34;A 3D printed Mac mini with a removable top that is holding red keyboard switches, with a screwdriver visible in the background&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;The possibilities for what you can store on your desk are now truly endless. Individually wrapped mints? Key switches? Screws? Paper clips? Rubber bands? Flash drives?&lt;/p&gt;
&lt;p&gt;Download link: &lt;a href=&#34;https://makerworld.com/en/models/793456&#34;&gt;https://makerworld.com/en/models/793456&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;a-very-green-sorta-mac&#34;&gt;A very green sorta Mac&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/11/mac-mini-m4-prints/plant1.jpg&#34; alt=&#34;A 3D printed Mac mini plant pot with a Haworthia succulent inside&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;First carbon neutral Mac is cool and all but what if your Mac mini literally had a plant in it?&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/11/mac-mini-m4-prints/plant2.jpg&#34; alt=&#34;A close-up of a 3D printed Mac mini plant pot with a Haworthia succulent inside&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Every desk needs a cute little plant.&lt;/p&gt;
&lt;p&gt;Download link: &lt;a href=&#34;https://makerworld.com/en/models/793464&#34;&gt;https://makerworld.com/en/models/793464&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;phone-holder&#34;&gt;Phone holder&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/11/mac-mini-m4-prints/phone-stand.jpg&#34; alt=&#34;A 3D printed Mac mini with a groove in the top surface to hold a phone upright&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;A phone/tablet holder is an essential item on my desk for debugging things, watching a video, or just keeping an eye on an Uber Eats order. Before, guests came over and saw my boring phone stand and judged me, now they come over and think I&amp;rsquo;m exciting and well-traveled.&lt;/p&gt;
&lt;p&gt;You can even charge your phone/tablet in portrait mode by pushing the cable through a tunnel made through the Ethernet port that then snakes up to the surface.&lt;/p&gt;
&lt;p&gt;Download link: &lt;a href=&#34;https://makerworld.com/en/models/793495&#34;&gt;https://makerworld.com/en/models/793495&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;pen-holder&#34;&gt;Pen holder&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/11/mac-mini-m4-prints/pen-holder.jpg&#34; alt=&#34;A 3D printed Mac mini plant pot with a Haworthia succulent inside&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;The Playdate had the cutest &lt;a href=&#34;https://play.date/stereo-dock/&#34;&gt;little pen/pencil holder accessory&lt;/a&gt; but it unfortunately never shipped and my desk is sad. This will be a nice stand in for your beloved pens, pencils, markers, and Apple Pencils.&lt;/p&gt;
&lt;p&gt;Download link: &lt;a href=&#34;https://makerworld.com/en/models/793470&#34;&gt;https://makerworld.com/en/models/793470&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;a-solid-model&#34;&gt;A solid model&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/11/mac-mini-m4-prints/solid.jpg&#34; alt=&#34;A 3D printed Mac mini plant pot with a Haworthia succulent inside&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Or if you just want to stare at it without any frills, you can just print the normal model too!&lt;/p&gt;
&lt;p&gt;Download link: &lt;a href=&#34;https://makerworld.com/en/models/793447&#34;&gt;https://makerworld.com/en/models/793447&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;printer-recommendation&#34;&gt;Printer recommendation&lt;/h2&gt;
&lt;p&gt;Whenever I post about 3D printing I understandably get a bunch of &amp;ldquo;Which 3D printer should I buy??&amp;rdquo; questions. This isn&amp;rsquo;t sponsored (I do use affiliate links on this blog when available though), but I&amp;rsquo;ve found over the last few years the answer has been pretty easy: something from Bambu Lab. Their printers are somehow super easy to use, well designed, and reasonably priced. Prusas are great too, but I think Bambu is hard to beat for the price. Don&amp;rsquo;t get an Ender.&lt;/p&gt;
&lt;p&gt;So if you&amp;rsquo;re looking for a printer now, Black Friday deals are aplenty so it&amp;rsquo;s pretty much the best time to pick one up. I&amp;rsquo;d grab something in their A series if you&amp;rsquo;re on a budget, or the P1S for a bit more if you can swing it (that&amp;rsquo;s what I use).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://shareasale.com/r.cfm?b=2377904&amp;amp;u=4504419&amp;amp;m=138211&amp;amp;urllink=&amp;amp;afftrack=&#34;&gt;My printer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://shareasale.com/r.cfm?b=2485357&amp;amp;u=4504419&amp;amp;m=138211&amp;amp;urllink=&amp;amp;afftrack=&#34;&gt;Budget pick&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the other hand if you just want to print one thing now and again, a lot of local libraries are starting to have 3D printers so that might be worth looking into! And online services exist too (eg: JLCPCB and PCBWay), but if you do it with any regularity a 3D printer is a really fun thing to pick up.&lt;/p&gt;
&lt;h2 id=&#34;enjoy-&#34;&gt;Enjoy! ❤️&lt;/h2&gt;
&lt;p&gt;Learning 3D modeling over the last year has been a ton of fun so I love a good excuse to practice, and shout out to &lt;a href=&#34;https://mastodon.social/@jerrod&#34;&gt;Jerrod Hofferth&lt;/a&gt; and his amazing &lt;a href=&#34;https://makerworld.com/en/models/756063&#34;&gt;3D printable Mac mini tower&lt;/a&gt; (that you should totally download) for the idea to solve my desire with some 3D printing! Also, the models are almost certainly not accurate down to the micrometer as I don&amp;rsquo;t actually have one, they&amp;rsquo;re based off Apple&amp;rsquo;s measurements as well as measuring screenshots. But it should be close!&lt;/p&gt;
&lt;p&gt;If you have a multi-color 3D printer, the linked models have the colors built-in for your ready to go, but if you want to print it in single-colors I also made versions available with the top and bottom separate as well as the logo, so you can print them separately in the individual colors then connect them with a touch of super glue or something.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Introducing Tiny Storage: a small, lightweight UserDefaults replacement</title>
      <link>https://christianselig.com/2024/10/introducing-tiny-storage/</link>
      <pubDate>Tue, 08 Oct 2024 14:53:55 -0300</pubDate>
      
      <guid>https://christianselig.com/2024/10/introducing-tiny-storage/</guid>
      <description>


&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2024/10/introducing-tiny-storage/hero.jpeg&#34; alt=&#34;A cute, purple floppy disk style animated character with their hands in the air with &amp;#39;Tiny Storage&amp;#39; to their right&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Hey I&amp;#39;m a developer not an artist&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Following my &lt;a href=&#34;https://christianselig.com/2024/10/beware-userdefaults/&#34;&gt;last blog post&lt;/a&gt; about difficulties surrounding &lt;code&gt;UserDefaults&lt;/code&gt; and edge cases that lead to data loss (give it a read if you haven&amp;rsquo;t, it&amp;rsquo;s an important precursor to this post!), I wanted to build something small and lightweight that would serve to fix the issues I was encountering with &lt;code&gt;UserDefaults&lt;/code&gt; and thus &lt;code&gt;TinyStorage&lt;/code&gt; was born! It&amp;rsquo;s open source so you can use it in your projects too if would like.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/christianselig/TinyStorage&#34;&gt;&lt;strong&gt;GitHub link 🐙&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;overview&#34;&gt;Overview&lt;/h2&gt;
&lt;p&gt;As mentioned in that blog post, &lt;code&gt;UserDefaults&lt;/code&gt; has more and more issues as of late with returning nil data when the device is locked and iOS &amp;ldquo;prelaunches&amp;rdquo; your app, leaving me honestly sort of unable to trust what &lt;code&gt;UserDefaults&lt;/code&gt; returns. Combined with an API that doesn&amp;rsquo;t really do a great job of surfacing whether it&amp;rsquo;s available, you can quite easily find yourself in a situation with difficult to track down bugs and data loss. This library seeks to address that fundamentally by not encrypting the backing file, allowing more reliable access to your saved data (if less secure, so don&amp;rsquo;t store sensitive data), with some niceties sprinkled on top.&lt;/p&gt;
&lt;p&gt;This means it&amp;rsquo;s great for preferences and collections of data like bird species the user likes, but not for &lt;strong&gt;sensitive&lt;/strong&gt; details. Do not store passwords/keys/tokens/secrets/diary entries/grammy&amp;rsquo;s spaghetti recipe, anything that could be considered sensitive user information, as it&amp;rsquo;s not encrypted on the disk. But don&amp;rsquo;t use &lt;code&gt;UserDefaults&lt;/code&gt; for sensitive details either as &lt;code&gt;UserDefaults&lt;/code&gt; data is still fully decrypted when the device is locked so long as the user has unlocked the device once after reboot. Instead use &lt;code&gt;Keychain&lt;/code&gt; for sensitive data.&lt;/p&gt;
&lt;p&gt;As with &lt;code&gt;UserDefaults&lt;/code&gt;, &lt;code&gt;TinyStorage&lt;/code&gt; is intended to be used with relatively small, &lt;em&gt;&lt;strong&gt;non-sensitive&lt;/strong&gt;&lt;/em&gt; values.  Don&amp;rsquo;t store massive databases in &lt;code&gt;TinyStorage&lt;/code&gt; as it&amp;rsquo;s not optimized for that, but it&amp;rsquo;s plenty fast for retrieving stored &lt;code&gt;Codable&lt;/code&gt; types. As a point of reference I&amp;rsquo;d say keep it under 1 MB.&lt;/p&gt;
&lt;p&gt;This reliable storing of small, non-sensitive data (to me) is what &lt;code&gt;UserDefaults&lt;/code&gt; was always intended to do well, so this library attempts to realize that vision. It&amp;rsquo;s pretty simple and just a few hundred lines, far from a marvel of filesystem engineering, but just a nice little utility hopefully!&lt;/p&gt;
&lt;p&gt;(Also to be clear, &lt;code&gt;TinyStorage&lt;/code&gt; is not a wrapper for &lt;code&gt;UserDefaults&lt;/code&gt;, it is a full replacement. It does not interface with the &lt;code&gt;UserDefaults&lt;/code&gt; system in any way.)&lt;/p&gt;
&lt;h2 id=&#34;features&#34;&gt;Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Reliable access: even on first reboot or in application prewarming states, &lt;code&gt;TinyStorage&lt;/code&gt; will read and write data properly&lt;/li&gt;
&lt;li&gt;Read and write Swift &lt;code&gt;Codable&lt;/code&gt; types easily with the API&lt;/li&gt;
&lt;li&gt;Similar to &lt;code&gt;UserDefaults&lt;/code&gt; uses an in-memory cache on top of the disk store to increase performance&lt;/li&gt;
&lt;li&gt;Thread-safe through an internal &lt;code&gt;DispatchQueue&lt;/code&gt; so you can safely read/write across threads without having to coordinate that yourself&lt;/li&gt;
&lt;li&gt;Supports storing backing file in shared app container&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;NSFileCoordinator&lt;/code&gt; for coordinating reading/writing to disk so can be used safely across multiple processes at the same time (main target and widget target, for instance)&lt;/li&gt;
&lt;li&gt;When using across multiple processes, will automatically detect changes to file on disk and update accordingly&lt;/li&gt;
&lt;li&gt;SwiftUI property wrapper for easy use in a SwiftUI hierarchy (Similar to &lt;code&gt;@AppStorage&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Can subscribe to to &lt;code&gt;TinyStorage.didChangeNotification&lt;/code&gt; in &lt;code&gt;NotificationCenter&lt;/code&gt;, and includes the key that changed in &lt;code&gt;userInfo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;OSLog&lt;/code&gt; for logging&lt;/li&gt;
&lt;li&gt;A function to migrate your &lt;code&gt;UserDefaults&lt;/code&gt; instance to &lt;code&gt;TinyStorage&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;limitations&#34;&gt;Limitations&lt;/h2&gt;
&lt;p&gt;Unlike &lt;code&gt;UserDefaults&lt;/code&gt;, &lt;code&gt;TinyStorage&lt;/code&gt; does not support mixed collections, so if you have a bunch of strings, dates, and integers all in the same array in &lt;code&gt;UserDefaults&lt;/code&gt; without boxing them in a shared type, &lt;code&gt;TinyStorage&lt;/code&gt; won&amp;rsquo;t work. Same situation with dictionaries, you can use them fine with &lt;code&gt;TinyStorage&lt;/code&gt; but the key and value must both be a &lt;code&gt;Codable&lt;/code&gt; type, so you can&amp;rsquo;t use &lt;code&gt;[String: Any]&lt;/code&gt; for instance where each string key could hold a different type of value.&lt;/p&gt;
&lt;h2 id=&#34;installation&#34;&gt;Installation&lt;/h2&gt;
&lt;p&gt;Simply add a &lt;strong&gt;Swift Package Manager&lt;/strong&gt; dependency for &lt;a href=&#34;https://github.com/christianselig/TinyStorage.git&#34;&gt;https://github.com/christianselig/TinyStorage.git&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;usage&#34;&gt;Usage&lt;/h2&gt;
&lt;p&gt;First, either initialize an instance of &lt;code&gt;TinyStorage&lt;/code&gt; or create a singleton and choose where you want the file on disk to live. To keep with &lt;code&gt;UserDefaults&lt;/code&gt; convention I normally create a singleton for the app container:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;extension&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;TinyStorage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;appGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TinyStorage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;appGroupID&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;group.com.christianselig.example&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;containerURL&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FileManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;containerURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forSecurityApplicationGroupIdentifier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;appGroupID&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;init&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;insideDirectory&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;containerURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(You can store it wherever you see fit though, in &lt;code&gt;URL.documentsDirectory&lt;/code&gt; is also an idea for instance!)&lt;/p&gt;
&lt;p&gt;Then, decide how you want to reference your keys, similar to &lt;code&gt;UserDefaults&lt;/code&gt; you can use raw strings, but I recommend a more strongly-typed approach, where you simply conform a type to &lt;code&gt;TinyStorageKey&lt;/code&gt; and return a &lt;code&gt;var rawValue: String&lt;/code&gt; and then you can use it as a key for your storage without worrying about typos. If you&amp;rsquo;re using something like an &lt;code&gt;enum&lt;/code&gt;, making it a &lt;code&gt;String&lt;/code&gt; enum gives you this for free, so no extra work!&lt;/p&gt;
&lt;p&gt;After that you can simply read/write values in and out of your &lt;code&gt;TinyStorge&lt;/code&gt; instance:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;enum&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;AppStorageKeys&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TinyStorageKey&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;likesIceCream&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pet&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;hasBeatFirstLevel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Read&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;pet&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Pet&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TinyStorage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;retrieve&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Pet&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;forKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;AppStorageKeys&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pet&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Write&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;TinyStorage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;store&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;forKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;AppStorageKeys&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;likesIceCream&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(If you have some really weird type or don&amp;rsquo;t want to conform to &lt;code&gt;Codable&lt;/code&gt;, just convert the type to &lt;code&gt;Data&lt;/code&gt; through whichever means you prefer and store &lt;em&gt;that&lt;/em&gt;, as &lt;code&gt;Data&lt;/code&gt; itself is &lt;code&gt;Codable&lt;/code&gt;.)&lt;/p&gt;
&lt;p&gt;If you want to use it in SwiftUI and have your view automatically respond to changes for an item in your storage, you can use the &lt;code&gt;@TinyStorageItem&lt;/code&gt; property wrapper. Simply specify your storage, the key for the item you want to access, and specify a default value.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TinyStorageItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;AppStorageKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pet&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;storage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;pet&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Pet&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Boots&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;species&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fish&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;hasLegs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;some&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;View&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pet&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can even use Bindings to automatically read/write.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TinyStorageItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;AppStorageKeys&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;storage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;some&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;View&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;VStack&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Stored Value: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;TextField&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Message&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;$&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It also addresses some of the annoyances of &lt;code&gt;@AppStorage&lt;/code&gt;, such as not being able to store collections:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TinyStorageItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;names&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;storage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;names&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or better support for optional values:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TinyStorageItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;nickname&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;storage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;nickname&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// or &amp;#34;Cool Guy&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can also migrate from a &lt;code&gt;UserDefaults&lt;/code&gt; instance to &lt;code&gt;TinyStorage&lt;/code&gt; with a handy helper function:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;keysToMigrate&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;favoriteIceCream&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;appFontSize&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;useCustomTheme&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;lastFetchDate&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;TinyStorage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;migrate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;userDefaults&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;standard&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;keys&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;keysToMigrate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;overwriteIfConflict&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(Read the &lt;code&gt;migrate&lt;/code&gt; function documentation for more details.)&lt;/p&gt;
&lt;p&gt;If you want to migrate multiple keys manually or store a bunch of things at once, rather than a bunch of single &lt;code&gt;store&lt;/code&gt; calls you can consolidate them into one call with &lt;code&gt;bulkStore&lt;/code&gt; which will only write to disk the once:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;item1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TinyStorageBulkStoreItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;AppGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pet&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pet&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;item2&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TinyStorageBulkStoreItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;AppGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;theme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;sunset&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;TinyStorage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bulkStore&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;items&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;item1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;item2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;hope-its-handy&#34;&gt;Hope it&amp;rsquo;s handy!&lt;/h2&gt;
&lt;p&gt;If you like it or have any feedback let me know! I&amp;rsquo;m going to start slowly integrating it into Pixel Pals and hopefully solve a few bugs in the process.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Beware UserDefaults: a tale of hard to find bugs, and lost data</title>
      <link>https://christianselig.com/2024/10/beware-userdefaults/</link>
      <pubDate>Sat, 05 Oct 2024 15:12:45 -0300</pubDate>
      
      <guid>https://christianselig.com/2024/10/beware-userdefaults/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2024/10/beware-userdefaults/hero.jpeg&#34; alt=&#34;An iPhone sitting on an open book showing its Lock Screen.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Excuse the alarmist title, but I think it&amp;rsquo;s justified, as it&amp;rsquo;s an issue that&amp;rsquo;s caused me a ton of pain in both support emails and actually tracking it down, so I want to make others aware of it so they don&amp;rsquo;t similarly burned.&lt;/p&gt;
&lt;h2 id=&#34;brief-intro&#34;&gt;Brief intro&lt;/h2&gt;
&lt;p&gt;For the uninitiated, &lt;a href=&#34;https://developer.apple.com/documentation/foundation/userdefaults&#34;&gt;&lt;code&gt;UserDefaults&lt;/code&gt;&lt;/a&gt; (née &lt;code&gt;NSUserDefaults&lt;/code&gt;) is the de facto iOS standard for persisting non-sensitive, non-massive data to &amp;ldquo;disk&amp;rdquo; (AKA offline). In other words, are you storing some user preferences, maybe your user&amp;rsquo;s favorite ice cream flavors? &lt;code&gt;UserDefaults&lt;/code&gt; is great, and used extensively from virtually every iOS app to &lt;a href=&#34;https://developer.apple.com/documentation/widgetkit/building_widgets_using_widgetkit_and_swiftui&#34;&gt;Apple sample code&lt;/a&gt;. Large amount of data, or sensitive data? Look elsewhere! This is as opposed to just storing it in memory where if the user restarts the app all the data is wiped out.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a really handy tool with a ton of nice, built-in things for you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No needing to mess with writing to files yourself, and better yet, no need to coordinate &lt;em&gt;when&lt;/em&gt; to persist values back to the disk&lt;/li&gt;
&lt;li&gt;Easy to share data between your app&amp;rsquo;s main target and secondary targets (like a widget target)&lt;/li&gt;
&lt;li&gt;Automatic serialization and deserialization: just feed in a String, Date, Int, and &lt;code&gt;UserDefaults&lt;/code&gt; handles turning it into bytes and back from bytes&lt;/li&gt;
&lt;li&gt;Thread-safe!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So it&amp;rsquo;s no wonder it&amp;rsquo;s used extensively. But yeah, keep the two limitations in mind that Apple hammers home:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.apple.com/documentation/appclip/sharing-data-between-your-app-clip-and-your-full-app#Make-local-App-Clip-data-available-to-the-full-app&#34;&gt;Don&amp;rsquo;t store sensitive data in UserDefaults&lt;/a&gt;, that&amp;rsquo;s what Keychain is for&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.apple.com/documentation/foundation/userdefaults/1617187-sizelimitexceedednotification#&#34;&gt;Don&amp;rsquo;t store large amounts of data in UserDefaults&lt;/a&gt;, use something like Core Data or Swift Data&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;okay-so-whats-the-problem&#34;&gt;Okay, so what&amp;rsquo;s the problem&lt;/h2&gt;
&lt;p&gt;Turns out, sometimes you can request your saved data back from &lt;code&gt;UserDefaults&lt;/code&gt; and it… just won&amp;rsquo;t have it! That&amp;rsquo;s a pretty big issue for a system that&amp;rsquo;s supposed to reliably store data for you.&lt;/p&gt;
&lt;p&gt;This can amount to an even bigger issue that leads to permanent data loss.&lt;/p&gt;
&lt;p&gt;Imagine a situation where a user has been meticulously opening your app for 364 days in a row. On day 365, your app promised a cool reward! When the user last closed the app, you stored &lt;code&gt;364&lt;/code&gt; to &lt;code&gt;UserDefaults&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The user wakes up on day 365, excited for their reward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;App launches&lt;/li&gt;
&lt;li&gt;App queries &lt;code&gt;UserDefaults&lt;/code&gt; for how many days in a row the user has opened the app&lt;/li&gt;
&lt;li&gt;App returns 0 (&lt;code&gt;UserDefaults&lt;/code&gt; is mysteriously unavailable so its API returns the default integer value of 0)&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s a new day, so you increment that value by 1, so that 0 changes to 1&lt;/li&gt;
&lt;li&gt;Save that new value back to &lt;code&gt;UserDefaults&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now, instead of your user having a fun celebration, their data has been permanently overwritten and reset! They are having a Sad Day™.&lt;/p&gt;
&lt;p&gt;It basically means, if at any point you trust &lt;code&gt;UserDefaults&lt;/code&gt; to accurately return your data (which you know, sounds like a fair assumption) you might just get incorrect data, which you then might make worse by overwriting good data with.&lt;/p&gt;
&lt;p&gt;And remember, you&amp;rsquo;re not meant to store &lt;em&gt;sensitive&lt;/em&gt; data in &lt;code&gt;UserDefaults&lt;/code&gt;, but even if it&amp;rsquo;s not sensitive data it might be &lt;em&gt;valuable&lt;/em&gt;. The user&amp;rsquo;s day streak above is not sensitive data that would be bad if leaked online like a password, but it is &lt;em&gt;valuable&lt;/em&gt; to that user. In fact I&amp;rsquo;d argue any data persisted to the disk is valuable, otherwise you wouldn&amp;rsquo;t be saving it. And you should be always be able to trust an API to reliably save your data.&lt;/p&gt;
&lt;h2 id=&#34;what-how-is-this-happening-&#34;&gt;What??? How is this happening? 😵‍💫&lt;/h2&gt;
&lt;p&gt;As I understand it, there&amp;rsquo;s basically two systems coming together (and working incorrectly, if you ask me) to cause this:&lt;/p&gt;
&lt;h3 id=&#34;1-sensitive-data-encryption&#34;&gt;1. Sensitive data encryption&lt;/h3&gt;
&lt;p&gt;When using Keychain or files directly, as a developer &lt;a href=&#34;https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlocked&#34;&gt;you can mark data that should be encrypted&lt;/a&gt; until the device is unlocked by Face ID/Touch ID/passcode. This way if you&amp;rsquo;re storing a sensitive data like a token or password on the device, the contents are encrypted and thus unreadable until the device is unlocked.&lt;/p&gt;
&lt;p&gt;This meant if the device was still locked, and you, say, had a Lock Screen Widget that performed an API request, you would have to show placeholder data until the user unlocked the device, because the sensitive data, namely the user&amp;rsquo;s API token, was encrypted and unable to be used by the app to fetch and show data until the user unlocked the device. Not the end of the world, but something to keep in mind for secure data like API tokens, passwords, secrets, etc.&lt;/p&gt;
&lt;h3 id=&#34;2-application-prewarming&#34;&gt;2. Application prewarming&lt;/h3&gt;
&lt;p&gt;Starting with iOS 15, iOS will sometimes wake up your application early so that when a user launches it down the road it launches even quicker for them, as iOS was able to do some of the heavy lifting early. This is called &lt;a href=&#34;https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence&#34;&gt;prewarming&lt;/a&gt;. Thankfully per Apple, your application doesn&amp;rsquo;t fully launch, it&amp;rsquo;s just some processes required to get your app working (spoiler: this is wrong, see below):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Prewarming executes an app’s launch sequence up until, but not including, when main() calls UIApplicationMain(&lt;em&gt;:&lt;/em&gt;:&lt;em&gt;:&lt;/em&gt;:).&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Okay, so what happened with these two?&lt;/p&gt;
&lt;p&gt;It seems at some point, even though &lt;code&gt;UserDefaults&lt;/code&gt; is intended for non-sensitive information, it started getting marked as data that needs to be encrypted and cannot be accessed until the user unlocked their device. I don&amp;rsquo;t know if it&amp;rsquo;s because Apple found developers were storing sensitive data in there even when they shouldn&amp;rsquo;t be, but the result is even if you just store something innocuous like what color scheme the user has set for your app, that theme cannot be accessed until the device is unlocked.&lt;/p&gt;
&lt;p&gt;Again, who cares? Users &lt;em&gt;have&lt;/em&gt; to unlock the device before launching my app, right? I thought so too! It turns out, even though Apple&amp;rsquo;s prewarming documentation states otherwise, developers have been reporting for years &lt;a href=&#34;https://stackoverflow.com/questions/71025205/ios-15-prewarming-causing-appwilllaunch-method-when-prewarm-is-done&#34;&gt;that that&amp;rsquo;s just wrong&lt;/a&gt;, and your app can effectively be fully launched at any time, including before the device is even unlocked.&lt;/p&gt;
&lt;p&gt;Combining this with the previous &lt;code&gt;UserDefaults&lt;/code&gt; change, you&amp;rsquo;re left with the above situation where the app is launched with crucial data just completely unavailable because the device is still locked.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UserDefaults&lt;/code&gt; also doesn&amp;rsquo;t make this clear at all, which it could do by for instance returning &lt;code&gt;nil&lt;/code&gt; when trying to access &lt;code&gt;UserDefaults.standard&lt;/code&gt; if it&amp;rsquo;s unavailable. Instead, it just looks like everything is as it should be, except none of your saved keys are available anymore, which can make your app think it&amp;rsquo;s in a &amp;ldquo;first launch after install&amp;rdquo; situation.&lt;/p&gt;
&lt;p&gt;The whole point of &lt;code&gt;UserDefaults&lt;/code&gt; is that it&amp;rsquo;s supposed to reliably store simple, non-sensitive data so it can be accessed whenever. The fact that this has now changed drastically, and at the same time your app can be launched effectively whenever, makes for an incredibly confusing, dangerous, and hard to debug situation.&lt;/p&gt;
&lt;h2 id=&#34;and-its-getting-worse-with-live-activities&#34;&gt;And it&amp;rsquo;s getting worse with Live Activities&lt;/h2&gt;
&lt;p&gt;If you use Live Activities at all, the cool new API that puts activities in your Dynamic Island and Lock Screen, it seems if your app has an active Live Activity and the user reboots their device, &lt;strong&gt;virtually 100% of the time&lt;/strong&gt; the above situation will occur where your app is launched in the background without &lt;code&gt;UserDefaults&lt;/code&gt; being available to it. That means the next time your user actually launches the app, if at any point during your app launching you trusted the contents of &lt;code&gt;UserDefaults&lt;/code&gt;, your app is likely in an incorrect state with incorrect data.&lt;/p&gt;
&lt;p&gt;This bit me badly, and I&amp;rsquo;ve had users email me over time that they&amp;rsquo;ve experienced data loss, and it&amp;rsquo;s been incredibly tricky to pinpoint why. It turns out it&amp;rsquo;s simply because the app started up, assuming &lt;code&gt;UserDefaults&lt;/code&gt; would return good data, and when it transparently didn&amp;rsquo;t, it would ultimately overwrite their good data with the returned bad data.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve talked to a few other developers about this, and they&amp;rsquo;ve also reported random instances of users being logged out or losing data, and after further experimenting been able to now pinpoint that this is what caused their bug. It happened in past apps to me as well (namely users getting signed out of Apollo due to a key being missing), and I could never figure out why, but this was assuredly it.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve ever scratched your head at a support email over a user&amp;rsquo;s app being randomly reset, hopefully this helps!&lt;/p&gt;
&lt;h2 id=&#34;i-dont-like-this-&#34;&gt;I don&amp;rsquo;t like this ☹️&lt;/h2&gt;
&lt;p&gt;I can&amp;rsquo;t overstate what a misstep I think this was. &lt;a href=&#34;https://thecyberpatch.com/security-con-finding-the-right-balance/&#34;&gt;Security is always a balance with convenience&lt;/a&gt;. Face ID and Touch ID strike this perfectly; they&amp;rsquo;re both ostensibly less secure per Apple&amp;rsquo;s own admission than, say, a 20 digit long password, but users are much more likely to adopt biometric security so it&amp;rsquo;s a massive overall win.&lt;/p&gt;
&lt;p&gt;Changing &lt;code&gt;UserDefaults&lt;/code&gt; in this way feels more on the side of &amp;ldquo;Your company&amp;rsquo;s sysadmin requiring you to change your password every week&amp;rdquo;: dubious security gains at the cost of user productivity and headaches.&lt;/p&gt;
&lt;p&gt;Further, it&amp;rsquo;s not as if &lt;code&gt;UserDefaults&lt;/code&gt; is truly secure with this change. It&amp;rsquo;s only encrypted &lt;a href=&#34;https://developer.apple.com/documentation/foundation/fileprotectiontype/1616633-completeuntilfirstuserauthentica&#34;&gt;between the time the device is rebooted and first unlocked&lt;/a&gt;. So sure, if you shut down your phone and someone powers it back up, the name of your pet cow might be knowable to the attacker, but literally any time after you unlock the device after first boot, even if you re-lock it, Mr. Moo will be unencrypted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: After more investigating it seems like this change has been around for years and years, but the results of it have just has gotten more prevalent with things like prewarming and Live Activities.&lt;/p&gt;
&lt;p&gt;I mean no shade to the nice folks at Apple working on &lt;code&gt;UserDefaults&lt;/code&gt;, I&amp;rsquo;m sure there&amp;rsquo;s a lot of legacy considerations to take into account that I cannot fathom that makes this a complex machine. I&amp;rsquo;m just sad that this lovely, simple API has some really sharp edges in 2024.&lt;/p&gt;
&lt;p&gt;But enough moaning, let&amp;rsquo;s fix it.&lt;/p&gt;
&lt;h2 id=&#34;solution-1&#34;&gt;Solution 1&lt;/h2&gt;
&lt;p&gt;Because iOS is now seemingly encrypting &lt;code&gt;UserDefaults&lt;/code&gt;, the easiest solution is to check &lt;a href=&#34;https://developer.apple.com/documentation/uikit/uiapplication/1622925-isprotecteddataavailable&#34;&gt;&lt;code&gt;UIApplication.isProtectedDataAvailable&lt;/code&gt;&lt;/a&gt; and if it returns &lt;code&gt;false&lt;/code&gt;, subscribe to &lt;code&gt;NotificationCenter&lt;/code&gt; for when &lt;code&gt;protectedDataDidBecomeAvailableNotification&lt;/code&gt; is fired. This was previously really useful for knowing when Keychain or locked files were accessible once the device was unlocked, but it now seemingly applies to &lt;code&gt;UserDefaults&lt;/code&gt; (despite not being mentioned anywhere in its documentation or &lt;code&gt;UserDefault&lt;/code&gt;&amp;rsquo;s documentation 🙃).&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t love this solution, because it effectively makes &lt;code&gt;UserDefaults&lt;/code&gt; either an asynchronous API (&amp;ldquo;Is it available? No? Okay I&amp;rsquo;ll wait here until it is.&amp;rdquo;), or one where you can only trust its values sometimes, because unlike the Keychain API for instance, &lt;code&gt;UserDefaults&lt;/code&gt; API itself does not expose any information about this when you try to access it when it&amp;rsquo;s in a locked state.&lt;/p&gt;
&lt;p&gt;Further, &lt;a href=&#34;https://stackoverflow.com/questions/59282237/isprotecteddataavailable-is-returning-true-always-in-ios&#34;&gt;some developers have reported&lt;/a&gt; &lt;code&gt;UserDefaults&lt;/code&gt; still being unavailable even once &lt;code&gt;isProtectedDataAvailable&lt;/code&gt; returns true, presumably where (for one reason or another) &lt;code&gt;UserDefaults&lt;/code&gt; isn&amp;rsquo;t reading the disk store back into memory even though the file became available.&lt;/p&gt;
&lt;p&gt;Further further, &lt;code&gt;UIApplication.isProtectedDataAvailable&lt;/code&gt; is just not a great API for this use case. For one, it&amp;rsquo;s weirdly only available in a main app context, so if you&amp;rsquo;re executing code from a widget extension and want to check if protected data is available, you&amp;rsquo;ll have to resort to some other method, like trying to write to disk and read it back. And on top of this, &lt;code&gt;isProtectedDataAvailable&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; when the device is locked, which will cause you to think &lt;code&gt;UserDefaults&lt;/code&gt; is also unavailable. But remember, &lt;code&gt;UserDefaults&lt;/code&gt; is only unavailable &lt;em&gt;before the first lock upon reboot&lt;/em&gt;, any time after that it&amp;rsquo;s totally accessible, and this API will still return &lt;code&gt;false&lt;/code&gt; in those cases as &lt;code&gt;UserDefaults&lt;/code&gt; doesn&amp;rsquo;t follow the strict protection mechanism that that API tracks.&lt;/p&gt;
&lt;h2 id=&#34;solution-2&#34;&gt;Solution 2&lt;/h2&gt;
&lt;p&gt;For the mentioned reasons, I don&amp;rsquo;t really like/trust Solution 1. I want a version of &lt;code&gt;UserDefaults&lt;/code&gt; that acts like what it says on the tin: simply, quickly, and &lt;em&gt;reliably&lt;/em&gt; retrieve persisted, non-sensitive values. This is easy enough to whip up ourselves, we just want to keep in mind some of the things &lt;code&gt;UserDefaults&lt;/code&gt; handles nicely for us, namely thread-safety, shared between targets, and an easy API where it serializes data without us having to worry about writing to disk. Let&amp;rsquo;s quickly show how we might approach some of this.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UserDefaults&lt;/code&gt; is fundamentally just a plist file stored on disk that is read into memory, so let&amp;rsquo;s create our own file, and instead of marking it as requiring encryption like iOS weirdly does, we&amp;rsquo;ll say that&amp;rsquo;s not required:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Example thing to save&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;favoriteIceCream&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;chocolate&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Save to your app&amp;#39;s shared container directory so it can be accessed by other targets outside main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;appGroupID&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Get the URL for the shared container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;containerURL&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FileManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;containerURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forSecurityApplicationGroupIdentifier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;appGroupID&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;bp&#34;&gt;fatalError&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;App Groups not set up correctly&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Create the file URL within the shared container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;fileURL&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;containerURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appendingPathComponent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Defaults&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;do&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;favoriteIceCream&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;using&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utf8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;write&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// No encryption please I&amp;#39;m just storing the name of my digital cow Mister Moo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FileManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;setAttributes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;protectionKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;kr&#34;&gt;none&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ofItemAtPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;File saved successfully at &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;catch&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Error saving file: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;localizedDescription&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(Note that you could &lt;em&gt;theoretically&lt;/em&gt; modify the system &lt;code&gt;UserDefaults&lt;/code&gt; file in the same way, but Apple documentation recommends against touching the &lt;code&gt;UserDefaults&lt;/code&gt; file directly.)&lt;/p&gt;
&lt;p&gt;Next let&amp;rsquo;s make it thread safe by using a &lt;code&gt;DispatchQueue&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;dispatchQueue&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;label&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;DefaultsQueue&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;retrieveFavoriteIceCream&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sync&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;containerURL&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FileManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;containerURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forSecurityApplicationGroupIdentifier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;app-group-id&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;fileURL&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;containerURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appendingPathComponent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fileName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;k&#34;&gt;do&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;contentsOf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;encoding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utf8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;catch&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Error retrieving file: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;localizedDescription&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;save&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;favoriteIceCream&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;n&#34;&gt;dispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sync&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;containerURL&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FileManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;containerURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forSecurityApplicationGroupIdentifier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;app-group-id&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;fileURL&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;containerURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appendingPathComponent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fileName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;k&#34;&gt;do&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;favoriteIceCream&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;using&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utf8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;write&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FileManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;setAttributes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;protectionKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;kr&#34;&gt;none&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ofItemAtPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;File saved successfully at &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;catch&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Error saving file: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;localizedDescription&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(You probably don&amp;rsquo;t need a concurrent queue for this, so I didn&amp;rsquo;t.)&lt;/p&gt;
&lt;p&gt;But with that we have to worry about data types, let&amp;rsquo;s just make it so long as the type conforms to &lt;code&gt;Codable&lt;/code&gt; we can save or retrieve it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;saveCodable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;codable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Codable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;forKey&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;do&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;JSONEncoder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;encode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;codable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Persist raw data bytes to a file like above&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;catch&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Unable to encode &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;codable&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;codable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;T&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Codable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forKey&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;T&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;kr&#34;&gt;Type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;T&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// Fetch raw data from disk as done above&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;do&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;JSONDecoder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;decode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;T&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;catch&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Error decoding &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;T&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt; for key &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt; with error: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Example usage:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;newFavoriteIceCream&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;strawberry&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;saveCodable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;newFavoriteIceCream&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;forKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;favorite-ice-cream&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;savedFavoriteIceCream&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;codable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;favorite-ice-cream&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Put those together, wrap it in a nice little library, and bam, you&amp;rsquo;ve got a &lt;code&gt;UserDefaults&lt;/code&gt; replacement that acts as you would expect. In fact if you like the encryption option you can add it back pretty easily (don&amp;rsquo;t change the file protection attributes) and you could make it clear in the API when the data is inaccessible due to the device being locked, either by &lt;code&gt;throw&lt;/code&gt;ing an error, making your singleton &lt;code&gt;nil&lt;/code&gt;, &lt;code&gt;await&lt;/code&gt;ing until the device is unlocked, etc.&lt;/p&gt;
&lt;h2 id=&#34;solution-3&#34;&gt;Solution 3&lt;/h2&gt;
&lt;p&gt;Update! I wrote a little library to kinda wrap all of the above into a handy little tool! It&amp;rsquo;s called &lt;a href=&#34;https://github.com/christianselig/TinyStorage&#34;&gt;TinyStorage&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;end&#34;&gt;End&lt;/h2&gt;
&lt;p&gt;Maybe this is super obvious to you, but I&amp;rsquo;ve talked to enough developers where it wasn&amp;rsquo;t, that I hope in writing this it can save you the many, many hours I spent trying to figure out why once in a blue moon a user would be logged out, or their app state would look like it reset, or worst of all: they lost data.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Juno for YouTube has been removed from the App Store</title>
      <link>https://christianselig.com/2024/10/juno-removed/</link>
      <pubDate>Tue, 01 Oct 2024 17:44:48 -0300</pubDate>
      
      <guid>https://christianselig.com/2024/10/juno-removed/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2024/10/juno-removed/hero.jpeg&#34; alt=&#34;Screenshot from App Store Connect showing Juno as &amp;#39;Removed from App Store&amp;#39;&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;For those not aware, a few months ago after reaching out to me, &lt;a href=&#34;https://christianselig.com/2024/06/announcing-juno-2/&#34;&gt;YouTube contacted the App Store&lt;/a&gt; stating that Juno does not adhere to YouTube guidelines and modifies the website in a way they don&amp;rsquo;t approve of, and alludes to their trademarks and iconography.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t personally agree with this, as Juno is just a web view, and acts as little more than a browser extension that modifies CSS to make the website and video player look more &amp;ldquo;visionOS&amp;rdquo; like. No logos are placed other than those already on the website, and the &amp;ldquo;for YouTube&amp;rdquo; suffix is &lt;a href=&#34;https://developers.google.com/youtube/terms/branding-guidelines&#34;&gt;permitted in their branding guidelines&lt;/a&gt;. Juno also doesn&amp;rsquo;t block ads in any capacity, for the curious.&lt;/p&gt;
&lt;p&gt;I stated as much to YouTube, they wouldn&amp;rsquo;t really clarify or budge any, and as a result of both parties not being able to come to a conclusion I received an email a few minutes ago from Apple that Juno has been removed from the App Store.&lt;/p&gt;
&lt;p&gt;Juno was a fun hobby project for me to build. As a developer I wanted to get some experience building for the Vision Pro, and as a user I wanted a nice way to watch YouTube on this cool new device. As a result I really enjoyed building Juno, but it was always something I saw as fundamentally an app I built for fun.&lt;/p&gt;
&lt;p&gt;Because of that, I have zero desire to spin this into a massive fight (at least more than I&amp;rsquo;ve fought in emails over the last few months) akin to what happened with Reddit years ago. That&amp;rsquo;s kind of the opposite of fun. I hope that&amp;rsquo;s understandable.&lt;/p&gt;
&lt;p&gt;For those who have Juno, to my knowledge it should continue to work fine until/unless the YouTube website updates in some fashion that breaks stuff. Sorry it had to end this way, I had some really cool stuff planned for it that I think would have been a lot of fun! It&amp;rsquo;s been genuinely awesome hearing all the kind words from Vision Pro users who have loved the app.&lt;/p&gt;
&lt;p&gt;🫡&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Server side Live Activities guide</title>
      <link>https://christianselig.com/2024/09/server-side-live-activities/</link>
      <pubDate>Mon, 23 Sep 2024 14:02:16 -0300</pubDate>
      
      <guid>https://christianselig.com/2024/09/server-side-live-activities/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2024/09/server-side-live-activities/hero.jpeg&#34; alt=&#34;A Live Activity on an iOS Lock Screen with an emoji and the text &amp;#39;I sent this from a server woooooo&amp;#39;.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;iOS 17.2 &lt;a href=&#34;https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications#Construct-the-payload-that-starts-a-Live-Activity&#34;&gt;gained the capability to start Live Activities&lt;/a&gt; from a server, which is pretty cool and handy! I&amp;rsquo;ve been playing around with it a bit and found some parts a bit confusing, so I thought I&amp;rsquo;d do a little write up for future me as well as anyone else who could benefit!&lt;/p&gt;
&lt;p&gt;(For the uninitiated, Live Activities are the cool iOS feature that adds a live view of a specific activity to your Dynamic Island (if available) and Lock Screen, for example to see how your Uber Eats order is coming along.)&lt;/p&gt;
&lt;h2 id=&#34;overview-of-starting-a-live-activity-server-side&#34;&gt;Overview of starting a Live Activity server-side&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s pretty straightforward, just a few steps:&lt;/p&gt;
&lt;h3 id=&#34;get-token&#34;&gt;Get token&lt;/h3&gt;
&lt;p&gt;In your AppDelegate&amp;rsquo;s &lt;code&gt;didFinishLaunching&lt;/code&gt; or somewhere very early in the lifecycle, start an async sequence to listen for a &lt;code&gt;pushToStartToken&lt;/code&gt; so you can get the token:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;Task&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pushToken&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Activity&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;IceCreamWidgetAttributes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pushToStartTokenUpdates&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;pushTokenString&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pushToken&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;reduce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;format&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;%02x&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Our push token is: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pushTokenString&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;use-token-to-start-live-activity-server-side&#34;&gt;Use token to start Live Activity server-side&lt;/h3&gt;
&lt;p&gt;Now that you have the token, we use that to send a push notification (using APNs) to start the Live Activity on the user&amp;rsquo;s device. There&amp;rsquo;s lots of server side libraries for this, or you can just use curl, or you can an online site to test &lt;a href=&#34;https://apnspush.com&#34;&gt;like this one&lt;/a&gt; (in the case of the latter where you&amp;rsquo;re uploading your token to random sites, please create a sample token that you delete afterward).&lt;/p&gt;
&lt;p&gt;The key points that differ from sending a &amp;ldquo;normal&amp;rdquo; push notification:&lt;/p&gt;
&lt;h4 id=&#34;headers&#34;&gt;Headers&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Set your push token to the &lt;code&gt;pushToStartToken&lt;/code&gt; you received above&lt;/li&gt;
&lt;li&gt;Set the topic to your app&amp;rsquo;s normal bundle ID with an added suffix of &lt;code&gt;.push-type.liveactivity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set the priority to 5 for low priority or 10 for a high priority notification (does not seem like any values in between work)&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;apns-push-type&lt;/code&gt; to &lt;code&gt;liveactivity&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;payload&#34;&gt;Payload&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;timestamp&lt;/code&gt; field with the current unix timestamp&lt;/li&gt;
&lt;li&gt;&lt;code&gt;event&lt;/code&gt; field set to &lt;code&gt;start&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attributes-type&lt;/code&gt; set to the name of your Swift Live Activities attributes &lt;code&gt;struct&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attributes&lt;/code&gt; with a dictionary representing your Swift Live Activity attributes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content-state&lt;/code&gt; with the initial content state as a dictionary, similar to &lt;code&gt;attributes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;alert&lt;/code&gt; field set with a &lt;code&gt;title&lt;/code&gt; and a &lt;code&gt;body&lt;/code&gt; (will silently fail if you just set &lt;code&gt;alert&lt;/code&gt; to a string like the old days)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that you cannot use the old certificate based authentication and instead have to use token based authentication and http2.&lt;/p&gt;
&lt;p&gt;Send the http request and it should start the Live Activity on the iOS device! 🎉&lt;/p&gt;
&lt;h4 id=&#34;aside&#34;&gt;Aside&lt;/h4&gt;
&lt;p&gt;Sending push notifications is kinda complicated, so you likely want a server-side library. I wanted to play around with Node for this for the first time, and in case you go down that path, in September 2024 Node is in a weirdly lacking spot for APNs libraries. The &lt;a href=&#34;https://github.com/node-apn/node-apn&#34;&gt;de facto one&lt;/a&gt; is abandoned, the &lt;a href=&#34;https://github.com/parse-community/node-apn&#34;&gt;community replacement for it&lt;/a&gt; doesn&amp;rsquo;t work with TypeScript, and there&amp;rsquo;s a third option with TypeScript support but it isn&amp;rsquo;t super popular &lt;a href=&#34;https://github.com/AndrewBarba/apns2/issues/80&#34;&gt;and has some issues&lt;/a&gt;. I ended up going back to Go, and there&amp;rsquo;s an &lt;a href=&#34;https://github.com/sideshow/apns2&#34;&gt;excellent APNs library there&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;its-broken-on-ios-17&#34;&gt;It&amp;rsquo;s broken on iOS 17&lt;/h2&gt;
&lt;p&gt;On iOS 17, getting the above push token is really hard (you can seemingly only get it once). I tried for ages to get the token before &lt;a href=&#34;https://forums.developer.apple.com/forums/thread/741939&#34;&gt;stumbling upon a thread on the Apple forums&lt;/a&gt; where a user said to delete the app, reboot your device, then fresh install it. Sure enough that worked and the token displayed, but if I just rebooted the device, or just reinstalled the app, it wouldn&amp;rsquo;t. Had to do all of them. And no it didn&amp;rsquo;t change if I used a release configuration.&lt;/p&gt;
&lt;p&gt;I tried this on iOS 17.6.1 (latest iOS 17 release at the time of writing). It does &lt;strong&gt;not&lt;/strong&gt; seem to be an issue at all on iOS 18.&lt;/p&gt;
&lt;p&gt;The difficulty in acquiring it makes it incredibly hard to use on iOS 17 if you add in the feature in an update and the user isn&amp;rsquo;t getting your app from a fresh install, to the extent that I can&amp;rsquo;t really trust its reliability on iOS 17 as a feature you could advertise, for instance.&lt;/p&gt;
&lt;p&gt;John Gruber &lt;a href=&#34;https://daringfireball.net/linked/2024/09/03/apple-sports-football-live-activities&#34;&gt;recently wrote about the Apple Sports app&lt;/a&gt; and wondered why its Live Activities feature is iOS 18 only. A reader wrote in to mention the new broadcast push notifications feature requiring iOS 18, and that well may be it, but I&amp;rsquo;d say it&amp;rsquo;s equally as likely that it just doesn&amp;rsquo;t work reliably enough on iOS 17 for even Apple to bother.&lt;/p&gt;
&lt;h2 id=&#34;updateend-the-activity&#34;&gt;Update/end the activity&lt;/h2&gt;
&lt;p&gt;This part admittedly confused me a bit. &lt;a href=&#34;https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications#Start-new-Live-Activities-with-ActivityKit-push-notifications&#34;&gt;The docs&lt;/a&gt; state:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Send the push-to-start token to your server and use the &lt;code&gt;pushToStartTokenUpdates&lt;/code&gt; sequence to receive token updates. Similar to the update token, update it on your server when needed and invalidate the old token.&lt;/li&gt;
&lt;li&gt;While the system starts the new Live Activity and wakes up your app, you receive the push token you use for updates. To update and end the Live Activity on devices that aren’t running iOS 18 or iPadOS 18, use this update push token as if you obtained it by starting a Live Activity from within your app.&lt;/li&gt;
&lt;/ul&gt;&lt;/blockquote&gt;
&lt;p&gt;I assumed that this all operated through that same &lt;code&gt;pushToStartTokenUpdates&lt;/code&gt;, because as soon as you start the activity server-side, your app wakes up, and your &lt;code&gt;pushToStartTokenUpdates&lt;/code&gt; async sequence fires again with a &amp;ldquo;new&amp;rdquo; token.&lt;/p&gt;
&lt;p&gt;However the &amp;ldquo;new&amp;rdquo; token is just the same one that you started the activity with, and if you try to end your activity server-side with this token, nothing happens.&lt;/p&gt;
&lt;p&gt;Turns out, your &lt;code&gt;pushToStartTokenUpdates&lt;/code&gt; is (per the name!) only able to &lt;em&gt;start&lt;/em&gt; Live Activities. Not sure why it fires a second time with the same token, but you &lt;em&gt;do&lt;/em&gt; want to use that async sequence to monitor for changes to the start token, because it might change and the next time you want to start a new Live Activity you&amp;rsquo;ll need that token.&lt;/p&gt;
&lt;p&gt;To update/end your Live Activity, what you actually want to do is create a separate async sequence to monitor your app for Live Activities that get created, and then monitor &lt;em&gt;its&lt;/em&gt; push token:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Listen for local Live Activity updates&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;activity&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Activity&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;IceCreamWidgetAttributes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;activityUpdates&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// Upon finding one, listen for its push token (it is not available immediately!)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;Task&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pushToken&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;activity&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pushTokenUpdates&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;pushTokenString&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pushToken&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;reduce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;format&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;%02x&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;New activity detected with push token: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pushTokenString&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;iOS will also call this async sequence on start of a new Live Activity from a server, and you use that token to update/end it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not blaming the documentation on this, I understand it as it is written now, but I wanted to clarify in case anyone else gets confused.&lt;/p&gt;
&lt;p&gt;Once your server is made aware of this token, you can end your Live Activity server-side with the following changes from the above &lt;code&gt;start&lt;/code&gt; considerations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Payload: &lt;code&gt;event&lt;/code&gt; should be &lt;code&gt;end&lt;/code&gt; or &lt;code&gt;update&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Payload: If ending you might want a &lt;code&gt;dismissal-date&lt;/code&gt; unix timestamp. If you don&amp;rsquo;t set this, iOS will immediately remove the Live Activity from the Dynamic Island but leave it on the Lock Screen for up to four hours. You may want this, but you can control how long it stays there by setting &lt;code&gt;dismissal-date&lt;/code&gt;, if you set it to now or in the past it will remove it upon receipt of the notification.&lt;/li&gt;
&lt;li&gt;Payload: Send a new &lt;code&gt;content-state&lt;/code&gt; if updating (optional if ending, if ending and you want to leave it on the lock screen (see previous point) you can set a &lt;code&gt;content-state&lt;/code&gt; which will serve as the final content state for the Live Activity)&lt;/li&gt;
&lt;li&gt;Payload: Do not send &lt;code&gt;attributes&lt;/code&gt; or &lt;code&gt;attributes-type&lt;/code&gt; as these are intended to be immutable through the life of the Live Activity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&amp;rsquo;s other interesting ones that you might want to consider but aren&amp;rsquo;t as important, like &lt;code&gt;stale-date&lt;/code&gt;, &lt;a href=&#34;https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications&#34;&gt;discussed in the docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;thats-it&#34;&gt;That&amp;rsquo;s it!&lt;/h2&gt;
&lt;p&gt;That should cover most things! I want to thank the incredible &lt;a href=&#34;https://x.com/ifrins&#34;&gt;Francesc Bruguera&lt;/a&gt; for helping me get unstuck a few places.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>The Caldera: a sleek split and wireless keyboard</title>
      <link>https://christianselig.com/2024/07/caldera-keyboard/</link>
      <pubDate>Sat, 20 Jul 2024 05:18:53 -0300</pubDate>
      
      <guid>https://christianselig.com/2024/07/caldera-keyboard/</guid>
      <description>&lt;p&gt;I designed my own keyboard and it&amp;rsquo;s freely available! I&amp;rsquo;m calling it the Caldera, and it&amp;rsquo;s basically my dream wireless split keyboard.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been using it for months, and I love it.&lt;/p&gt;
&lt;h2 id=&#34;video-overview&#34;&gt;Video overview&lt;/h2&gt;

    
        
        &lt;div style=&#34;position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;&#34;&gt;
          &lt;iframe allow=&#34;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&#34; allowfullscreen=&#34;allowfullscreen&#34; loading=&#34;eager&#34; referrerpolicy=&#34;strict-origin-when-cross-origin&#34; src=&#34;https://www.youtube.com/embed/7UXsD7nSfDY?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0&#34; style=&#34;position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;&#34; title=&#34;YouTube video&#34;
          &gt;&lt;/iframe&gt;
        &lt;/div&gt;
&lt;p&gt;If you&amp;rsquo;re a visual person, I made a fun little video showcasing the creation process as well as an overview of the keyboard. It serves as a nice tutorial too if you&amp;rsquo;re even vaguely interested in building your own custom keyboard. I&amp;rsquo;ve been wanting to make some more videos going forward, and this was a fun way to get into it! If you&amp;rsquo;d like some more videos on things I&amp;rsquo;m planning to create, subscribe to my YouTube channel!&lt;/p&gt;
&lt;h2 id=&#34;why-and-how&#34;&gt;Why and how?&lt;/h2&gt;
&lt;p&gt;I love split keyboards, they&amp;rsquo;re so nice on your wrists when you type all day and have helped my wrist pain immensely, but none of the existing keyboards were quite what I wanted. They were either wired, had key layouts I didn&amp;rsquo;t vibe with, or were too &amp;ldquo;techy&amp;rdquo; looking. I wanted a layout with all the right keys, a sleek design, and fully wireless with long battery life.&lt;/p&gt;
&lt;p&gt;Of course there&amp;rsquo;s many resources for &amp;ldquo;building your own keyboard&amp;rdquo;, but they&amp;rsquo;re normally just an assembly kit for someone else&amp;rsquo;s keyboard, letting you change the switches or key caps. I wanted something &lt;em&gt;fully custom&lt;/em&gt;, with the keys positioned exactly where I wanted them, and a clean design to boot. That means it needs a custom PCB.&lt;/p&gt;
&lt;p&gt;I learned about a cool piece of free, open source software called &lt;a href=&#34;https://github.com/ergogen/ergogen&#34;&gt;Ergogen&lt;/a&gt; that helps you to build your own completely custom keyboard and I got to work learning it. The process was surprisingly easy, and the video above goes into how to use it from a beginner&amp;rsquo;s perspective. The video also touches on KiCad, which Ergogen hands off the PCB to, and Fusion 360 which I use to design the case. Then you just have to flash some simple firmware on it &lt;a href=&#34;https://github.com/zmkfirmware/zmk&#34;&gt;from ZMK&lt;/a&gt; where you just tell it about your keyboard.&lt;/p&gt;
&lt;h2 id=&#34;resources&#34;&gt;Resources&lt;/h2&gt;
&lt;p&gt;Everything that I built for this keyboard &lt;a href=&#34;https://github.com/christianselig/caldera-keyboard&#34;&gt;is available open source on GitHub&lt;/a&gt;, from the Ergogen files, to the PCB files, to the 3D printable case files, to the ZMK firmware files. It&amp;rsquo;s all honestly pretty easy! The 3D printable files specifically &lt;a href=&#34;https://makerworld.com/en/models/545431&#34;&gt;can be found here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you want this keyboard for yourself, simply take the PCB files (the .zip gerber files on GitHub) and upload them to a PCB manufacturer (like JLCPCB or PCBWay, not sponsored by either), the PCBs will arrive, then it just requires a very beginner level amount of soldering (great first project honestly), some 3D printable cases, some standard keyboard supplies like keys and a controller, and then you just have to flash the firmware and you&amp;rsquo;ll have a functioning Caldera keyboard, or a basis for you to build your own completely custom keyboard! (It doesn&amp;rsquo;t have to be a split keyboard either!)&lt;/p&gt;
&lt;h2 id=&#34;supplies&#34;&gt;Supplies&lt;/h2&gt;
&lt;p&gt;Here are all the supplies needed for this project. It seems like a fair bit, but many are just great to have things that you can also use for other projects if you don&amp;rsquo;t have them already, and the rest are cheap just to grab for this project. None of these are affiliate links.&lt;/p&gt;
&lt;h4 id=&#34;general-tools&#34;&gt;General tools&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://pine64.com/product/pinecil-smart-mini-portable-soldering-iron/&#34;&gt;Pinecil soldering iron&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://store.bambulab.com/products/p1s&#34;&gt;Bambu Lab P1S printer&lt;/a&gt; (can likely also have a friend or local business 3D print the case for you!)&lt;/li&gt;
&lt;li&gt;Filament (I used Bambu&amp;rsquo;s &lt;a href=&#34;https://store.bambulab.com/products/pla-marble&#34;&gt;white marble PLA&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Kotto fume extractor (optional but highly suggested, please don&amp;rsquo;t breathe in solder fumes!)&lt;/li&gt;
&lt;li&gt;Solder (I use 0.6mm and recommend lead-free for health and environmental reasons)&lt;/li&gt;
&lt;li&gt;Tweezers (idk pick some up at a pharmacy if you don&amp;rsquo;t have any weirdo)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;keyboard-specific-things&#34;&gt;Keyboard specific things&lt;/h4&gt;
&lt;p&gt;I get most of these from Typeractive (except the PCB), makes one order easy, and has free shipping (not sponsored).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PCBs (see above)&lt;/li&gt;
&lt;li&gt;Choc low profile key switches of your choice (I like Choc Pro Reds personally, &lt;a href=&#34;https://typeractive.xyz/products/choc-switches&#34;&gt;Typeractive has them&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;nice!nano controller x2 (&lt;a href=&#34;https://typeractive.xyz/products/nice-nano&#34;&gt;Typeractive&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Controller hot swap sockets (&lt;a href=&#34;https://typeractive.xyz/products/machine-sockets-and-pins&#34;&gt;Typeractive&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;PS3 controller replacement battery (&lt;a href=&#34;https://www.amazon.com/Replacement-Playstation-Controller-CECHZC2E-CECHZC2U-3/dp/B09726K2LC&#34;&gt;Amazon&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Battery input jack (&lt;a href=&#34;https://typeractive.xyz/products/battery-jack&#34;&gt;Typeractive&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Diodes (&lt;a href=&#34;https://typeractive.xyz/products/smd-diodes&#34;&gt;Typeractive&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Reset button (&lt;a href=&#34;https://typeractive.xyz/products/reset-button&#34;&gt;Typeractive&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Kailh Choc hot swap sockets (&lt;a href=&#34;https://typeractive.xyz/products/hotswap-sockets?variant=45742200324327&#34;&gt;Typeractive&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Key caps (I like Choc MBK key caps, &lt;a href=&#34;https://typeractive.xyz/products/mbk-keycaps&#34;&gt;which Typeractive has&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Battery power switch (&lt;a href=&#34;https://typeractive.xyz/products/power-switch&#34;&gt;Typeractive&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;10x M2x3 screws, and 5x M2x4 standoffs for assembling each side of the case (so 20 screws and 10 standoffs in total) (I&amp;rsquo;d just grab a small kit on Amazon)&lt;/li&gt;
&lt;li&gt;(Optional) 6mm (0.24&amp;quot;) diameter x 2mm (0.08&amp;quot;) height rubber feet for the bottom of the case&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Totalling up the cost of the project, I&amp;rsquo;d estimate you could put this keyboard together for under $200 (talking about consumables, not including tools that you may or may not already have).&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/07/caldera-keyboard/banana.jpeg&#34; alt=&#34;The Caldera keyboard on a kitchen island with bananas around it and a LEGO figurine of Aloy from Horizon Zero Dawn&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;enjoy&#34;&gt;Enjoy!&lt;/h2&gt;
&lt;p&gt;If you build the keyboard or have feedback on my video about building it, I&amp;rsquo;d love to know! Hit me up on Twitter/X, Mastodon, or Threads! It was a really fun process to learn and was a lot easier than I ever imagined it to be, so I&amp;rsquo;d definitely encourage you to go out and try if there&amp;rsquo;s a keyboard you wish existed!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Announcing Juno 2.0</title>
      <link>https://christianselig.com/2024/06/announcing-juno-2/</link>
      <pubDate>Thu, 27 Jun 2024 10:16:17 -0500</pubDate>
      
      <guid>https://christianselig.com/2024/06/announcing-juno-2/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2024/06/announcing-juno-2/hero.jpeg&#34; alt=&#34;A Bento-style promotional image for Juno 2.0, a YouTube app for visionOS, highlighting its new features. The features include 360° and 180° video with 6K Metal upscaling, improved voice search, end card selection, faster performance, better video compatibility, new UI, streams and live videos, Siri control, watch history sync, playlists, smart window resizing, remember settings, and bug fixes&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Juno is &lt;a href=&#34;https://christianselig.com/2024/02/introducing-juno/&#34;&gt;an app I built&lt;/a&gt; for watching YouTube on the Apple Vision Pro, and it&amp;rsquo;s been such a fun project to build. It&amp;rsquo;s also been so great hearing how other people have been enjoying Juno since its launch, as well as providing awesome feedback and input to improve it.&lt;/p&gt;
&lt;p&gt;Today I’m releasing Juno 2.0, which incorporates a ton of that community feedback, and truly brings the app to the next level through extensive improvements and new features. Using it over the last little while I have had so many moments where I catch myself smiling. Browsing and watching YouTube on visionOS through Juno is honestly just so fun, immersive, and downright futuristic, and I genuinely think the best way to watch your favorite videos.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://juno.vision&#34;&gt;&lt;strong&gt;You can download Juno for YouTube 2.0&lt;/strong&gt;&lt;/a&gt; for just a fiver (and it’s a free update for existing customers), and unlock an amazing visionOS experience for YouTube.&lt;/p&gt;
&lt;p&gt;I think this version really lives up to the 2.0 moniker, so let’s get into all the improvements!&lt;/p&gt;
&lt;h2 id=&#34;immersive-360-and-180-degree-video-with-a-metal-twist&#34;&gt;Immersive 360 and 180 degree video (with a Metal twist!)&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/06/announcing-juno-2/lion-vr.jpg&#34; alt=&#34;A woman with an Apple Vision Pro on staring out at a savannah with a lion&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;You can now watch (what I would imagine to be) the internet’s largest source of 360° and 180° immersive video with Juno. Simply tap on the video from YouTube, and Juno will offers to open it in an immersive space, where the video will fully surround you as you watch it. You can soar through volcanoes, experience tours of incredible foreign lands, dive underwater, skydive, pretty much whatever you can imagine, it’s probably out there and now watchable in Juno for visionOS.&lt;/p&gt;
&lt;p&gt;The twist is, Juno even takes this a step further, and leverages Apple’s Metal engine to perform advanced AI upscaling and take the existing limitation of 4K video, and upscale it to 6K. This is the same technology that games like No Man’s Sky and Resident Evil Village have recently used to unlock incredible performance on Apple silicon, and with Juno it makes video feel even sharper and more realistic, with over twice the pixels versus 4K.&lt;/p&gt;
&lt;p&gt;From a technical perspective, this was a TON of work. As mentioned in previous blog posts, Juno leverages the YouTube website, as Google/YouTube do not give an (official) way to access video feeds directly (as doing so would allow you to circumvent ads and make them angry).&lt;/p&gt;
&lt;p&gt;With just the website, and no direct access to the video, creating an immersive experience is incredibly challenging, but using Metal and impressive GPU on the Vision Pro, Juno essentially takes a series of rapid snapshots of the web browser and stitches them together in a video feed, running each frame through Apple’s MetalFX machine learning upscaling engine, and then projecting that frame onto a sphere that surrounds the user.&lt;/p&gt;
&lt;p&gt;The result is truly incredible and I’ve seen some really mind-blowing videos. To quote a recent email I got from TestFlight, “I don’t know how you did it, this is ****ing incredible”.&lt;/p&gt;
&lt;p&gt;It’s not perfect (it&amp;rsquo;s capped at 30 fps, which is the normal frame rate for VR videos on YouTube), and there’s bound to be some bugs (please report!). Metal AI upscaling also does not work with all videos (it can introduce some lag for some very &amp;ldquo;busy&amp;rdquo; videos), so you can turn it on or off at will in the player.&lt;/p&gt;
&lt;p&gt;As mentioned in the last post, Apple’s M2 chip does not have a hardware decoder for the AV1 video codec that YouTube uses for 8K videos so it’s highly likely true 8K YouTube videos will be a ways away for the Vision Pro, and Juno bridges this gap nicely by offering all content in smart-upscaled 6K (which also covers the vast majority of immersive videos that don’t even have an 8K version uploaded).&lt;/p&gt;
&lt;p&gt;I do want to take a chance to massively thank everyone who has answered questions on social media or Discord about Metal, RealityKit, and more, without you this never would have been possible. I want to say an extra big thanks to Khaos Tian, Arthur Schiller, and Finn Voorhees, who I truly talked the ears off of, and as a result there’s a findable easter egg in Juno now where the immersive theater is named the KSV Immersive Theater.&lt;/p&gt;
&lt;h2 id=&#34;new-ui&#34;&gt;New UI&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/06/announcing-juno-2/ui.jpg&#34; alt=&#34;Juno&amp;#39;s new UI with native navigation bar, tab bar, and search bar floating in a hotel room&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;I really wanted to have an app for watching YouTube on visionOS available on the platform for day one, and I put a lot of hours into making that happen. I was admittedly a bit up against the clock however, so I’m glad with this update I was able to take some extra time to really go over the UI with a fine-toothed comb and make some nice improvements, and now Juno feels even more like a beautiful, native visionOS app (even if it’s basically just a very complex browser extension).&lt;/p&gt;
&lt;p&gt;With 2.0 Juno now enjoys a beautiful new tab bar, which loads content much faster and keeps them in memory. There’s also a new, beautiful navigation bar with a progressive blur to the content behind it, with a native search bar that allows you to just look at the microphone and talk to quickly search YouTube. It feels awesome, and is in many ways what I always wanted Juno to be from the outset.&lt;/p&gt;
&lt;h2 id=&#34;new-video-engine-better-video-compatibility-and-watch-history&#34;&gt;New video engine: better video compatibility, and watch history!&lt;/h2&gt;
&lt;p&gt;Juno 1.0 leveraged Apollo’s code that used the YouTube embed player to play back content. This allowed me to ship quickly, but at the same time had issues such as some video creators disabling playback in the embed player (which would cause Juno to fallback to the website), and other ramifications like not marking that you watched that video in your history.&lt;/p&gt;
&lt;p&gt;Juno 2.0 now just uses the website as its video component, as it does with the rest of the app. With that, more videos are able to play back, videos are correctly added to your watch history, and also just greatly simplifies Juno, as Juno is now simply loading the YouTube video URL as a web browser would, and just applying custom CSS and interacting with it through JavaScript to make it feel at home on visionOS.&lt;/p&gt;
&lt;p&gt;(And yes, if you’re curious, Juno still does not block ads for reasons of not angering Google, but as it’s just using the website if you have YouTube Premium you will not see any ads in Juno.)&lt;/p&gt;
&lt;h2 id=&#34;back-button&#34;&gt;Back button&lt;/h2&gt;
&lt;p&gt;Yeah, this gets its own category, because it was surprisingly really, really hard! Back in 2004 when both the world and websites were much simpler, you would click a link on a website, and the browser would load a new webpage. Now we got all fancy with JavaScript and whatnot, and websites like YouTube (for good reason, it makes things feel faster), instead of loading a whole new webpage each click, normally just swap out the contents of the current website with JavaScript, and then update the URL to indicate the change.&lt;/p&gt;
&lt;p&gt;Cool! That’s great! I’m so happy for you! But that makes a back button a lot more annoying, because now web browsers get confused a lot when you hit back (you didn’t really change a page), and often the browser will update the URL but the page contents will stay the same, and you’re just staring at your screen confused. Twitter, Threads and Mastodon have told me I’m not imagining things and that does happen a lot to other folks.&lt;/p&gt;
&lt;p&gt;So Juno has a back button, but also sprinkles some special YouTube magic sauce (do you sprinkle sauce? maybe flakes) where it attempts to detect the web browser being confused, by checking if the HTML of the current webpage is a reasonable match for the current URL. If they don’t match, it means the web browser got confused and just updated the URL without updating the actual page contents, and Juno does a full refresh of the webpage which forces the web browser to refetch the proper contents for the current URL. The system doesn’t have to step in that often (maybe 10% of the time), but when it does, it’s very helpful and I could not ship a half–working back button.&lt;/p&gt;
&lt;h2 id=&#34;playlist-support&#34;&gt;Playlist support&lt;/h2&gt;
&lt;p&gt;I suppose “playlist support” covers two new features! The first, is that if you select a video from a playlist, for instance a video series a YouTuber made, it will automatically play the next video in the series upon each video ending.&lt;/p&gt;
&lt;p&gt;The second aspect would be the new playlists page, rather than just a text menu like in Juno 1.0, you now get access to the full YouTube website playlist view, with rich thumbnails!&lt;/p&gt;
&lt;h2 id=&#34;better-window-resizing&#34;&gt;Better window resizing&lt;/h2&gt;
&lt;p&gt;Juno’s always been a pretty smart cookie about window sizing, and automatically adapts the window to the aspect ratio of the video so you don’t get any black bars. But with this update, Juno will also properly manage the size of the window. So if you want to make your window even bigger, Juno will remember that approximate size and adapt the aspect ratio accordingly, so you can keep your ideal video watching experience perfectly intact across a variety of videos.&lt;/p&gt;
&lt;h2 id=&#34;juno-remembers&#34;&gt;Juno remembers&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/06/announcing-juno-2/pie.jpg&#34; alt=&#34;A video about blueberry pie in Juno with the playback speed controls visible&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Now Juno is much better at remembering your settings between subsequent video watches. In addition to the window sizing mentioned above, Juno will remember features like captions, playback rate, and more.&lt;/p&gt;
&lt;h2 id=&#34;select-end-cardsauto-play&#34;&gt;Select end cards/auto-play&lt;/h2&gt;
&lt;p&gt;There are two kinds of YouTube users, those who leave “auto-play” enabled (so YouTube will just play the next recommended video automatically after you finish your current one), and folks who have that turned off (YouTube will show a grid of video recommendations to watch next). Juno loves everyone and now supports both of those, so if you have it enabled on the website, Juno will auto-play the next video automatically with its countdown, and if you have that disabled, you can pick from YouTube’s recommended next videos.&lt;/p&gt;
&lt;h2 id=&#34;siri-integration&#34;&gt;Siri integration&lt;/h2&gt;
&lt;p&gt;This is a really handy one and was very highly requested (for good reason). Now you can just say “Siri, pause”, “Siri, playback speed 2 times”, “Siri jump to 5 minutes and 30 seconds”, etc. and Juno will listen accordingly so you don’t even have to pick up a finger!&lt;/p&gt;
&lt;h2 id=&#34;time-linking&#34;&gt;Time-linking&lt;/h2&gt;
&lt;p&gt;If you select a video on the website that’s linked to a specific time (say, 2 minutes and 28 seconds), Juno will incorporate that instead of just playing from the beginning. Same if you drag and drop a YouTube link onto Juno from a friend. On the flip side, if you share a YouTube URL from Juno, it will now embed the current time into the URL so your friend (or enemy? idk) that receives the link doesn’t have to guess what part of the video you were talking about (or worse, manually scrub to it).&lt;/p&gt;
&lt;h2 id=&#34;options-menu-integration&#34;&gt;Options menu integration&lt;/h2&gt;
&lt;p&gt;You can now properly interact with the YouTube options menus (those vertical ••• menus, for the unaware), so if you want to add a video to a playlist, remove something from your watch history, or anything like that, you’re now easily able to!&lt;/p&gt;
&lt;h2 id=&#34;open-in-safari&#34;&gt;Open in Safari&lt;/h2&gt;
&lt;p&gt;The Share menu for a video (in addition to fixing a potential crash that could occur), now allows you to quickly open a YouTube video in Safari if you so choose, which can be a great way to access features of YouTube that Juno might not offer, like reading comments, for instance.&lt;/p&gt;
&lt;h2 id=&#34;bug-fixes&#34;&gt;Bug fixes&lt;/h2&gt;
&lt;p&gt;Lots of little bug fixes across the board that should make everything operate even smoother. Captions should work more reliably (and tell you in cases where they’re unavailable), drag and drop as well as sharing should no longer crash in weird cases, the double-tap to go forward/back 10 seconds should be faster, seeking should be smoother, among many others!&lt;/p&gt;
&lt;h2 id=&#34;misc-disabling-spatial-audio&#34;&gt;Misc: disabling spatial audio&lt;/h2&gt;
&lt;p&gt;A common request has been to be able to disable spatial audio (right now visionOS makes Juno’s audio feel like it’s coming from the window itself, so if it’s beside you, the audio will feel beside you). Some folks don’t love this and wish it just sounded like normal audio. Unfortunately Apple doesn’t expose an API to control this for web views, BUT iOS does have a kinda hidden way to override it. If you bring up the system Control Center and long-press the volume slider, you can disable it there. So that’s a good solution if you’re looking for a way!&lt;/p&gt;
&lt;h2 id=&#34;lastly-answering-googleyoutubes-issues-with-juno&#34;&gt;Lastly: answering Google/YouTube&amp;rsquo;s issues with Juno&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve said &lt;a href=&#34;https://christianselig.com/2024/02/introducing-juno/&#34;&gt;from the initial launch&lt;/a&gt;, Juno is built as a web-wrapper for YouTube, akin to a browser extension, and purposefully built with full respect for the YouTube website and experience, and as a result does not block ads in any capacity, nor does it introduce extra functionality like downloading videos offline that could facilitate that. Further, Juno doesn&amp;rsquo;t even use &lt;em&gt;any&lt;/em&gt; &lt;a href=&#34;https://developers.google.com/youtube/v3&#34;&gt;YouTube APIs&lt;/a&gt;, as it has no need to: it just wraps the website, and uses CSS and JavaScript to style the website and functionality more in line with visionOS. This is in contrast to other third-party tools that for instance scrape the YouTube website for applicable video URLs and use those directly, or those that integrate ad-blocking functionality.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/06/announcing-juno-2/google-email.png&#34; alt=&#34;An email from YouTube Legal about Juno, the contents of which are linked below.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;In April, YouTube announced it would be &lt;a href=&#34;https://support.google.com/youtube/thread/269521462?hl=en&#34;&gt;cracking down specifically on ad-blocking third-party apps&lt;/a&gt;, and weirdly, Juno got an email at the end of April from YouTube Legal that voiced some concerns. You can view the full contents of their email here: &lt;a href=&#34;https://christianselig.com/juno-youtube-email-april-26-24.txt&#34;&gt;https://christianselig.com/juno-youtube-email-april-26-24.txt&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The short of it is that, while no issues with ad-blocking were presented, they did take issue with a few areas, firstly that Juno is in violation of the YouTube API Terms of Service, and secondly that Juno alludes to YouTube trademarks and iconography. Both of these issues were very puzzling.&lt;/p&gt;
&lt;p&gt;For the first issue, as I mentioned, Juno makes no use of the YouTube API so it&amp;rsquo;s unclear to me how it could be in violation of it. Juno operates in much the same way a browser extension would through CSS and JavaScript. Google&amp;rsquo;s own Chrome both has native support for browser extensions, and even has native features that &lt;a href=&#34;https://support.google.com/chrome/answer/14218344?hl=en&#34;&gt;customize the styling and experience of webpages&lt;/a&gt;. They also mentioned they did not like that Juno uses the embed player, &lt;a href=&#34;https://github.com/youtube/youtube-ios-player-helper&#34;&gt;despite Google themselves having a library&lt;/a&gt; showing this as the preferred way to integrate YouTube videos into apps.&lt;/p&gt;
&lt;p&gt;Secondly, for iconography and trademarks, I can only assume YouTube is referring to the YouTube logo present on the YouTube.com homepage that Juno loads. Obviously, as I&amp;rsquo;m just loading a website, I am not putting that there. For the name, &amp;ldquo;Juno for YouTube&amp;rdquo;, &lt;a href=&#34;https://developers.google.com/youtube/terms/branding-guidelines&#34;&gt;YouTube has branding guidelines&lt;/a&gt; that specifically permit this: &amp;ldquo;For example, you cannot call your application &amp;ldquo;YouTube for Kids&amp;rdquo; or &amp;ldquo;YouTube Education&amp;rdquo;. However, you may reference the fact that your app is for YouTube or works with YouTube by stating that it is a &amp;ldquo;great app for YouTube&amp;rdquo; or using other similar language&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Like I said, from the get-go I&amp;rsquo;ve wanted be just a well-behaved visionOS wrapper for the website, so in the interest of making YouTube happy, back in April I responded to Google that as Juno does not use any YouTube APIs, I do not see how it could be violating them, however I would be putting an update out to attempt to address concerns. This is that update, and I switched from the embed player to just using styling the website player, I manually removed the YouTube logo from the homepage, and I added &amp;ldquo;Unofficial&amp;rdquo; to the subtitle and description of Juno on the App Store.&lt;/p&gt;
&lt;p&gt;Now that this update is out, hopefully this appeases Google, as they have also (as of a few days ago) filed a complaint with the App Store directly. I&amp;rsquo;ll obviously push back, as I believe Juno is just getting caught up in the crosshairs of Google&amp;rsquo;s targetting of apps that do have ad-blocking, and an app that fundamentally themes a website is nothing new, novel, or insidious.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;But&lt;/strong&gt;&lt;/em&gt;, if you&amp;rsquo;re hoping for a bunch of drama to spin out of this (akin to last year with Reddit), I&amp;rsquo;m sorry to say I&amp;rsquo;m not looking to go down that route again. Juno has been a ton of fun to build, a ton of fun to use, and in my opinion a great benefit to both the Vision Pro platform as well as YouTube users, but it&amp;rsquo;s fundamentally a fun hobby project for me, and if it comes down to fighting a drawn out battle against a giant like Google again… well, that no longer sounds like fun and I&amp;rsquo;ll happily let Google win, haha. Worst case scenario I&amp;rsquo;ve had a ton of fun building this, and I got to get my feet wet with visionOS, so it&amp;rsquo;s all good!&lt;/p&gt;
&lt;p&gt;That being said, if Juno does disappear from sale from the App Store, I don&amp;rsquo;t see any reason to believe that it will cease to work for existing users as it just operates on the website, unless YouTube/Google dramatically rework the website. Again, this wouldn&amp;rsquo;t seem beneficial to them as Juno is just the website and shows all ads and whatnot. Also this is genuinely not a coy way of saying to get the app before it goes away, it may cease to work and I wouldn&amp;rsquo;t want anyone buying it going into it with a false belief that it will work forever. But if I had to guess it will work.&lt;/p&gt;
&lt;h2 id=&#34;thank-you-&#34;&gt;Thank you! ❤️&lt;/h2&gt;
&lt;p&gt;I really hope you enjoy the Juno 2.0 update, I had a lot of fun building it and I’d love to know what you think. If you haven’t checked out Juno yet, &lt;a href=&#34;https://juno.vision&#34;&gt;you can download it here&lt;/a&gt; (if you think you’re going to forget, you can also purchase it from your iPhone or iPad). And if you have checked out Juno, you’re awesome, no doubt very well-read, have incredible taste, and I thank you!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Choosing a travel pack is hard</title>
      <link>https://christianselig.com/2024/04/choosing-a-backpack-is-hard/</link>
      <pubDate>Sun, 28 Apr 2024 12:49:02 -0300</pubDate>
      
      <guid>https://christianselig.com/2024/04/choosing-a-backpack-is-hard/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2024/04/choosing-a-backpack-is-hard/hero.png&#34; alt=&#34;An assorted group of 10 different black travel backpacks, two rows with 5 columns each.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;I love the &amp;ldquo;carry-on only&amp;rdquo; traveling style, it&amp;rsquo;s cheaper and you don&amp;rsquo;t have to worry about airlines losing your stuff. Outside of requiring a bit more planning, what&amp;rsquo;s not to love?&lt;/p&gt;
&lt;p&gt;Turns out this is a beloved product category with a passionate community behind it, and as a result a lot of manufacturers are making really awesome bags. As a result you see different bags with different strategies, and start to develop a taste for what &lt;em&gt;you&lt;/em&gt; want.&lt;/p&gt;
&lt;p&gt;These bags are all incredible, but none are a &lt;em&gt;perfect&lt;/em&gt; fit for me. I had the flu a few weeks ago and was writing down my thoughts on bags for an easy reference (I kept rediscovering the same bags every few months), but I thought I&amp;rsquo;d make it into a little blog post in case it was helpful to someone else bag searching. I&amp;rsquo;ll basically list a popular bag, and my thoughts on it.&lt;/p&gt;
&lt;h2 id=&#34;what-i-want&#34;&gt;What I want&lt;/h2&gt;
&lt;p&gt;Quick precursor to anyone yet bit by the backpack bug: there&amp;rsquo;s basically 3 categories of bags. The first, day bags, are normally between 10-20L and intended as a smaller bag to carry a few things at your destination. Every day carry bags (EDC) are normally 20-25L, a bit bigger, and for a short weekend trip, or just a good, general purpose bag that carries a decent amount but isn&amp;rsquo;t too bulky. The last category is a proper travel backpack, normally 30-45L, whose goal is to hold all your stuff for a decent length trip, while still being small enough to fit the carry-on dimensions for most airlines. 40L is ideal for me, 35L is a bit on the smaller side, and 45L gets me scared with certain airlines.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m looking for that last category, a travel backpack, with specific goals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Not a roller bag, too heavy, less flexible, and those with roller bags are often the first airline employees target when they need to start checking bags. I want a backpack&lt;/li&gt;
&lt;li&gt;Not too heavy, ideally 2.5 lb - 3.5 lb (1.1 kg to 1.6 kg) for just the bag. I find beyond this the bag itself starts to have some heft, and it feels largely unnecessary when there&amp;rsquo;s super well-made bags in that &amp;ldquo;Goldilocks zone&amp;rdquo;. My scale is kinda: &amp;gt; 4 lb: why, &amp;gt; 3.5 lb: okay, &amp;gt; 3 lb: good, &amp;gt; 2 lb: dang!, &amp;lt; 2 lb: how. Essentially: my bag with nothing in it should not weigh more than my MacBook Pro.&lt;/li&gt;
&lt;li&gt;External water bottle holder. Really don&amp;rsquo;t get why some manufacturers started putting them inside the bag. At worst it leaks a bit on your expensive stuff, at best it&amp;rsquo;s much more inconvenient to get at when walking around the airport and takes up space in the interior of your bag&lt;/li&gt;
&lt;li&gt;Good laptop storage, with a slot for an iPad too. Not a big fan of laptop storage on the side, when you&amp;rsquo;re trying to grab your laptop in close quarters and just have the bag between your legs, having to rotate the bag versus just yanking it out from the top is less ideal&lt;/li&gt;
&lt;li&gt;Not a zillion compartments. Some &lt;em&gt;love&lt;/em&gt; this, which is totally cool obviously, but I like using packing cubes and a tech pouch, so I&amp;rsquo;d rather just have one massive vacuous main compartment rather than having a bunch of mini-compartments that take up space and add weight that I won&amp;rsquo;t use&lt;/li&gt;
&lt;li&gt;A space to put a small day bag. I love travelling with a good sized travel pack, but having a day bag &lt;em&gt;inside&lt;/em&gt; it so when I get to the destination I can leave the hefty bag at the hotel/Airbnb and just carry a light bag. I use the Aer Go Pack 2 for this, which isn&amp;rsquo;t as packable as some daypacks because I still want something with some laptop padding, so I need an area to slide this in, which is normally pretty easy&lt;/li&gt;
&lt;li&gt;Stowable backpack straps. Sometimes you&amp;rsquo;re just throwing it in the back of a car and driving somewhere, and being able to hide the backpack straps to make it just a sleek little grab bag is super handy&lt;/li&gt;
&lt;li&gt;Compression straps on the outside to cinch it down a bit if needed&lt;/li&gt;
&lt;li&gt;35L to 40L capacity, I find below that and it&amp;rsquo;s too small for a decent length trip, and above that you start to get into issues with some airlines deeming it too big a carry-on&lt;/li&gt;
&lt;li&gt;Quality back harness system. Be it trekking between two far apart terminals, or a long walk to your hotel at the destination, having the bag be super comfy on your back, with padded straps, a sternum belt, and a hip belt, and ideally load lifters to position the bag on your back, is super awesome&lt;/li&gt;
&lt;li&gt;Zip-open suitcase/clamshell opening style. It has to be able to open fully versus a normal backpack that just zips down partway, otherwise it&amp;rsquo;s a pain to get at anything at the bottom of the bag. Bonus points if it opens horizontally versus vertically which I find just a bit nicer, but not a deal-breaker&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;what-are-liters-even&#34;&gt;What are liters even&lt;/h2&gt;
&lt;p&gt;One quick note before we start, is that bags are measured in liters (a measurement of volume), but how manufacturers measure their volume is all over the place. Some measure how much the interior will hold, some measure how much space the bag itself takes up, some seem a little disingenuous period. It&amp;rsquo;s all very hard to tell just based on the listed capacity of the bag.&lt;/p&gt;
&lt;p&gt;Shout out to &lt;a href=&#34;https://www.youtube.com/@OneBagTravels&#34;&gt;OneBagTravels&lt;/a&gt;, far and away my favorite YouTube bag reviewer because he actually stuffs the bags with packing peanuts and measures the resulting capacity rather than just repeating manufacturer claims. I wish every reviewer did this.&lt;/p&gt;
&lt;h2 id=&#34;backpacks&#34;&gt;Backpacks&lt;/h2&gt;
&lt;p&gt;Okay, here are a bunch of the bags I&amp;rsquo;ve encountered, and my notes on why I love them and why I don&amp;rsquo;t. Again, this is not saying any of these bags are bad, one or more might fit you perfectly, but for my priorities personally they&amp;rsquo;re all missing something.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m putting a ⭐️ beside the ones that I personally like a fair bit, and ✨ for ones I like a bit less but that I think with some modifications from the manufacturer could be awesome, in no particular order. Also note that the vast majority of these I haven&amp;rsquo;t tried, I&amp;rsquo;m just trying to summarize other reviews and their specs.&lt;/p&gt;
&lt;h3 id=&#34;-osprey-farpoint&#34;&gt;⭐️ Osprey Farpoint&lt;/h3&gt;
&lt;p&gt;This has been my go-to bag for the better part of a decade, and it&amp;rsquo;s gone all over the globe with me. I like it a fair bit, but I don&amp;rsquo;t love it.&lt;/p&gt;
&lt;h4 id=&#34;good&#34;&gt;Good&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;40L and holds a ton&lt;/li&gt;
&lt;li&gt;Light (my bag weighs 1.32 kg or 2.9 lb)&lt;/li&gt;
&lt;li&gt;Despite being light and trekking around the globe with me for ages still almost looks new, so I can safely say this bag is very well made&lt;/li&gt;
&lt;li&gt;Super comfortable, robust harness system that you can zip up to completely hide&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;bad&#34;&gt;Bad&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;The laptop compartment is at the front of the bag, as opposed to close to your back, so the weight balance of the bag isn&amp;rsquo;t great, and it can be hard to put the laptop in (this has been fixed in a more recent bag revision, though)&lt;/li&gt;
&lt;li&gt;The water bottle holders are just… bad, in that they&amp;rsquo;re both at the front (hard to access when wearing the bag), and hold water bottles so poorly that the bottles almost jump out of the bag&lt;/li&gt;
&lt;li&gt;There&amp;rsquo;s also a lot of dangly clips and straps everywhere&lt;/li&gt;
&lt;li&gt;I wish you could hide the hip belt separately since I only use it once in awhile, but you either have to hide all the backpack strap area, or leave it all open&lt;/li&gt;
&lt;li&gt;It only zips open like 90% of the way so packing things into the bottom is a bit awkward&lt;/li&gt;
&lt;li&gt;The shape when fully packed out is kinda weird and bulges from the center out, kinda looking like a potato chip at the top, would prefer something a bit more boxy&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;aer-travel-pack-3&#34;&gt;Aer Travel Pack 3&lt;/h3&gt;
&lt;p&gt;I love Aer bags, so I bought this with high hopes but ultimately returned it.&lt;/p&gt;
&lt;h4 id=&#34;good-1&#34;&gt;Good&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;I love how it looks&lt;/li&gt;
&lt;li&gt;Sidewalls are slightly less floppy than my Osprey Farpoint so easier to pack into&lt;/li&gt;
&lt;li&gt;Very comfortable harness system (though lacks a hip belt)&lt;/li&gt;
&lt;li&gt;Straps are well thought out so it doesn&amp;rsquo;t dangle a bunch&lt;/li&gt;
&lt;li&gt;Zippers feel super premium&lt;/li&gt;
&lt;li&gt;Nice, functional boxy shape&lt;/li&gt;
&lt;li&gt;Love the front slash pocket for something like a light rain jacket&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;bad-1&#34;&gt;Bad&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;It&amp;rsquo;s really heavy, at 4.1 lb it&amp;rsquo;s 40% heavier than my Osprey Farpoint and you can really tell. It feels like the difference between a MacBook Air and a MacBook Pro, except it holds less than the Osprey which is already incredibly made so it kinda just feels… over-engineered?&lt;/li&gt;
&lt;li&gt;It doesn&amp;rsquo;t feel like it holds that much, especially compared to the Osprey. On the pack it says a 5L difference but it feels closer to 10L&lt;/li&gt;
&lt;li&gt;The water bottle holder is almost criminally bad, it&amp;rsquo;s on the side at least, but does not do a good job of holding literally any water bottle I own, it falls out super easily. My other two Aer bags, a Go Pack 2 and a Pro Pack 20L, both have infinitely better water bottle holders despite being half the size, I&amp;rsquo;m so confused&lt;/li&gt;
&lt;li&gt;Definitely a neutral for some, but I don&amp;rsquo;t like the admin/tech compartment. Like I mentioned, I use a tech pouch anyway (so I can easily transfer things to my day bag) and this cuts into the available storage area while also adding a decent amount of weight (it&amp;rsquo;s a hefty tech compartment, my attempts to measure it with a kitchen scale put it at about 0.4 lb alone). Also losing the admin compartment means less zippers on the outside, which makes it easier to know what to open to get to all your stuff.&lt;/li&gt;
&lt;li&gt;Can&amp;rsquo;t hide the backpack straps at all&lt;/li&gt;
&lt;li&gt;Minor one, but the top grab handle is in the middle of the pack, versus closer to one edge, which means if you try to hang it on a hook at a bathroom stall at an airport or something it&amp;rsquo;s really hard, versus bags like Ospreys that put it closer to one edge so a hook can still reach it&lt;/li&gt;
&lt;li&gt;The laptop compartment almost feels too padded to the extent that it&amp;rsquo;s bulky. I only bring this up because the bag is already heavier than most, and this feels like an area where they unnecessarily added weight. All my other bags protect my laptop beautifully with much less bulk, though probably Aer&amp;rsquo;s alone could survive an actual fall out of a plane&lt;/li&gt;
&lt;li&gt;Inside isn&amp;rsquo;t super visible, it&amp;rsquo;s like a dark grey, wish it was more of a light grey like the Aer Pro Pack 20L so you&amp;rsquo;d get increased contrast against your belongings. The darker it is inside the more it&amp;rsquo;s like an abyssal void&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;d love to see Aer do what Tortuga did (see below) and release a &amp;ldquo;Lite&amp;rdquo; version that cuts a ton of weight off. We&amp;rsquo;re kinda due for a Travel Pack 4, so maybe?&lt;/p&gt;
&lt;h3 id=&#34;evergoods-civic-travel-bag-35&#34;&gt;Evergoods Civic Travel Bag 35&lt;/h3&gt;
&lt;p&gt;Great, but heavy (4 lb), and can&amp;rsquo;t stow backpack straps. Love that it just has one, vacuous main compartment though. Very expensive. Props for having a water bottle holder, but minus props for only having a side laptop compartment (and it&amp;rsquo;s weirdly big), especially for the weight I wish it was a proper, full-zip compartment. Love the look, and love how when packed out it still has a nice shape.&lt;/p&gt;
&lt;h3 id=&#34;-minaal-carry-on-30-bag&#34;&gt;⭐️ Minaal Carry-On 3.0 Bag&lt;/h3&gt;
&lt;p&gt;This is one of my favorites. Only weighs 3.1 lb, no tech compartment, has a pretty sleek look, and one big main compartment for item storage. I also love how the laptop compartment opens all the way, I think all bags should do this, that means I can easily stash my day bag in there flattened down. Stowable backpack straps too.&lt;/p&gt;
&lt;p&gt;On the bad side the water bottle holder looks as bad as Aer&amp;rsquo;s, it&amp;rsquo;s very expensive, the inside is black so it&amp;rsquo;s like a void, and the storage system seems weird. Like, instead of the part closer to your body holding the clothes, it flops open and the floppy part holds the clothes, and it looks quite floppy. Also it looks like the main compartment has packing cube style zippers built into it which take up volume, and again I&amp;rsquo;d rather just use my own.&lt;/p&gt;
&lt;p&gt;I feel like this bag is a small revision away from being perfect. Again we&amp;rsquo;re due for a Carry-On 4.0 revision, so maybe? I would rank this number 4 currently.&lt;/p&gt;
&lt;h3 id=&#34;away-outdoor-convertible-backpack&#34;&gt;Away Outdoor Convertible Backpack&lt;/h3&gt;
&lt;p&gt;Pretty good! 45L and 3.3 lb is impressive, wish it was maybe a tad smaller though. Love the nice big bucket to throw everything into, and love that it opens horizontally. No water bottle holder means straight to jail though. Also the harness system seems a little lacking (no load lifters, no hip belt), and I&amp;rsquo;m not a big fan of side laptop compartments, and this one doesn&amp;rsquo;t even have an iPad slot in the compartment.&lt;/p&gt;
&lt;h3 id=&#34;ablecarry-max&#34;&gt;Ablecarry Max&lt;/h3&gt;
&lt;p&gt;30L is kind of awkward, a large bag for daily use but a bit small for traveling. Love the design visually though. No external water bottle holder.&lt;/p&gt;
&lt;h3 id=&#34;tortuga-backpack-pro&#34;&gt;Tortuga Backpack Pro&lt;/h3&gt;
&lt;p&gt;Too heavy (4.5 lb!!), not personally a big fan of having a separate tech compartment, price is kinda bonkers. Lacks stowable backpack straps.&lt;/p&gt;
&lt;h3 id=&#34;-tortuga-backpack-lite&#34;&gt;⭐️ Tortuga Backpack Lite&lt;/h3&gt;
&lt;p&gt;Love this one a lot, in my top three. 1lb less weight than the Pro version. Downside is no iPad compartment, which is unfortunate because even just a small piece of fabric separation would have been nice, but they did ditch the admin compartment which is nice. Would have been great if they made the laptop compartment zip all the way open like the Minaal so you could shove a day bag in there easily too. I also wish they would also ditch the dividers in the main compartment which take up space and require you to organize your things within them, rather than just using packing cubes which I&amp;rsquo;d prefer. Yay you can hide backpack straps, and looks like it has an awesome water bottle holder. Have to knock down a point for being incredibly hard to acquire in Canada for some reason. Looks like a nice functional boxy shape too.&lt;/p&gt;
&lt;p&gt;Again, almost perfect, and $100 cheaper than the Pro version at $250. I would rank this number 1 of these bags for me, but still not perfect.&lt;/p&gt;
&lt;h3 id=&#34;airback&#34;&gt;Airback&lt;/h3&gt;
&lt;p&gt;Kind of a whacky one, uses vacuum compression to suck the bag down, and it at least &lt;em&gt;seems&lt;/em&gt; like it relies on that to get the actual quoted 48L capacity? I already compress my clothes a fair bit with a compression packing cube (again shout out to Peak Design), so the big vacuum bag in the center kinda just seems like it would get in the way for me, and I&amp;rsquo;m not sure how much it holds &amp;ldquo;normally&amp;rdquo; without factoring in that it says up to 50% more gear thanks to compression, so that would put it at about 31L? A bit small, and if that&amp;rsquo;s the case at 3.7 lb the weight is not great. But the water bottle holder and harness system both seem not that great as well. Laptop compartment with separate iPad stash though, just wish the compartment opened up fully. Interesting bag for the right person, but not for me.&lt;/p&gt;
&lt;h3 id=&#34;tropicfeel-shell&#34;&gt;Tropicfeel Shell&lt;/h3&gt;
&lt;p&gt;Really like the design of this one, and it&amp;rsquo;s on the lighter side at 3.3 lb. Water bottle holder looks good. Seems quite small for its claim of holding 40L, wish a channel would do the ol&amp;rsquo; packing peanuts test but I couldn&amp;rsquo;t find any. It almost seems like it can only store 40L if you open/unroll the top fully but then your stuff is kinda exposed? Can sorta stow backpacks traps with a clever clip-across system. Wish the laptop compartment was fully separate so I could stow my day bag in there separately.&lt;/p&gt;
&lt;h3 id=&#34;tom-bihn-synik&#34;&gt;Tom Bihn Synik&lt;/h3&gt;
&lt;p&gt;No external water bottle holder, a bit small at only 30L. Harness system seems a bit minimal. Not much laptop padding and no iPad slot. Love how vacuous the bucket storage area looks though.&lt;/p&gt;
&lt;h3 id=&#34;tom-bihn-aeronaut&#34;&gt;Tom Bihn Aeronaut&lt;/h3&gt;
&lt;p&gt;This looks pretty nice, uses its size really well at 45L, while still being just under 3 lb in some fabrics. Harness system seems kinda meh, with no hip belt or air channels for breathability. Also no laptop compartment at all is kind of a head scratcher. Tom Bihn&amp;rsquo;s institutional hatred of easy access to water will also perpetually vex me.&lt;/p&gt;
&lt;h3 id=&#34;-tom-bihn-techonaut&#34;&gt;⭐️ Tom Bihn Techonaut&lt;/h3&gt;
&lt;p&gt;45L, just under 3 lb (impressive). Has a padded laptop compartment, but no iPad compartment which is unfortunate. Seems like Tom Bihn is slowly inching toward a normal water bottle pocket with this one but it&amp;rsquo;s still (per reviews) annoying to get at. Has a hip belt which is nice, but harness system still seems a little anemic for a 45L bag, would love to at least see load lifters on the backpack straps. You can even stow them! Love the fabric and color options. Honestly with a normal water bottle holder, and a laptop compartment that zipped all the way open with an iPad holder this would be a compelling bag. 45L just feels slightly too big for me, would prefer in the realm of 40L. Also eye-wateringly expensive though at $430.&lt;/p&gt;
&lt;h3 id=&#34;-pakt&#34;&gt;✨ Pakt&lt;/h3&gt;
&lt;p&gt;Love the creativity and thought put into this bag, but for me a bit too organization heavy, and as a result also a hefty boy at 4.4 lb for the 45L version, or 4.1 lb for the 35L version. Also very expensive, and the water bottle holder looks dubious. I like that the bag is expandable or has room for a day bag, which would be nice for the 35L version to get a bit more space. Wish it had stowable backpack straps.&lt;/p&gt;
&lt;p&gt;Would love to see them produce a slightly more minimal version that&amp;rsquo;s a bit cheaper, and lighter while still retaining the half-and-half main compartment opening where you can put packing cubes on either side.&lt;/p&gt;
&lt;h3 id=&#34;heimplanet-transit-line-travel-pack&#34;&gt;Heimplanet Transit Line Travel Pack&lt;/h3&gt;
&lt;p&gt;34L, okay weight at 3.6 lb. Doesn&amp;rsquo;t open fully clamshell, only about 2/3 of the way down if you want to open it &amp;ldquo;normally&amp;rdquo;, but if you remove the divider between the laptop compartment and main compartment it zips all the way, but then it&amp;rsquo;s one big compartment shared with the laptop, and the packing part is on the floppy side of the bag.&lt;/p&gt;
&lt;h3 id=&#34;topo-designs-40l&#34;&gt;Topo Designs 40L&lt;/h3&gt;
&lt;p&gt;I like that the backpack straps are nicely stowable. No iPad holder in laptop compartment, which seems small overall and on the side. Has a tech compartment unfortunately, but I like that the inside is just a big bucket. Water bottle holder is external but looks quite tiny.&lt;/p&gt;
&lt;h3 id=&#34;thule-aion&#34;&gt;Thule Aion&lt;/h3&gt;
&lt;p&gt;I like this one a fair bit, 40L and a svelte 3.2 lb. I don&amp;rsquo;t love the exterior material (waxed canvas), and wish they went with something a bit more… normal? It looks like it breaks in weirdly per YouTube reviews. Love how it just has one massive compartment that opens horizontally, but I wish that the compartment on the &amp;ldquo;ceiling&amp;rdquo; of the main compartment didn&amp;rsquo;t have its own volume, which it appears to which can take away from the main &amp;ldquo;bucket&amp;rdquo;. Good water bottle holder. Not in love with the horizontal laptop compartment, but at least it has room for an iPad. If it was a fully zip-open compartment I could put a day bag in there too which would be nice, but as-is not sure where I&amp;rsquo;d put that. Has a nice small admin compartment/pocket thing at the top. Grab handles look pretty meh. No external compression straps which feels like a bit of a miss. No stowable straps.&lt;/p&gt;
&lt;h3 id=&#34;-thule-landmark&#34;&gt;⭐️ Thule Landmark&lt;/h3&gt;
&lt;p&gt;40L, 3.2 lb, nice job Thule (same as Aion)! Nicer material than the Aion, but the weird helmet thing at the top is certainly a visual choice, and Pack Hacker still didn&amp;rsquo;t really find the material that durable. Stowable straps, okay harness system. Big cavernous bucket for storing items, with no other organization which is nice. Laptop storage is on the side (ehh) versus zipping all the way down, but does at least have a place for an iPad. I think a redo of this bag with better materials, getting rid of the funny helmet, and making a proper laptop compartment would make it pretty darn compelling.&lt;/p&gt;
&lt;h3 id=&#34;rei-rucksafe-40&#34;&gt;REI Rucksafe 40&lt;/h3&gt;
&lt;p&gt;Pretty impressive bag. 40L, under $200, and 2.85 lb. Has an external water bottle holder. No padded laptop compartment is really meh though, so is the lack of iPad storage. Really don&amp;rsquo;t like that it zippers all the way open to the extent that the interior would only have 3 walls and stuff then spills out the top, and it also doesn&amp;rsquo;t zip all the way to the bottom of the bag. No stowable backpack straps.&lt;/p&gt;
&lt;h3 id=&#34;-tomtoc-navigator-40l&#34;&gt;⭐️ TomToc Navigator 40L&lt;/h3&gt;
&lt;p&gt;40L, 2.65 lb, and $80 is kinda bonkers. That price is low enough that it&amp;rsquo;s almost concerning that the construction must be less well-made than other bags, but maybe TomToc just has some magic. OneBagTravels did call the material &amp;ldquo;mid tier&amp;rdquo;, which is expected for the price, but dang it makes me wonder what this bag could be like with X-Pac or something fancy. OneBagTravels also with the ol&amp;rsquo; packing peanut test managed to fit 35L in before overstuffing it too much, so it might not be 40L exactly. I kinda like the minimal boxy look of it. Water bottle pockets are external and seem okay but decent when combined with the compression straps to really lock it in. Speaking of, compression straps seem solid. Laptop compartment zips all the way open, and has an iPad spot, nice! Harness system seems pretty meh though, no load lifters, no hip belt, no stowable straps, which sucks. Big cavernous main bucket area though which I love, it has the Tom Bihn Techonaut style opening where the zippers are a horseshoe slightly inset from the sides, which allows it to keep its shape nicely when opened up and empty (has a bit of the Minaal situation though where it would be nicer if the inside was a lighter color for better visibility). I would rank this number 3 currently, but would be a firm number 1 if the materials were better, and the harness system was upgraded. Here&amp;rsquo;s a &lt;a href=&#34;https://www.youtube.com/watch?v=tSoJHcv3Opo&#34;&gt;great review&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;kathmandu-litehaul-38&#34;&gt;Kathmandu Litehaul 38&lt;/h3&gt;
&lt;p&gt;Hard to tell but looks like the newer bag moved the water bottle holder so it takes up room inside the bag, bleh. Side laptop compartment (eh), with no iPad storage. Nice compression straps. Stowable backpack straps with a great harness system in general. Love how cavernous it is with minimal organization (yay). No real place to stow a daybag, maybe the &amp;ldquo;ceiling&amp;rdquo; of the main compartment but the zipper looks a bit small.&lt;/p&gt;
&lt;h3 id=&#34;wandrd-prvke-41l&#34;&gt;Wandrd Prvke 41L&lt;/h3&gt;
&lt;p&gt;Not a lot of reviews on this one so hard to tell a bunch (most reviews are geared toward it as a camera bag), but looks like a nice bag. 41L (46 with rolltop extended), and a firmly &amp;ldquo;okay&amp;rdquo; 3.7 lb. I think I&amp;rsquo;d prefer to see a less complex version without a rolltop and without the inner division pocket thing in the main compartment. Water bottle holder looks good. Wish laptop compartment was separate so I could shove a day bag in there too but it does have a nice iPad holder. Doesn&amp;rsquo;t really advertise stowable straps but looks like you could sorta fish the straps underneath the luggage passthrough and it would organize it a bit?&lt;/p&gt;
&lt;h3 id=&#34;-deuter-aviant-access-38&#34;&gt;✨ Deuter AViANT Access 38&lt;/h3&gt;
&lt;p&gt;Great harness system, fully stowable with hip belt and load lifters. Only concern is that it seems really big for a 38L pack, at 22&amp;quot; × 13.25&amp;quot; × 11&amp;quot; (56 × 34 × 28cm), normally 9&amp;quot; is the depth requirement for most airlines in North America. But it weirdly doesn&amp;rsquo;t seem super thick, maybe that&amp;rsquo;s just if you over pack it? 3.4 lb is a pretty good weight too. Good compression straps. Side laptop compartment is kinda meh, with no iPad compartment. There&amp;rsquo;s a weird lip at the top of the bag to prevent rain/theft, and it makes opening the zipper to the main compartment a bit annoying per reviews. Just a big ol&amp;rsquo; simple bucket to store things in, which I love! No water bottle holder gets a resounding &amp;ldquo;boooo&amp;rdquo;, though.&lt;/p&gt;
&lt;h3 id=&#34;timbuk2-impulse&#34;&gt;Timbuk2 Impulse&lt;/h3&gt;
&lt;p&gt;Love how this looks, love how it&amp;rsquo;s just a big cavernous compartment. 45L is impressive, 3.7 lb is okay. Side laptop compartment is okay, no iPad storage though. But no external water bottle holder, straight to jail. Stowable straps though!&lt;/p&gt;
&lt;h3 id=&#34;timbuk2-wingman&#34;&gt;Timbuk2 Wingman&lt;/h3&gt;
&lt;p&gt;Also no external water bottle holder, come on Timbuk2! 38L, but a hefty 4 lb. Stowable backpack straps! Side laptop storage, no iPad storage. Love the cavernous inside. This bag confuses me, seems like a heavier version of their Impulse that holds less?&lt;/p&gt;
&lt;h3 id=&#34;-dakine-split-adventure-38l&#34;&gt;⭐️ Dakine Split Adventure 38L&lt;/h3&gt;
&lt;p&gt;Confusing one because there&amp;rsquo;s different versions of this seemingly? From the one I saw, 38L, great size, and at an impressive 2.8 lb. Also love that it looks quite a bit like a backpack so it&amp;rsquo;s kinda &amp;ldquo;stealthy&amp;rdquo; versus some that look like a suitcase strapped to your back. As the name implies, it splits open horizontally into two compartments, the right side has a mesh cover, the left side has two mesh covers as it&amp;rsquo;s kinda split in two. That&amp;rsquo;s pretty nice, you can split your packing cubes to either side without much organization being imposed upon you. Wish the interior compression straps were removable though, I find those always just get in the way for me personally. Good water bottle holders, but it seems like a newer version might have got rid of them for side handles? That would be lame, as both are totally possible to have and both are very handy. No lockable zippers is kinda unfortunate. Stowable backpack straps with load lifters, but even though there appears to be a place to hide a hip belt, I don&amp;rsquo;t see one nor anywhere you could attach one. Laptop compartment opens all the way. Not a ton of reviews on this one but &lt;a href=&#34;https://www.youtube.com/watch?v=ezW0t-Sznmg&#34;&gt;here&amp;rsquo;s a good one&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;goruck&#34;&gt;Goruck&lt;/h3&gt;
&lt;p&gt;Totally get why people love this, very iconic design, looks super well built. Just not my style visually, a bit too tactical, and they&amp;rsquo;re pretty hefty in weight (4.5 lb for the 40L).&lt;/p&gt;
&lt;h3 id=&#34;waterfield-x-air&#34;&gt;Waterfield X Air&lt;/h3&gt;
&lt;p&gt;Pretty decent weight at 3.6 lb, with a 40L capacity. Water bottle holder looks a little concerning (Aer-TP3-like). Love the yellow interior, but don&amp;rsquo;t love the inclusion of the front admin compartment personally. Love that it opens horizontally like a suitcase rather than vertically, both are great but horizontal just feels even nicer. Overall not that many reviews of this bag so hard to tell a bunch.&lt;/p&gt;
&lt;h3 id=&#34;north-face-basecamp-35l&#34;&gt;North Face Basecamp 35L&lt;/h3&gt;
&lt;p&gt;Love the look, but really wish North Face toned down their branding. 3.5 lb and 35L is pretty decent, but the harness system seems kinda meh, no stowable straps, no hip belt. Admin compartment seems to take up a decent amount of volume. Laptop compartment doesn&amp;rsquo;t seem to have an iPad compartment. Love how it&amp;rsquo;s just one big compartment. Honestly would be a pretty great bag if it was a hair bigger, had a hip belt, and the laptop compartment opened all the way. Great price too.&lt;/p&gt;
&lt;h3 id=&#34;-north-face-router-40&#34;&gt;✨ North Face Router 40&lt;/h3&gt;
&lt;p&gt;2.9 lb, nice! Water bottle pockets don&amp;rsquo;t seem super well reviewed (they&amp;rsquo;re very big without much stretchiness so lots of water bottles will just fall out). Love the minimal front pocket. Wish it had stowable straps. Compression straps seem awesome. Laptop compartment opens pretty wide, not all the way down, but could probably shove a day bag in there easily, also has room for an iPad. Main compartment doesn&amp;rsquo;t open all the way which is really unfortunate. I&amp;rsquo;m almost doubtful it holds a full 40L somehow, wish a YouTuber would do a packing peanut test.&lt;/p&gt;
&lt;h3 id=&#34;linus-tech-tips-backpack&#34;&gt;Linus Tech Tips Backpack&lt;/h3&gt;
&lt;p&gt;Not really a true travel backpack at only 26L, seems more geared as a personal item and seems great at that. Mostly including it here to preempt recommendations for it as a travel bag.&lt;/p&gt;
&lt;h3 id=&#34;nomatic-travel-bag&#34;&gt;Nomatic Travel Bag&lt;/h3&gt;
&lt;p&gt;Aesthetics kinda don&amp;rsquo;t do it for me personally, looks a bit like a cooler which concerns me functionally too as the &amp;ldquo;hardshell&amp;rdquo; looking bags are typically the first that airline employees target when force checking bags. Good weight at 3.4 lb though at 40L capacity. No external water bottle holder.&lt;/p&gt;
&lt;h3 id=&#34;alpaka-elements-travel-backpack&#34;&gt;Alpaka Elements Travel Backpack&lt;/h3&gt;
&lt;p&gt;Kinda seems like an Aer Travel Pack 3 style (even similar price) but slightly lighter (3.5lb versus 3.9lb) and with a better water bottle holder. Wish it ditched the admin compartment and then its 35L size would be pretty compelling. Looks weirdly tall though, I&amp;rsquo;d add an extra inch to the width, get rid of the admin compartment, and you&amp;rsquo;d be close to 40L and a hair lighter which would make for an awesome bag. I like that the laptop compartment opens pretty wide, but would be even better if it opened all the way to the bottom.&lt;/p&gt;
&lt;h3 id=&#34;peak-design-45l&#34;&gt;Peak Design 45L&lt;/h3&gt;
&lt;p&gt;4.5 lb make it a hefty one (the heaviest on this list?), makes sense given that it&amp;rsquo;s 45L which is a bit more than I&amp;rsquo;d want to gamble with on flights. Lots of great touches though, and dang do I love Peak Design&amp;rsquo;s packing cubes and Peak Design stuff in general, so I so wanted to love this. Probably the most innovative and simple way to hide the backpack straps too, since you can just quickly slide them under the back padding. Wish they did a 40L bag and made it lighter. Just too heavy.&lt;/p&gt;
&lt;h3 id=&#34;-cotopaxi-allpa-35&#34;&gt;✨ Cotopaxi Allpa 35&lt;/h3&gt;
&lt;p&gt;3.5 lb, 35L. 42L version also available, and is 4.2 lb. Neither are great weight:capacity ratios, but not terrible. Some organization in the internal compartment, but not so much that it&amp;rsquo;s forced upon you, I think I could make it work. No big admin compartment either, just a small pouch at the top with some organization, but it&amp;rsquo;s maybe a bit too big. No external water bottle storage, and only side laptop storage but it does have an iPad compartment. Stowable shoulder straps is nice. Also just &lt;em&gt;love&lt;/em&gt; their logo. A bit lighter and a better laptop compartment (zipped open all the way), with proper water bottle storage, and this would be really compelling.&lt;/p&gt;
&lt;h3 id=&#34;-bellroy-transit-backpack-plus&#34;&gt;✨ Bellroy Transit Backpack Plus&lt;/h3&gt;
&lt;p&gt;No external water bottle pocket, straight to jail. Honestly would be a pretty compelling bag if not though, has just one big bucket and 38L. 3.3 lb is impressive. Can&amp;rsquo;t stow straps either. Doesn&amp;rsquo;t look like any iPad storage available.&lt;/p&gt;
&lt;h3 id=&#34;fjallraven-travel-pack&#34;&gt;Fjallraven Travel Pack&lt;/h3&gt;
&lt;p&gt;40L, 3.6 lb, not too heavy! Don&amp;rsquo;t really like the design visually, and I normally quite like Fjallraven stuff.&lt;/p&gt;
&lt;h3 id=&#34;-decathlon-forclaz-travel-500&#34;&gt;✨ Decathlon Forclaz Travel 500&lt;/h3&gt;
&lt;p&gt;40L, 2.9 lb, well done! Very inexpensive too at about $100, that&amp;rsquo;s inexpensive enough that I&amp;rsquo;d honestly start to worry about material choices. Good water bottle holders, interior seems well thought out with some mesh organization but it&amp;rsquo;s very loosey goosey in a nice way, so you&amp;rsquo;re not stuck to their organizational system. Nice compression straps. Not a lot of reviews on this one though. No stowable straps from what I can tell, and side laptop compartment with no iPad storage. I like the look of this one visually.&lt;/p&gt;
&lt;h3 id=&#34;-eagle-creek-tour-travel-pack&#34;&gt;⭐️ Eagle Creek Tour Travel Pack&lt;/h3&gt;
&lt;p&gt;40L, 2.8 lb (nice!!). Stowable straps (similar to Osprey where it zips, but you can separately stow the hip straps). Nice compression straps. Opens horizontally into a big ol&amp;rsquo; bucket, love the water bottle holder. Don&amp;rsquo;t love how dark the interior is, don&amp;rsquo;t love how the ceiling of the main compartment seems to have its own volume which takes away from the rest of the main compartment (gimme &lt;em&gt;just&lt;/em&gt; a big ol&amp;rsquo; bucket), don&amp;rsquo;t love the compression straps in the interior not being removable (I never use those and they just get in the way), and don&amp;rsquo;t love the laptop compartment which feels like a bit of an afterthought (no lockable zipper there either even), rain cover seems pretty bulky when stored and doesn&amp;rsquo;t seem fully removable. But those last things are nitpicks, I think they did a great job with this bag. Pretty great price at around $180 too.&lt;/p&gt;
&lt;h3 id=&#34;-ula-dragonfly&#34;&gt;✨ ULA Dragonfly&lt;/h3&gt;
&lt;p&gt;Holy crap, 1.5 lb. Little on the small side at 30L though for the main interior area per tests like OneBagTravels, wish it was a smidge bigger. Really impressive looking bag given the weight, would love to see a 35-40L version of this bag.&lt;/p&gt;
&lt;h3 id=&#34;-patagonia-black-hole-mini-mlc-travel-pack&#34;&gt;⭐️ Patagonia Black Hole Mini MLC Travel Pack&lt;/h3&gt;
&lt;p&gt;Well that&amp;rsquo;s a mouthful of a name! Seems to be just a massive pit that holds a full 30L which is awesome. Looks like an awesome water bottle holder. Don&amp;rsquo;t love that it has a mesh covering over the interior, so you have to unzip two things to get to it, or leave a thing just in perpetual flapping mode. No load lifters on the back though is kind of a bummer, but it does allow you to hide the backpack straps. Laptop compartment area also zips fully open and has a small iPad area. Wish it had better compression straps though.&lt;/p&gt;
&lt;p&gt;2.8 lb makes it even lighter than my Osprey Farpoint, and it even seems to have more side structure for easily packing things in.&lt;/p&gt;
&lt;p&gt;If this one was offered in a 35L or a 40L (the 45L is slightly too big and exceeds a lot of carry-on size limits for airlines I use) it&amp;rsquo;d basically be perfect. I would rank this number 2.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I basically want a combination of the Tortuga Lite, Patagonia, TomToc, and Minaal. I saw &lt;a href=&#34;https://old.reddit.com/r/ManyBaggers/comments/1bstsg7/11l_self_made_bag/&#34;&gt;a post on Reddit the other day&lt;/a&gt; where someone made their own really cool bag from scratch, while that&amp;rsquo;s undoubtedly an incredible difficult task that kinda sounds fun, maybe I&amp;rsquo;ll slowly try to learn that!&lt;/p&gt;
&lt;p&gt;If you have a bag recommendation that you think I might like and isn&amp;rsquo;t on this list hit me up on &lt;a href=&#34;https://twitter.com/christianselig&#34;&gt;Twitter&lt;/a&gt;, &lt;a href=&#34;https://mastodon.social/@christianselig&#34;&gt;Mastodon&lt;/a&gt;, or &lt;a href=&#34;https://www.threads.net/@christianselig&#34;&gt;Threads&lt;/a&gt;!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>A free, 3D printable Meta Quest 3 stand</title>
      <link>https://christianselig.com/2024/04/meta-quest-3-stand/</link>
      <pubDate>Mon, 22 Apr 2024 10:47:15 -0300</pubDate>
      
      <guid>https://christianselig.com/2024/04/meta-quest-3-stand/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2024/04/meta-quest-3-stand/hero.jpeg&#34; alt=&#34;A vertical, 3D printable Meta Quest 3 stand sitting on an oak desk with a monitor in the background&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;People were really kind and seemed to enjoy my &lt;a href=&#34;https://christianselig.com/2024/02/vision-pro-stand/&#34;&gt;3D printable Apple Vision Pro stand&lt;/a&gt;, a stand I designed in Fusion 360 with the goal of being visually appealing and compact as it stored the headset vertically so it wouldn&amp;rsquo;t take up too much space on your desk.&lt;/p&gt;
&lt;p&gt;Turns out there were quite a few folks requesting a similar style stand for their Meta Quest 3 so this weekend I set aside a bit of time to design such a variation, and I&amp;rsquo;m really happy with how it came out.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://makerworld.com/en/models/441014&#34;&gt;Download link&lt;/a&gt;. It&amp;rsquo;s completely free to download, and consider giving it a boost by clicking the little purple rocket! It&amp;rsquo;s like when YouTubers ask you for a like, except in this case boosts give me a small credit toward a 3D printing accessory I&amp;rsquo;ve had my eyes on.&lt;/p&gt;
&lt;h2 id=&#34;changes-versus-vision-pro-stand&#34;&gt;Changes versus Vision Pro stand&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/04/meta-quest-3-stand/side-by-side.jpeg&#34; alt=&#34;The Meta Quest 3 stand beside the Vision Pro stand, both holding their devices, on an oak desk with a monitor.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;The Meta Quest 3 has quite a few differences versus the Vision Pro so I redesigned the stand in some significant ways to better match the headset:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spots for docking your controllers in the base. This both acts as a counterweight as the stand no longer has a need for a battery slot like the Vision Pro stand, and serves as a handy place to set your controllers. Set them down and they settle into the carved out grooves very satisfyingly&lt;/li&gt;
&lt;li&gt;Top &amp;ldquo;pringle&amp;rdquo; that the headset rests on is slightly tweaked in width and height/amplitude to better nestle the Quest 3&lt;/li&gt;
&lt;li&gt;So as to easily differentiate it form the Vision Pro stand if you have both side by side, I added a subtle design on the top pringle mimicking the Meta Quest 3&amp;rsquo;s front sensor array. Fun fact: it&amp;rsquo;s the exact same size and spacing!&lt;/li&gt;
&lt;li&gt;Moved dowel/rod more toward center of stand, which allows for better compatibility with the Quest&amp;rsquo;s headband style&lt;/li&gt;
&lt;li&gt;Removed cable organizer as there are no cables to organize!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As before, you can use any kind of 3/4&amp;quot; (18.5mm) dowel to add a splash of customizability, from walnut to maple to copper to steel, or the download also just includes a 3D printable dowel.&lt;/p&gt;
&lt;p&gt;Hope you enjoy it! It&amp;rsquo;s a lot of fun learning Fusion 360, haha. The base&amp;rsquo;s size is unchanged from the Vision Pro&amp;rsquo;s and the pringle is very similar, so if you&amp;rsquo;re fortunate enough to have both a Vision Pro and Quest 3 both stands should look very nice next to each other.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Qi2 is kinda underwhelming</title>
      <link>https://christianselig.com/2024/04/qi2-is-kinda-underwhelming/</link>
      <pubDate>Sat, 20 Apr 2024 15:46:42 -0300</pubDate>
      
      <guid>https://christianselig.com/2024/04/qi2-is-kinda-underwhelming/</guid>
      <description>&lt;p&gt;Using MagSafe for portable battery packs has so many niceties versus Qi1:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Increased communication with the device, allowing for better efficiency due to better thermal management and charging&lt;/li&gt;
&lt;li&gt;Easily view the charge percentage of the external battery when first attaching it, and at any other point right from the OS&lt;/li&gt;
&lt;li&gt;Reverse-wireless-charging, so if you charge your phone while the pack is attached, the phone will charge up first and then send energy to the battery pack&lt;/li&gt;
&lt;li&gt;Magnets for better charging reliability (no vibrating off the small charging zone and waking up to a dead phone) and better efficiency (induction points are perfectly lined up), though this point is almost always mimicked in Qi1 battery packs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Qi2 was supposed to be a &lt;a href=&#34;https://quotepark.com/quotes/1908891-steve-jobs-its-like-giving-a-glass-of-ice-water-to-somebody/&#34;&gt;glass of ice water to those in hell of Qi1&lt;/a&gt;, and I was hyped! Apple &lt;a href=&#34;https://9to5mac.com/2023/09/12/apple-stops-selling-magsafe-battery-pack-magsafe-duo/&#34;&gt;stopped making MagSafe battery packs themselves&lt;/a&gt;, and their old pack used Lightning instead of the newer USB-C, so I was excited to see third-parties bring MagSafe into the golden age of USB-C.&lt;/p&gt;
&lt;p&gt;The reality though is… kinda lame right now with most of those benefits not being a thing?&lt;/p&gt;
&lt;h2 id=&#34;precursor-most-magsafe-batteries-are-just-qi1&#34;&gt;Precursor: most &amp;ldquo;MagSafe&amp;rdquo; batteries are just Qi1&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/04/qi2-is-kinda-underwhelming/fred.jpeg&#34; alt=&#34;Fred from Scooby Doo unmasking some person who has a disguise on that says &amp;#39;totally legit MagSafe&amp;#39; and when the disguise is off it&amp;#39;s revealed it&amp;#39;s just Qi1 with a magnet&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;So many magnetic battery pack makers say &amp;ldquo;MagSafe compatible&amp;rdquo; on the product page, which leads people to think they&amp;rsquo;re getting the more efficient charging of MagSafe as well as the extra functionality.&lt;/p&gt;
&lt;p&gt;The word &amp;ldquo;compatible&amp;rdquo; is doing a lot of heavy lifting there, just indicating that the battery packs have a magnet in them and using just regular Qi1 charging. None of the actual MagSafe benefits are available. This means they&amp;rsquo;re kinda &amp;ldquo;dumb&amp;rdquo; and don&amp;rsquo;t communicate well with the host device, leading to hotter devices (and thus faster battery degradation) and lower efficiency due to energy loss as heat.&lt;/p&gt;
&lt;h2 id=&#34;just-use-cables&#34;&gt;&amp;ldquo;Just use cables!&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;Cables are much more efficient than wireless charging, so this sounds like a great idea until you try it, and going through an airport with cables dangling and potentially snagging on things is so, so much less convenient than just having a slightly thicker phone. I&amp;rsquo;d take a dip in efficiency for a massive increase in convenience, but if you can deal with cable charging while running around, I tip my hat to you.&lt;/p&gt;
&lt;p&gt;That being said, the battery cases of yesteryear &lt;em&gt;were&lt;/em&gt; a nice middle ground.&lt;/p&gt;
&lt;h2 id=&#34;the-only-qi2-battery-pack-is-kinda-lacking&#34;&gt;The only Qi2 battery pack is kinda lacking?&lt;/h2&gt;
&lt;p&gt;Despite being announced last year, there&amp;rsquo;s still like… only one manufacturer offering Qi2 battery packs: &lt;a href=&#34;https://www.anker.com/products/a1643-maggo-6600mah-qi2-power-bank-magsafe-compatible&#34;&gt;Anker&lt;/a&gt;. The rest are still &amp;ldquo;coming soon&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Outside of only being offered in bulky sizes, Anker&amp;rsquo;s offerings seem to miss some of the biggest niceties of MagSafe, presumably through no fault of their own.&lt;/p&gt;
&lt;p&gt;Firstly, Qi2 battery packs seemingly don&amp;rsquo;t even support OS level battery status! I can only assume this is an omission on Apple&amp;rsquo;s part rather than Anker&amp;rsquo;s, and is hopefully fixed in the future, but that was one of the aspects of Qi2 I was looking forward to the most. All you get is a slightly larger indicator of the phone&amp;rsquo;s battery level, but not the pack&amp;rsquo;s. Being able to easily see the percentage of your battery pack when using the phone and connecting it is super handy.&lt;/p&gt;
&lt;p&gt;A minor one, but it also seems to get slightly warmer than Apple&amp;rsquo;s offering. Qi2 is supposed to also offer 15W of output whereas Apple&amp;rsquo;s battery was 7.5W, but Max Tech did a &lt;a href=&#34;https://www.youtube.com/watch?v=DUYqiunkN5o&#34;&gt;thorough review versus Apple&amp;rsquo;s old pack&lt;/a&gt;, and while it&amp;rsquo;s worth a watch, the tl;dw is that it seems to warm up quickly and slow down pretty considerably as a result (and be no faster than 7.5W).&lt;/p&gt;
&lt;p&gt;Lastly, there&amp;rsquo;s no reverse wireless charging like Apple&amp;rsquo;s MagSafe pack has, so if you&amp;rsquo;re charging your iPhone over USB-C and have the pack attached, the pack won&amp;rsquo;t charge. You&amp;rsquo;d have to plug the pack itself in, which would transfer more heat to the iPhone rather than the other way around with MagSafe (I&amp;rsquo;d rather have the cheap battery pack get hotter, rather than the expensive iPhone).&lt;/p&gt;
&lt;h2 id=&#34;why-apples-old-pack-is-actually-good&#34;&gt;Why Apple&amp;rsquo;s old pack is actually good&lt;/h2&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2024/04/qi2-is-kinda-underwhelming/battery-widget.png&#34; alt=&#34;Apple battery widget showing status of MagSafe battery pack&amp;#39;&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Credit: Apple&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Apple&amp;rsquo;s offering on paper seems pretty great: smaller than the competitors, and integrates perfectly with iOS, meaning you get more intelligent charging and power delivery (and a cooler device), and you can see the status of the battery right in the operating system.&lt;/p&gt;
&lt;p&gt;Two issues though: price/availability (at almost twice the competitors&amp;rsquo;), and the Lightning connector (in our beautiful new USB-C world, I do not want to be carrying a Lightning cable around anymore).&lt;/p&gt;
&lt;p&gt;The first one&amp;rsquo;s indeed tricky. As Apple&amp;rsquo;s is discontinued, they&amp;rsquo;re hard to find, and there&amp;rsquo;s a &lt;em&gt;lot&lt;/em&gt; of counterfeit ones online and in local classifieds, so be careful.&lt;/p&gt;
&lt;p&gt;For the Lightning issue, the iPhone supports reverse wireless charging (Qi2 does not on iPhones as of April 2024), so if you plug in your USB-C iPhone with the pack still attached, it&amp;rsquo;ll charge up the battery after it&amp;rsquo;s done charging up the phone! No Lightning cable needed. In fact, even the new Qi2 batteries don&amp;rsquo;t support this, they only support plugging in the battery itself which charges the iPhone, which sounds fine, but the battery is the one inductively charging the iPhone, so the iPhone bears the brunt of the heat, rather than the other way around, which is less than ideal for battery health.&lt;/p&gt;
&lt;p&gt;Lastly, Apple&amp;rsquo;s is much thinner than the competitor&amp;rsquo;s offerings. Which is fine, I don&amp;rsquo;t need to double my battery life, I just want to extend it when I know I might otherwise be cutting it close by the end of the day.&lt;/p&gt;
&lt;p&gt;(Though one bonus for Qi2 battery packs is that they &lt;em&gt;do&lt;/em&gt; support wired charging between the battery and iPhone though, unlike Apple&amp;rsquo;s, which would be a handy feature for charging faster in a pinch, but not a deal-breaker for me.)&lt;/p&gt;
&lt;h2 id=&#34;apples-battery-capacity-is-so-small&#34;&gt;&amp;ldquo;Apple&amp;rsquo;s battery capacity is so small&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;There were some strange musings at the beginning complaining that Apple&amp;rsquo;s is only 1,500 mAh, while everyone else is 5,000 mAh, and that&amp;rsquo;s a perfect indication of why mAh is such a terrible unit for measuring batteries (the recent Vision Pro battery size story &lt;a href=&#34;https://www.gsmarena.com/apple_vision_pro_battery_specs_revealed-news-61422.php&#34;&gt;being another one&lt;/a&gt;). Battery capacity is a function of Amp-hours &lt;strong&gt;and crucially&lt;/strong&gt; Voltage (multiply them together to get Watt-hours, an actual measurement of capacity), Apple&amp;rsquo;s battery uses twice the Voltage (7.6 V versus 3.7 V), so the actual &lt;strong&gt;capacities&lt;/strong&gt; are 11 Watt-hours for Apple&amp;rsquo;s and 18.5 Watt-hours for others.&lt;/p&gt;
&lt;p&gt;Further, if you take the smallest previewed Qi2 case: Belkin&amp;rsquo;s 5,000 mAh option (&lt;a href=&#34;https://www.apple.com/au/shop/product/HRE62ZM/A/belkin-boost%E2%86%91charge%E2%84%A2-pro-magnetic-power-bank-5k-magsafe-compatible&#34;&gt;available in Australia&lt;/a&gt;), it&amp;rsquo;s 17mm thick, where Apple&amp;rsquo;s is only 11mm.&lt;/p&gt;
&lt;p&gt;So Apple&amp;rsquo;s battery capacity being 40% smaller than Belkin&amp;rsquo;s (11 versus 18.5) kinda makes sense when you see that it&amp;rsquo;s because it&amp;rsquo;s 35% thinner.&lt;/p&gt;
&lt;h2 id=&#34;end-notes&#34;&gt;End notes&lt;/h2&gt;
&lt;p&gt;All in all, maybe someone like Belkin will release their Qi2 and it&amp;rsquo;ll be faster, more energy dense, less hot than Apple&amp;rsquo;s, and have USB-C, but even then at least as of April 2024 it will still lack the super handy OS-level battery status, as well as reverse-charging. Maybe Apple will add those in iOS 18 and will be well in the world, or maybe Apple will surprise us all and release a new, USB-C MagSafe battery pack.&lt;/p&gt;
&lt;p&gt;(Also, my one criticism of &lt;em&gt;all&lt;/em&gt; battery packs is they and the iPhone really need a magnet connection near the bottom too, right now the top-half is secure but the bottom half can just swing around. That part kinda makes me miss old school battery cases.)&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Waterfield&#39;s weirdly compact Apple Vision Pro Case</title>
      <link>https://christianselig.com/2024/04/waterfield-vision-pro-case/</link>
      <pubDate>Tue, 02 Apr 2024 12:56:46 -0300</pubDate>
      
      <guid>https://christianselig.com/2024/04/waterfield-vision-pro-case/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2024/04/waterfield-vision-pro-case/hero.jpeg&#34; alt=&#34;Vision Pro on top of a blue lunchbox style case for it, next to a black and white cat looking at its paws&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Waterfield sent this in exchange for a review. Yeah, that probably colors something on a deep-down, subconscious level, but I won&amp;rsquo;t say anything that I don&amp;rsquo;t truly believe.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Unlike a phone or laptop, the Vision Pro is one of those products that is particularly tricky to take around without a case. I&amp;rsquo;ve got around this by wrapping it in a hoodie and throwing it in my backpack, but I was looking for a more… tidy solution longterm.&lt;/p&gt;
&lt;p&gt;Apple&amp;rsquo;s own case was an obvious option, but the size kinda scared me. I like packing pretty light for trips, and only ever bring one bag, so the thought of half the bag being taken up by a Vision Pro case wasn&amp;rsquo;t the most alluring, so a compact size was pretty near the top of my list, so &lt;a href=&#34;https://www.sfbags.com/products/vision-pro-shield-case&#34;&gt;when Waterfield announced&lt;/a&gt; their case offering and toted its size my ears immediately perked up.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve traveled a bit with it now, and I&amp;rsquo;ve really come to like it. Here&amp;rsquo;s my thoughts, plus some questions from folks on Twitter and Mastodon.&lt;/p&gt;
&lt;h2 id=&#34;designbuild-quality&#34;&gt;Design/build quality&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/04/waterfield-vision-pro-case/open.jpeg&#34; alt=&#34;Case open with Vision Pro nestled snuggly inside with an additional fleece inner case that holds accessories. HomePod mini and iPod nano in background.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;The design is reminiscent of a really well-made lunchbox. It&amp;rsquo;s sturdy, and the outside feels like that ballistic nylon material that a good backpack is made out of, while the inside is a really soft fleece. The inside houses a second (also fleece-wrapped) case which can house all the Vision Pro accessories and even has individual slots for ZEISS lenses if you have those. In mine I put a charging brick, the polishing cloth, and the headband (note on that below), and spare contact lenses in the ZEISS slots. There&amp;rsquo;s a separate spot on the &amp;ldquo;ceiling&amp;rdquo; of the inner case for the Vision Pro&amp;rsquo;s external battery.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/04/waterfield-vision-pro-case/battery.jpeg&#34; alt=&#34;Case open with battery slid into a fleece pocket on the lid of the case, with a black cat in the background.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Everything just fits super snuggly and thoughtfully, which is kinda what I like most about it. So many Vision Pro cases on the market are just versions for other headsets that happen to fit the Vision Pro to different levels of success, and while that&amp;rsquo;s obviously totally fine, maybe it&amp;rsquo;s because the Vision Pro was so expensive, but there&amp;rsquo;s something really nice feeling about a case designed specifically around it. It&amp;rsquo;s like using a baseball mitt made just for your hand versus borrowing your friend&amp;rsquo;s: both are cool, one just makes you go &lt;em&gt;oooooo&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;(Though the battery fits &lt;em&gt;so&lt;/em&gt; snuggly that it&amp;rsquo;s a bit tricky to get in when you wrap the cable around it. Instead, wrap the cable around your fingers, slide the battery in, and then slide the organized cable on top of the battery.)&lt;/p&gt;
&lt;p&gt;The zippers feel great, &lt;del&gt;not YKK&lt;/del&gt; (oops, I&amp;rsquo;ve been told they &lt;em&gt;are&lt;/em&gt; YKK, that makes sense given the quality, they just have nice &amp;lsquo;Waterfield&amp;rsquo; branding on them) but metal and have a water-resistant coating which I always like to see. It has a grab handle on top, and attachments for shoulder straps that I likely won&amp;rsquo;t use. Same with the top, it has a little zipper pocket that I&amp;rsquo;m not sure I&amp;rsquo;d use beyond the AirTag pocket in it, but you could put something thin up there (like another cord, or a small external battery, or maybe a very small foldable external keyboard), and even if you don&amp;rsquo;t use it the pocket is pretty flat so you don&amp;rsquo;t lose any room to it.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/04/waterfield-vision-pro-case/water.jpeg&#34; alt=&#34;Water droplets beading on top of the case and zipper.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;I do wish they had more colorways. I&amp;rsquo;m &lt;a href=&#34;https://techcrunch.com/2023/09/12/no-more-leather&#34;&gt;on Apple&amp;rsquo;s train&lt;/a&gt; and not a big fan of wrapping my devices in dead animals (though Apple&amp;rsquo;s FineWoven solution there seems to have missed the mark, but &lt;a href=&#34;https://aptera.us/take-a-peek-at-apteras-interior-design/&#34;&gt;Aptera has some really cool&lt;/a&gt; plant-based biodegradable leathers for their car), and wish Waterfield had options outside of black and blue for non-leather (that black and white stormtrooper style one looks so cool). That being said the photographs of the blue on their website almost don&amp;rsquo;t do it justice, it&amp;rsquo;s a really nice navy in real life with just the right amount of color to be a bit fun. But I still want stormtrooper!&lt;/p&gt;
&lt;h2 id=&#34;compactness&#34;&gt;Compactness&lt;/h2&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2024/04/waterfield-vision-pro-case/vhs.jpeg&#34; alt=&#34;Case open with various items inside, including a banana, an iPhone 15 Pro, a light bulb, a VHS copy of The Mogul, and a Blue Eyes White Dragon card.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Some items you may have around for scale&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;It&amp;rsquo;s really compact, there&amp;rsquo;s honestly not really any room to possibly shrink it further. It&amp;rsquo;s not tiny per se, but going off numbers from each website, Apple&amp;rsquo;s Vision Pro case is about 10.9 Liters in volume (0.38 cubic feet), and the Waterfield case is 5.0 Liters (0.18 cubic feet), which is a substantial difference. If you throw it in a 20 Liter everyday carry backpack, you&amp;rsquo;ve gone from it taking 55% of the interior space to just 25%.&lt;/p&gt;
&lt;p&gt;A big part of how they accomplish this is by having you take the headband off which saves a ton of room length-wise versus storing it fully expanded. This is something I was hoping someone would do &lt;a href=&#34;https://twitter.com/ChristianSelig/status/1752346123862179915&#34;&gt;well before this case was even announced&lt;/a&gt;, and it&amp;rsquo;s plain to see how much room it saves.&lt;/p&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2024/04/waterfield-vision-pro-case/hatbox.jpeg&#34; alt=&#34;The separate fleece inner case that holds items&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;The cutely named “Hat Box” that zippers open to add accessories&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;My idea was just to fold the fabric of the headband in a bit, and when I saw Waterfield required you to actually disconnect the headband I was kind of disappointed because that sounds like a pain. But in all honesty, if you kept the band connected, you would have to bend it more on the side part than I would be personally comfortable to get it &lt;em&gt;as&lt;/em&gt; compact (see below, though), and if you only folded in the back part (and not the sides) it would add a decent amount more length to the case. Still a bunch of space savings to be sure, but in my opinion unless you&amp;rsquo;re putting it in the case every night the compactness this creates is worth the minor inconvenience of disconnecting the headband.&lt;/p&gt;
&lt;h2 id=&#34;protectiveness&#34;&gt;Protectiveness&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/04/waterfield-vision-pro-case/dumbbell.jpeg&#34; alt=&#34;Dumbbell sitting on top of a book (&amp;#39;what if?&amp;#39; by Randall Monroe) sitting on top of the case showing no deforming.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;I was somewhat worried where it&amp;rsquo;s not an actual hardshell style case like Apple&amp;rsquo;s or others that it would be more like a tech pouch and not have much protectiveness, but honestly it&amp;rsquo;s pretty darn sturdy. That&amp;rsquo;s hard to articulate, but as an example if I put a 20 lb dumbbell on top of it with nothing inside, you can see it doesn&amp;rsquo;t deform at all. (This does not mean it will survive a 20 lb dumbbell actually dropping on it, to be clear.)&lt;/p&gt;
&lt;p&gt;It definitely won&amp;rsquo;t be as protective as a hardcase, but it&amp;rsquo;s still pretty darn protective. Mine will always be stored in my backpack but if it were to take a small fall alone I personally wouldn&amp;rsquo;t worry.&lt;/p&gt;
&lt;h2 id=&#34;questions&#34;&gt;Questions&lt;/h2&gt;
&lt;p&gt;I asked on Twitter and Mastodon if anyone had any questions about it, and there were some great ones that I thought I&amp;rsquo;d answer here.&lt;/p&gt;
&lt;h4 id=&#34;do-you-still-need-apples-front-cover&#34;&gt;Do you still need Apple&amp;rsquo;s front cover?&lt;/h4&gt;
&lt;p&gt;The inside is soft felt so I personally don&amp;rsquo;t bother, but it does still fit if you&amp;rsquo;re so inclined.&lt;/p&gt;
&lt;h4 id=&#34;do-you-have-to-take-the-headband-off-to-store-it&#34;&gt;Do you HAVE to take the headband off to store it?&lt;/h4&gt;
&lt;p&gt;Technically no, it fits with the band still attached, but to me it&amp;rsquo;s like that scene in Cinderella when all the stepsisters try to fit in the glass slipper and are shoving their foot in to just barely make it fit. In other words, it &lt;em&gt;seems&lt;/em&gt; to put a bit more pressure on the side of the headband than I&amp;rsquo;d like, but hey, if you want to risk it the $99 to replace the Solo Knit band if I&amp;rsquo;m right is one of the more affordable Vision Pro accessories. (All this also applies to the Dual Loop band, too.)&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/04/waterfield-vision-pro-case/squish.jpeg&#34; alt=&#34;Vision Pro with the Solo Loop band still on and showing it quite squished at the extremity.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;h4 id=&#34;could-a-cat-sleep-on-it&#34;&gt;Could a cat sleep on it?&lt;/h4&gt;
&lt;p&gt;If a 20 lb dumbbell doesn&amp;rsquo;t deform it I think most cats could sleep on it fine, the issue is that it&amp;rsquo;s kinda small so not the most comfy. My cats stick to sleeping on my backpacks.&lt;/p&gt;
&lt;h4 id=&#34;does-the-battery-fit-in-it-could-it-hit-the-glass&#34;&gt;Does the battery fit in it? Could it hit the glass?&lt;/h4&gt;
&lt;p&gt;Yeah, there&amp;rsquo;s a proper space right above the Vision Pro. If you put it in the intended way (front of Vision Pro in toward the non-zippered edge) the Vision Pro&amp;rsquo;s front will be toward the front of the case, and the battery in the ceiling will be toward the back of the case, sitting on the storage accessory, so no chance of contact.&lt;/p&gt;
&lt;h4 id=&#34;can-the-battery-stay-plugged-in-in-the-case&#34;&gt;Can the battery stay plugged in in the case?&lt;/h4&gt;
&lt;p&gt;Yeah! I find the standby life of the Vision Pro isn&amp;rsquo;t the best, and it sometimes gets warm, so I personally would unplug it for travel, but you definitely don&amp;rsquo;t have to.&lt;/p&gt;
&lt;h4 id=&#34;does-a-mac-mini-fit-inside&#34;&gt;Does a Mac mini fit inside?&lt;/h4&gt;



    &lt;img src=&#34;https://christianselig.com/2024/04/waterfield-vision-pro-case/mac-mini.jpeg&#34; alt=&#34;A Mac mini diagonally in the case indicating it won&amp;#39;t quite fit.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Nope, just slightly more compact. That would have been cool though.&lt;/p&gt;
&lt;h4 id=&#34;can-you-use-it-like-a-lunchbox&#34;&gt;Can you use it like a lunchbox?&lt;/h4&gt;
&lt;p&gt;Honestly felt is a pretty effective thermal insulator so probably, but I&amp;rsquo;d worry about condensation build up.&lt;/p&gt;
&lt;h4 id=&#34;ease-of-zippingspeed-of-use&#34;&gt;Ease of zipping/speed of use&lt;/h4&gt;
&lt;p&gt;I find water-resistant zippers always have a bit more friction than their normal counterparts, so it&amp;rsquo;s a bit of a two-handed operation to zip open/closed. But I feel like if I was on an airplane, it&amp;rsquo;d be pretty quick to disconnect the headband, throw it on the pouch, throw in the Vision Pro, zip it up and leave. Not as fast as yeeting your Vision Pro into a backpack if there was like an emergency, but pretty reasonable if you have a second.&lt;/p&gt;
&lt;h4 id=&#34;what-does-it-carry&#34;&gt;What does it carry?&lt;/h4&gt;



    &lt;img src=&#34;https://christianselig.com/2024/04/waterfield-vision-pro-case/items.jpeg&#34; alt=&#34;The case on a table showing the items it can carry, including the Vision Pro, a USB-C cable, the two included headbands, an AirTag, the battery, AirPods Pro, a polishing cloth, a PSA 9 first edition Zubat Pokémon card, the inner fleece case, two contact lenses, and a charging brick.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;It can carry the Vision Pro, both headbands, polishing cloth, a wall brick, a USB-C cable, ZEISS inserts (or contact lenses), an AirTag, and something small in the top pocket. Here&amp;rsquo;s what I have in mine.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;At $159 for my non-leather version, it&amp;rsquo;s not cheap (though it&amp;rsquo;s cheaper than Apple&amp;rsquo;s own case), but I keep coming back to it reminding me of a really nice backpack. You can go on Amazon and find an obscure brand backpack for super cheap that will absolutely get the job done at the cost of long term confidence, or, if you want to treat yourself you can buy a really quality backpack from a trusted brand with a bunch of delightful touches that make you smile when you use it even years later. Some people get weirdly into nice backpacks, I&amp;rsquo;m unfortunately one of them.&lt;/p&gt;
&lt;p&gt;So all in all, it&amp;rsquo;s a great example of how something seemingly simple can be elevated by thoughtful design and quality materials.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.sfbags.com/products/vision-pro-shield-case&#34;&gt;&lt;em&gt;Non-affiliate link to the Waterfield case&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Recreating Apple&#39;s beautiful visionOS search bar</title>
      <link>https://christianselig.com/2024/03/recreating-visionos-search-bar/</link>
      <pubDate>Sun, 24 Mar 2024 15:16:12 -0300</pubDate>
      
      <guid>https://christianselig.com/2024/03/recreating-visionos-search-bar/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2024/03/recreating-visionos-search-bar/hero.jpeg&#34; alt=&#34;visionOS Music app in the Joshua Tree environment with the window showing a search bar at the top with rounded corners&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Many of Apple&amp;rsquo;s own visionOS apps, like Music, Safari, and Apple TV, have a handy search bar front and center on the window so you can easily search through your content. Oddly, as of visionOS 1.1, replicating this visually as a developer using SwiftUI or UIKit is not particularly easy due to lack of a direct API, but it&amp;rsquo;s still totally possible, so let&amp;rsquo;s explore how.&lt;/p&gt;
&lt;p&gt;First let&amp;rsquo;s get a few ideas out of the way to maybe save you some time.&lt;/p&gt;
&lt;p&gt;On the SwiftUI side &lt;code&gt;.searchable()&lt;/code&gt; in is an obvious API to try, but even with the &lt;code&gt;placement&lt;/code&gt; API, there&amp;rsquo;s no way to put in the center (by default it&amp;rsquo;s to the far right, and you can either put it to the far left, or &lt;em&gt;under&lt;/em&gt; the navigation bar, by passing different values). With &lt;code&gt;toolbarRole&lt;/code&gt;, similar deal, values like &lt;code&gt;.browser&lt;/code&gt; will put it to the left instead, but not middle. &lt;code&gt;ToolbarItem(placement: .principal)&lt;/code&gt; meets a similar fate, as in visionOS, the &lt;code&gt;principal&lt;/code&gt; position is to the left, not center.&lt;/p&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2024/03/recreating-visionos-search-bar/right.jpeg&#34; alt=&#34;Basic SwiftUI window with search bar to the left and text in the middle that simply says &amp;#39;Perhaps the coolest View ever&amp;#39;&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Default SwiftUI searchable() position&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;In UIKit, the situation is similar, where &lt;code&gt;navigationItem.titleView&lt;/code&gt; is to the left, not center, on visionOS, and I was unable to find any other APIs that worked here.&lt;/p&gt;
&lt;p&gt;You could technically recreate navigation bar &lt;code&gt;UIView&lt;/code&gt;/&lt;code&gt;View&lt;/code&gt; from scratch, but navigation bars on visionOS have a nice progressive blur background that wouldn&amp;rsquo;t be fun to recreate, not to mention all the other niceties they have.&lt;/p&gt;
&lt;p&gt;All this to say, it&amp;rsquo;s totally possible there&amp;rsquo;s a clear API to do it, but I&amp;rsquo;ve dug around and poked a bunch of different people so it&amp;rsquo;s well hidden if it does exist! I&amp;rsquo;m assumning Apple&amp;rsquo;s using an internal-only API, or at least a custom UI here.&lt;/p&gt;
&lt;h2 id=&#34;step-1-creating-a-search-bar&#34;&gt;Step 1: Creating a search bar&lt;/h2&gt;
&lt;p&gt;SwiftUI doesn&amp;rsquo;t directly have the concept of a search bar view unfortunately, just the &lt;code&gt;.searchable&lt;/code&gt; modifier that only takes a few arguments, so… you know…&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/03/recreating-visionos-search-bar/bart.jpeg&#34; alt=&#34;That Simpsons meme where they say &amp;#39;Say the line, Bart!&amp;#39; but he responds &amp;#39;Let&amp;#39;s use UIKit&amp;#39; with much sadness&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;We&amp;rsquo;ll create a SwiftUI interface into UIKit&amp;rsquo;s &lt;code&gt;UISearchBar&lt;/code&gt; that allows us to store the typed text and respond when the user hits enter/return.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewRepresentable&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Binding&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;onSearchButtonClicked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Void&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;makeUIView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBar&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;searchBar&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;delegate&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;coordinator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;updateUIView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;uiView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;uiView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;makeCoordinator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Coordinator&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SearchBarCoordinator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SearchBarCoordinator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NSObject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBarDelegate&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;parent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SearchBar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;init&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;parent&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;textDidChange&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchText&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;parent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchText&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;searchBarSearchButtonClicked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;parent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;onSearchButtonClicked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;resignFirstResponder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we can easily use it as so:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;ContentView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;View&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;State&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;searchText&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;some&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;View&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;SearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;$&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;searchText&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;User hit return&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


    &lt;img src=&#34;https://christianselig.com/2024/03/recreating-visionos-search-bar/overlap.jpeg&#34; alt=&#34;Search bar at the very top but taking up full width so it overlaps the title in an ugly way&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Hmm, looks a little off.&lt;/p&gt;
&lt;h2 id=&#34;step-2-positioning&#34;&gt;Step 2: Positioning&lt;/h2&gt;
&lt;p&gt;Cool, we have a search bar, how do we position it? Again, tons of ways to do this. Perhaps the &amp;ldquo;most correct&amp;rdquo; way would be to completely wrap a &lt;code&gt;UINavigationBar&lt;/code&gt; or &lt;code&gt;UIToolbar&lt;/code&gt;, add a &lt;code&gt;UISearchBar&lt;/code&gt; as a subview and then move it around in &lt;code&gt;layoutSubviews&lt;/code&gt; relative to the other bar button items, titles, and whatnot. But that&amp;rsquo;s probably overkill, and we want a simple SwiftUI solution, so (as the great &lt;a href=&#34;https://twitter.com/drewolbrich&#34;&gt;Drew Olbrick&lt;/a&gt; suggested) we can just overlay it on top of our &lt;code&gt;NavigationStack&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;NavigationStack&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Welcome to my cool view&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;navigationTitle&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Search&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;overlay&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;alignment&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;top&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;SearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;$&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;searchText&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;User hit return&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is actually great, as we get all the niceties of the normal SwiftUI APIs, and the system even appropriately spaces our search bar from the top of the window. Only issue is an obvious one, the width is all wrong. Studying how Apple does it, in the Music and Apple TV app the search bar just stays a stationary width as the window can&amp;rsquo;t get too narrow, but let&amp;rsquo;s modify ours slightly a bit so if it does get too narrow, our search bar never takes up more than half the window&amp;rsquo;s width (Apple&amp;rsquo;s probably does something similar, but more elegantly), by wrapping things in a &lt;code&gt;GeometryReader&lt;/code&gt;. The height is fine to stay as-is.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;View&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Binding&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;onSearchButtonClicked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Void&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;some&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;View&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;GeometryReader&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;proxy&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;InternalSearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;$&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;onSearchButtonClicked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;onSearchButtonClicked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;frame&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;bp&#34;&gt;min&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mf&#34;&gt;500.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;proxy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;2.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;frame&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;maxWidth&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;infinity&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;alignment&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;center&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;InternalSearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewRepresentable&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Binding&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;onSearchButtonClicked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Void&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;makeUIView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBar&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;searchBar&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;delegate&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;coordinator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;updateUIView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;uiView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;uiView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;makeCoordinator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SearchBarCoordinator&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SearchBarCoordinator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SearchBarCoordinator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NSObject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBarDelegate&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;parent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;InternalSearchBar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;init&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;InternalSearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;parent&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;textDidChange&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchText&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;parent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchText&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;searchBarSearchButtonClicked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;parent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;onSearchButtonClicked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;searchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;resignFirstResponder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which results in…&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/03/recreating-visionos-search-bar/center.jpeg&#34; alt=&#34;Search bar at the top of the window, centered horizontally and not taking up the full width&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Bam.&lt;/p&gt;
&lt;h2 id=&#34;step-3-corner-radius&#34;&gt;Step 3: Corner radius&lt;/h2&gt;
&lt;p&gt;Our corner radius looks different than Apple&amp;rsquo;s at the top of the article!&lt;/p&gt;
&lt;p&gt;One oddity I noticed is different Apple apps on visionOS use different corner radii despite being that same, front and center search bar. (Rounded rectangle: Apple TV, Photos, App Store; circular: Music, Safari) Presumably this is just an oversight, but after poking some Apple folks it seems like the rounded option is the correct one in this case, and I too prefer the look of that, so let&amp;rsquo;s go with that one.&lt;/p&gt;
&lt;p&gt;One issue… The default is a rounded rectangle, not circular/capsule, and API to directly change this (as far as I can tell) is private API. But &lt;code&gt;cornerRadius&lt;/code&gt; is just a public API on &lt;code&gt;CALayer&lt;/code&gt;, so we just have to find the correct layer(s) and tweak them so they&amp;rsquo;re circular instead. We can do this by subclassing &lt;code&gt;UISearchBar&lt;/code&gt; and monitoring its subviews for any changes to their layer&amp;rsquo;s corner radius, and changing those layers to our own circular corner radius.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;CircularSearchBar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UISearchBar&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;didObserveSubviews&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;desiredCornerRadius&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;22.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;observedLayers&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NSHashTable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CALayer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;weakObjects&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;deinit&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// We need to manually track and remove CALayers we add observers for, the OS seemingly does not handle this properly for us, perhaps because we&amp;#39;re adding observers for sublayers as well and there&amp;#39;s timing issues with deinitialization?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// (Also don&amp;#39;t store strong references to layers or we can introduce reference cycles)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;object&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;observedLayers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;objectEnumerator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;layer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;object&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CALayer&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;continue&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;layer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;removeObserver&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;forKeyPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;cornerRadius&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;willMove&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;toWindow&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;newWindow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIWindow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;willMove&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;toWindow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;newWindow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;     
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Adding to window&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;didObserveSubviews&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;didObserveSubviews&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;observeSubviews&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;observeSubviews&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;observedLayers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;contains&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;layer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;layer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addObserver&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;forKeyPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;cornerRadius&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;options&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;observedLayers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;add&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;layer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subviews&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forEach&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;observeSubviews&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;observeValue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forKeyPath&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;keyPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;of&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;object&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Any&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;change&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;NSKeyValueChangeKey&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Any&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]?,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UnsafeMutableRawPointer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;keyPath&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;cornerRadius&amp;#34;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;observeValue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forKeyPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;keyPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;of&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;object&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;change&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;change&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;layer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;object&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CALayer&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;layer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cornerRadius&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;desiredCornerRadius&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;layer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cornerRadius&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;desiredCornerRadius&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which gives us this beautiful, circular result once we replace &lt;code&gt;UISearchBar&lt;/code&gt; with &lt;code&gt;CircularSearchBar&lt;/code&gt;.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/03/recreating-visionos-search-bar/circular.jpeg&#34; alt=&#34;Search bar at the top of the window with a fully circular corner radius&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;step-4-remove-hairline&#34;&gt;Step 4: Remove hairline&lt;/h2&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2024/03/recreating-visionos-search-bar/hairline.jpeg&#34; alt=&#34;A hairline border underneath the search bar in the center&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Nooo, what IS that?&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Just when you think you&amp;rsquo;re done, &lt;a href=&#34;https://www.youtube.com/watch?v=2NL2lRwlDbw&#34;&gt;you notice there&amp;rsquo;s a little hairline border underneath the search bar&lt;/a&gt; that looks kinda off in our context. This is also not easily addressable with an API, but we can find it ourselves and hide it. You&amp;rsquo;d think you&amp;rsquo;d just find a thin &lt;code&gt;UIView&lt;/code&gt; and hide it, but Apple made this one nice and fun by making it a normal sized image view set to an image of a thin line.&lt;/p&gt;
&lt;p&gt;Knowing that, we could find the image view and sets its image to &lt;code&gt;nil&lt;/code&gt;, or hide it, but through something done behind the scenes those operations seem to be overwritten, however just setting the &lt;code&gt;alpha&lt;/code&gt; to 0 also hides it perfectly.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;hideImageViews&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;imageView&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;view&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIImageView&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;imageView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;alpha&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subviews&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forEach&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;hideImageViews&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And add &lt;code&gt;hideImageViews(self)&lt;/code&gt; to our &lt;code&gt;willMove(toWindow:)&lt;/code&gt; method.&lt;/p&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2024/03/recreating-visionos-search-bar/penguin.jpeg&#34; alt=&#34;Search bar at the top of the window, without any border underneath, shown in an app called Penguin Finder with a penguin as the window&amp;#39;s background image with a progressive blur at the top under the search bar&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;That&amp;#39;s it! 🎉&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;With that, we&amp;rsquo;re done and we should have nice solution for a search bar that more closely mimics how visionOS shows prominent search bars, at least until Apple hopefully adds a more straightforward way to do this! (FB13696963)&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Trials and tribulations of 360° video in Juno</title>
      <link>https://christianselig.com/2024/02/trials-360-juno-video/</link>
      <pubDate>Sun, 25 Feb 2024 21:53:34 -0400</pubDate>
      
      <guid>https://christianselig.com/2024/02/trials-360-juno-video/</guid>
      <description>&lt;p&gt;In building &lt;a href=&#34;https://juno.vision&#34;&gt;Juno, a visionOS app for YouTube&lt;/a&gt;, a question that&amp;rsquo;s come up from users a few times is whether it supports 360° and 180° videos (for the unfamiliar, it&amp;rsquo;s an immersive video format that fully surrounds you). The short answer is no, it&amp;rsquo;s sort of a niche feature without much adoption, but for fun I wanted to take the weekend and see what I could come up with. Spoiler: it&amp;rsquo;s not really possible, but the &lt;em&gt;why&lt;/em&gt; is kinda interesting, so I thought I&amp;rsquo;d write a post talking about it, and why it&amp;rsquo;s honestly not a big loss at this stage.&lt;/p&gt;
&lt;h2 id=&#34;how-do-you-even-show-a-360-video&#34;&gt;How do you even… show a 360° video?&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/02/trials-360-juno-video/realitykit.png&#34; alt=&#34;Logo for Apple&amp;#39;s RealityKit framework, which is three stacked rectangles, dark grey, grey, then yellow at the top, with a 3D sphere, a cylinder, and a cone in white sitting on top&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;It&amp;rsquo;s actually a lot easier than you might think. A 360° (or 180°) video isn&amp;rsquo;t some crazy format in some crazy shape, it&amp;rsquo;s just a regular, rectangular video file in appearance, but it&amp;rsquo;s been recorded and stored in that rectangle slightly warped, with the expectation that however you display it will unwarp it.&lt;/p&gt;
&lt;p&gt;So how do you display it? Also pretty simply, you just create a hollow sphere, and you tell your graphical engine (in iOS&amp;rsquo; case: RealityKit) to stretch the video over the inside of the sphere. Then you put the user at the center of that sphere (or half-sphere, in the case of 180° video), and bam, immersive video.&lt;/p&gt;
&lt;h2 id=&#34;theres-always-a-catch&#34;&gt;There&amp;rsquo;s always a catch&lt;/h2&gt;
&lt;p&gt;In RealityKit, you get shapes, and you get materials you can texture those shapes with. But you can&amp;rsquo;t just use &lt;em&gt;anything&lt;/em&gt; as a texture, silly. Applying a texture to a complex 3D shape can be a pretty intensive thing to do, so RealityKit basically only wants images, or videos (a bit of an oversimplification but it holds for this example). You can&amp;rsquo;t, for instance, show a scrolling recipe list or a dynamic map of your city and stretch that over a cube. &amp;ldquo;Views&amp;rdquo; in SwiftUI and UIKit (labels, maps, lists, web views, buttons, etc.) are not able to be used as a material (yet?).&lt;/p&gt;
&lt;p&gt;This is a big problem for us. If you &lt;a href=&#34;https://christianselig.com/2024/02/introducing-juno/&#34;&gt;don&amp;rsquo;t remember&lt;/a&gt;, while Juno obviously shows videos, it uses web views to accomplish this, as it&amp;rsquo;s the only way YouTube allows developers to show YouTube videos (otherwise you could, say, avoid viewing ads which YouTube doesn&amp;rsquo;t want), and I don&amp;rsquo;t want to annoy Google/YouTube.&lt;/p&gt;
&lt;p&gt;Web views, while they show a video, are basically a portal into a video watching experience. You&amp;rsquo;re just able to see the video, you don&amp;rsquo;t have access to the underlying video directly, so you can&amp;rsquo;t apply it as a texture with RealityKit. So we can&amp;rsquo;t show it on a sphere, so we can&amp;rsquo;t do 360° video.&lt;/p&gt;
&lt;p&gt;Unless…&lt;/p&gt;
&lt;h2 id=&#34;lets-get-inventive&#34;&gt;Let&amp;rsquo;s get inventive&lt;/h2&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2024/02/trials-360-juno-video/vision-vision.jpeg&#34; alt=&#34;Vision from Wandavision TV show saying &amp;#39;What is video, if not a bunch of images persevering?&amp;#39;&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Do I get points for including Vision in an article about Vision Pro&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Okay, so we only have access to the video through a web view. We can see the video though, so what if we just continue to use the web player, and as it plays for the user, we took snapshots of each video frame and painted those snapshots over the sphere that surrounds the user. Do it very rapidly, say, 24 times per second, and you effectively have 24 fps video. Like a flip book!&lt;/p&gt;
&lt;p&gt;Well, easier said than done! The first big hurdle is that when you take a snapshot of a webview (&lt;code&gt;WKWebView&lt;/code&gt;), everything renders into an image perfectly… &lt;a href=&#34;https://developer.apple.com/forums/thread/683984&#34;&gt;&lt;em&gt;except&lt;/em&gt; the playing video&lt;/a&gt;. I assume this is because the video is being hardware decoded in a way that is separate from how iOS performs the capture, so it&amp;rsquo;s absent. (It&amp;rsquo;s not because of DRM or anything like that, it also occurs just for local videos on my website.)&lt;/p&gt;
&lt;p&gt;This is fixable though, with JavaScript we can draw the HTML video element into a separate canvas, and then snapshot the canvas instead.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;video&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;document&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;querySelector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;video&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;document&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;createElement&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;canvas&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;video&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;videoWidth&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;height&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;video&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;videoHeight&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;getContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;2d&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;drawImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;video&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;video&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;style&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;display&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;none&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;document&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;prepend&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, now we have the video frame visible. How do we capture it? There&amp;rsquo;s a &lt;em&gt;bunch&lt;/em&gt; of different tactics I tried for this, and I couldn&amp;rsquo;t quite get any of them to be fast enough to be able to simulate 24 FPS (in order to get 24 captured frames per second, each frame capture must be less than 42 ms). But let&amp;rsquo;s enumerate them from slowest to fastest in taking a snapshot of a 4K video frame (average of 10 runs).&lt;/p&gt;
&lt;h4 id=&#34;calayer-renderin-cgcontext&#34;&gt;CALayer render(in: CGContext)&lt;/h4&gt;
&lt;p&gt;Renders a &lt;code&gt;CALayer&lt;/code&gt; into a &lt;code&gt;CGImage&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;UIGraphicsBeginImageContextWithOptions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;webView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bounds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;context&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIGraphicsGetCurrentContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;webView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;layer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;render&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;image&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIGraphicsGetImageFromCurrentImageContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;UIGraphicsEndImageContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;⏱️ Time: 270 ms&lt;/p&gt;
&lt;h4 id=&#34;metal-texture&#34;&gt;Metal texture&lt;/h4&gt;
&lt;p&gt;(&lt;a href=&#34;https://stackoverflow.com/a/61862728&#34;&gt;Code from Chris on StackOverflow&lt;/a&gt;)&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;extension&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;UIView&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;takeTextureSnapshot&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;device&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;MTLDevice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;MTLTexture&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bounds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;height&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bounds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;context&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bitsPerComponent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bytesPerRow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;space&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGColorSpaceCreateDeviceRGB&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bitmapInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGImageAlphaInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;premultipliedLast&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rawValue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;layer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;render&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;desc&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;MTLTextureDescriptor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;texture2DDescriptor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pixelFormat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rgba8Unorm&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;mipmapped&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;texture&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;device&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;makeTexture&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;descriptor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;desc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;texture&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;replace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;region&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;MTLRegionMake2D&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;mipmapLevel&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;withBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bytesPerRow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bytesPerRow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;texture&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;texture&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;webView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;takeTextureSnapshot&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;device&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;MTLCreateSystemDefaultDevice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;⏱️ Time: 250 ms (&lt;em&gt;I really thought this would be faster, and maybe I&amp;rsquo;m doing something wrong, or perhaps Metal textures are hyper-efficient once created, but take a bit to create in the first place&lt;/em&gt;)&lt;/p&gt;
&lt;h4 id=&#34;uiview-drawhierarchy&#34;&gt;UIView drawHierarchy()&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;rendererFormat&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIGraphicsImageRendererFormat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;rendererFormat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;opaque&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;renderer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIGraphicsImageRenderer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;webView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bounds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;format&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;rendererFormat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;image&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;renderer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;image&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;webView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;drawHierarchy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;webView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bounds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;afterScreenUpdates&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;⏱️ Time: 150 ms&lt;/p&gt;
&lt;h4 id=&#34;javascript-transfer&#34;&gt;JavaScript transfer&lt;/h4&gt;
&lt;p&gt;What if we relied on JavaScript to do all the heavy lifting, and had the canvas write its contents into a base64 string, and then using WebKit messageHandlers, communicate that back to Swift?&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;video&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;document&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;querySelector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;video&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;document&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;createElement&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;canvas&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;video&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;videoWidth&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;height&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;video&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;videoHeight&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;getContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;2d&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;drawImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;video&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;video&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;style&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;display&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;none&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;document&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;prepend&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 🟢 New code
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;imageData&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;canvas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;toDataURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;image/jpeg&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;webkit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;messageHandlers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;imageHandler&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;postMessage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;imageData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then convert that to UIImage.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;userContentController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;userContentController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;WKUserContentController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;didReceive&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;WKScriptMessage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;imageHandler&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;dataURLString&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;body&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;image&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;convertToUIImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dataURLString&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;convertToUIImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dataURLString&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIImage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;dataURL&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;URL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dataURLString&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;!&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;contentsOf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dataURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;⏱️ Time: 130 ms&lt;/p&gt;
&lt;h4 id=&#34;wkwebview-takesnapshot&#34;&gt;WKWebView takeSnapshot()&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;webView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;takeSnapshot&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;with&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;image&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;error&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;image&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;⏱️ Time: 70 ms&lt;/p&gt;
&lt;h4 id=&#34;test-results&#34;&gt;Test results&lt;/h4&gt;
&lt;p&gt;As you can see, the best of the best got to about 14 frames per second, which isn&amp;rsquo;t quite up to video playback level. Close, but not quite. I&amp;rsquo;m out of ideas.&lt;/p&gt;
&lt;p&gt;There were some interesting suggestions to use the WebCodecs VideoFrame API, or an OffscreenCanvas, but maybe due to my lack of experience with JavaScript I couldn&amp;rsquo;t get them meaningfully faster than the above JavaScript code with a normal canvas.&lt;/p&gt;
&lt;p&gt;If you have another idea, that you get working, I&amp;rsquo;d love to hear it.&lt;/p&gt;
&lt;h2 id=&#34;why-not-just-get-the-direct-video-file-then&#34;&gt;Why not just get the direct video file then?&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s two good answers to this question.&lt;/p&gt;
&lt;p&gt;First, the obvious one, Google/YouTube doesn&amp;rsquo;t like this. If you get the direct video URL, you can circumvent ads, which they&amp;rsquo;re not a fan of. I want Juno to happily exist as an amazing visionOS experience for YouTube, and Google requests you do so through the web player, and I think I can build an awesome app with that so that&amp;rsquo;s fine by me. 360° video is a small feature and I don&amp;rsquo;t think it&amp;rsquo;s worth getting in trouble over.&lt;/p&gt;
&lt;p&gt;Secondly, having access to the direct video still wouldn&amp;rsquo;t do you any good. Why? Codecs.&lt;/p&gt;
&lt;h2 id=&#34;battle-of-the-codecs&#34;&gt;Battle of the codecs&lt;/h2&gt;
&lt;p&gt;Quick preamble. For years, pretty much all web video was H264. Easy. It&amp;rsquo;s a format that compresses video to a smaller file size while still keeping a good amount of detail. The act of uncompressing it is a little intensive (think, unzipping a big zip file), so computers have dedicated chips specifically built to do this mega fast. You can do it without these, purely in software, but it takes longer and consumes more power, so not ideal.&lt;/p&gt;
&lt;p&gt;Time went on, videos got bigger, and the search for something that compresses video even better than H264 started (and without licensing fees). The creatively named H265 (aka HEVC) was born, and Apple uses it a bunch (it still has licensing fees, however). Google went in a different direction and developed VP9 and made it royalty-free, though there were still concerns around patents. These formats can produce video files that are half the size of H264 but with the same visual quality.&lt;/p&gt;
&lt;p&gt;Apple added an efficient H265 hardware decoder to the iPhone 7 back in 2016, but to my knowledge VP9 decoding is done completely in software to this day and just relies on the raw power and efficiency of Apple&amp;rsquo;s CPUs.&lt;/p&gt;
&lt;p&gt;Google wanted to use their own VP9 format, and &lt;strong&gt;for 4K videos and above, only VP9 is available&lt;/strong&gt;, no H264.&lt;/p&gt;
&lt;h2 id=&#34;okay-and&#34;&gt;Okay, and?&lt;/h2&gt;
&lt;p&gt;So if we want to play back a 4K YouTube video on our iOS device, we&amp;rsquo;re looking at a VP9 video plain and simple. The catch is, you cannot play VP9 videos on iOS unless you&amp;rsquo;re granted a special entitlement by Apple. &lt;a href=&#34;https://forums.macrumors.com/threads/apple-m1-vp9-av1-decoding.2269938/page-2?post=29417159#post-29417159&#34;&gt;The YouTube app has this special entitlement&lt;/a&gt;, called &lt;code&gt;com.apple.developer.coremedia.allow-alternate-video-decoder-selection&lt;/code&gt;, and so does Safari (and presumably other large video companies like Twitch, Netflix, etc.)&lt;/p&gt;
&lt;p&gt;But given that I cannot find any official documentation on that entitlement from Apple, safe to say it&amp;rsquo;s not an entitlement you or I are going to be able to get, so we cannot play back VP9 video, meaning we cannot play back 4K YouTube videos. Your guess is as good as mine why, maybe it&amp;rsquo;s very complex to implement if there&amp;rsquo;s indeed not a native hardware decoder, so Apple doesn&amp;rsquo;t like giving it out. So if you want 4K YouTube, you&amp;rsquo;re looking at either a web view or the YouTube app.&lt;/p&gt;
&lt;h2 id=&#34;sidebar-the-new-av1-codec&#34;&gt;Sidebar: the new AV1 codec&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/02/trials-360-juno-video/m3.jpeg&#34; alt=&#34;Apple keynote slide for the M3 chip listing AV1 decode support&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Given that no one could agree on a video format, everyone went back to the drawing board, formed a collective group called the Alliance for Open Media (has Google, Apple, Samsung, Netflix, etc.), and authored the AV1 codec, hopefully creating the one video format to rule them all, with no licensing fees and hopefully no patent issues.&lt;/p&gt;
&lt;p&gt;Google uses this on YouTube, and Apple even added a hardware decoder for AV1 in their latest A17 and M3 chips. This means on my iPhone 15 Pro I can play back an AV1 video in iOS&amp;rsquo; &lt;code&gt;AVPlayer&lt;/code&gt; like butter.&lt;/p&gt;
&lt;p&gt;Buuuuttttt, the Apple Vision Pro ships with an M2, which has no such hardware decoder.&lt;/p&gt;
&lt;h2 id=&#34;why-its-not-a-big-loss&#34;&gt;Why it&amp;rsquo;s not a big loss&lt;/h2&gt;
&lt;p&gt;So the tl;dr so far is YouTube uses the VP9 codec for 4K YouTube, and unless you&amp;rsquo;re special, you can&amp;rsquo;t playback VP9 video directly, which we need to do to be able to project it onto a sphere. Why not just do 1080p video?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Because even 4K video looks &lt;em&gt;bad&lt;/em&gt; in 360 degrees.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Wait, what? Yeah, 4K looks &lt;em&gt;incredible&lt;/em&gt; on the big TV in front of you, but you have to remember for 360° video, that same resolution is &lt;em&gt;completely surrounding you&lt;/em&gt;. At any given point, the area you&amp;rsquo;re looking at is a small subset of the full resolution! In other words, the Vision Pro&amp;rsquo;s resolution is 4K per eye, meaning any area you look can show a 4K image, and when you stretch a 4K video all around you, everywhere you look is &lt;em&gt;not&lt;/em&gt; 4K. Almost like the Vision Pro&amp;rsquo;s resolution per eye drops enormously. If you&amp;rsquo;re familiar with the pixels per degree (PPD) measurement for VR headsets, 4K immersive video has a quite bad PPD measurement.&lt;/p&gt;
&lt;p&gt;To test this further, I downloaded a 4K 360° video and projected it onto a sphere. The video is stretched from my feet to over my head. When I look straight, I&amp;rsquo;d say I&amp;rsquo;m looking at maybe 25% of the total height of the video. That means in a 4K video, which is 2,160 pixels tall, I can see maybe 25% of those pixels, or 540 pixels, so it looks a bit better than a 480p video but far from even 720p.&lt;/p&gt;
&lt;p&gt;Quick attempted visualization, showing the area you look at with a 4K TV:&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/02/trials-360-juno-video/living-room-tv.png&#34; alt=&#34;A TV in a living room in the center of your vision, labeled as 2160 pixels in height.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Versus the area you look at a 4K 360° video:&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/02/trials-360-juno-video/full-immersive.png&#34; alt=&#34;A visualization of being fully immersed in a 4K video, showing the center point that you&amp;#39;re actually looking at only being maybe 540 pixels in height.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;So in short, it might be 4K, but it&amp;rsquo;s stretched over a far more massive area than you&amp;rsquo;re used to when you think about 4K. Imagine your 4K TV is the size of your wall and you&amp;rsquo;re watching it from a foot away, it&amp;rsquo;d be immersive, but much less sharp. That means in reality it only looks a bit better than 480p or so.&lt;/p&gt;
&lt;p&gt;So while it&amp;rsquo;d be cool to have 4K 360° video in Juno, I don&amp;rsquo;t think it looks good enough that it&amp;rsquo;s that compelling an experience.&lt;/p&gt;
&lt;h2 id=&#34;enter-8k&#34;&gt;Enter 8K&lt;/h2&gt;
&lt;p&gt;For the demo videos on the Apple Vision Pro (and the videos they show at the Apple Store), those are recorded in 8K, which gives you twice as many vertical and horizontal pixels to work with, and it levels up the experience &lt;strong&gt;a ton&lt;/strong&gt;. Apple wasn&amp;rsquo;t flexing here, I&amp;rsquo;d say 8K is the minimum you want for a compelling, immersive video experience.&lt;/p&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2024/02/trials-360-juno-video/apple-immersive.jpeg&#34; alt=&#34;A person in a yellow jacket walking across a rope spread over a valley, with a large camera on a crane recording her.&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;Apple&amp;#39;s impressive 8K immersive video recording setup&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;And YouTube &lt;strong&gt;does&lt;/strong&gt; have 8K, 360° videos! They&amp;rsquo;re rare since the hardware to record that isn&amp;rsquo;t cheap, but they are available. &lt;a href=&#34;https://www.youtube.com/watch?v=1La4QzGeaaQ&#34;&gt;And pretty cool&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;But if I was a betting man, I doubt that&amp;rsquo;s ever coming to the first generation Vision Pro.&lt;/p&gt;
&lt;p&gt;Why? As mentioned 8K video on YouTube is only available in VP9 and AV1. The Vision Pro does not have a hardware AV1 decoder as it has an M2 not an M3, so it would have to do it in software. Testing on my M1 Pro MacBook Pro, which seems to Geekbench similarly to the Vision Pro, trying to playback 8K 360° video in Chrome is quite choppy and absolutely hammers my CPU. Apple&amp;rsquo;s chips may be powerful enough to grunt through 4K without a hardware decoder, but it doesn&amp;rsquo;t seem you can brute force 8K without a hardware decoder.&lt;/p&gt;
&lt;p&gt;Maybe I&amp;rsquo;m wrong or missing something, or Google works with Apple and re-encodes their 360° videos in a specific, Vision-Pro-only H265 format, but I&amp;rsquo;m not too hopeful that this generation of the product, without an M3, will have 8K 360° YouTube playback. That admittedly is an area the Quest 3 has the Vision Pro beat, in that its Qualcomm chip has an AV1 decoder.&lt;/p&gt;
&lt;p&gt;Does this mean the Vision Pro is a failure and we&amp;rsquo;ll never see 8K immersive video? Not at all, you could do it in a different codec, Apple has already shown it&amp;rsquo;s possible, I&amp;rsquo;m just not too hopeful for YouTube videos at this stage.&lt;/p&gt;
&lt;h2 id=&#34;in-conclusion&#34;&gt;In Conclusion&lt;/h2&gt;
&lt;p&gt;As a developer, playing back the codec YouTube uses for its 4K video does not appear possible. It also doesn&amp;rsquo;t &lt;em&gt;seem&lt;/em&gt; possible to snapshot frames fast enough to project it in realtime 3D. And even if it was, 4K video does not look too great unfortunately, ideally you want 8K, which seems even less likely.&lt;/p&gt;
&lt;p&gt;But dang, it was a fun weekend learning and trying things. If you manage to figure out something that I was unable to, tell me on &lt;a href=&#34;https://mastodon.social/@christianselig&#34;&gt;Mastodon&lt;/a&gt; or &lt;a href=&#34;https://twitter.com/christianselig&#34;&gt;Twitter&lt;/a&gt;, and I&amp;rsquo;ll name the 3D Theater in Juno after you. 😛 In the meantime, I&amp;rsquo;m going to finish up Juno 1.2!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Additonal thanks to: Khaos Tian, Arthur Schiller, Drew Olbrich, Seb Vidal, Sanjeet Suhag, Eric Provencher, and others! ❤️&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>My little Apple Vision Pro stand</title>
      <link>https://christianselig.com/2024/02/vision-pro-stand/</link>
      <pubDate>Mon, 19 Feb 2024 13:41:49 -0400</pubDate>
      
      <guid>https://christianselig.com/2024/02/vision-pro-stand/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2024/02/vision-pro-stand/hero.jpeg&#34; alt=&#34;Three-quarter shot of the Vision Pro stand showing the Vision Pro standing vertically on a stand with a Pringle chip style top that it rests on, connected to the base with a walnut dowel, with the base holding the battery with an organizer for the cable&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;I want somewhere to put my Vision Pro when not in use. Many people use the original box, and there&amp;rsquo;s &lt;a href=&#34;https://www.etsy.com/ca/listing/1656444234/apple-vision-pro-dock-display-stand-for&#34;&gt;beautiful stands that exist out there&lt;/a&gt;, but I was looking for something more compact and vertical so it would take up less room on my desk.&lt;/p&gt;
&lt;p&gt;So I opened Fusion 360 (which I am still very much learning), grabbed my calipers, and set out to design a little stand. &lt;a href=&#34;https://twitter.com/ChristianSelig/status/1755331992575451547&#34;&gt;There was interest&lt;/a&gt; when I showed the first version, so I set out to tidying it up a bit before making it available. Mainly the rod going up to the pringle part was a bit weak, so I ended up beefing up the diameter to 3/4&amp;quot;. This also now means you can either 3D print the rod, or pick up a bespoke 3/4&amp;quot; rod of your own in a cool material, like walnut, maple, brass, steel, copper, etc. for pretty cheap from Home Depot or Amazon that is 215 mm in length. Then just use some superglue to bind them.&lt;/p&gt;
&lt;p&gt;Anywho, I quite like the end result, it&amp;rsquo;s compact, seems pretty secure, holds the battery and has a spot for the cable and rubber feet. Mine is printed in marble PLA with a $3 walnut dowel. It&amp;rsquo;s designed around my 23W light seal, I admittedly don&amp;rsquo;t know how it works with other sizes.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/02/vision-pro-stand/battery-cable.jpeg&#34; alt=&#34;Close up shot of the bottom of the stand holding a battery and having an area to keep the cables tidy&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;You can &lt;a href=&#34;https://makerworld.com/en/models/193582#profileId-214008&#34;&gt;download it on MakerWorld&lt;/a&gt;, I like them because I can send the designs to my Bambu printer really easily, and they give free filament with enough downloads, haha. You can also download &lt;a href=&#34;https://makerworld.com/en/models/193597#profileId-214021&#34;&gt;just the top pringle&lt;/a&gt; if you want a lens shield for your bag or something. If you really like the design please give it a Boost on Makerworld too!&lt;/p&gt;
&lt;p&gt;For a period of time, if you say the promo code &lt;em&gt;&amp;ldquo;free free free for me me me&amp;rdquo;&lt;/em&gt; to your computer, the download will be available for free.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m unfortunately not assembling/selling this myself, but if you don&amp;rsquo;t have a 3D printer (I quite like my Bambu P1S, if you&amp;rsquo;re looking for a suggestion), lots of local libraries have 3D printers nowadays, or you can submit it to sites like &lt;a href=&#34;https://www.pcbway.com&#34;&gt;PCBWay&lt;/a&gt; to have them make it for you. I don&amp;rsquo;t quite have the expertise/time to manufacture and sell this myself for folks without a 3D printer, but if you the reader would like to on Etsy or something, go for it! (Just maybe link credit back, and if you make a bunch of money donate some to your local animal shelter and I&amp;rsquo;d be stoked.)&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Juno 1.1</title>
      <link>https://christianselig.com/2024/02/juno-1-1/</link>
      <pubDate>Wed, 14 Feb 2024 16:22:25 -0400</pubDate>
      
      <guid>https://christianselig.com/2024/02/juno-1-1/</guid>
      <description>&lt;p&gt;&lt;em&gt;&lt;strong&gt;If you&amp;rsquo;re new, &lt;a href=&#34;https://christianselig.com/2024/02/introducing-juno/&#34;&gt;Juno is a visionOS app for YouTube&lt;/a&gt;!&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Juno&amp;rsquo;s initial launch blew my socks off. It was such a cool feeling to release an app on day one of the Apple Vision Pro&amp;rsquo;s launch, and having people be so excited about it and have such great feedback made it that much better. After coding like crazy all week to get it submitted, then driving 10 hours to New Hampshire from Canada, sitting down and reading all the comments in my hotel room made me really happy and even more excited to keep building onto the app for this cool new platform.&lt;/p&gt;
&lt;p&gt;It was a little brutal though realizing the long drive back I had to finish first before I could get much work done though, but I made it home just in time for a delightful snowstorm!&lt;/p&gt;
&lt;p&gt;After that, I got to work, and one week later Juno 1.1 is now available and addresses a bunch of the great feedback given and makes the app that much better. I&amp;rsquo;ve also got some fun stuff cooking for 1.2 and beyond, being able to actually use it on the device makes building it that much more fun. :)&lt;/p&gt;
&lt;p&gt;Here are the changes in 1.1:&lt;/p&gt;
&lt;h2 id=&#34;quality-options&#34;&gt;Quality options&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/02/juno-1-1/hero.jpeg&#34; alt=&#34;Video of Casey Neistat video playing in Juno with the new quality options overlaid&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Juno makes a best guess at what resolution to play back at, and while that&amp;rsquo;s still present it felt a bit better in the simulator than it did on the real device so a lot of folks requested manual control over the playback quality, and Juno 1.1 adds exactly that! Want 4K? Have at it! 240p aficionado? Got you covered too.&lt;/p&gt;
&lt;h2 id=&#34;volume-control&#34;&gt;Volume control&lt;/h2&gt;
&lt;p&gt;Added quicker access to volume controls, so now you can tweak Juno&amp;rsquo;s volume right from the video player (you can still also change it by reaching up to the top of the device&amp;rsquo;s dial and then look at the volume icon to adjust). Right now this affects the video player&amp;rsquo;s volume specifically, not the whole system&amp;rsquo;s, but I&amp;rsquo;m looking into how to improve that.&lt;/p&gt;
&lt;h2 id=&#34;drag-and-drop-support&#34;&gt;Drag and drop support&lt;/h2&gt;
&lt;p&gt;If someone sends you a funny YouTube video, just drag and drop the link onto Juno to open it!&lt;/p&gt;
&lt;h2 id=&#34;captionssubtitles&#34;&gt;Captions/subtitles&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/02/juno-1-1/captions.jpeg&#34; alt=&#34;Video of Sanago video playing in Juno in Korean with English subtitles showing&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Haven&amp;rsquo;t quite learned Korean yet but still want to know what the person is saying? Or simply want some auto-generated captions for your own language? Juno has you covered now.&lt;/p&gt;
&lt;h2 id=&#34;url-scheme&#34;&gt;URL scheme&lt;/h2&gt;
&lt;p&gt;If you want to open a video in Juno via Shortcuts or another app, simply change the &lt;code&gt;https://&lt;/code&gt; part of the URL to &lt;code&gt;juno://&lt;/code&gt;, so for instance &lt;code&gt;https://www.youtube.com/watch?v=dtp6b76pMak&lt;/code&gt; becomes &lt;code&gt;juno://www.youtube.com/watch?v=dtp6b76pMak&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;redesigned-end-of-video-screen&#34;&gt;Redesigned end of video screen&lt;/h2&gt;
&lt;p&gt;When a video ends you can now quickly close the video or restart the video with a friendly little screen.&lt;/p&gt;
&lt;h2 id=&#34;even-faster-video-load-times&#34;&gt;Even faster video load times&lt;/h2&gt;
&lt;p&gt;Found an area of my code that I was able to increase the efficiency of substantially and videos should load even faster now! 🏎️&lt;/p&gt;
&lt;h2 id=&#34;video-player-ui-improvements&#34;&gt;Video player UI improvements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;You can now more easily jump between playback speeds&lt;/li&gt;
&lt;li&gt;Improved video scrubbing control (volume control also uses it) with a new custom view that expands on selection called JunoSlider (planning to open source soon).&lt;/li&gt;
&lt;li&gt;Corner radius is less dramatic during video playback so as to crop out less of the video&lt;/li&gt;
&lt;li&gt;When video playback controls fade out, the system &amp;lsquo;grab bar&amp;rsquo; now also fades out as it could be distracting to your immersion&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;less-accidental-input&#34;&gt;Less accidental input&lt;/h2&gt;
&lt;p&gt;This is a funny one. When designing Juno I had a bunch of fun ideas to make video playback nicer, and most of them went over well!&lt;/p&gt;
&lt;p&gt;On the other side, I had the idea to make it so you could &amp;ldquo;scrub anywhere&amp;rdquo; on the video screen to go backward and forward in time, a beloved feature in Apollo which worked great in the visionOS simulator with a mouse and keyboard, but on the actual device when you&amp;rsquo;re pinching and looking around at all sorts of stuff it introduced &lt;em&gt;a lot&lt;/em&gt; of accidental input and was much more of a pain than an actual feature. So I nixed this one, and instead made the video scrubber control at the bottom even better.&lt;/p&gt;
&lt;p&gt;Another feature similar to that was how on iOS if the video controls are hidden, and you tap the middle of the screen, it assumes you want to pause and does so. I added this to Juno, because it felt great in the simulator, but yeah, same situation where a lot of people thought this was also an annoying bug, and just looking at the pause button is already pretty fast, so not much need for this one.&lt;/p&gt;
&lt;h2 id=&#34;a-better-memory&#34;&gt;A better memory&lt;/h2&gt;
&lt;p&gt;Juno now remembers your playback speed and volume settings from the previous video and automatically applies them.&lt;/p&gt;
&lt;h2 id=&#34;bug-fixes&#34;&gt;Bug fixes&lt;/h2&gt;
&lt;p&gt;In addition to the above there&amp;rsquo;s also a bunch of bug fixes for things folks with keen eyes were kind enough to point out!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fixed bug where you could be signed out temporarily if tapping on the Home button&lt;/li&gt;
&lt;li&gt;Fixed bug where video controls (like the close button) could disappear sometimes, particularly after resizing&lt;/li&gt;
&lt;li&gt;Fixed bug where video title could sometimes be wrong&lt;/li&gt;
&lt;li&gt;Fixed bug where you couldn&amp;rsquo;t tap the video categories at the top of the home page&lt;/li&gt;
&lt;li&gt;Fixed bug where video could get cropped down on resize&lt;/li&gt;
&lt;li&gt;Fixed bug where video controls would still automatically fade away when paused&lt;/li&gt;
&lt;li&gt;Fixed bug where the icon for going forward could be backward&lt;/li&gt;
&lt;li&gt;Fixed bug where playback bar could be stretched&lt;/li&gt;
&lt;li&gt;Fixed bug where video controls could persist after closing video&lt;/li&gt;
&lt;li&gt;A bunch of other smaller fixes and tweaks&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;thank-you-&#34;&gt;Thank You ❤️&lt;/h2&gt;
&lt;p&gt;The receipt to Juno really has been phenomenal and kind, I really thank you for that, it&amp;rsquo;s been a ton of fun developing and hearing what you think.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re enjoying the work, leaving a positive review on the App Store would be awesome, and if you haven&amp;rsquo;t checked it out yet, I&amp;rsquo;d love if you did and let me know what you think.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got a bunch of more fun ideas cooking for Juno! Keep the feedback coming!&lt;/p&gt;
&lt;h2 id=&#34;download-link&#34;&gt;Download link&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://juno.vision&#34;&gt;juno.vision&lt;/a&gt; 🥽&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Introducing Juno for Apple Vision Pro</title>
      <link>https://christianselig.com/2024/02/introducing-juno/</link>
      <pubDate>Thu, 01 Feb 2024 22:29:26 -0500</pubDate>
      
      <guid>https://christianselig.com/2024/02/introducing-juno/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2024/02/introducing-juno/hero.jpeg&#34; alt=&#34;Apple Vision Pro view of a living room with a floating window showing an iJustine video with the Juno app icon floating above it&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;YouTube is probably one of the parts of the internet I consume the most, so I was more than a little sad when YouTube announced that they don&amp;rsquo;t have plans to build a visionOS app, and disabled the option to load the iPad app. This leaves you with Safari, and the website is okay, but definitely doesn&amp;rsquo;t feel like a visionOS app. Couple that with visionOS not having the option to add websites to your Home Screen, and YouTube isn&amp;rsquo;t that convenient on visionOS by default.&lt;/p&gt;
&lt;p&gt;Then I remembered for years my old app, Apollo, played back YouTube videos submitted to Reddit pretty well, and I developed a pretty good understanding of how YouTube worked. That sparked the idea to reuse some of Apollo&amp;rsquo;s code there and build a little YouTube client of my own for visionOS, and after a mad week of coding &amp;ldquo;Juno for YouTube&amp;rdquo; is born.&lt;/p&gt;
&lt;h2 id=&#34;how-does-it-work-technically&#34;&gt;How does it work… technically?&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/02/introducing-juno/cleo.jpeg&#34; alt=&#34;Cleo Abram video where she&amp;#39;s hanging out with a Boston Dynamics robot&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;YouTube has a few different APIs.&lt;/p&gt;
&lt;p&gt;They have a &amp;ldquo;Data API&amp;rdquo; for fetching information (thumbnail, duration, etc.) for a video, that requires an API key, auditing, and you can only call so many times a day. This API doesn&amp;rsquo;t actually get you the video to play or anything, it&amp;rsquo;s purely for metadata, and for uploading.&lt;/p&gt;
&lt;p&gt;They have private/internal APIs that they get grumpy at you for using because you can circumvent ads. The goal with this app was to not make Google grumpy.&lt;/p&gt;
&lt;p&gt;Lastly, they have an &lt;a href=&#34;https://developers.google.com/youtube/iframe_api_reference&#34;&gt;embed API&lt;/a&gt; that&amp;rsquo;s pretty powerful, and is what I used in Apollo and now Juno. There&amp;rsquo;s no API keys, or limits to how many times a day you can call it, as it literally just loads the video in a webview, and provides JavaScript methods to interact with the video, such as pause, play, speed up, etc. It&amp;rsquo;s really nice, you can play YouTube videos back, and YouTube still gets to show ads (if the user doesn&amp;rsquo;t have YouTube Premium) and whatnot so no one is grumpy.&lt;/p&gt;
&lt;p&gt;This means you can build a fully native visionOS UI that then using JavaScript interacts with the underlying YouTube player, so you get the best of both worlds. Juno even supports detecting aspect ratios of the videos and will resize the window automatically, so ultra-wide 21:9 movie trailers are respected, as are nostalgic 4:3 uploads.&lt;/p&gt;
&lt;p&gt;The one downside is that occasionally you&amp;rsquo;ll get a creator who disabled playback for YouTube embeds. This is rare, especially with videos made in the last few years, but for those Juno will auto-detect that and just load up the normal video website page rather than the fancy player.&lt;/p&gt;
&lt;h2 id=&#34;what-about-the-browsing-itself&#34;&gt;What about the browsing itself?&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2024/02/introducing-juno/search.jpeg&#34; alt=&#34;Searching &amp;#39;Apple Vision Pro&amp;#39; in Juno, showing search results for day one reviews with a website feel combined with visionOS aesthetics&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;At its core, Juno uses the YouTube website itself. No, not scraped. It presents the website as you would load it, but similar to how browser extensions work, it tweaks the theming of the site through CSS and JavaScript.&lt;/p&gt;
&lt;p&gt;That results in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tweaking backgrounds so the beautiful glassy look of visionOS shows through. As the great &lt;a href=&#34;https://twitter.com/settern/status/1752789654787752044&#34;&gt;Serenity Caldwell once said&lt;/a&gt;, &amp;ldquo;&lt;em&gt;Opaque windows can feel heavy and constricting, especially at large sizes. Whenever possible, prefer the glass material (which pulls light from people’s surroundings).&lt;/em&gt;&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Increasing contrast so items are properly visible&lt;/li&gt;
&lt;li&gt;Making buttons like the button to view your subscriptions native UI, and then loading the relevant portions of the website accordingly&lt;/li&gt;
&lt;li&gt;You get your full recommendations, subscriptions and whatnot, just as you would on the normal YouTube site or app&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It was a lot of work tweaking the CSS to get the YouTube website to something that felt comfortable and at home on visionOS, but I&amp;rsquo;m really happy with how it turned out. Does it feel like a &lt;em&gt;perfectly&lt;/em&gt; native visionOS app? Well no, but it&amp;rsquo;s a heck of a lot nicer than the website, and to be fair Google apps normally do their own thing rather than use iOS system UI, so not sure we&amp;rsquo;ll ever fully see that. :)&lt;/p&gt;
&lt;h2 id=&#34;does-it-block-ads&#34;&gt;Does it block ads?&lt;/h2&gt;
&lt;p&gt;It doesn&amp;rsquo;t, I don&amp;rsquo;t think Google would like that, but if you have YouTube Premium you won&amp;rsquo;t see ads, just like the website. Honestly, YouTube Premium is like one of the most essential subscriptions for me, it&amp;rsquo;s so handy to never worry about ads &lt;em&gt;and&lt;/em&gt; it&amp;rsquo;s pretty cool in that it also supports the creators &lt;a href=&#34;https://www.youtube.com/watch?v=MDsJJRNXjYI&amp;amp;t=897s&#34;&gt;substantially more&lt;/a&gt; than if you watched ads. So I dunno, if you can afford an expensive Apple Vision Pro, I&amp;rsquo;d really consider treating yourself to YouTube Premium!&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2024/02/introducing-juno/mkbhd.jpeg&#34; alt=&#34;An MKBHD video that is positioned slightly off center in your peripheral vision&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;features&#34;&gt;Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Beautiful translucent visionOS interface&lt;/li&gt;
&lt;li&gt;Automatic aspect ratio detection&lt;/li&gt;
&lt;li&gt;Speed up or slow down video&lt;/li&gt;
&lt;li&gt;Native controls for video playback&lt;/li&gt;
&lt;li&gt;Pinch-drag anywhere to scrub through video (an Apollo classic)&lt;/li&gt;
&lt;li&gt;Double-pinch either side of the video to jump forward or back 10 seconds in time&lt;/li&gt;
&lt;li&gt;Quick launch YouTube from Home Screen&lt;/li&gt;
&lt;li&gt;Dim your surroundings to focus on the video&lt;/li&gt;
&lt;li&gt;View your recommendations, subscriptions, playlists, etc.&lt;/li&gt;
&lt;li&gt;Resizable (while maintaining correct aspect ratio)&lt;/li&gt;
&lt;li&gt;Automatic quality selection, should scale up or down based on the size of your window all the way to 4K&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;features-im-looking-into&#34;&gt;Features I&amp;rsquo;m looking into&lt;/h2&gt;
&lt;p&gt;This was a bit of a mad dash to get finished in time for the Apple Vision Pro launch, so I&amp;rsquo;m hoping to add some more things with time.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ability to see comments (I mean, they&amp;rsquo;re useful sometimes…)&lt;/li&gt;
&lt;li&gt;Maybe select quality directly if interest is there&lt;/li&gt;
&lt;li&gt;Caption controls (couldn&amp;rsquo;t quite get this working in time for 1.0)&lt;/li&gt;
&lt;li&gt;More immersive environments&lt;/li&gt;
&lt;li&gt;Multiview for multiple videos&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If there&amp;rsquo;s more you&amp;rsquo;d like to see, let me know!&lt;/p&gt;
&lt;h2 id=&#34;can-i-give-feedback&#34;&gt;Can I give feedback?&lt;/h2&gt;
&lt;p&gt;Yes please, I&amp;rsquo;d love that! I&amp;rsquo;ve only been able to develop this in the simulator, which obviously has its limitations, so once I get my hands on a device this Friday I&amp;rsquo;ll probably have a lot of thoughts on things I want to improve as well. That also means there will probably be some bugs here and there too. But I&amp;rsquo;d love to hear your experience and feedback with the app, so feel free to reach out to me on &lt;a href=&#34;https://mastodon.social/@christianselig/&#34;&gt;Mastodon&lt;/a&gt; or &lt;a href=&#34;https://twitter.com/christianselig/&#34;&gt;Twitter&lt;/a&gt;!&lt;/p&gt;
&lt;h2 id=&#34;check-it-out&#34;&gt;Check it out!&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s available on the App Store for $5! A fun URL to find it is &lt;a href=&#34;https://juno.vision&#34;&gt;juno.vision&lt;/a&gt; No subscriptions or in-app purchases, just a one-time paid up front app like it&amp;rsquo;s 2008. I considered making it free, or like a buck, but it&amp;rsquo;s a premium platform, and I think paying a few bucks for a good app is something we should encourage if we want more developers building for this platform.&lt;/p&gt;
&lt;p&gt;I think the result is a really comfy way to browse YouTube on visionOS, and having a way to quickly launch YouTube right from your Home Screen is super convenient.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m looking forward to doing more with it, and cheers to &lt;a href=&#34;https://matthewskiles.com&#34;&gt;Matthew Skiles&lt;/a&gt; for designing the icon! He actually made some beautiful alternate icons as well, but those apparently &lt;a href=&#34;https://mastodon.social/@christianselig/111851919905815611&#34;&gt;aren&amp;rsquo;t supported in visionOS 1.0&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://juno.vision&#34;&gt;&lt;strong&gt;Download it today!&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Autonomous Standing Desk and Chair Review</title>
      <link>https://christianselig.com/2023/11/autonomous-desk-chair-review/</link>
      <pubDate>Tue, 14 Nov 2023 12:42:37 -0400</pubDate>
      
      <guid>https://christianselig.com/2023/11/autonomous-desk-chair-review/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2023/11/autonomous-desk-chair-review/hero.jpeg&#34; alt=&#34;Black cat sitting on red chair facing camera in front of white standing desk&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Autonomous was nice enough to send me one of both their &lt;a href=&#34;https://www.autonomous.ai/sale?utm_source=&amp;amp;utm_medium=&amp;amp;utm_campaign=&amp;amp;utm_content=&amp;amp;utm_term=22BFSELIG&#34;&gt;Smart Desk Pro standing desks and ErgoChair Pro chairs&lt;/a&gt; in exchange for posting about them on Twitter, and I wanted to cover them in more detail on my blog as well so I could give my full thoughts on them for anyone in the market for a standing desk and chair. I care a lot about a quality, ergonomic desk set up and have tried a lot of different products, so I like to think I have a decent perspective here.&lt;/p&gt;
&lt;p&gt;The tl;dr is that they&amp;rsquo;re both really nice, though they have a few small things I would personally tweak.&lt;/p&gt;
&lt;p&gt;Small note: I&amp;rsquo;m Canadian but I&amp;rsquo;m putting all the prices in US dollars since most of my readers are American.&lt;/p&gt;
&lt;h2 id=&#34;the-desk&#34;&gt;The Desk&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2023/11/autonomous-desk-chair-review/desk.jpeg&#34; alt=&#34;Black cat walking across white standing desk top in high desk position&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;I&amp;rsquo;m a programmer and care a lot about the place that I sit and stand at for an inordinate amount of time, so over the years I&amp;rsquo;ve tried a lot of different products to make my setup more comfortable, inviting, and just more pleasurable to use. That includes a few standing desks.&lt;/p&gt;
&lt;p&gt;My first standing desk was the cheapest one I could find on Amazon that had memory presents (trust me, you really want to spend the extra twenty bucks or whatever to not have to hold a button down for awhile and hope you hit the approximate height you like).&lt;/p&gt;
&lt;p&gt;I would not recommend this approach. It had a single motor in one of the legs that then twisted a rod that moved the other leg up. This results in a cheaper desk, but a much less reliable one, and I pressed the button one day to find &lt;a href=&#34;https://twitter.com/ChristianSelig/status/1189184471301410816&#34;&gt;my desk surface at a 45° angle&lt;/a&gt; when the motor had an issue getting the other side to cooperate. The desk had a comically short warranty so I was just out of luck and instead of just originally spending a bit more for a quality desk, I was now just doing that anyway but out the cost of an additional desk.&lt;/p&gt;
&lt;p&gt;I ended up buying a Jarvis standing desk by Fully after being recommended by a friend, and it&amp;rsquo;s been great. Both that desk and the Autonomous one are very similar and have dual motors with one per leg. At the time of writing, both have Black Friday sales but the Autonomous desk comes in a fair bit cheaper at $489.00 (with my code &amp;ldquo;22BFSELIG&amp;rdquo;) for the Autonomous and $599 for the Jarvis.&lt;/p&gt;
&lt;p&gt;I can&amp;rsquo;t speak to the Jarvis desktops (I bought mine just as the frame itself, as at the time shipping to Canada was very expensive), but I like the white laminate one from Autonomous a lot. I originally thought it would be one of those IKEA style ones that are filled with cardboard, but this sucker is heavy and has nice grommets to route your wires through. Funnily enough those I visually prefer the square corners of the IKEA cardboard edition versus the rounded Autonomous ones, so I ended up using it with the IKEA top.&lt;/p&gt;
&lt;p&gt;Autonomous also offers an even cheaper &amp;ldquo;SmartDesk Core&amp;rdquo; but it does not offer the height adjustability that the Pro offers so that model was a non-starter for me. In order to have an ergonomic wrist angle with my keyboard I like to put the desk quite low, so make sure you have an idea of where your ideal desk height is and that the desk you buy supports that. I was kinda surprised to see that the Autonomous desk, despite being listed as having a higher minimum height than the Jarvis, actually beats the Jarvis. At the lowest height, the Autonomous desk is 25&amp;quot; off the ground, while the Jarvis sits a quarter inch higher at 25.25&amp;quot;.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2023/11/autonomous-desk-chair-review/measure.jpeg&#34; alt=&#34;Measuring tape showing the Autonomous desk at 25 inches tall at its lowest position&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;The Autonomous desk is also noticeably quieter than the Jarvis when in operation. Not to say the Jarvis is loud per se, but the Autonomous is a fair bit less noticeable when going up and down. My initial reaction was that maybe the Autonomous has weaker motors, but if I sit on both desks, both are still easily able to go up and down, and I weight 180 lbs, and I&amp;rsquo;m not sure anyone&amp;rsquo;s every day desk has more weight than that on it. They also go up and down at pretty much the exact same speed as far as I can tell.&lt;/p&gt;
&lt;p&gt;One area where I will give it to the Jarvis though is I slightly prefer their control system. I imagine it&amp;rsquo;s to prevent accidental input, but the Autonomous requires you hold down the buttons for a beat before they engage and start moving the desk, while the Jarvis is as soon as you touch it. This is probably a matter of preference, but it also manifests when you&amp;rsquo;re manually moving the desk with the up and down arrows, and when you reach your setting and release your finger, the Autonomous takes a second before it stops so unless you account for that you typically overshoot your target slightly. I&amp;rsquo;d love to see a dip switch or something on the controller that would allow you to control this functionality.&lt;/p&gt;
&lt;p&gt;Overall, especially with the price advantage, I&amp;rsquo;d go with the Autonomous desk personally. Pleased as punch, and now I have an extra desk set up in the corner of my office in case I… want to change it up I suppose? Now I finally have a use for that LG Ultrafine 5K that&amp;rsquo;s been sitting in my closet for two years!&lt;/p&gt;
&lt;p&gt;Oh, and get a standing desk mat. This is not negotiable, there&amp;rsquo;s a reason grocery store employees all use them. Standing for hours on a hard surface is not great for you, and will catch up to you eventually. It will make the standing desk a million times more inviting and comfortable to use, and they&amp;rsquo;re pretty inexpensive.&lt;/p&gt;
&lt;h2 id=&#34;the-chair&#34;&gt;The Chair&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2023/11/autonomous-desk-chair-review/cat-front.jpeg&#34; alt=&#34;Black cat sitting on red office chair in front of desk staring deeply into camera&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Standing desks always seem like a slam dunk, &amp;ldquo;duh&amp;rdquo; upgrade to people, but chairs seem unfortunately underappreciated. But they shouldn&amp;rsquo;t be! All the time you&amp;rsquo;re not standing, you&amp;rsquo;re sitting, and doing so in a good quality chair will pay dividends in the health of your body over the years.&lt;/p&gt;
&lt;p&gt;So naturally, I&amp;rsquo;ve tried quite a few desk chairs over the years. About seven years ago I went on a quest to get a good quality chair, and tried out all the &amp;ldquo;greats&amp;rdquo; of that era. I rented a Herman Miller Embody, a Herman Miller Aeron, a Steelcase Gesture, and a Steelcase Leap. While they were all quality chairs, the Steelcase Leap ended up being my favorite by a fair bit.&lt;/p&gt;
&lt;p&gt;Chairs are super personal, and the Leap just seemed to meld with my body the best, so I encourage you to try out chairs in person if possible, or order from a company that allows returns if you&amp;rsquo;re not satisfied. Seriously, the Herman Miller Embody from what people said sounded like it descended from the heavens, but I just wasn&amp;rsquo;t a big fan of how it fit against my back despite attempted adjustments. Autonomous seems to fit the bill for allowing to send back if you&amp;rsquo;re not a fan, though if buying during a sale like Black Friday I would contact them to see if that still applies, as they seem to have an asterisks for sale items.&lt;/p&gt;
&lt;p&gt;So basically, take the time to learn the adjustments on the chair, watch a YouTube video or two on proper ergonomics, and adjust the chair to fit you. Just through sheer combinations, the configuration that a chair comes in out of the box is likely not the one that best suits you!&lt;/p&gt;
&lt;p&gt;Long story short, the Autonomous ErgoChair Pro (they also have an ErgoChair Plus that seems closer to the Herman Miller Embody style) is a really comfy chair, and I love the red color I ordered it in. Also, between my Leap, my girlfriend&amp;rsquo;s office chair, and the Autonomous chair, my cat Ruby always chooses the Autonomous to sleep on, so that must mean something (I think it&amp;rsquo;s the fact it has the widest butt cushion area, which makes it feel a bit like you&amp;rsquo;re sitting on a throne).&lt;/p&gt;
&lt;p&gt;The price difference is pretty substantial too, at the time of writing the ErgoChair Pro is well under half the price of the Steelcase Leap (again, use that 22BFSELIG code to grab an additional 10% off for Black Friday).&lt;/p&gt;



&lt;figure&gt;
    &lt;img src=&#34;https://christianselig.com/2023/11/autonomous-desk-chair-review/cat-top.jpeg&#34; alt=&#34;Black cat lying on red office chair staring at camera from above with a relaxed expression&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;
    &lt;figcaption&gt;She won’t let me sit :(&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I&amp;rsquo;ve been using the Autonomous chair for about a week now at my desk, and while I think it&amp;rsquo;s a quality chair, I think I still have a slight preference for my Leap. This shouldn&amp;rsquo;t be super surprising at over double the price, but there&amp;rsquo;s a few things that push it slightly in the favor of the Leap for me.&lt;/p&gt;
&lt;p&gt;For one, the Autonomous chair is very adjustable, but I can&amp;rsquo;t quite get the lumbar support to a place I like versus the Leap. It&amp;rsquo;s just slightly more dramatically pushed in on the Autonomous versus the Steelcase, and that is adjustable, but I can&amp;rsquo;t dial it back enough (the Leap&amp;rsquo;s lumbar is a lot more adjustable).&lt;/p&gt;
&lt;p&gt;I also like on the Leap how when you recline, the seat cushion automatically slides forward a bit with you, so you don&amp;rsquo;t like slump off the chair as much. I also like that you can choose how far it can recline, so you can allow for a bit of a recline rather than going like super far back. Lastly, the height of the arms on the Leap can go lower than the Autonomous, which is just enough that I can push the Leap under my desk but not the Autonomous. You could always just not install the arms on the Autonomous though if you don&amp;rsquo;t use them.&lt;/p&gt;
&lt;p&gt;All that said, my girlfriend has your generic $70 Staples chair and I gave her the Autonomous to try for a bit, and she really, really liked it. Again, it&amp;rsquo;s more expensive, so not super surprising, but a quality chair is important. She also really liked that the Autonomous chair has a headrest versus my Steelcase one (you can buy one for the Steelcase, but it costs extra and isn&amp;rsquo;t nearly as customizable as the Autonomous one) and is now happily using it. See what I mean about personal preference?&lt;/p&gt;
&lt;p&gt;I really wish my Leap had the Autonomous&amp;rsquo; adjustment for the angle of the seat, though. And I have to admit the Autonomous looks a fair bit cooler than the Steelcase Leap which kinda just looks like your run of the mill corporate America office chair.&lt;/p&gt;
&lt;h2 id=&#34;overall&#34;&gt;Overall&lt;/h2&gt;
&lt;p&gt;I don&amp;rsquo;t get sent a lot of free stuff despite loving free stuff, so I was somewhat scared these were going to arrive and I might have to contact Autonomous and be like &amp;ldquo;ehhhhh, thanks for sending but not a fan&amp;rdquo;, but I&amp;rsquo;m delighted that they&amp;rsquo;re both very nice, price competitive options in the ergonomic desk setup space that I have no issue recommending. And if you do end up going with Autonomous, Black Friday is a great opportunity, and be sure to take 10% off on top of the existing sales with my coupon: &lt;em&gt;22BFSELIG&lt;/em&gt;. (I don&amp;rsquo;t get any kickback, but you might as well save some extra money! :p)&lt;/p&gt;
&lt;p&gt;Seriously, be it these options or something entirely different, do yourself a favor (if you have the means) and treat yourself to a quality desk and chair (and ideally keyboard, mouse, and monitor height). They can make a big difference in your health, especially compounded over years and years and years of heavy use if you&amp;rsquo;re in a desk-heavy job like programming, design, customer support, etc.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s always that adage about treating yourself to quality things if they separate you from the ground, which is normally said in the context of shoes and a mattress, but given that many of us spend as much time at our desk as we do sleeping, I think a quality desk setup easily qualifies as well.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Smart Open Xcode</title>
      <link>https://christianselig.com/2023/08/smartly-open-xcode/</link>
      <pubDate>Wed, 02 Aug 2023 14:58:22 -0300</pubDate>
      
      <guid>https://christianselig.com/2023/08/smartly-open-xcode/</guid>
      <description>


    &lt;img src=&#34;https://christianselig.com/2023/08/smartly-open-xcode/command-tab-switcher.jpg&#34; alt=&#34;Multiple versions of Xcode in the macOS Command Tab switcher&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;If you&amp;rsquo;re like me, you often have multiple versions of Xcode installed. One or two beta versions, a stable version, and maybe another version in case the most recent stable version has something weird about it.&lt;/p&gt;
&lt;p&gt;I also really like mapping my Caps Lock key to something more useful, and after reading &lt;a href=&#34;https://brettterpstra.com/2012/12/08/a-useful-caps-lock-key/&#34;&gt;Brett Terspstra&amp;rsquo;s excellent article on making a Hyper key&lt;/a&gt; many years ago, I&amp;rsquo;ve gotten used to hitting Caps Lock + X to jump to Xcode thanks to Karabiner Elements and Alfred. It works a lot like Command + Tab, but doesn&amp;rsquo;t require hitting Tab until you find it. It basically maps Caps Lock to holding down Command, Option, Control, and Shift all at once, a modifier that is very unlikely to conflict with anything else.&lt;/p&gt;
&lt;p&gt;Anyway, this setup works by mapping a hotkey to a specific app, which works great 99% of the time, but if you&amp;rsquo;re working in a beta version of Xcode, and you have that keyboard shortcut mapped to the stable version, it opens the wrong app and can even sometimes get Xcode confused about accessing a file in multiple locations. Not good!&lt;/p&gt;
&lt;p&gt;So, we need a smarter version rather than just hardcoding the app. Lots of options exist, like AppleScript (had a small delay though) and Keyboard Maestro, probably Shortcuts too, but I like a little Lua scripting utility called &lt;a href=&#34;https://www.hammerspoon.org&#34;&gt;Hammerspoon&lt;/a&gt;. Just download it and drop it in your Applications folder.&lt;/p&gt;
&lt;p&gt;At that point, we can add a short script to our &lt;code&gt;init.lua&lt;/code&gt; file that uses Hammerspoon&amp;rsquo;s &lt;code&gt;find()&lt;/code&gt; API to, well, &lt;em&gt;find&lt;/em&gt; the version of Xcode that&amp;rsquo;s currently running, and open that, rather than a hardcoded one. Nice! (If you keep multiple versions of Xcode open at once, I don&amp;rsquo;t know what to tell you, weirdo.)&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;hs.hotkey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bind&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;cmd&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;alt&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;ctrl&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;shift&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;X&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;function&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;-- Use the bundle ID rather than just &amp;#39;Xcode&amp;#39; to prevent it from trying to open utility apps like &amp;#39;Xcodes&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;local&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xcode&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;hs.application&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;find&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;com.apple.dt.Xcode&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;-- If nothing is found then just alert the user to open one, it would be very hard to guess which one they want if none are open!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xcode&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;then&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;hs.notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;title&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Xcode is not open! 🫨&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;informativeText&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Manually launch an Xcode instance and then I’ll be able to work!&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}):&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;send&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kr&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;--- If not the frontmost app, make it the frontmost! If it already is, hide it (makes it more toggle-y).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;isFrontmost&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;xcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hide&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;xcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;activate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;end&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Voilà! Works like a charm. Here&amp;rsquo;s an addition that you can add as well that extends it to Simulators:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;hs.hotkey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bind&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;cmd&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;alt&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;ctrl&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;shift&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;A&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;function&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;-- Use the bundle ID rather than just &amp;#39;Simulator&amp;#39; to prevent it from trying to open random background processes, and yes &amp;#39;iphonesimulator&amp;#39; works for iPads as well&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;local&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;simulator&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;hs.application&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;find&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;com.apple.iphonesimulator&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;-- If nothing is found then just alert the user to open one, it would be very hard to guess which one they want if none are open!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;simulator&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;then&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;hs.notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;title&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Simulator is not open! 🫨&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;informativeText&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;Run your project in Xcode to launch it in a simulator, then I&amp;#39;ll know what to open!&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}):&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;send&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kr&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;--- If not the frontmost app, make it the frontmost! If it already is, hide it (makes it more toggle-y).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;simulator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;isFrontmost&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;simulator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hide&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;simulator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;activate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;end&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
    </item>
    
    <item>
      <title>Instant Pan Gesture Interactions</title>
      <link>https://christianselig.com/2023/05/instant-pan-gesture-interactions/</link>
      <pubDate>Sat, 13 May 2023 11:45:03 -0300</pubDate>
      
      <guid>https://christianselig.com/2023/05/instant-pan-gesture-interactions/</guid>
      <description>&lt;p&gt;Apple has a really awesome WWDC 2018 video called &lt;a href=&#34;https://developer.apple.com/videos/play/wwdc2018/803/&#34;&gt;Designing Fluid Interfaces&lt;/a&gt;, and one of the key takeaways from the videos that one of the presenters, Chan Karunamuni, said is &amp;ldquo;Look for delays everywhere. Everything needs to respond instantly.&amp;rdquo; (6:28)&lt;/p&gt;
&lt;p&gt;A really great example of this is a scroll view on iOS. If you flick through your contacts and touch your finger to the screen, &lt;em&gt;instantly&lt;/em&gt; the scroll view stops and let&amp;rsquo;s you reposition it. This kind of instantaneous behavior is really important in our own views, interactions, and animations.&lt;/p&gt;
&lt;p&gt;Okay, so I was building a view where I wanted this behavior. Basically, you flick a box from the left side of the screen to the right, and as you release your finger it keeps going. Of note though is that you should be able to grab the box while it&amp;rsquo;s in flight to stop it.&lt;/p&gt;
&lt;p&gt;Turns out this is trickier than it seems, as &lt;code&gt;UIPanGestureRecognizer&lt;/code&gt; has a small delay in startup where it requires you to move your finger before it recognizes the gesture starting. If you just touch your finger on the moving object, that&amp;rsquo;s not technically a &amp;ldquo;pan&amp;rdquo;, so it ignores you (makes sense), which means the object just keeps moving until you move enough to trigger a &amp;ldquo;pan&amp;rdquo;, the result of this is an interaction that doesn&amp;rsquo;t feel very responsive.&lt;/p&gt;
&lt;h2 id=&#34;first-attempt-at-a-solution&#34;&gt;First attempt at a solution&lt;/h2&gt;
&lt;p&gt;One solution that works in many cases for many people is to implement an &amp;ldquo;InstantPanGestureRecognizer&amp;rdquo;, where unlike the normal one, it enters its &amp;ldquo;began&amp;rdquo; state as soon as a finger is touched down, allowing you to respond instantly to user input and pause the animation as soon as their finger touches the screen.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://medium.com/@nathangitter/building-fluid-interfaces-ios-swift-9732bb934bf5&#34;&gt;Nathan Gitter has an awesome article&lt;/a&gt; showing code examples of many of the interactions shown in Apple&amp;rsquo;s video. It&amp;rsquo;s an amazing article. He has a section on implementing this custom pan gesture:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;InstantPanGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIPanGestureRecognizer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;touchesBegan&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;touches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UITouch&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIEvent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touchesBegan&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;began&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For awhile this worked, but something changed in a recent version of iOS that effectively broke it. On iPhones it mostly works fine, but on iPads, and on iPhones if the view is near the home indicator at the bottom, there&amp;rsquo;s a delay in between the state being updated to &lt;code&gt;.began&lt;/code&gt;, and the pan gesture handler being notified of the state change. It&amp;rsquo;s about 0.75 seconds, which is an enormous amount of time for a gesture (on a 120 Hz device, that&amp;rsquo;s 90 frames rendered before your input is recognized!).&lt;/p&gt;
&lt;p&gt;Based on console logs and inspecting the &lt;code&gt;UIGestureRecognizers&lt;/code&gt; attached to the app&amp;rsquo;s &lt;code&gt;UIWindow&lt;/code&gt;, it looks like there&amp;rsquo;s something called a &amp;ldquo;system gesture gate&amp;rdquo;, that I&amp;rsquo;m assuming gates off gestures from conflicting with system ones (particularly on the iPad, which has lots of multitasking gestures), which means ones attached to the app run at a lower priority and could be delayed.&lt;/p&gt;
&lt;p&gt;(Note that the &lt;code&gt;preferredScreenEdgesDeferringSystemGestures&lt;/code&gt; property does not seem to have an effect here.)&lt;/p&gt;
&lt;p&gt;That being said, since the state &lt;em&gt;does&lt;/em&gt; update instantly, it&amp;rsquo;s just the &lt;em&gt;communication&lt;/em&gt; of the state change that&amp;rsquo;s delayed, it&amp;rsquo;s pretty easy to work around this and still have an InstantPanGestureRecognizer. Add the code for the custom gesture above, but instead of using &lt;code&gt;addTarget(...)&lt;/code&gt; to listen for changes to the gesture recognizer, which has a delay in some cases, just jump straight to &lt;a href=&#34;https://developer.apple.com/documentation/swift/using-key-value-observing-in-swift&#34;&gt;KVO&lt;/a&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;gestureContext&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;viewDidLoad&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;viewDidLoad&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;panGestureRecognizer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;InstantPanGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;panGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addObserver&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;forKeyPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;#&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;keyPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UIPanGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;options&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;Self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;gestureContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;panGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;observeValue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forKeyPath&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;keyPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;of&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;object&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Any&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;change&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;NSKeyValueChangeKey&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Any&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]?,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UnsafeMutableRawPointer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;Self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;gestureContext&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Be sure to pass on any observes that we didn&amp;#39;t explicitly ask for!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;observeValue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forKeyPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;keyPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;of&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;object&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;change&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;change&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;panGestureRecognizer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;object&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIPanGestureRecognizer&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;switch&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;panGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;began&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// This is no longer delayed! Stop animation if has not already ended&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;changed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Update position of object&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ended&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Continue object&amp;#39;s momentum with animation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;.began&lt;/code&gt; state will now trigger without needing to move your finger first, &lt;strong&gt;and&lt;/strong&gt; without delay on iPads.&lt;/p&gt;
&lt;p&gt;But I think there&amp;rsquo;s an even better solution for some cases, read on!&lt;/p&gt;
&lt;h2 id=&#34;a-solution-i-like-more&#34;&gt;A solution I like more&lt;/h2&gt;
&lt;p&gt;While that previous gesture setup works great, especially in the case Nathan Gitter outlined in the aforementioned tutorial, I think there&amp;rsquo;s one catch that could trip you up in some situations: pan gesture recognizers have a movement delay before starting &lt;em&gt;for a reason&lt;/em&gt;. This small delay can be important, because it allows us to have some information about the pan by the time it starts, like which direction the user is moving in.&lt;/p&gt;
&lt;p&gt;This movement data can be super handy, for, say, only making the gesture start if it&amp;rsquo;s moving horizontally. For example, say we have a box inside a scroll view, and we want to let the user pan the box from left to right, but still allow the scroll view to scroll up and down, we could look at our object&amp;rsquo;s pan gesture and only allow it to start if the movement is horizontal. (If the pan gesture were to start as soon as the user&amp;rsquo;s finger touches the screen, we won&amp;rsquo;t be able to know which direction they&amp;rsquo;re panning in to stop it.)&lt;/p&gt;
&lt;p&gt;So while the small delay introduced in a recent OS version on iPads is unfortunate, it gives us an opportunity to look outside of an instant pan gesture recognizer and its limitations.&lt;/p&gt;
&lt;p&gt;What would that be? Something that would allow the pan gesture recognizer to operate normally, but have a secondary system that allows us to know as soon as the user touches down on the screen.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a few ways to do this, but since we&amp;rsquo;re describing an interaction, I think an additional gesture recognizer would work perfectly here. &lt;code&gt;UITapGestureRecognizer&lt;/code&gt; immediately comes to mind, but requires the user&amp;rsquo;s finger to lift up afterward, which rules it out for us (we want to be able to just touch down to pause, then pan around if desired). &lt;code&gt;UILongPressGestureRecognizer&lt;/code&gt; could actually work here if we set &lt;code&gt;minimumPressDuration&lt;/code&gt; to a super small number, but that feels kinda hacky (it&amp;rsquo;s not really a &lt;em&gt;long press&lt;/em&gt; at that point, is it)?&lt;/p&gt;
&lt;p&gt;Okay, so let&amp;rsquo;s build our own. It&amp;rsquo;s actually really simple:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;TouchesBeganGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIGestureRecognizer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;canPrevent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;preventedGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Bool&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;canBePrevented&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;by&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;preventingGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Bool&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;touchesBegan&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;touches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UITouch&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIEvent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touchesBegan&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;began&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;touchesMoved&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;touches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UITouch&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIEvent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touchesMoved&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ended&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;touchesCancelled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;touches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UITouch&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIEvent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touchesMoved&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cancelled&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;touchesEnded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;touches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UITouch&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIEvent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touchesEnded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ended&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We create a custom gesture recognizer that both can&amp;rsquo;t be prevented by other gesture recognizers, and can&amp;rsquo;t prevent others (this means it works &lt;em&gt;on top of&lt;/em&gt; other gesture recognizers). And as far as states goes, it enters the &lt;code&gt;.began&lt;/code&gt; state as soon as the user touches down, but as soon as anything else happens, it ends (there&amp;rsquo;s no &lt;code&gt;.changed&lt;/code&gt; state).&lt;/p&gt;
&lt;p&gt;Then we can take this new gesture and add it in addition to our pan gesture. Here&amp;rsquo;s a simple example showing this, where we use a pan gesture to move a box, when the pan gesture ends the box continues moving in the direction the user threw it, and they can then instantly grab it while it&amp;rsquo;s in flight to stop it and/or start moving it around again. Note that for sake of keeping the example short, the direction is only left-to-right, and the gesture&amp;rsquo;s velocity is not calculated into the animation (for the latter Nathan&amp;rsquo;s article above goes over conserving momentum as well if you&amp;rsquo;re curious!).&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;ViewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewController&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;box&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;frame&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGRect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;x&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;y&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;200.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;200.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;200.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;propertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewPropertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;viewDidLoad&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;viewDidLoad&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;box&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;backgroundColor&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;systemOrange&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addSubview&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;box&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;panGestureRecognizer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIPanGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;target&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;action&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;#selector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;panGestureUpdated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;panGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;box&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;panGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;touchesBeganGestureRecognizer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TouchesBeganGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;target&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;action&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;#selector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touchesBeganGestureRecognizerUpdated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touchesBeganGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;box&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touchesBeganGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;@objc&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;panGestureUpdated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;panGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIPanGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;panGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;began&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;c1&#34;&gt;// Normalize the gesture&amp;#39;s starting translation with where the box is at the start&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;panGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;setTranslation&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CGPoint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;x&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;box&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;frame&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;origin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;x&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;y&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;box&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;panGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;changed&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;box&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;frame&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;origin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;x&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;panGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;translation&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;box&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;).&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;x&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;panGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ended&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;propertyAnimator&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewPropertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;duration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;1.5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;curve&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;linear&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addAnimations&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;box&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;frame&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;origin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;x&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bounds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;box&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bounds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addCompletion&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;position&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;startAnimation&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;@objc&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;touchesBeganGestureRecognizerUpdated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;touchesBeganGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TouchesBeganGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;touchesBeganGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;began&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;propertyAnimator&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;stopAnimation&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There you go! Hopefully that gives you a nice way of making a pan gesture animation feel even more responsive.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>First on New Blog</title>
      <link>https://christianselig.com/2023/01/first-on-new-blog/</link>
      <pubDate>Sat, 28 Jan 2023 17:12:52 -0400</pubDate>
      
      <guid>https://christianselig.com/2023/01/first-on-new-blog/</guid>
      <description>&lt;p&gt;Converted the blog over to a new Hugo theme! Hopefully everything here sorta works. Test post will remove.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2023/01/first-on-new-blog/gubgub.jpeg&#34; alt=&#34;A very cute small goat&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;This goat&amp;rsquo;s name is apparently Gubgub, and very cute.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Theming Apps on iOS is Hard</title>
      <link>https://christianselig.com/2022/02/difficulty-theming-ios/</link>
      <pubDate>Mon, 07 Feb 2022 12:03:01 -0400</pubDate>
      
      <guid>https://christianselig.com/2022/02/difficulty-theming-ios/</guid>
      <description>&lt;p&gt;Theming apps (the ability to change up the color scheme for an app from say, a white background with blue links to a light green background with green links) is a pretty common feature across a lot of apps. It&amp;rsquo;s one of the core features of the new &amp;ldquo;Twitter Blue&amp;rdquo; subscription, Tweetbot and Twitterific have had it for awhile, my app Apollo has it (and a significant subset of users use it), and it&amp;rsquo;s basic table stakes in text editors. When you use the heck out of an app, it&amp;rsquo;s pretty nice to be able to tweak it in a way that suits you more.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2022/02/difficulty-theming-ios/theme-examples.png&#34; alt=&#34;Three examples of different dark themes in the Apollo app&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;For the longest time, by default, an app had one color scheme. Dark mode didn&amp;rsquo;t exist at the iOS level, so it was up to apps to have two sets of colors to swap between individually. With iOS 12 Apple made that &lt;em&gt;a lot&lt;/em&gt; nicer, and made switching between a light color scheme and a dark color scheme really easy.&lt;/p&gt;
&lt;h2 id=&#34;the-current-lightdark-mode-system&#34;&gt;The Current Light/Dark Mode System&lt;/h2&gt;
&lt;p&gt;The current system is &lt;strong&gt;great&lt;/strong&gt; for switching between light mode and dark mode. Each &amp;ldquo;color&amp;rdquo; basically has two colors: a light mode version and a dark mode version, and instead of calling it &amp;ldquo;whiteColor&amp;rdquo;, the color might be called &amp;ldquo;backgroundColor&amp;rdquo;, and have a lightish color for light mode and a darker color for dark mode. You set that on whatever you&amp;rsquo;re theming, and bam, iOS handles the rest, automatically switching when the iOS system theme changes. Heck, Apple even defines a bunch of built in ones, like &amp;ldquo;label&amp;rdquo; and &amp;ldquo;secondaryLabel&amp;rdquo;, so you likely don&amp;rsquo;t even have to define your own colors.&lt;/p&gt;
&lt;p&gt;The code defining, say, a custom blue accent/tint color for your app looks basically like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lightMode&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// A rich blue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hexcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;007aff&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// A little brighter blue to show up on dark backgrounds&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hexcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;4BA1FF&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;(For a thorough explanation of this system, &lt;a href=&#34;https://nshipster.com/dark-mode/&#34;&gt;NSHipster has a great article&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;This quickly falls apart when you introduce theming. Maybe blue is a safe bet as your app&amp;rsquo;s &amp;ldquo;button color&amp;rdquo; for 95% of users, but a subset are going to want to make that more personal. Maybe a mint color? A pink! If we&amp;rsquo;ve learned anything through the craze of app&amp;rsquo;s like &lt;a href=&#34;https://twitter.com/nguyedav/status/1122047011698638848/&#34;&gt;Widgetsmith&lt;/a&gt;, people &lt;em&gt;love&lt;/em&gt; to make things their own.&lt;/p&gt;
&lt;p&gt;But wait, how do we do this when the system is built around only having two options: one for light mode, and one for dark? We might want to have a &amp;ldquo;Mint&amp;rdquo; theme, with a delightful green tint instead.&lt;/p&gt;
&lt;p&gt;Perhaps something like this?&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lightMode&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;mintSelected&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Minty!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hexcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;26C472&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// A rich blue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hexcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;007aff&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;     
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;mintSelected&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Dark mode minty!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hexcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;84FFBF&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// A little brighter blue to show up on dark backgrounds&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hexcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;4BA1FF&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Beautiful! This actually works super well, if we start up our app, iOS will see that mint is selected and choose the mint colors instead.&lt;/p&gt;
&lt;p&gt;However, there&amp;rsquo;s a serious catch. If the app started up in normal (AKA non-minty) mode, and the user selects the mint theme at some point, iOS kinda looks the other way and ignores the change, sticking with blue instead. The conversation kinda goes like:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Me&lt;/strong&gt;: Hey iOS! The theme is minty now, blue is so last season. Can you update those buttons to mint-colored?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;iOS&lt;/strong&gt;: Well, I asked earlier and you said blue. No take backs. The paint is dry, no updates allowed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Me&lt;/strong&gt;: But if the user changes the device theme to dark mode, you&amp;rsquo;ll happily update the colors! Could you just do that same thing now for me?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;iOS&lt;/strong&gt;: Hard pass.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Me&lt;/strong&gt;: But the header file for &lt;code&gt;hasDifferentColorAppearanceComparedToTraitCollection&lt;/code&gt; even says changes in certain traits could affect the dynamic colors, could you just wrap what those changes call into a general function?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;iOS&lt;/strong&gt;: I said no take backs! But let&amp;rsquo;s together hope one of the awesome folks who works on me adds that in my next major version!&lt;/p&gt;
&lt;p&gt;So what do you do? Have the user force-quit the app and relaunch every time they want to change the theme? That&amp;rsquo;s not very Apple-y. Reinitialize the app&amp;rsquo;s view hierarchy? That can mess with lots of things like active keyboards.&lt;/p&gt;
&lt;h2 id=&#34;lets-go-back-in-time&#34;&gt;Let&amp;rsquo;s Go Back in Time&lt;/h2&gt;
&lt;p&gt;Remember how I said in the pre-iOS 12 days, where iOS didn&amp;rsquo;t even had a dark mode, developers had to get a bit more inventive? Apollo&amp;rsquo;s theming system was actually written way back then, so I&amp;rsquo;m pretty familiar with it! Basically how it works is you don&amp;rsquo;t talk to &lt;em&gt;iOS&lt;/em&gt; like above, instead you talk to &lt;em&gt;each view&lt;/em&gt; on screen directly. Cut out the middleman!&lt;/p&gt;
&lt;p&gt;Leveraging something like &lt;code&gt;NSNotificationCenter&lt;/code&gt; (or a more type-safe version via &lt;code&gt;NSHashTable&lt;/code&gt; with weak object references) you&amp;rsquo;d basically go to each view you wanted to color, and say &amp;ldquo;Hey, you&amp;rsquo;re blue now, but why don&amp;rsquo;t you give me your phone number so if anything changes I&amp;rsquo;ll let you know?&amp;rdquo; and you&amp;rsquo;d register that view. Then when the user asked to go to dark mode, you&amp;rsquo;d quickly phone up all the views in the app and say &amp;ldquo;Change! Now! Green!&amp;rdquo; and they would all do that.&lt;/p&gt;
&lt;p&gt;The beauty is that when you &amp;ldquo;phone them up&amp;rdquo;, you can tell them any color under the sun! You have full control!&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a quick example of what this might look like, somewhat based on how I do it in Apollo:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;protocol&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Themeable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;AnyObject&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;applyTheme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;theme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Theme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;enum&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;ColorScheme&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;`&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;`,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pumpkin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Theme&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;isLightModeActive&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Bool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;colorScheme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ColorScheme&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;backgroundColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;switch&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;colorScheme&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;isForLightMode&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hexcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;ffffff&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hexcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;000000&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pumpkin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;isForLightMode&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hexcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;ff6700&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hexcode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;733105&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// Add more colors for things like tintColor, textColor, separators, inactive states, etc.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;ThemeManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NSObject&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;shared&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ThemeManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;currentTheme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Theme&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// initialize value from UserDefaults or something similar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;listeners&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NSHashTable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;AnyObject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;weakObjects&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// This would be called by an external event, such as iOS changing or the user selecting a new theme&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;themeChangeDidOccur&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;toTheme&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;newTheme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Theme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;currentTheme&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;newTheme&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;refreshListeners&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;makeThemeable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;object&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Themeable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;listeners&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;add&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;object&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;object&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;applyTheme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;theme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;currentTheme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;refreshListeners&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;listenersAllObjects&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;compactMap&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$0&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Themeable&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forEach&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;applyTheme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;theme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;currentTheme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Do this in every view controller/view:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;IceCreamViewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Themeable&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;leftBarButtonItem&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIBarButtonItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;title&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Accounts&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;viewDidLoad&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;viewDidLoad&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;ThemeManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;shared&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;makeThemeable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;applyTheme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;theme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Theme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// e.g.:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;leftBarButtonItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tintColor&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;theme&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tintColor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So this works but has a lot of downsides. For one, it&amp;rsquo;s a lot harder. Rather than just setting &lt;code&gt;view.textColor = appTextColor&lt;/code&gt; in a single call and have it automatically switch between light and dark mode colors that you defined as needed, you have to set the color, register the view, have a separate theming function, and then go back and talk to that view whenever anything changes. A lot more arduous in comparison.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s other aspects to consider as well. Because iOS is smart, when an app goes into the background, iOS quickly takes a screenshot of the app to show up in the app switcher, but it &lt;em&gt;also&lt;/em&gt; quickly toggles the app to the opposite theme (so dark mode if the system is in light mode) and takes a screenshot of that as well, so if the system theme changes iOS can instantly update the screenshot in the app switcher.&lt;/p&gt;
&lt;p&gt;The result of this is that iOS rapidly asks your app to change its theme twice in a row (to the opposite theme, and then back to the normal), if you don&amp;rsquo;t do this quickly, you&amp;rsquo;re in trouble. Indeed, it&amp;rsquo;s one of my top crashers as of iOS 15, and I assume it&amp;rsquo;s because I use this old method of talking to every single view to update, and iOS uses a more efficient method under the hood.&lt;/p&gt;
&lt;p&gt;You also hit speed bumps you don&amp;rsquo;t really think of when you start out. For instance, say parts of your app &lt;a href=&#34;https://github.com/christianselig/Markdownosaur&#34;&gt;support Markdown rendering&lt;/a&gt; where links embedded in a block of text reflect a specific theme&amp;rsquo;s tint color. When the theme changes, with this system you get that notification, and what do you do? Recompute the &lt;code&gt;NSAttributedString&lt;/code&gt; each time you get a theme change? Perhaps only do it the first time, cache the result, and then on theme change iterate over that specific attribute and update only those attributes to the new color. You know what&amp;rsquo;s a lot nicer than all that rigamarole each time? Just setting the dynamic color in your Markdown renderer/attributed string once, and having iOS handle all the color changes like in the newer solution.&lt;/p&gt;
&lt;p&gt;So as you may have guessed I&amp;rsquo;ve been meaning to update my old system to this newer one. (Wonder why I was writing this blog post?)&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(For a thorough writeup on this kind of system, the &lt;a href=&#34;https://developers.soundcloud.com/blog/dark-mode-observer-pattern&#34;&gt;SoundCloud Developer Blog has a great article&lt;/a&gt;, and Joe Fabisevich also has a really cool variation &lt;a href=&#34;https://twitter.com/mergesort/status/1490711742090989568&#34;&gt;based on Combine&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;swiftui&#34;&gt;SwiftUI&lt;/h2&gt;
&lt;p&gt;SwiftUI is new and really exciting, and something I&amp;rsquo;m looking forward to using more in my app. The tricky thing with this antiquated solution is it doesn&amp;rsquo;t work too well with SwiftUI, subscribing everything into NotificationCenter calls and callbacks isn&amp;rsquo;t exactly very SwiftUI-esque and ruins a lot of the elegance of creating views in SwiftUI and at best adds a lot of boilerplate.&lt;/p&gt;
&lt;p&gt;So if the old system isn&amp;rsquo;t great, what about the newer, post-iOS 12 dynamic color one? While SwiftUI has its own &lt;code&gt;Color&lt;/code&gt; object which unlike &lt;code&gt;UIColor&lt;/code&gt; lacks support for custom dynamic colors (I believe) you &lt;em&gt;can&lt;/em&gt; initialize a &lt;code&gt;Color&lt;/code&gt; object with a &lt;code&gt;UIColor&lt;/code&gt; and SwiftUI will dynamically update when light/dark mode changes occur, just like UIKit! Which makes the &amp;ldquo;newer&amp;rdquo; solution a lot nicer as it works well in both &amp;ldquo;worlds&amp;rdquo;.&lt;/p&gt;
&lt;h2 id=&#34;what-would-be-the-perfect-solution-from-apple&#34;&gt;What Would be the Perfect Solution from Apple?&lt;/h2&gt;
&lt;p&gt;The perfect solution would be Apple simply having a method like &lt;code&gt;UIApplication.shared.refreshUserInterfaceStyle()&lt;/code&gt; that performs the same thing that occurs when iOS switches from light mode to dark mode. In that situation, there&amp;rsquo;s a code path/method on iOS that says &amp;ldquo;Hey app, update all your colors, things have changed&amp;rdquo;, and simply making it so app developers could call that on their own app would make everything perfect. Theme changes would redraw as requested, no having to force-quit or talk to each and every view manually, and it would work nicely with SwiftUI! (Apple folks: FB9887856)&lt;/p&gt;
&lt;h2 id=&#34;hacksville&#34;&gt;Hacksville&lt;/h2&gt;
&lt;p&gt;In the absence of that method (fingers crossed for iOS 16!), can we make our own method that accomplishes effectively the same thing? An app color refresh? Well, there&amp;rsquo;s a couple ways!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Martin Rechsteiner mentioned &lt;a href=&#34;https://twitter.com/rechsteiner/status/1444097375576989700&#34;&gt;a clever way&lt;/a&gt; on Twitter, wherein you change the app&amp;rsquo;s displayed color gamut. Since the color profile of the entire app is changing, iOS will indeed update all the colors. The downside is, well, you&amp;rsquo;re changing the app&amp;rsquo;s color gamut from say, P3 to SRGB, which can presumably have some effects on how colors look. It shouldn&amp;rsquo;t be super obvious, since from what I can tell &lt;code&gt;UIImageView&lt;/code&gt;s and whatnot have their embedded color profiles separate from app, so pictures and whatnot should still display correctly. But it&amp;rsquo;s still suboptimal. You could always immediately switch back to the previous color gamut after, but that has the problems of solution 2.&lt;/li&gt;
&lt;li&gt;If you&amp;rsquo;re in light mode set &lt;code&gt;overrideUserInterfaceStyle&lt;/code&gt; to dark mode on the app&amp;rsquo;s &lt;code&gt;UIWindow&lt;/code&gt;, and then change it back (or vice-versa). The downside here, is that if you do it in the same pass of the runloop, colors will update but &lt;code&gt;traitCollectionDidChange&lt;/code&gt; does not fire in the relevant view controllers which may be important for things like &lt;code&gt;CALayer&lt;/code&gt; updates. You can dispatch it to the next loop with good ol&amp;rsquo; &lt;code&gt;DispatchQueue.main.async { ... }&lt;/code&gt; on the second call, but then &lt;code&gt;traitCollectionDidChange&lt;/code&gt; will be called twice, and unless you do a bit more work the screen will have a quick flash as it jumps between light and dark mode very quickly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of the two, I &lt;em&gt;think&lt;/em&gt; I prefer the second solution slightly. Even though it calls the method twice, and flashes a bit, you can negate the flash by putting a view overtop the main window (say, a snapshot from immediately before that pleasantly fades to the new theme) and the &lt;code&gt;traitCollectionDidChange&lt;/code&gt; being called twice likely isn&amp;rsquo;t much concern.&lt;/p&gt;
&lt;h2 id=&#34;put-those-two-together-pb--j-sandwich&#34;&gt;Put Those Two Together? PB &amp;amp; J Sandwich?&lt;/h2&gt;
&lt;p&gt;Another solution would be to take parts of both systems that work and put them together: use dynamic colors for 97% of the heavy lifting, but when a color has to change immediately in response to a user changing themes, &lt;em&gt;then&lt;/em&gt; you use the &amp;ldquo;notify all the views in the app manually&amp;rdquo; method. This would likely be fine when going into the background and snapshotting, because that would use dynamic colors, and the &amp;ldquo;notifying all the views&amp;rdquo; would only occur when the app is in the foreground with the user manually changing the theme.&lt;/p&gt;
&lt;p&gt;Still, I don&amp;rsquo;t really like that we have to have a separate system maintained where we have to keep track of every view in the app that might need a color change, for the 3% of the time the user might change the theme. That&amp;rsquo;s a lot of boilerplate and excess code for something that could simply be handled by a &lt;code&gt;refresh&lt;/code&gt; method on &lt;code&gt;UIApplication&lt;/code&gt;. (And yes, you could say &amp;ldquo;if it&amp;rsquo;s that rare, just have them force quit the app or something else gross&amp;rdquo;, but you want the user to be able to quickly preview different themes without a ton of friction in between.)&lt;/p&gt;
&lt;p&gt;So all in all, I &lt;em&gt;think&lt;/em&gt; I&amp;rsquo;m going to go with the &lt;code&gt;overrideUserInterfaceStyle&lt;/code&gt; kinda hack, and hope iOS 16 sees a proper, built-in way to refresh the app&amp;rsquo;s colors. But if you have a better solution I&amp;rsquo;m all ears, &lt;a href=&#34;https://twitter.com/christianselig&#34;&gt;hit me up on Twitter&lt;/a&gt;!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Table of Contents Selector View</title>
      <link>https://christianselig.com/2021/04/table-of-contents-selector/</link>
      <pubDate>Sun, 25 Apr 2021 21:06:06 -0300</pubDate>
      
      <guid>https://christianselig.com/2021/04/table-of-contents-selector/</guid>
      <description>&lt;p&gt;I wrote a new little view for a future version of Apollo that makes some changes to the default iOS version (that seems to be a weird trend in my recent programming, despite me loving built-in components). Here&amp;rsquo;s some details about it! It&amp;rsquo;s &lt;a href=&#34;https://github.com/christianselig/TableOfContentsSelector&#34;&gt;also available as a library on GitHub&lt;/a&gt; if you&amp;rsquo;re interested!&lt;/p&gt;



    &lt;video class=&#34;width-50&#34; controls autoplay muted loop playsinline&gt;&lt;source src=&#34;https://christianselig.com/2021/04/table-of-contents-selector/table-of-contents.mov&#34;&gt;&lt;/source&gt;&lt;/video&gt;

&lt;p&gt;Are you familiar with &lt;code&gt;UITableView&lt;/code&gt;&amp;rsquo;s &lt;code&gt;sectionIndexTitles&lt;/code&gt; API? The little alphabet on the side of some tables for quickly jumping to sections? &lt;a href=&#34;https://www.appcoda.com/ios-programming-index-list-uitableview/&#34;&gt;Here&amp;rsquo;s a tutorial if you&amp;rsquo;re unfamiliar&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is a view very similar to that (very little in the way of originality here, folks) but offers a few nice changes I was looking for, so I thought I&amp;rsquo;d open source it in case anyone else wanted it too.&lt;/p&gt;
&lt;h2 id=&#34;benefits&#34;&gt;Benefits&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&#34;https://www.appcoda.com/ios-programming-index-list-uitableview/&#34;&gt;UITableView API&lt;/a&gt; is great, and you should try to stick with built-in components when you can avoid adding in unnecessary dependencies. That being said, here are the advantages this brought me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🐇 Symbols support! SF Symbols are so pretty, and sometimes a section in your table doesn&amp;rsquo;t map nicely to a letter. Maybe you have some quick actions that you could represent with a lightning bolt or bunny!&lt;/li&gt;
&lt;li&gt;🌠 Optional overlay support. I really liked on my old iPod nano how when you scrolled really quickly an a big overlay jumped up with the current alphabetical section you were in so you could quickly see where you were. Well, added!&lt;/li&gt;
&lt;li&gt;🖐 Delayed gesture activation to reduce gesture conflict. For my app, an issue I had was that I had an optional swipe gesture that could occur from the right side of the screen. Whenever a user activated that gesture, it would also activate the section index titles and jump everywhere. This view requires the user long-press it to begin interacting. No conflicts!&lt;/li&gt;
&lt;li&gt;🏛 Not tied to sections. If you have a less straight forward data structure for your table, where maybe you want to be able to jump to multiple specific items within a section, this doesn&amp;rsquo;t require every index to be a section. Just respond to the delegate and you can do whatever you want.&lt;/li&gt;
&lt;li&gt;🏓 Not tied to tables. Heck, you don&amp;rsquo;t even have to use this with tables at all. If you want to overlay it in the middle of a &lt;code&gt;UIImageView&lt;/code&gt; and each index screams a different Celine Dion song, go for it.&lt;/li&gt;
&lt;li&gt;🏂 Let&amp;rsquo;s be honest, a slightly better name. The Apple engineers created a beautiful API but I can never remember what it&amp;rsquo;s called to Google. &lt;code&gt;sectionIndexTitles&lt;/code&gt; doesn&amp;rsquo;t roll off the tongue.&lt;/li&gt;
&lt;li&gt;🌝 Haha moon emoji&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;how-to-install&#34;&gt;How to Install&lt;/h2&gt;
&lt;p&gt;No package managers here. Just drag and drop &lt;code&gt;TableOfContentsSelector.swift&lt;/code&gt; into your Xcode project. You own this code now. You have to &lt;a href=&#34;https://christianselig.com/2021/04/table-of-contents-selector/kaguya.jpg&#34;&gt;raise it as your own&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;how-to-use&#34;&gt;How to Use&lt;/h2&gt;
&lt;p&gt;Create your view.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;tableOfContentsSelector&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TableOfContentsSelector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(Optional: set a font. Supports increasing and decreasing font for accessibility purposes)&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;tableOfContentsSelector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;font&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIFont&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;systemFont&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ofSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;12.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;weight&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;semibold&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// Default&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The table of contents needs to know the height it&amp;rsquo;s working with in order to lay itself out properly, so let it know what it should be&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;tableOfContentsSelector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;frame&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;height&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bounds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Set up your items. The items in the model are represented by the &lt;code&gt;TableOfContentsItem&lt;/code&gt; enum, which supports either a letter (&lt;code&gt;.letter(&amp;quot;A&amp;quot;)&lt;/code&gt;) case or a symbol case (&lt;code&gt;.symbol(name: &amp;quot;symbol-sloth&amp;quot;, isCustom: true)&lt;/code&gt;), which can also be a &lt;a href=&#34;https://developer.apple.com/documentation/xcode/creating_custom_symbol_images_for_your_app&#34;&gt;custom SF Symbol&lt;/a&gt; that you created yourself and imported into your project. As a helper, there&amp;rsquo;s a variable called &lt;code&gt;TableOfContentsSelector.alphanumericItems&lt;/code&gt; that supplies A-Z plus just as the UITableView API does.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;tableOfContentsItems&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TableOfContentsItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;symbol&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;star&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;isCustom&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;symbol&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;house&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;isCustom&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;symbol&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;symbol-sloth&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;isCustom&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TableOfContentsSelector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;alphanumericItems&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;tableOfContentsSelector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;updateWithItems&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tableOfContentsItems&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At this point add it to your subview and position it how you see fit. You can use &lt;code&gt;sizeThatFits&lt;/code&gt; to get the proper width as well.&lt;/p&gt;
&lt;p&gt;Lastly, implement the delegate methods so you can find out what&amp;rsquo;s going on.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;viewToShowOverlayIn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;selectedItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;TableOfContentsItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// You probably want to do something with the selection! :D&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;beganSelection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;endedSelection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it! If you&amp;rsquo;re curious, internally it&amp;rsquo;s just a single &lt;code&gt;UILabel&lt;/code&gt; with a big ol&amp;rsquo; attributed string. Hope you enjoy!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>More Efficient/Faster Average Color of Image</title>
      <link>https://christianselig.com/2021/04/efficient-average-color/</link>
      <pubDate>Fri, 02 Apr 2021 17:18:46 -0300</pubDate>
      
      <guid>https://christianselig.com/2021/04/efficient-average-color/</guid>
      <description>&lt;p&gt;&lt;em&gt;Skip to the &amp;lsquo;Juicy Code 🧃&amp;rsquo; section if you just want the code and don&amp;rsquo;t care about the preamble of why you might want this!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finding the average color of an image is a nice trick to have in your toolbelt for spicing up views. For instance on iOS, it&amp;rsquo;s used by Apple to make their pretty homescreen widgets where you put the average color of the image behind the text so the text is more readable. Here&amp;rsquo;s Apple&amp;rsquo;s News widget, and my Apollo widget, for instance:&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2021/04/efficient-average-color/widgets.png&#34; alt=&#34;News and Apollo widgets on home screen&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;core-image-approach-pitfalls&#34;&gt;Core Image Approach Pitfalls&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s lots of articles out there on how to do this on iOS, but all of the code I&amp;rsquo;ve encountered accomplishes it with Core Image. Something like the following makes it really easy:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;coreImageAverageColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// Shrink down a bit first&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;aspectRatio&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;resizeSize&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;40.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;40.0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;aspectRatio&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;renderer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIGraphicsImageRenderer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;resizeSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;baseImage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;resizedImage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;renderer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;image&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;baseImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;draw&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGRect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;origin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;zero&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;resizeSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// Core Image land!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;inputImage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CIImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;image&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;resizedImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;extentVector&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CIVector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;x&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;inputImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;extent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;origin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;x&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;y&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;inputImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;extent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;origin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;y&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;inputImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;extent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;w&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;inputImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;extent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;filter&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CIFilter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;CIAreaAverage&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;parameters&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;kCIInputImageKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;inputImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;kCIInputExtentKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;extentVector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;outputImage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;bp&#34;&gt;filter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;outputImage&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;bitmap&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;UInt8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;](&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;repeating&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;bp&#34;&gt;count&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;context&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CIContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;options&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;workingColorSpace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;kCFNull&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Any&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;render&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;outputImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;toBitmap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bitmap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;rowBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bounds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGRect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;x&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;y&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;format&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;RGBA8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;colorSpace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;red&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bitmap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;255&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;green&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bitmap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;255&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;blue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bitmap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;255&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;alpha&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bitmap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;255&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Core Image is a great framework capable of some insanely powerful things, but in my experience isn&amp;rsquo;t optimal for something as simple as finding the average color of an image because it takes up quite a bit more memory and time, things that you don&amp;rsquo;t have a lot of when creating widgets. That or I don&amp;rsquo;t know enough about Core Image (it&amp;rsquo;s a substantial framework!) to figure out how to optimize the above code (which is entirely possible, but hey the other solution is easier to understand, I think).&lt;/p&gt;
&lt;p&gt;You have around 30 MB of headroom with widgets, and from my tests the normal Core Image filter way was taking about 5 MB of memory just for the calculation. That&amp;rsquo;s about 17% of the total memory you get for the entire widget for a single operation, which could really hurt you if you&amp;rsquo;re up close to the limit. And you don&amp;rsquo;t want to break that 30MB limit if you can avoid it, from what I can see it seems iOS (understandably) penalizes you for it, and repeated offenses mean your widget doesn&amp;rsquo;t get updated as often.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m no Core Image expert, but I&amp;rsquo;m guessing since it&amp;rsquo;s this super powerful GPU-based framework the memory consumption seems inconsequential when you&amp;rsquo;re doing crazy realtime image filters or something. But who knows, I&amp;rsquo;m just going off measurements.&lt;/p&gt;
&lt;p&gt;You can see in Xcode&amp;rsquo;s memory debugger very clearly when Core Image kicks in for instance, causing a little spike, and almost more concerning is that it doesn&amp;rsquo;t seem to normalize back down any time soon.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2021/04/efficient-average-color/perf-before.png&#34; alt=&#34;Memory useclass= before, spike&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;(That might not be the most egregious example. It can be worse.)&lt;/p&gt;
&lt;h2 id=&#34;just-iterating-over-pixels-approach&#34;&gt;Just Iterating Over Pixels Approach&lt;/h2&gt;
&lt;p&gt;An easy approach would just be to iterate over every pixel in the image, add up all their colors, then average them. Downside is there could be a lot of pixels (think of a 4K image), but thankfully for us we can just resize the image down a bunch first (fast), and the &amp;ldquo;gist&amp;rdquo; of the color information will be preserved and we have a lot less pixels to deal with.&lt;/p&gt;
&lt;p&gt;One other catch is that just &amp;lsquo;iterating over the pixels&amp;rsquo; isn&amp;rsquo;t as easy as it sounds when the image you&amp;rsquo;re dealing with could be in a variety of different formats, (CMYK, RGBA, ARGB, BBQ, etc.). I came across a &lt;a href=&#34;https://stackoverflow.com/a/34596653/7498519&#34;&gt;great answer&lt;/a&gt; on StackOverflow that linked to an &lt;a href=&#34;https://developer.apple.com/library/mac/qa/qa1509/_index.html&#34;&gt;Apple Technical Q&amp;amp;A&lt;/a&gt; that recommended just drawing out the image anew in a standard format you can always trust, so that solves that.&lt;/p&gt;
&lt;p&gt;Lastly, there&amp;rsquo;s some debate over which algorithm is best for averaging out all the colors in an image. &lt;a href=&#34;https://sighack.com/post/averaging-rgb-colors-the-right-way&#34;&gt;Here&amp;rsquo;s a very interesting blog post&lt;/a&gt; that talks about how a sum of squares approach could be considered better. Through a bunch of tests, I see how it could be with approximating a bunch of color blocks of a larger imager, but the &amp;lsquo;simpler&amp;rsquo; way by just summing seems to have better color results, and more closely mimics Core Image&amp;rsquo;s results. The code below includes both options, and I&amp;rsquo;ll include a comparison table so you can choose for yourself.&lt;/p&gt;
&lt;h2 id=&#34;the-juicy-code-&#34;&gt;The Juicy Code 🧃&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s the code I landed on, feel free to change it as you see fit. I like to keep in lots of comments so if I come back to it later I can understand what&amp;rsquo;s going on, especially when it&amp;rsquo;s dealing with bitmasking and color profile bit structures and whatnot, which I don&amp;rsquo;t use often in my day-to-day and requires a bit of a rejogging of the Computer Sciencey part of my brain, and it&amp;rsquo;s really pretty simple once you read it over.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;extension&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;UIImage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;/// There are two main ways to get the color from an image, just a simple &amp;#34;sum up an average&amp;#34; or by squaring their sums. Each has their advantages, but the &amp;#39;simple&amp;#39; option *seems* better for average color of entire image and closely mirrors CoreImage. Details: https://sighack.com/post/averaging-rgb-colors-the-right-way&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;enum&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;AverageColorAlgorithm&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;simple&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;squareRoot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;findAverageColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;algorithm&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;AverageColorAlgorithm&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;simple&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;cgImage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cgImage&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// First, resize the image. We do this for two reasons, 1) less pixels to deal with means faster calculation and a resized image still has the &amp;#34;gist&amp;#34; of the colors, and 2) the image we&amp;#39;re dealing with may come in any of a variety of color formats (CMYK, ARGB, RGBA, etc.) which complicates things, and redrawing it normalizes that into a base color format we can deal with.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 40x40 is a good size to resize to still preserve quite a bit of detail but not have too many pixels to deal with. Aspect ratio is irrelevant for just finding average color.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;size&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;height&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;totalPixels&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;colorSpace&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGColorSpaceCreateDeviceRGB&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// ARGB format&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;bitmapInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;UInt32&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGBitmapInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;byteOrder32Little&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rawValue&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGImageAlphaInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;premultipliedFirst&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rawValue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 8 bits for each color channel, we&amp;#39;re doing ARGB so 32 bits (4 bytes) total, and thus if the image is n pixels wide, and has 4 bytes per pixel, the total bytes per row is 4n. That gives us 2^8 = 256 color variations for each RGB channel or 256 * 256 * 256 = ~16.7M color options in total. That seems like a lot, but lots of HDR movies are in 10 bit, which is (2^10)^3 = 1 billion color options!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;context&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bitsPerComponent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bytesPerRow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;space&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;colorSpace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bitmapInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bitmapInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Draw our resized image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;draw&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cgImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGRect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;origin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;zero&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;pixelBuffer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Bind the pixel buffer&amp;#39;s memory location to a pointer we can use/access&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;pointer&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pixelBuffer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bindMemory&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;UInt32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;capacity&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Keep track of total colors (note: we don&amp;#39;t care about alpha and will always assume alpha of 1, AKA opaque)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;totalRed&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;totalBlue&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;totalGreen&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Column of pixels in image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;x&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;..&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;c1&#34;&gt;// Row of pixels in image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;y&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;..&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;c1&#34;&gt;// To get the pixel location just think of the image as a grid of pixels, but stored as one long row rather than columns and rows, so for instance to map the pixel from the grid in the 15th row and 3 columns in to our &amp;#34;long row&amp;#34;, we&amp;#39;d offset ourselves 15 times the width in pixels of the image, and then offset by the amount of columns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;pixel&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pointer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;y&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;x&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;r&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;red&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pixel&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;g&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;green&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pixel&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;b&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;blue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pixel&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;switch&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;algorithm&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;simple&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;totalRed&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;totalBlue&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;totalGreen&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;g&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;squareRoot&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;totalRed&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;totalGreen&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;g&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;totalBlue&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;averageRed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;averageGreen&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;averageBlue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;switch&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;algorithm&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;simple&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;averageRed&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalRed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalPixels&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;averageGreen&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalGreen&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalPixels&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;averageBlue&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalBlue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalPixels&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;squareRoot&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;averageRed&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;sqrt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalRed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalPixels&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;averageGreen&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;sqrt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalGreen&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalPixels&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;averageBlue&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;sqrt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalBlue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;totalPixels&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Convert from [0 ... 255] format to the [0 ... 1.0] format UIColor wants&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;red&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;averageRed&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;255.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;green&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;averageGreen&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;255.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;blue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;averageBlue&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;255.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;alpha&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;1.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;red&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pixelData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;UInt32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;UInt8&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// For a quick primer on bit shifting and what we&amp;#39;re doing here, in our ARGB color format image each pixel&amp;#39;s colors are stored as a 32 bit integer, with 8 bits per color chanel (A, R, G, and B).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// So a pure red color would look like this in bits in our format, all red, no blue, no green, and &amp;#39;who cares&amp;#39; alpha:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 11111111 11111111 00000000 00000000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;//  ^alpha   ^red     ^blue    ^green&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// We want to grab only the red channel in this case, we don&amp;#39;t care about alpha, blue, or green. So we want to shift the red bits all the way to the right in order to have them in the right position (we&amp;#39;re storing colors as 8 bits, so we need the right most 8 bits to be the red). Red is 16 points from the right, so we shift it by 16 (for the other colors, we shift less, as shown below).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Just shifting would give us:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 00000000 00000000 11111111 11111111&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;//  ^alpha   ^red     ^blue    ^green&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// The alpha got pulled over which we don&amp;#39;t want or care about, so we need to get rid of it. We can do that with the bitwise AND operator (&amp;amp;) which compares bits and the only keeps a 1 if both bits being compared are 1s. So we&amp;#39;re basically using it as a gate to only let the bits we want through. 255 (below) is the value we&amp;#39;re using as in binary it&amp;#39;s 11111111 (or in 32 bit, it&amp;#39;s 00000000 00000000 00000000 11111111) and the result of the bitwise operation is then:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 00000000 00000000 11111111 11111111&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 00000000 00000000 00000000 11111111&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// -----------------------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 00000000 00000000 00000000 11111111&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// So as you can see, it only keeps the last 8 bits and 0s out the rest, which is what we want! Woohoo! (It isn&amp;#39;t too exciting in this scenario, but if it wasn&amp;#39;t pure red and was instead a red of value &amp;#34;11010010&amp;#34; for instance, it would also mirror that down)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;UInt8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pixelData&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;255&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;green&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pixelData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;UInt32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;UInt8&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;UInt8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pixelData&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;255&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;blue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pixelData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;UInt32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;UInt8&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;UInt8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pixelData&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;255&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-results&#34;&gt;The Results&lt;/h2&gt;



    &lt;img src=&#34;https://christianselig.com/2021/04/efficient-average-color/perf-after.png&#34; alt=&#34;Memory use after,class= no spike&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;As you can see, we don&amp;rsquo;t see any memory spike whatsoever from the call. Yay! If anything, it kinda dips a bit. Did we find the secret to infinte memory?&lt;/p&gt;
&lt;p&gt;In terms of speed, it&amp;rsquo;s also about 4x faster. The Core Image approach takes about 0.41 seconds on a variety of test images, whereas the &amp;lsquo;Just Iterating Over Pixels&amp;rsquo; approach (I need a catchier name) only takes 0.09 seconds.&lt;/p&gt;
&lt;p&gt;These tests were done on an iPhone 6s, which I like as a test device because it&amp;rsquo;s the oldest iPhone that still supports iOS 13/14.&lt;/p&gt;
&lt;h2 id=&#34;comparison-of-colors&#34;&gt;Comparison of Colors&lt;/h2&gt;
&lt;p&gt;Lastly, here&amp;rsquo;s a quick comparison chart showing the differences between the &amp;lsquo;simple&amp;rsquo; summing algorithm, the &amp;lsquo;sum of squares&amp;rsquo; algorithm, and the Core Image filter. As you can see, especially for the second flowery image, the &amp;lsquo;simple/sum&amp;rsquo; approach seems to have the most desirable results and closely mirrors Core Image.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2021/04/efficient-average-color/average-colors.jpg&#34; alt=&#34;Comparison of average colors from Simple, Squared, andclass= Core Image&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;Okay, that&amp;rsquo;s all I got! Have fun with colors!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Trials and Tribulations of Making an Interruptable Custom View Controller Transition on iOS</title>
      <link>https://christianselig.com/2021/02/interruptible-view-controller-transitions/</link>
      <pubDate>Fri, 19 Feb 2021 15:41:43 -0400</pubDate>
      
      <guid>https://christianselig.com/2021/02/interruptible-view-controller-transitions/</guid>
      <description>&lt;p&gt;I think it&amp;rsquo;s safe to say while the iOS custom view controller transition API is a very powerful one, with that power comes a great deal of complexity. It can be tricky, and I&amp;rsquo;m having one of those days where it&amp;rsquo;s getting the better of me and I just cannot get it to do what I want it to do, even though what I want it to do seems pretty straightforward. Interruptible/cancellable custom view controller transitions.&lt;/p&gt;
&lt;h2 id=&#34;what-i-want&#34;&gt;What I Want&lt;/h2&gt;
&lt;p&gt;I built a &lt;a href=&#34;https://github.com/christianselig/ChidoriMenu&#34;&gt;little library called ChidoriMenu&lt;/a&gt; that effectively just reimplements iOS 14&amp;rsquo;s &lt;a href=&#34;https://developer.apple.com/design/human-interface-guidelines/ios/controls/pull-down-menus/&#34;&gt;Pull Down Menus&lt;/a&gt; as a custom view controller for added flexibility.&lt;/p&gt;
&lt;p&gt;As it always goes, 99% of it went smoothly as could be, but then I was playing around in the Simulator with Apple&amp;rsquo;s version, and noticed with Apple&amp;rsquo;s you could tap outside the menu &lt;strong&gt;while it was being presented&lt;/strong&gt; to cancel the presentation and it would smoothly retract. With mine, you have to wait for the animation to finish before dismissing. 0.4 seconds can be a long time. &lt;strong&gt;I NEED IT&lt;/strong&gt;. The fluidity/cancellability of iOS&amp;rsquo; animations is one of the most fun parts of the operating system, and a big reason the iPhone X&amp;rsquo;s swipe up to go home feels so nice.&lt;/p&gt;
&lt;p&gt;Here is Apple&amp;rsquo;s with &lt;em&gt;Toggle Slow Animations&lt;/em&gt; enabled to better illustrate how you can interrupt/cancel it.&lt;/p&gt;



    &lt;video class=&#34;width-50&#34; controls autoplay muted loop playsinline&gt;&lt;source src=&#34;https://christianselig.com/2021/02/interruptible-view-controller-transitions/ios-context-menu.mov&#34;&gt;&lt;/source&gt;&lt;/video&gt;

&lt;h2 id=&#34;how-i-implemented-my-menu&#34;&gt;How I Implemented My Menu&lt;/h2&gt;
&lt;p&gt;Mine&amp;rsquo;s pretty simple. Just a custom view controller presentation that is non-interactive, using an animation controller and a &lt;code&gt;UIPresentationController&lt;/code&gt; subclass. You just tap to summon the menu, and tap away to close it, not really anything interactive, and virtually every tutorial on the web about interactive view controller transitions have &amp;ldquo;the interaction&amp;rdquo; being driven by something like &lt;code&gt;UIPanGestureRecognizer&lt;/code&gt;, so it didn&amp;rsquo;t seem really needed in this case. So it&amp;rsquo;s just an animation controller that animates it on and off screen.&lt;/p&gt;
&lt;h2 id=&#34;catch-1&#34;&gt;Catch #1&lt;/h2&gt;
&lt;p&gt;Well, how do I make this interruptable? Say I manually set the animation duration to 10 seconds, and then programatically dismiss it 2 seconds after it starts as a test.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;tappedPoint&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;tapGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;location&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;chidoriMenu&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ChidoriMenu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;menu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;existingMenu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;summonPoint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;tappedPoint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;present&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;chidoriMenu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;animated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;completion&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;DispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;main&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;asyncAfter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;deadline&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;seconds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;chidoriMenu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dismiss&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;animated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;completion&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No dice. It queues up the dismissal and it occurs at the 10 second mark, right after the animation concludes. Not exactly interrupting anything.&lt;/p&gt;
&lt;p&gt;Okay, let&amp;rsquo;s see. Bruce Nilo and Michael Turner of the UIKit team did a great talk at WWDC 2016 &lt;a href=&#34;https://developer.apple.com/videos/play/wwdc2016/216/&#34;&gt;about view controller transitions&lt;/a&gt; and making them interruptible.&lt;/p&gt;
&lt;p&gt;The animation is powered by &lt;code&gt;UIViewPropertyAnimator&lt;/code&gt;, and they mention in iOS 10 they added a method called &lt;code&gt;interruptibleAnimator(using:context:)&lt;/code&gt;, wherein you return your animator as a means for the transition to be interruptible. They even state the following at the 25:40 point:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you do not implement the interaction controller, meaning you only implement a custom animation controller, then you need to implement animateTransition. And you would do so very simply, like this method. You take the interruptible animator that you would return and you would basically tell it to start.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Which sounds great, as mine is just a normal, non-interactive animation controller. Let&amp;rsquo;s do that!&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;animatorForCurrentSession&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewPropertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;interruptibleAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;using&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewControllerContextTransitioning&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewImplicitlyAnimating&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// Required to use the same animator for life of transition, so don&amp;#39;t create multiple times&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;animatorForCurrentSession&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;animatorForCurrentSession&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;animatorForCurrentSession&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;propertyAnimator&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewPropertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;duration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionDuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;using&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dampingRatio&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.75&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;isInterruptible&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;isUserInteractionEnabled&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// ... animation set up goes here ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// Animate! 🪄&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addAnimations&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;chidoriMenu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;transform&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;finalTransform&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;chidoriMenu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;alpha&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;finalAlpha&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addCompletion&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;position&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;position&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;end&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;completeTransition&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;transitionWasCancelled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;animatorForCurrentSession&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;animatorForCurrentSession&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;propertyAnimator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;animateTransition&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;using&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewControllerContextTransitioning&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;interruptableAnimator&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;interruptibleAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;using&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;presentation&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;chidoriMenu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ChidoriMenu&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;viewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UITransitionContextViewControllerKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ChidoriMenu&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;containerView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addSubview&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;chidoriMenu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;interruptableAnimator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;startAnimation&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, it still doesn&amp;rsquo;t interrupt it at the 2 second point, still opting to wait until the 10 second point that the animation completes. It calls the method, but it&amp;rsquo;s still not interruptible. I tried intercepting the dismiss call and calling &lt;code&gt;.isReversed = true&lt;/code&gt; manually on the property animator, but it still waits 10 seconds before the completion handler is called.&lt;/p&gt;
&lt;p&gt;After that above quote, they then state &amp;ldquo;However, we kind of advise that you use an interaction controller if you&amp;rsquo;re going to make it interruptible.&amp;rdquo; so I&amp;rsquo;m going to keep that in mind.&lt;/p&gt;
&lt;h2 id=&#34;catch-2&#34;&gt;Catch #2&lt;/h2&gt;
&lt;p&gt;Even if the above did work, it has to be powered by a user tapping outside the menu to close it. This is accomplished in my &lt;code&gt;UIPresentationController&lt;/code&gt; subclass by adding a tap gesture recognizer to a background view, which then calls dismiss upon being tapped.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;presentationWillBegin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kc&#34;&gt;super&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;presentationWillBegin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;darkOverlayView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;backgroundColor&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIColor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;white&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;alpha&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;presentingViewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tintAdjustmentMode&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dimmed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;containerView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addSubview&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;darkOverlayView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;tapGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addTarget&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;action&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;#selector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tappedDarkOverlayView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tapGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;darkOverlayView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tapGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;@objc&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;tappedDarkOverlayView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tapGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UITapGestureRecognizer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;presentedViewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dismiss&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;animated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;completion&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Problem is, all taps &lt;strong&gt;also&lt;/strong&gt; refuse to be registered until the animation completes. And it&amp;rsquo;s not an issue with the &lt;code&gt;UITapGestureRecognizer&lt;/code&gt;, adding a simple &lt;code&gt;UIButton&lt;/code&gt; results in the same behavior where it becomes tappable as soon as the animation ends.&lt;/p&gt;
&lt;p&gt;(Note: when switching to an interactive transition below, &lt;code&gt;UIPresentationController&lt;/code&gt; becomes freed up and accepts these touches.)&lt;/p&gt;
&lt;h2 id=&#34;all-signs-point-to-interactive&#34;&gt;All Signs Point to Interactive&lt;/h2&gt;
&lt;p&gt;Between the advice of the UIKit engineers in the WWDC video, and the fact it doesn&amp;rsquo;t seem interactible during the presentation, let&amp;rsquo;s just bite the bullet and make it an interactive transition. Plus, the &lt;a href=&#34;https://developer.apple.com/videos/play/wwdc2013/218&#34;&gt;WWDC 2013 video on Custom Transitions Using View Controllers&lt;/a&gt; states (paraphrasing) &amp;ldquo;Interactive transitions don&amp;rsquo;t need to be powered by gestures only, anything iterable works&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;My issue here is, what is iterating? It&amp;rsquo;s just a &amp;ldquo;fire and forget&amp;rdquo; animation from the tap of a button. Essentially the API works by incrementing a &amp;ldquo;progress&amp;rdquo; value throughout the animation so the custom transition is aware of where you&amp;rsquo;re at in the transition. For instance if you&amp;rsquo;re swiping back to dismiss, it would be a measurement from 0.0 to 1.0 of how close to the left side of the screen you are. There&amp;rsquo;s many examples online, Apple included, showing how to implement interactive view controllers powered by a &lt;code&gt;UIPanGestureRecognizer&lt;/code&gt;, but I&amp;rsquo;m really having trouble wrapping my head around what is iterating or driving the progress updates here.&lt;/p&gt;
&lt;p&gt;The only thing I could really think of was &lt;code&gt;CADisplayLink&lt;/code&gt; (which is basically just an &lt;code&gt;NSTimer&lt;/code&gt; synchronized with the refresh rate of the screen — 60 times per second typically) that just tracks how long it&amp;rsquo;s been since the animation started. If it&amp;rsquo;s a 10 second animation, and 5 seconds have passed, you&amp;rsquo;re 50% done! Here&amp;rsquo;s an implementation, after I changed my animation controller to be a subclass of &lt;code&gt;UIPercentDrivenInteractiveTransition&lt;/code&gt; rather than &lt;code&gt;NSObject&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;displayLink&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CADisplayLink&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewControllerContextTransitioning&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;presentationAnimationTimeStart&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CFTimeInterval&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;override&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;startInteractiveTransition&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIViewControllerContextTransitioning&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;presentationAnimationTimeStart&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CACurrentMediaTime&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;displayLink&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CADisplayLink&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;target&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;selector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;#selector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;displayLinkUpdate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;displayLink&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;displayLink&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;displayLink&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;displayLink&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;add&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;forMode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;common&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kr&#34;&gt;@objc&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;displayLinkUpdate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;displayLink&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CADisplayLink&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;timeSinceAnimationBegan&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;displayLink&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;timestamp&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;presentationAnimationTimeStart&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;progress&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;timeSinceAnimationBegan&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionDuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;using&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;update&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;progress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// &amp;lt;-- secret sauce&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Again, this seems kinda counter intuitive to me. In our case &lt;strong&gt;time&lt;/strong&gt; powers the animation, and we&amp;rsquo;re trying to shoehorn it into an interactive progress API by measuring time itself. But hey, if it works, it works.&lt;/p&gt;
&lt;p&gt;But alas, it doesn&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id=&#34;catch-3&#34;&gt;Catch #3&lt;/h2&gt;
&lt;p&gt;The issue now is that, once the animation starts, it no longer obeys our custom timing curve. Mimicking Apple&amp;rsquo;s, we want our view controller to present with a subtle little bounce, rather than a boring, linear animation. But using &lt;code&gt;CADisplayLink&lt;/code&gt; to power it results in the animation being shown with a linear animation, despite the &lt;code&gt;interruptiblePropertyAnimator&lt;/code&gt; we returned looking like this: &lt;code&gt;UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), dampingRatio: 0.75)&lt;/code&gt;. See that damping? That&amp;rsquo;s springy! I even tried really spelling it out to the &lt;code&gt;UIPercentDrivenInteractiveTransition&lt;/code&gt; with a &lt;code&gt;self.timingCurve = propertyAnimator.timingParameters&lt;/code&gt;. No luck still.&lt;/p&gt;
&lt;p&gt;But wait, that&amp;rsquo;s really weird. I use interactive view controller transitions in Apollo to power the custom navigation controller animations, and I distinctly remember it annoyingly following the animation curve during the interactive transition. I specifically had to program around this, because when you&amp;rsquo;re actually interactive, say following a user&amp;rsquo;s finger, you &lt;strong&gt;need&lt;/strong&gt; it to be linear so that it follows the finger predictably.&lt;/p&gt;
&lt;p&gt;Okay, so I check out Apollo&amp;rsquo;s code. Ah ha, I wrote it a few years back, so it uses the older school &lt;code&gt;UIView.animate…&lt;/code&gt; rather than &lt;code&gt;UIViewPropertyAnimator&lt;/code&gt;. Surely that can&amp;rsquo;t be it.&lt;/p&gt;
&lt;p&gt;… It was it.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;UIView&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;animate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;withDuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionDuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;using&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;delay&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;usingSpringWithDamping&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.75&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;initialSpringVelocity&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;options&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;allowUserInteraction&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;beginFromCurrentState&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;chidoriMenu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;transform&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;finalTransform&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;chidoriMenu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;alpha&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;finalAlpha&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;completion&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;didComplete&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;isPresenting&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;transitionWasCancelled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;isPresenting&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;transitionWasCancelled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;presentingViewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tintAdjustmentMode&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;automatic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;completeTransition&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;transitionContext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;transitionWasCancelled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It works if I use the old school &lt;code&gt;UIView.animate&lt;/code&gt; APIs in &lt;code&gt;startInteractiveTransition&lt;/code&gt; and remove the &lt;code&gt;interruptibleAnimator&lt;/code&gt; method, and &lt;code&gt;CADisplayLink&lt;/code&gt; perfectly follows the animation curve. Okay what gives, implementing &lt;code&gt;interruptibleAnimator&lt;/code&gt; was supposed to bridge this gap, there&amp;rsquo;s even a question &lt;a href=&#34;https://stackoverflow.com/questions/47127217/why-does-uiview-animate-work-with-an-interactive-controller-transition-but-uivi&#34;&gt;on StackOverflow about it&lt;/a&gt; but I suppose that question doesn&amp;rsquo;t say anything about animation curves. So, bug maybe?&lt;/p&gt;
&lt;h2 id=&#34;end-result&#34;&gt;End Result&lt;/h2&gt;
&lt;p&gt;So I guess that kinda works? But this all feels so hacky. I don&amp;rsquo;t like &lt;code&gt;CADisplayLink&lt;/code&gt; much here, it seems to have a few jitters when dismissing as opposed to the first solution (only on device, not Simulator), and it would be nice to know how to use it with the newer &lt;code&gt;UIViewPropertyAnimator&lt;/code&gt; APIs. I get a general &amp;ldquo;fragile&amp;rdquo; feeling with my code here that I don&amp;rsquo;t really want to ship, so I reverted back to the initial, non-interactive solution. (Additional minor thing that might not even be possible is that Apple&amp;rsquo;s also allows you to add another one as the existing one is dismissing, which my code doesn&amp;rsquo;t do and I didn&amp;rsquo;t even realize was possible.) And worst of all, you ask? &lt;code&gt;CADisplayLink&lt;/code&gt; means &amp;ldquo;Toggle Show Animations&amp;rdquo; in the Simulator doesn&amp;rsquo;t work for the animation anymore!&lt;/p&gt;
&lt;p&gt;(Maybe I just need to rebuild Apollo in SwiftUI.)&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s some gists showing the two final &amp;ldquo;solutions&amp;rdquo;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://gist.github.com/christianselig/5a931677d0a05d2b159220c22024bc8a&#34;&gt;Old school/UIView.animate based&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://gist.github.com/christianselig/400845c3b9171bfb3bed679506804a8d&#34;&gt;New school/UIViewPropertyAnimator based with gross linear animation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;a-call-for-help&#34;&gt;A Call for Help&lt;/h3&gt;
&lt;p&gt;If you know your way around the custom view controller transition APIs and have any insight, you&amp;rsquo;d be my favorite person on the planet. Making animations more interruptible would be a fun skill to learn, I&amp;rsquo;m just at wit&amp;rsquo;s end with trying to implement it. I&amp;rsquo;ve linked the gists in the previous paragraph, and ChidoriMenu in its entirety with the non-interactive implementation &lt;a href=&#34;https://github.com/christianselig/ChidoriMenu&#34;&gt;is also on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m curious if there&amp;rsquo;s a way to implement it without requiring an interactive transition, but if not, it&amp;rsquo;d be neat to know if it actually does require &lt;code&gt;CADisplayLink&lt;/code&gt;, and if it does, it&amp;rsquo;d be neat to know what I&amp;rsquo;m still doing wrong in the above code, haha.&lt;/p&gt;
&lt;p&gt;DMs are open &lt;a href=&#34;https://twitter.com/christianselig&#34;&gt;on my Twitter&lt;/a&gt;, feel free to reach out (alternatively my email is me@ my domain name).&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Logging information from iOS Widgets</title>
      <link>https://christianselig.com/2020/12/ios-widget-logging/</link>
      <pubDate>Tue, 01 Dec 2020 16:09:32 -0400</pubDate>
      
      <guid>https://christianselig.com/2020/12/ios-widget-logging/</guid>
      <description>&lt;p&gt;Lately users have been emailing me with a few odd things happening with their Apollo iOS 14 home screen widgets, and some well-placed logs can really help with identifying what&amp;rsquo;s going wrong. iOS has a sophisticated built in logging mechanism, &lt;code&gt;os_log&lt;/code&gt;, and now with &lt;code&gt;SwiftLogger&lt;/code&gt; in iOS 14, but unfortunately &lt;a href=&#34;https://mjtsai.com/blog/2019/03/06/problems-with-os_log/&#34;&gt;they don&amp;rsquo;t provide an easy for users to provide you with the logs&lt;/a&gt; so they&amp;rsquo;re not optimal in this case.&lt;/p&gt;
&lt;p&gt;Normally I use &lt;a href=&#34;https://github.com/CocoaLumberjack/CocoaLumberjack&#34;&gt;CocoaLumberjack&lt;/a&gt; for this in Apollo because a logging can be pretty complex and I like to use a battle-tested solution, but for whatever reason I cannot get it working in my Widget Extension. I&amp;rsquo;ve tried &lt;a href=&#34;https://github.com/CocoaLumberjack/CocoaLumberjack/issues/439#issuecomment-135375597&#34;&gt;setting it up to log to the shared app group container&lt;/a&gt; as well as &lt;a href=&#34;https://github.com/CocoaLumberjack/CocoaLumberjack/blob/master/Documentation/ProblemSolution.md&#34;&gt;disabling async logging&lt;/a&gt; to no avail.&lt;/p&gt;
&lt;p&gt;However this little logging use case in widgets is simple enough that I figure I&amp;rsquo;ll just whip up a simple little logger (per the suggestion of &lt;a href=&#34;https://twitter.com/BrianMueller333&#34;&gt;Brian Mueller&lt;/a&gt;), and I thought I&amp;rsquo;d include it here in case anyone else would benefit from it.&lt;/p&gt;
&lt;p&gt;The main gist of it is that it writes to the shared app group container (make sure you have &lt;a href=&#34;https://www.atomicbird.com/blog/sharing-with-app-extensions/&#34;&gt;App Groups set up&lt;/a&gt;) so both the Widget Extension as well as the main app can access it. It uses just a single file (that is created if it doesn&amp;rsquo;t exist), and once it gets too long (I defined as 2MB) it trims the the older half of the logs so that the log file doesn&amp;rsquo;t bloat unnecessarily (it does this by just finding a newline near half point from the Data, rather than reading the entire String into memory). It also automatically capture the line, file, and function the issue occurs in. Per &lt;a href=&#34;https://twitter.com/FlorianBuerger/status/1334163241216454662&#34;&gt;Florian Bürger&lt;/a&gt; be careful with using &lt;code&gt;DateFormatter&lt;/code&gt; willy-nilly, I&amp;rsquo;m not encountering any performance issues, but if you encounter any consider caching the &lt;code&gt;DateFormatter&lt;/code&gt; instance for reuse.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;WidgetLogger&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;URL&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;/// Write to shared app group container so both the widget and the host app can access&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FileManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;containerURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forSecurityApplicationGroupIdentifier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;group.com.christianselig.apollo&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;appendingPathComponent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;widget.log&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;file&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;#file&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;function&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;#function&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;line&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;#line&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;dateFormatter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DateFormatter&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DateFormatter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;dateFormatter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dateFormat&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;MMM d, HH:mm:ss.SSS&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;dateString&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dateFormatter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Date&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;timestampedMessage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dateString&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt; [&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\((&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;file&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NSString&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;lastPathComponent&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;function&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;line&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;]: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\n&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;messageData&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;timestampedMessage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;using&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utf8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Could not encode String to Data.&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FileManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fileExists&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;atPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;do&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;fileAttributes&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FileManager&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;attributesOfItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;atPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;twoMegabytes&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1_024&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1_024&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;c1&#34;&gt;// In order to avoid having a log file that is enormous, trim out the oldest entries if file size is larger than 3 MB&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;c1&#34;&gt;// (Checking file size is more performant than counting total lines each time)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;size&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileAttributes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;size&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;twoMegabytes&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;c1&#34;&gt;// Find the first newline after the halfway point in the file, and only keep everything past that point to trim the file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;logsData&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;contentsOf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;options&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mappedIfSafe&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;newlineData&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\n&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;using&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utf8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;dataSize&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;logsData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;count&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;halfwayPoint&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dataSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGFloat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mf&#34;&gt;2.0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;range&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;logsData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;range&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;of&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;newlineData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;options&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[],&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;halfwayPoint&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;..&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dataSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                        &lt;span class=&#34;bp&#34;&gt;assertionFailure&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;A newline should have been found&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;remainingLogs&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;logsData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subdata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;range&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;endIndex&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;..&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dataSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;remainingLogs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;write&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;options&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;atomicWrite&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;fileHandle&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FileHandle&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forWritingTo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;fileHandle&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;seekToEndOfFile&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;fileHandle&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;write&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;messageData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;fileHandle&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;closeFile&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;catch&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Error trying to write to end of file: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;do&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;timestampedMessage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;write&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;atomically&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;encoding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utf8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;catch&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;bp&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Error creating file to log to: &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Usage:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;WidgetLogger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Called getTimeline at &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;\(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Date&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;())&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;`)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can then add a way for the user to email this file to you from within your app, I have a little &amp;ldquo;Logs&amp;rdquo; button that they can shoot over as part of troubleshooting. The code for attaching it to &lt;a href=&#34;https://developer.apple.com/documentation/messageui/mfmailcomposeviewcontroller&#34;&gt;MFMailComposeViewController&lt;/a&gt; (which might not be the best choice with the iOS 14 feature of setting alternate email clients as the default, since that API doesn&amp;rsquo;t work with it yet) is:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;contentsOf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;WidgetLogger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;mailViewController&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;MFMailComposeViewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;mailViewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;addAttachmentData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;mimeType&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;text/plain&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fileName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;WidgetLogger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;lastPathComponent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or add it as a file to a &lt;code&gt;UIActivityViewController&lt;/code&gt; that they can share:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;activityViewController&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIActivityViewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;activityItems&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;WidgetLogger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;applicationActivities&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or just make it into a String and do whatever you want with it!&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;contentsOf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;WidgetLogger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fileURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;encoding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utf8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anyway, that&amp;rsquo;s it! Happy logging!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Using PHPickerViewController Images in a Memory-Efficient Way</title>
      <link>https://christianselig.com/2020/09/phpickerviewcontroller-efficiently/</link>
      <pubDate>Sat, 26 Sep 2020 18:43:57 -0300</pubDate>
      
      <guid>https://christianselig.com/2020/09/phpickerviewcontroller-efficiently/</guid>
      <description>&lt;p&gt;&lt;code&gt;PHPickerViewController&lt;/code&gt; is (in my opinion) one of the more exciting parts of iOS 14. We developers now have a fully-fledged photo picker that we can just use, rather than having to spend a bunch of our time creating our own (much like &lt;code&gt;SFSafariViewController&lt;/code&gt; did for developers and having to write in-app web browsers). Similar to &lt;code&gt;SFSafariViewController&lt;/code&gt; it also has terrific privacy benefits, in that previously for our custom UIs, in order to show the pictures to choose from, we had to request access to all the user&amp;rsquo;s photos, which is not something users or developers really wanted to contend with. &lt;code&gt;PHPickerController&lt;/code&gt; works differently in that iOS throws up the picker in a separate process, and the host app only sees the pictures that the user gave the app access to, and not a single one more. Much nicer!&lt;/p&gt;
&lt;p&gt;(Note we did/still do have &lt;code&gt;UIImagePickerController&lt;/code&gt;, but many of us didn&amp;rsquo;t use it due to the missing functionality like selecting multiple photos that &lt;code&gt;PHPickerController&lt;/code&gt; does brilliantly.)&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://apolloapp.io&#34;&gt;Apollo&lt;/a&gt; uses this API in iOS 14 to power its image uploader, so you can upload images directly into your comments or posts.&lt;/p&gt;
&lt;h3 id=&#34;how-to-use&#34;&gt;How to Use&lt;/h3&gt;
&lt;p&gt;The API is even really nice and simple to integrate. The only hitch I ran into is that the API callback when the user selects the photos provides you with essentially a bunch of objects that wrap &lt;code&gt;NSItemProvider&lt;/code&gt; objects, which seemed a little intimidating at first glance versus something &amp;ldquo;simpler&amp;rdquo; like a bunch of &lt;code&gt;UIImage&lt;/code&gt; objects (but there&amp;rsquo;s good reason they don&amp;rsquo;t do the latter).&lt;/p&gt;
&lt;p&gt;Presenting the picker in the first place is easy:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;configuration&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PHPickerConfiguration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;configuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;selectionLimit&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;configuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;filter&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;images&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;configuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;preferredAssetRepresentationMode&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// Don&amp;#39;t bother modifying how they&amp;#39;re represented since we&amp;#39;re just turning them into Data anyway&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;picker&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PHPickerViewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;configuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;configuration&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;picker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;delegate&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;present&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;picker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;animated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;completion&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But acting on the user&amp;rsquo;s selections is where you can have some trouble:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;picker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;picker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PHPickerViewController&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;didFinishPicking&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;results&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PHPickerResult&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;/// What do I do here?! 👉🥺👈&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In reality though, it&amp;rsquo;s not too hard.&lt;/p&gt;
&lt;h3 id=&#34;what-not-to-do&#34;&gt;What Not to Do&lt;/h3&gt;
&lt;p&gt;My first swing at bat was… not great. If the user selected a bunch of photos to upload and the images were decently sized (say, straight off a modern iPhone camera) the memory footprint of the app could temporarily swell to multiple gigabytes. Yeah, with a g. Caused some crashing and user confusion, understandably, and was quite silly of me.&lt;/p&gt;
&lt;p&gt;At first my naive solution was something along the lines of (simplified):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;images&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UIImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;result&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;results&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;itemProvider&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;loadObject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ofClass&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;object&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;image&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;object&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIImage&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;resizedImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIImage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UIGraphicsImageRenderer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2_000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;height&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2_000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)).&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;image&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;image&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;draw&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGRect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;origin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGPoint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;zero&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;newSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;images&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;resizedImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Long story short, decoding the potentially large image objects into full-fledged &lt;code&gt;UIImage&lt;/code&gt; objects, and especially then going and re-drawing them to resize them is a very memory-expensive operation, which is multiplied with each image. Bad. Don&amp;rsquo;t do this. I know better. You know better.&lt;/p&gt;
&lt;p&gt;(If you&amp;rsquo;re curious for more information, Jordan Morgan has a great overview with his &lt;a href=&#34;https://www.youtube.com/watch?v=vl3aXaNPKE0&#34;&gt;try! Swift NYC talk on The Life of an Image&lt;/a&gt; and there&amp;rsquo;s also an excellent WWDC session from 2018 called &lt;a href=&#34;https://developer.apple.com/videos/play/wwdc2018/219&#34;&gt;Image and Graphics Best Practices&lt;/a&gt; that goes even more in depth.)&lt;/p&gt;
&lt;h3 id=&#34;what-you-should-do&#34;&gt;What You Should Do&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s a tiny bit longer because we have to dip down into Core Graphics, but don&amp;rsquo;t fret, it&amp;rsquo;s really not that bad. I&amp;rsquo;ll break it down.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;dispatchQueue&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;label&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;com.christianselig.Apollo.AlbumImageQueue&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;selectedImageDatas&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?](&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;repeating&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;bp&#34;&gt;count&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;results&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;count&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// Awkwardly named, sure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;totalConversionsCompleted&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;results&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;enumerated&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;itemProvider&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;loadFileRepresentation&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forTypeIdentifier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UTType&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;image&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;identifier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;url&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;url&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;url&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;dispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sync&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;totalConversionsCompleted&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;sourceOptions&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;kCGImageSourceShouldCache&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CFDictionary&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;source&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGImageSourceCreateWithURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;url&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CFURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;sourceOptions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;dispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sync&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;totalConversionsCompleted&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;downsampleOptions&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;kCGImageSourceCreateThumbnailFromImageAlways&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;kCGImageSourceCreateThumbnailWithTransform&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;kCGImageSourceThumbnailMaxPixelSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2_000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CFDictionary&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;cgImage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGImageSourceCreateThumbnailAtIndex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;source&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;downsampleOptions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;dispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sync&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;totalConversionsCompleted&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NSMutableData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;imageDestination&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGImageDestinationCreateWithData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;kUTTypeJPEG&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;dispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sync&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;totalConversionsCompleted&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Don&amp;#39;t compress PNGs, they&amp;#39;re too pretty&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;isPNG&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Bool&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;utType&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cgImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utType&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utType&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UTType&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;png&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;identifier&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;destinationProperties&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;kCGImageDestinationLossyCompressionQuality&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;isPNG&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;1.0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.75&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CFDictionary&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;CGImageDestinationAddImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;imageDestination&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cgImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;destinationProperties&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;CGImageDestinationFinalize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;imageDestination&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;dispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sync&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;selectedImageDatas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;totalConversionsCompleted&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;break-it-down-now&#34;&gt;Break it Down Now&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s a bit to unpack here, but I&amp;rsquo;ll try to hit everything.&lt;/p&gt;
&lt;p&gt;The core concept is we&amp;rsquo;re no longer loading the full &lt;code&gt;UIImage&lt;/code&gt; and/or drawing it into a context each time (which can be monstrously large, and why &lt;code&gt;PHPicker&lt;/code&gt; doesn&amp;rsquo;t just give us &lt;code&gt;UIImage&lt;/code&gt; objects), especially because in my case I&amp;rsquo;m just uploading the &lt;code&gt;Data&lt;/code&gt; and getting a resulting &lt;code&gt;URL&lt;/code&gt;, I don&amp;rsquo;t &lt;em&gt;ever&lt;/em&gt; need the image. But if you do, creating a &lt;code&gt;UIImage&lt;/code&gt; from the smaller &lt;code&gt;CGImage&lt;/code&gt; will be much better all the same.&lt;/p&gt;
&lt;p&gt;Okay! So we start off with a queue, and the data to be collected. &lt;code&gt;loadFileRepresentation&lt;/code&gt; fires on an async queue, and the docs don&amp;rsquo;t mention if it executes serially (in practice, it does, but that could change), so create a queue to ensure you&amp;rsquo;re not writing to this array of &lt;code&gt;Data&lt;/code&gt; across multiple threads. Also note that the array itself is set up in a way that we can maintain the order of the images, otherwise the order the user selected the photos in and the order they&amp;rsquo;re processed in may not line up 1:1. Lastly we keep a separate counter to know when we&amp;rsquo;re done.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;dispatchQueue&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;label&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;com.christianselig.Apollo.AlbumImageQueue&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;selectedImageDatas&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;?](&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;repeating&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;bp&#34;&gt;count&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;results&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;count&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// Awkwardly named, sure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;totalConversionsCompleted&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Moving onto the main loop, instead of asking &lt;code&gt;NSItemProvider&lt;/code&gt; to serve us up a potentially enormous &lt;code&gt;UIImage&lt;/code&gt;, we approach more cautiously by requesting a &lt;code&gt;URL&lt;/code&gt; to the image in the &lt;code&gt;tmp&lt;/code&gt; directory. More freedom.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;itemProvider&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;loadFileRepresentation&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;forTypeIdentifier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UTType&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;image&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;identifier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;url&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We then go onto create a &lt;code&gt;CGImage&lt;/code&gt; but with certain requirements around the image size so as to not create something larger than we need. These Core Graphics functions can seem a little intimidating, but between their names and the corresponding docs they paint a clear picture as to what they&amp;rsquo;re doing.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;sourceOptions&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;kCGImageSourceShouldCache&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CFDictionary&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;source&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGImageSourceCreateWithURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;url&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CFURL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;sourceOptions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;dispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sync&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;totalConversionsCompleted&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;downsampleOptions&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;kCGImageSourceCreateThumbnailFromImageAlways&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;kCGImageSourceCreateThumbnailWithTransform&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;kCGImageSourceThumbnailMaxPixelSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2_000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CFDictionary&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;cgImage&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGImageSourceCreateThumbnailAtIndex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;source&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;downsampleOptions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;dispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sync&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;totalConversionsCompleted&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Lastly, we convert this into &lt;code&gt;Data&lt;/code&gt; with a bit of compression (only if it&amp;rsquo;s &lt;em&gt;not&lt;/em&gt; a PNG though, PNGs are typically screenshots and whatnot, and I personally don&amp;rsquo;t want to hurt the quality of those).&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NSMutableData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;imageDestination&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CGImageDestinationCreateWithData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;kUTTypeJPEG&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;dispatchQueue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sync&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;totalConversionsCompleted&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Don&amp;#39;t compress PNGs, they&amp;#39;re too pretty&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;isPNG&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Bool&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;guard&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;utType&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cgImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utType&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utType&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UTType&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;png&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;identifier&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;destinationProperties&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;kCGImageDestinationLossyCompressionQuality&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;isPNG&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;1.0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.75&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CFDictionary&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;CGImageDestinationAddImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;imageDestination&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cgImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;destinationProperties&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;CGImageDestinationFinalize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;imageDestination&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we have &lt;strong&gt;much&lt;/strong&gt; smaller compressed &lt;code&gt;Data&lt;/code&gt; objects kicking around, rather than our previously large &lt;code&gt;UIImage&lt;/code&gt; objects, and we can &lt;code&gt;POST&lt;/code&gt; those to an API endpoint for upload or whatever you&amp;rsquo;d like! Thanks to everyone &lt;a href=&#34;https://twitter.com/ChristianSelig/status/1309890126408019969&#34;&gt;on Twitter who gave me pointers here as well&lt;/a&gt;. In the end this went from spiking to in excess of 2GB to a small blip of 30MB for a few seconds.&lt;/p&gt;
&lt;p&gt;Adopt this API! It&amp;rsquo;s great!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Apollo for Reddit 1.9</title>
      <link>https://christianselig.com/2020/09/apollo-1-9/</link>
      <pubDate>Sat, 12 Sep 2020 13:58:12 -0300</pubDate>
      
      <guid>https://christianselig.com/2020/09/apollo-1-9/</guid>
      <description>&lt;p&gt;Apollo 1.9&amp;rsquo;s a massive update to Apollo that&amp;rsquo;s taken months and months to complete, but I&amp;rsquo;m really happy with the result, and it brings together a ton of ideas from the community to make Apollo even nicer to use. The update includes a variety of features around crossposts, flair, new app icons, translation, and quality of life improvements. Thanks to everyone who writes in via email or via the &lt;a href=&#34;https://reddit.com/r/apolloapp&#34;&gt;ApolloApp subreddit&lt;/a&gt;, your suggestions for what you want to see in Apollo help immensely and really motivate me to keep making Apollo better and better.&lt;/p&gt;
&lt;p&gt;Without further ado, here are the changes included in this 1.9 update to Apollo:&lt;/p&gt;
&lt;h2 id=&#34;crosspost-viewing&#34;&gt;Crosspost Viewing&lt;/h2&gt;
&lt;p&gt;Crossposting (taking an existing post and reposting it to a similar subreddit) has been a big part of Reddit for ages, but recently it became a full-fledged feature where you can see exactly which subreddit it came from, and quickly jump to the original post. Apollo now supports this fully, so you can see the interesting content of the post, but also quickly jump over to read the original discussion! Often it&amp;rsquo;s like getting two interesting discussions in one!&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2020/09/apollo-1-9/changelog-1-9-view-crosspost-light.png&#34; alt=&#34;Viewing a crosspost in Apollo&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;crossposting&#34;&gt;Crossposting&lt;/h2&gt;
&lt;p&gt;Similar to being able to view crossposts, you can also easily perform a crosspost if you want as well! Simply select the post you want to crosspost, write a title, select the subreddit to crosspost it to, and bam, you&amp;rsquo;re off to the races.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2020/09/apollo-1-9/changelog-1-9-perform-crosspost-light.png&#34; alt=&#34;performing a crosspost in Apollo&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;image-flair&#34;&gt;Image Flair&lt;/h2&gt;
&lt;p&gt;Flair is a little “tag” users can add to their usernames in subreddit, and some subreddits even allow small images/icons to be added in addition to text, like the icon for your favorite sports team, or a character from your favorite TV show. Apollo now shows these beautifully!&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2020/09/apollo-1-9/changelog-1-9-image-flair-light.png&#34; alt=&#34;Viewing flair with images in Apollo&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;setting-your-flair&#34;&gt;Setting Your Flair&lt;/h2&gt;
&lt;p&gt;In addition to being able to view the flair as discussed in the previous item, you can now set your own flair! Simply go to the subreddit of your choosing, and you can choose from a list of customizable flairs so you can add a little personality to your comments, showing which language you’re learning, your username in a video game the subreddit is about, your fitness goals, etc.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2020/09/apollo-1-9/changelog-1-9-set-flair-light.png&#34; alt=&#34;Setting your flair in Apollo&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;view-long-flair&#34;&gt;View Long Flair&lt;/h2&gt;
&lt;p&gt;Some users set loooong flair, and as a result it can get off, which can be annoying when you’re trying to figure out what it says. Well be annoyed no longer, for you can simply tap on the long flair to bring up a window that expands it fully!&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2020/09/apollo-1-9/changelog-1-9-long-flair-light.png&#34; alt=&#34;Viewing long flair in Apollo&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;find-posts-with-same-flair&#34;&gt;Find Posts with Same Flair&lt;/h2&gt;
&lt;p&gt;If the subreddit lets users tag their posts with individual flairs (say, being able to tag whether your question is about a certain character, or a certain topic), you can now simply tap on that flair and Apollo will show you all the other posts in the subreddit that have been tagged with that same flair.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2020/09/apollo-1-9/changelog-1-9-search-flair-light.png&#34; alt=&#34;Filtering posts with the same flair in Apollo&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;5-yeah-five-new-app-icons&#34;&gt;5 (Yeah, Five!) New App Icons!&lt;/h2&gt;
&lt;p&gt;This update has taken a ton of time to work on, and as a result I was slightly behind in including the Ultra icons I wanted to include, but as a result there&amp;rsquo;s now a proper Icon Bonanza, with five new icons being included in this update. The first three are Ultra icons, all made by the same incredibly talented designer, Matthew Skiles, who I’ve been a fan of for a long time. I love how these turned out, we have our beloved Apollo mascot reimagined as an angel, a devil, as well as a zany pilot, all in gorgeous, colorful iconography. But those three icons aren&amp;rsquo;t all! Next up, we have a beautiful new Apollo icon representing the trans pride flag (originally created by Monica Helms), which came out really awesome and is a great addition. And last but not least, our incredible community designer, FutureIncident, makes his second appearance with the Japanese-inspired Apollo-san icon! I love this set of icons so much, it’s going to be really hard to choose.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2020/09/apollo-1-9/changelog-1-9-app-icons.png&#34; alt=&#34;5 new app icons available in this Apollo update&#34; class=&#34;width-75&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;easy-language-translation&#34;&gt;Easy Language Translation&lt;/h2&gt;
&lt;p&gt;Reddit is home to a diverse set of communities that have a variety of fascinating conversations, but sometimes it’s tricky to understand what’s being said if the conversation is in a language you’re not familiar with. Heck, you might even have no idea what the language is! Now Apollo will be able to detect if the language of a comment or post is different than the language of your iOS device, and if so, offer to quickly translate it so you can understand the conversation! It is so handy, whether you’re following a fascinating conversation or even trying to learn a new language!&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2020/09/apollo-1-9/changelog-1-9-translate.png&#34; alt=&#34;Post/comment translation in Apollo&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;fast-subreddit-selector&#34;&gt;Fast Subreddit Selector&lt;/h2&gt;
&lt;p&gt;Whethering you’re trying to add a single subreddit to a filter, or adding multiple subreddits at a time to a multireddit, Apollo is now even faster at doing these tasks, with an auto-completing window that makes it super fast to search and add subreddits.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2020/09/apollo-1-9/changelog-1-9-subreddit-selector-light.png&#34; alt=&#34;Fast subreddit selector in Apollo&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;total-collapsed-comments--remembering-collapsed-comments&#34;&gt;Total Collapsed Comments &amp;amp; Remembering Collapsed Comments&lt;/h2&gt;
&lt;p&gt;Two handy new additions to collapsing comments in Apollo. The first, Apollo will show you at a glance how many comments are in the collapsed conversation, which can be super handy for viewing a comment thread. The second thing, if you collapse a bunch of comments, and then come back to that same comment section later, Apollo will now remember which comments you had collapsed, and keep them collapsed for you!&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2020/09/apollo-1-9/changelog-1-9-total-collapsed-light.png&#34; alt=&#34;Total collapsed comments in Apollo&#34; class=&#34;width-50&#34; loading=&#34;lazy&#34; /&gt;

&lt;h2 id=&#34;new-settings-filters-tweaks-bug-fixes-and-more&#34;&gt;New Settings, Filters Tweaks, Bug Fixes, and More!&lt;/h2&gt;
&lt;p&gt;A bunch of awesome new settings have been added to Apollo, like being able to disable the auto-looping of videos with audio, or being able to make it so translation options always show up. Filtering is also even more powerful, with your filters being able to target flair and links as well (in addition to the title), and fixes a few filtering bugs. Apollo also now shows videos from Reddit’s experimental &amp;lsquo;RPAN&amp;rsquo; service, which is essentially a kind of live stream post that you can now view within Apollo. Of course there&amp;rsquo;s a bunch of other small bug fixes around Apollo, from the occasional account accidentally signing out, to video bugs, to Apollo quitting in the background when it shouldn’t, as well as a bunch of other small tweaks across the app to improve your quality of life while browsing!&lt;/p&gt;
&lt;h2 id=&#34;thank-you&#34;&gt;Thank You!&lt;/h2&gt;
&lt;p&gt;I really hope you enjoy the update, thank you for using Apollo! More great things to come!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>The Case for Getting Rid of TestFlight Review</title>
      <link>https://christianselig.com/2020/06/testflight-review/</link>
      <pubDate>Fri, 12 Jun 2020 17:14:26 -0300</pubDate>
      
      <guid>https://christianselig.com/2020/06/testflight-review/</guid>
      <description>&lt;p&gt;I &lt;a href=&#34;https://twitter.com/ChristianSelig/status/1271479995278544898&#34;&gt;tweeted today&lt;/a&gt; about how I think TestFlight review should become a thing of the past and many developers seemed to agree, but some had questions so I wanted to expand on my thoughts a little.&lt;/p&gt;
&lt;p&gt;TestFlight&amp;rsquo;s awesome. But like App Store submissions, TestFlight betas also require a review by Apple. At first blush, such a review sounds sensical. TestFlight can distribute apps to up to 10,000 users. If that were to run completely unchecked you could have potentially mini-App Stores running around with sketchy apps being distributed to lots of people.&lt;/p&gt;
&lt;p&gt;But the point I&amp;rsquo;ll try to make in this article is that the current system TestFlight employs doesn&amp;rsquo;t do much to prevent this, and further creates a lot of friction for legitimate developers.&lt;/p&gt;
&lt;h2 id=&#34;the-review-process&#34;&gt;The Review Process&lt;/h2&gt;
&lt;p&gt;For TestFlight, when you submit a new &lt;strong&gt;version number&lt;/strong&gt;, it requires a new review. But new build numbers do not (build numbers are like a secondary ID for a version as it goes through development). For instance, I could push a new version of Apollo to TestFlight, version 1.8 (build number 50) and it would need review, but builds 51, 52, 53, etc. of the same version do not require any review.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Do you see the issue here? There&amp;rsquo;s not really any oversight into what you can change in those new builds. You could completely change your app into something different, upload it under a different build number, and so long as the previous version was approved and you don&amp;rsquo;t change the version number, you could send the new one out to thousands of people.&lt;/p&gt;
&lt;p&gt;Someone looking to distribute, say a console emulator (that Apple doesn&amp;rsquo;t allow in the App Store), could upload their app as a fun turtle themed calculator app (TurtleCalc™) and get approved on TestFlight, only to update it into that emulator for build 2 and send it out to thousands of people.&lt;/p&gt;
&lt;h2 id=&#34;as-a-developer&#34;&gt;As a Developer&lt;/h2&gt;
&lt;p&gt;On the flip side, for an actual developer with an app on the App Store, it causes a &lt;em&gt;ton&lt;/em&gt; of friction, because the other rule of TestFlight is such that once a new version goes live on the App Store, you can&amp;rsquo;t push any new builds to TestFlight without a new version and starting the review process again.&lt;/p&gt;
&lt;p&gt;So if you find a bug in the public version of your app, and want to beta test the fix, you have to wait a day or two for it to be reviewed by Apple before it can even go into beta testing. A 3-lines-of-code bug fix requires re-review, meanwhile, if you&amp;rsquo;re a bad actor and you just leave the app in TestFlight without ever pushing it to the App Store, you can just update it endlessly without any review whatsoever.&lt;/p&gt;
&lt;p&gt;That means as a developer you&amp;rsquo;re stuck in this gamble of &amp;ldquo;Should I just release it to the App Store without any testing? It&amp;rsquo;s just a bug fix after all, what could go wrong?&amp;rdquo; versus &amp;ldquo;Should I let it keep crashing and wait for the TestFlight review to occur so I can test this new build first, even if it means crashing for days more?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;In a perfect world, you could push that fix out to testers &lt;em&gt;immediately&lt;/em&gt;, validate the fix, then submit it to the App Store.&lt;/p&gt;
&lt;p&gt;As a result you have this system that A) doesn&amp;rsquo;t seem to do anything to stop people submitting nefarious updates but B) introduces a ton of friction to legitimate developers.&lt;/p&gt;
&lt;h2 id=&#34;it-serves-as-an-early-review-for-the-app-store-before-continued-development&#34;&gt;&amp;ldquo;It Serves as an Early Review for the App Store Before Continued Development&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;Some argue that it lets you &amp;ldquo;test the waters&amp;rdquo; with an app or an update before submitting it to the App Store at large. For instance you have an idea that you&amp;rsquo;re not sure will get through app review, so you build a quick version of the app, and submit it to TestFlight, and the review will let you know if Apple will approve it.&lt;/p&gt;
&lt;p&gt;Unfortunately it doesn&amp;rsquo;t work like that. Getting through TestFlight review has no bearing on getting through the eventual App Store review. I&amp;rsquo;ve had builds go through TestFlight review, get the stamp of approval, test it in TestFlight for months, and then when I ultimately submit it &lt;a href=&#34;https://www.reddit.com/r/apolloapp/comments/9l3ema/apollo_13_rejected/&#34;&gt;the update gets rejected&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;TestFlight reviews are not at all an accurate way to gauge what the reviewers will think. It&amp;rsquo;s far more lax.&lt;/p&gt;
&lt;h2 id=&#34;it-often-requires-double-review&#34;&gt;It Often Requires Double Review&lt;/h2&gt;
&lt;p&gt;Even more confusingly, if I decide to take the gamble and just release the bug fix to the App Store and hope all goes well, it&amp;rsquo;ll goes through a quick review, then it will go live on the App Store.&lt;/p&gt;
&lt;p&gt;But if I want the TestFlight users to use that same version that just got approved, they straight up can&amp;rsquo;t. Even though it went through the more strict public App Store review, the exact same build has to be reviewed separately for TestFlight. This adds a confusing delay for testers (not to mention extra work for Apple) and is very weird.&lt;/p&gt;
&lt;h2 id=&#34;testflight-review-takes-longer-than-app-store-review&#34;&gt;TestFlight Review Takes Longer than App Store Review&lt;/h2&gt;
&lt;p&gt;Despite being more lax a review process (as shown above), it takes longer to review. This kinda makes sense, you would hope the majory of staff would be focused on the public App Store review which affects the most users, but it feels bizarre to submit an app to the App Store and TestFlight at the same time (because double review) and the App Store version goes out the same day while the TestFlight version takes a day or two.&lt;/p&gt;
&lt;p&gt;This greatly disincentivizes testing builds when the process to actually get them out takes so long.&lt;/p&gt;
&lt;h2 id=&#34;theres-already-workarounds&#34;&gt;There&amp;rsquo;s Already Workarounds&lt;/h2&gt;
&lt;p&gt;A lot of developers, aware of the above constraints, employ strategies for getting around this process almost completely.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;As soon as you submit the version to the App Store, you can immediately submit the same version plus one (so 1.8.3 on the App Store, 1.8.4 on TestFlight) even without any changes (just a bumped version number), get it through review, and then the next time you need to test a beta build you have an approved version you can start shoveling new builds onto.&lt;/li&gt;
&lt;li&gt;An even more clever method some employ, is to just have an astronomically high version number only for TestFlight. So if your App Store version is 1.8, your TestFlight version is 1,0000. That way your TestFlight build is always ahead of the App Store version, and once that version gets approved the first time, you can indefinitely add new builds onto it. A lot of developers do this, and it&amp;rsquo;s clever, but I personally fear angering the App Store folks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You might be asking, &amp;ldquo;Okay… why not just do one of those methods then?&amp;rdquo;. And you totally can, but in neither case is the app actually being reviewed, in the first it&amp;rsquo;s an identical version that&amp;rsquo;s tweaked &amp;ldquo;secretly&amp;rdquo; later, and in the second it&amp;rsquo;s a single version that gets tweaked forever. This effectively shows how little the review process actually contributes.&lt;/p&gt;
&lt;h2 id=&#34;getting-rid-of-testflight-review-could-speed-up-normal-review&#34;&gt;Getting Rid of TestFlight Review Could Speed up Normal Review&lt;/h2&gt;
&lt;p&gt;If TestFlight review were to go away for the reasons outlined above, all the awesome folks on that team could be relocated to the &amp;ldquo;normal&amp;rdquo; App Store team, which could see an even faster review process. The review process is so much better now than it has been in the past, typically under a day (it used to be over a week!), but can you imagine submitting a build and it being available within a few hours being the norm? That would be fantastic!&lt;/p&gt;
&lt;h2 id=&#34;solution&#34;&gt;Solution&lt;/h2&gt;
&lt;p&gt;I think just getting rid of it completely is fair. As shown, the current process does next to nothing to prevent people from distributing questionable builds, and instead is just a pain for legitimate developers.&lt;/p&gt;
&lt;p&gt;Is it possible that behind the scenes Apple re-reviews builds and might yank them if they find out they break their rules, say a game console app that&amp;rsquo;s been getting new builds but no new reviews from Apple for a year? Totally! And I think that&amp;rsquo;s the system they should simply extend everywhere.&lt;/p&gt;
&lt;p&gt;Do away with the review system all together, and have a random review process that occurs &lt;em&gt;after the fact&lt;/em&gt;, every so often, perhaps transparently and based on the amount of testers in the beta (a beta with 8,000 users is more dangerous than one with three people).&lt;/p&gt;
&lt;p&gt;So you submit your version, it immediately goes out to all testers, and then a little while after Apple might flag it for random review. If it passes, it&amp;rsquo;s completely transparent to you. If it gets rejected, it&amp;rsquo;ll be pulled.&lt;/p&gt;
&lt;h2 id=&#34;end&#34;&gt;End&lt;/h2&gt;
&lt;p&gt;TestFlight&amp;rsquo;s great and I love it, but decreasing friction in beta testing would be a massive help.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Announcing Apollo: a new Reddit app for iPhone</title>
      <link>https://christianselig.com/2015/01/announcing-apollo-a-gorgeous-reddit-app-for-iphone/</link>
      <pubDate>Fri, 30 Jan 2015 19:38:21 -0300</pubDate>
      
      <guid>https://christianselig.com/2015/01/announcing-apollo-a-gorgeous-reddit-app-for-iphone/</guid>
      <description>&lt;p&gt;I’m really excited to unveil a project I’ve been working on for the last year or so. It’s called &lt;a href=&#34;https://apolloapp.io&#34;&gt;Apollo&lt;/a&gt; and it’s a new Reddit app for iPhone.&lt;/p&gt;
&lt;p&gt;I’ve been a Reddit user for about four years now, and the site is a constant source of interesting discussion, hilarity and news for me every day. I’ve never been completely happy with the current Reddit apps out there today, so I set out to scratch an itch and build the best Reddit experience on the iPhone that I could. And I’m really proud of the result.&lt;/p&gt;
&lt;p&gt;Apollo went through a really long design phase, and I sweated every detail. Last Spring I was lucky enough to get an offer to work at Apple as an intern for the summer, which meant no time for developing apps for a few months. But after that summer, I learned so much from so many smart people, had a really &lt;a href=&#34;https://developer.apple.com/swift/&#34;&gt;cool new language&lt;/a&gt; to experiment with and my motivation to build something incredible had never been higher.&lt;/p&gt;
&lt;p&gt;Since then I’ve been working super hard to build this app, and today I’m finally at a stage where I can comfortably announce it. It’s not available yet, and won’t be for a little while yet, but it’s getting close and I’d love to have some input. (I made a Reddit thread &lt;a href=&#34;http://www.reddit.com/r/apple/comments/2u86mj/announcing_apollo_a_brand_new_gorgeous_reddit_app/&#34;&gt;here&lt;/a&gt;) I’ll also be launching a public beta in the coming weeks, so keep an eye out for that if you want to get an early look at what’s to come.&lt;/p&gt;



    &lt;img src=&#34;https://christianselig.com/2015/01/announcing-apollo-a-gorgeous-reddit-app-for-iphone/apollo-frontpage-inbox.png&#34; alt=&#34;Apollo&amp;#39;s frontpage and inbox&#34; class=&#34;&#34; loading=&#34;lazy&#34; /&gt;

&lt;p&gt;I really put an emphasis on making Apollo feel at home on the iPhone with a super comfortable browsing experience. It has beautiful, large images, smooth gestures, really nicely organized comments and I baked in a lot of the great features that iOS 8 brought about. There’s a ton more as well. I also made sure that it took advantage of as many of Reddit’s great features as possible.&lt;/p&gt;
&lt;p&gt;From a technical standpoint, it’s built for the most part in Swift. I’ve been really happy with the language so far (bar a few issuse) and it was awesome to build an app with it.&lt;/p&gt;
&lt;p&gt;I’ve made a page where you can find out more about it, and if you’d like sign up to be notified when it’s released: &lt;a href=&#34;https://apolloapp.io&#34;&gt;https://apolloapp.io&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I’d love to hear input. You can reach me on &lt;a href=&#34;https://christianselig.com&#34;&gt;Twitter&lt;/a&gt;, post in the &lt;a href=&#34;http://www.reddit.com/r/apple/comments/2u86mj/announcing_apollo_a_brand_new_gorgeous_reddit_app/&#34;&gt;Reddit thread&lt;/a&gt; or email me if you’d like. I’ll also be posting updates on my &lt;a href=&#34;https://dribbble.com/christianselig&#34;&gt;dribbble page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Can’t wait to share more in the coming weeks!&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>